# Debug Menu
A standalone debugging toolkit for Minecraft Fabric. It collects debug toggles, HUD overlays, and player behavior logging into one scrollable menu, and exposes an API so other mods can plug their own debug toggles into the same screen.
- Minecraft: 1.20.4 (default) / 1.20.1 (single source tree, target picked at build time)
- Fabric Loader: >= 0.15.0 (requires Fabric API)
- Java: 17
- Environment: client + server
- Author: liuzeen1234 (liuzeen1234@qq.com)
- License: MIT
## Features
### Unified debug menu
Open the menu with a configurable keybind (unbound by default, set it under Options → Controls → Debug Menu). The screen reads every registered debug toggle and groups them by mod ID, with scrolling when the list overflows. If nothing is registered, only the built-in HUD settings entry is shown.
### HUD overlays
- **Entity health** (top-right): shows the name and health of the entity under your crosshair as `[name][current/max]`. Non-living entities render as `[name][-/-]`. Detailed NBT display can be enabled; the client requests entity NBT from the server and caches the response. Trace distance is configurable (1–256, default 128).
- **Held item info** (top-left): shows the main-hand item name and stack count. Advanced mode adds durability and the full set of NBT tags, rendered at reduced scale with automatic line wrapping.
### Live player behavior log
Once enabled, player actions are written through the `DebugMenu` logger:
- Combat and status: attacks, damage taken, death, hunger changes
- Movement and pose: jumping, movement, sprint / sneak / swim / fly transitions
- Items and interaction: dropping items, hotbar switching, item use, block right-click, block breaking
- Client input: key presses, mouse clicks and scroll, screen open/close
### Persistent configuration
Toggle states and HUD settings are stored in `config/debug-menu.json` and saved immediately on change, so they survive a restart.
## API for other mod developers
Register a toggle during your mod's initialization and the debug menu builds the UI for it automatically:
```java
DebugMenuApi.register(new DebugToggleEntry(
"my-mod", // owning mod ID (used for grouping)
"my-mod:feature_debug", // unique key
"Feature Debug", // display name in the menu
() -> myDebugEnabled, // getter
v -> { myDebugEnabled = v; saveConfig(); } // setter
));
```
Other available methods:
- `DebugMenuApi.registerAll(Collection<DebugToggleEntry>)` — register in bulk
- `DebugMenuApi.isEnabled(String key)` — query a toggle from your own code
- `DebugMenuApi.getEntries()` / `getEntriesByMod()` / `getEntry(key)` — read registered entries
The registry is backed by a `CopyOnWriteArrayList`, so reads are safe across threads.
### Custom group display name
The menu groups entries by `modId` and uses `modId` as the group header. To show a friendlier title, register a display name once during init:
```java
DebugMenuApi.setModDisplayName("my-mod", "My Mod");
```
- Applies to all entries under that `modId` (boolean toggles / numeric sliders / multi-state switches); call it just once.
- When not set, the header falls back to `modId`, so it is fully backward compatible.
- Grouping, collapsing and lookups still key off `modId`; changing the display name does not affect them.
- Passing `null` or a blank string clears the registered name (falls back to `modId`); `getModDisplayName(modId)` reads the current name (returns `modId` when unset).
### Numeric entry (slider, with server sync)
When you need an integer value constrained by min/max/step, register a `DebugValueEntry` and the menu renders it as a slider:
```java
DebugMenuApi.registerValue(new DebugValueEntry(
"my-mod", // owning mod ID
"my-mod:spawn_rate", // unique key
"Spawn Rate", // display name
0, 100, // min / max (inclusive)
() -> spawnRate, // getter
v -> { spawnRate = v; saveConfig(); } // setter (value is clamped to [min, max] internally)
));
```
Key conventions:
- **Register on both sides**: the same `key` must be registered **once on the client and once on the server**. The client entry drives the UI (local slider display, sends packets); the server entry runs on the server main thread when a sync packet arrives. The two are matched by the shared `key`.
- **Side tagging**: each entry is tagged with `DebugValueEntry.Side` (`CLIENT` / `SERVER` / `BOTH`). In single-player the client and integrated server share one JVM, so side filtering avoids double-rendering the UI and updating the wrong object on write-back. Use `BOTH` only when both getters/setters point at the **same state**.
- **Permission**: before applying a client value, the server runs a permission check that defaults to requiring permission level `>= 2`. Override it by passing a custom `BiPredicate<ServerPlayerEntity, Integer>` to the full constructor.
- **Options**: the full constructor supports a custom step (`step > 0`) and a unit suffix (e.g. `"blocks"`, `"%"`).
- Other methods: `registerAllValues(...)` to bulk register, `getValueEntry(key)` / `getValueEntry(key, side)` to query, `getValueEntriesByMod(side)` to group by mod (filtered by side and de-duplicated).
### Conditional / nested toggles
A toggle can be shown only when a condition holds, letting you build a "parent toggle → child option" hierarchy. Pass a visibility predicate to `DebugToggleEntry`:
```java
// Shown only while the parent boolean toggle my-mod:feature is on
DebugMenuApi.register(new DebugToggleEntry(
"my-mod", "my-mod:detail", "Detail Sub-option",
() -> detailOn, v -> { detailOn = v; save(); },
DebugMenuApi.visibleWhenEnabled("my-mod:feature")));
// Shown only while the parent multi-state switch my-mod:mode is "Advanced" or "Expert"
DebugMenuApi.register(new DebugToggleEntry(
"my-mod", "my-mod:expert_opt", "Expert Option",
() -> expertOn, v -> { expertOn = v; save(); },
DebugMenuApi.visibleWhenOption("my-mod:mode", "Advanced", "Expert")));
```
- `visibleWhenEnabled(parentKey)`: visible while the parent boolean toggle is on; a missing parent key counts as off.
- `visibleWhenOption(parentKey, states...)`: visible while the parent multi-state switch's current state matches one of `states`; a missing key or non-matching state hides it.
### Multi-state switch (custom state names)
Besides on/off boolean toggles, you can register a switch with **multiple states whose names are fully custom** (e.g. a language selector). The menu renders it as a button that cycles through the states on click:
```java
DebugMenuApi.registerOption(new DebugOptionEntry(
"my-mod", // owning mod ID (used for grouping)
"my-mod:language", // unique key
"Language", // display name
java.util.List.of("English", "简体中文", "日本語"), // state names (order = cycle order)
() -> currentLanguage, // getter: return the current state name
v -> { currentLanguage = v; saveConfig(); } // setter: store the new state name
));
```
Notes:
- State is identified by **name (String)**. The `getter` should return one of the listed states; if it returns an invalid name (or `null`), the menu falls back to the first state instead of crashing.
- A multi-state switch is a **client-side** concept (like boolean toggles): state changes only on the client, with no server sync.
- Other methods: `registerAllOptions(...)` to bulk register, `getSelectedOption(String key)` to query the current state name, and `getOptionEntries()` / `getOptionEntriesByMod()` / `getOptionEntry(key)` to read registered entries.
## Building
The default target comes from `default_mc` in `gradle.properties` (currently **1.20.4**), so plain commands just work:
```powershell
./gradlew build
./gradlew runClient
```
Switch targets with `-Pmc` (quote it in PowerShell):
```powershell
./gradlew build "-Pmc=1.20.1"
./gradlew runClient "-Pmc=1.20.1"
```
Artifacts land in `build/libs/` with the game version in the file name, e.g. `debug-menu-mc1.20.4-1.0.0.jar`.
> Compilation is pinned to JDK 17 (see `org.gradle.java.home` in `gradle.properties` and the toolchain in `build.gradle`). If the JDK 17 path differs on another machine, edit that one line in `gradle.properties`.
Yarn mappings and Fabric API versions per target live in the `supportedVersions` map in `build.gradle`; adding a new target is one entry.

