diff --git a/.github/workflows/build_status.yml b/.github/workflows/build_status.yml index 56758789..44047433 100644 --- a/.github/workflows/build_status.yml +++ b/.github/workflows/build_status.yml @@ -42,7 +42,7 @@ jobs: - name: Build and test run: | chmod +x ./gradlew - ./gradlew clean check build + ./gradlew clean check build --stacktrace - name: Upload build artifacts continue-on-error: true diff --git a/build.gradle b/build.gradle index f9d810c4..49826ad0 100644 --- a/build.gradle +++ b/build.gradle @@ -224,7 +224,11 @@ spotless { ratchetFrom 'origin/master-2.0-1.21.1-neo' java { target 'src/**/*.java' - eclipse('4.35').configFile('gradle/java-formatter.xml') + eclipse('4.35') + // Use the same Eclipse distribution through its FAU mirror; the primary P2 + // host times out on CI even when Maven dependencies resolve successfully. + .withP2Mirrors(['https://download.eclipse.org/': 'https://ftp.fau.de/eclipse/']) + .configFile('gradle/java-formatter.xml') custom 'importBlocks', importBlocksStep } } diff --git a/docs/block-entities.md b/docs/block-entities.md index 5ab08b03..84a72540 100644 --- a/docs/block-entities.md +++ b/docs/block-entities.md @@ -182,7 +182,7 @@ tanks must be registered with the lifecycle to participate in persistence, clear | Base behavior | Subclass extension | |---|---| -| `getEnergy()`, `getEnergyCapacity()` | Backed by the owned buffer | +| `screenEnergy()` | Exposes the owned buffer to `EnergyGaugeElement`, which supplies its own snapshots and tooltip | | `getProgress()`, `getTime()`, `getPaused()` | First registered recipe processor; zero/false if none | | `screenFeatures()`, `screenStats()` | Owned host; statistics include energy and all feature statistics providers | | `screenAutomation()` | Lifecycle's automation inventory, when configured | diff --git a/docs/capabilities.md b/docs/capabilities.md index 0e759766..833bfd07 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -37,6 +37,11 @@ directly, and automation gets per-face views of it. machine facing up or down, `TOP` is the north side. `RelativeFace.facing(state)` reads the `facing` or `horizontal_facing` property, and defaults to north when the state has neither. +`ScreenHost.screenFluids()` exposes storage to resource-bound screen elements. `FluidTankElement` +owns the player view for its selected tank: manual extraction is allowed from input/manual tanks, +while insertion filters, output-role restrictions and reservations remain enforced by storage. +Side modes apply to automation. The storage has no tooltip or cursor-container interaction code. + ## Reservations `ItemStorage` and the client menu handler share `PlannedItemHandler` for insertion, extraction and @@ -70,9 +75,27 @@ loads, so load the storages first. ## Stable storage identities and recovery +`ItemInventories` groups named, **independent `ItemStorage` handlers**. Each handler owns its +contents, limits, filters, revisions and reservation plans. `inventory("input")` returns that +physical handler; `address(global)` / `index(name, local)` translate the optional combined view +used by block capabilities and legacy callers. Both `ItemStorage` and `ItemInventories` implement +`ItemStore`, so recipe groups and `MachineInventoryLifecycle` support either. Transactions track +the physical handler and local index even when several recipe aliases address the same slot. + +Named saves store each handler under `Inventories/`, with slot identities scoped to that +name. Inventory order can change without moving contents between groups. Loading also accepts +old flat `ItemStorage` saves: globally unique old slot IDs and legacy numeric mappings migrate +into the configured handlers. Removed inventories and reduced limits retain displaced entries +in `Recovery`; restoring the inventory makes those entries available again. Keep inventory names +stable as well as slot IDs. `ItemInventories.partition(names)` allocates handlers while retaining +an existing combined slot order, including interleaved groups. + Assign durable identities with `items.slotId(index, "input")` and `fluids.tankId(index, "coolant")`. IDs must be nonblank and unique within that storage. They are save identities, separate from numeric recipe/menu indexes and from resource-group or XML element IDs. +Assign explicit IDs before the first save/load. `ItemInventories` supplies IDs for remaining +unnamed slots at that point, choosing unused local numeric IDs (prefixed with underscores when +needed), so explicit numeric IDs such as `"1"` and `"2"` cannot collide with constructor defaults. Saves include `SlotId` / `TankId` where configured. Loading matches identified entries by ID; an unknown ID never falls back to its former numeric position. Saves without identities load by @@ -175,8 +198,9 @@ is checked every tick, keep a NeoForge `BlockCapabilityCache` instead. - `ItemStorage` keeps its configured slot count and `FluidStorage` its configured tanks. Entries that cannot be restored remain in recovery; see stable identities above. -- `EnergyBuffer` stores up to `Long.MAX_VALUE`. Screens show the full `long` through a getter - such as `#getEnergy` (`stored()`), while other mods see at most `Integer.MAX_VALUE`. +- `EnergyBuffer` stores up to `Long.MAX_VALUE`. `EnergyGaugeElement` reads the full `long` + from `ScreenHost.screenEnergy()` and owns its UI snapshots and tooltip. Other mods see at + most `Integer.MAX_VALUE` through the automation capability. - The runtime smoke test covers roles, filters, limits, faces, out-of-range indices, transfer, NBT round trips, int clamping, and the face mapping. Real cables and pipes still need testing in game. diff --git a/docs/custom-fluid-element.md b/docs/custom-fluid-element.md new file mode 100644 index 00000000..8833f964 --- /dev/null +++ b/docs/custom-fluid-element.md @@ -0,0 +1,378 @@ +# A complete custom fluid element + +This example targets **NeoForge 1.21.1 / Java 21** and the current MMDLib API. It adds +a water-only, 4,000 mB tank with persistent storage, a custom gauge, cursor-container +transfers, and a namespaced XML tag. All Java files below include their imports. + +The example plugs into a mod with ID `demo` and an existing registered machine block +and block-entity type `demo:machine`. Keep your normal block/item/type registration, +assets and MMDLib dependency declaration; those are outside this element example. +Use the host class below as that type's block entity, passing its registered type to +the constructor. Replace `demo` and `com.example.demo` consistently for your mod. + +The responsibilities are: + +| Piece | Owns | +|---|---| +| `MyFluidStorage` | Capacity, accepted fluids and change notifications | +| `MyMachineBlockEntity` | Storage lifetime and persistence | +| `MyFluidTankElement` | Server snapshots, tooltip rows and transfer clicks | +| `MyElements` | Common element declaration and XML registration | +| `MyClientElements` | Client-only renderer registration and drawing | + +## 1. Supply your own storage + +`MyFluidStorage.java` uses NeoForge's `FluidTank`, independently of MMDLib's +`FluidStorage`. You can replace it with your own `IFluidHandler` implementation. +The handler returned to each element must expose exactly one tank at local index 0. +Its fill/drain operations must honor simulation without modifying contents. + +```java +package com.example.demo; + +import net.minecraft.world.level.material.Fluids; +import net.neoforged.neoforge.fluids.capability.templates.FluidTank; + +public final class MyFluidStorage extends FluidTank { + private final Runnable changed; + + public MyFluidStorage(Runnable changed) { + super(4000, stack -> stack.getFluid() == Fluids.WATER); + this.changed = changed; + } + + @Override + protected void onContentsChanged() { + changed.run(); + } +} +``` + +`MyTankHost.java` is the small contract between the element and your machine: + +```java +package com.example.demo; + +import net.neoforged.neoforge.fluids.capability.IFluidHandler; +import zone.moddev.mc.lib.gui.runtime.ScreenHost; + +public interface MyTankHost extends ScreenHost { + IFluidHandler uiTank(int index); +} +``` + +`MyMachineBlockEntity.java` stores the tank, marks changes for saving and persists its +contents. It has no machine item slots; the screen can still show the player's inventory. + +```java +package com.example.demo; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; +import net.neoforged.neoforge.items.IItemHandlerModifiable; +import net.neoforged.neoforge.items.ItemStackHandler; + +public class MyMachineBlockEntity extends BlockEntity implements MyTankHost { + private final MyFluidStorage coolant = new MyFluidStorage(this::setChanged); + private final ItemStackHandler items = new ItemStackHandler(0); + + public MyMachineBlockEntity( + BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); + } + + @Override + public IFluidHandler uiTank(int index) { + if (index != 0) throw new IllegalArgumentException("Unknown tank: " + index); + return coolant; + } + + @Override + public IItemHandlerModifiable screenItems() { + return items; + } + + @Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("coolant", coolant.writeToNBT(registries, new CompoundTag())); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + coolant.readFromNBT(registries, tag.getCompound("coolant")); + } +} +``` + +Your block-entity factory constructs this class using the registered `demo:machine` +type, position and block state. Your existing block's interaction handler can open it +with `ScreenRuntime.open(player, blockEntity)` (`gui.runtime.ScreenRuntime`), or use +the `mmdlib:open_menu` callback for an MMDLib-defined block. See [opening screens](gui-screens.md). + +The UI accesses storage through `MyTankHost`, so it does not require a block fluid +capability. To support pipes too, separately register `Capabilities.FluidHandler.BLOCK` +for your machine type, returning the appropriate handler for each side. This example +does not add automation side rules or expose storage automatically. + +## 2. Own the snapshots and server clicks + +`MyFluidTankElement.java` inherits dimensions, numeric fluid display and ordinary click +dispatch. It replaces every inherited hook that would otherwise use +`ScreenHost.screenFluids()` and MMDLib's `FluidStorage`. + +```java +package com.example.demo; + +import java.util.List; +import java.util.Optional; + +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.neoforge.fluids.FluidUtil; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; + +import zone.moddev.mc.lib.gui.model.Common; +import zone.moddev.mc.lib.gui.model.ElementSnapshot; +import zone.moddev.mc.lib.gui.model.Orientation; +import zone.moddev.mc.lib.gui.model.ResourceBinding; +import zone.moddev.mc.lib.gui.model.elements.FluidTankElement; +import zone.moddev.mc.lib.gui.runtime.ScreenClick; +import zone.moddev.mc.lib.gui.runtime.ScreenHost; + +public class MyFluidTankElement extends FluidTankElement { + public MyFluidTankElement(Common common, int tank) { + super(common, tank, Orientation.VERTICAL, 54); + } + + private IFluidHandler storage(ScreenHost host) { + return ((MyTankHost) host).uiTank(tank()); + } + + @Override + public void validateHostType(Class type) { + if (!MyTankHost.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Requires MyTankHost"); + } + } + + @Override + public void validateHost(ScreenHost host) { + if (!(host instanceof MyTankHost)) { + throw new IllegalArgumentException("Requires MyTankHost"); + } + if (storage(host).getTanks() != 1) { + throw new IllegalArgumentException("uiTank must expose exactly one tank"); + } + } + + @Override + public Optional resourceBinding() { + // Our tank is outside MMDLib's indexed resource bindings. + return Optional.empty(); + } + + @Override + public Optional snapshot(ScreenHost host) { + var storage = storage(host); + return Optional.of(new ElementSnapshot( + storage.getFluidInTank(0).copy(), + storage.getTankCapacity(0) + )); + } + + @Override + public List tooltip(ScreenHost host, ItemStack carried) { + var storage = storage(host); + var fluid = storage.getFluidInTank(0); + return List.of( + Component.translatable("demo.gui.tank.title"), + fluid.isEmpty() + ? Component.translatable("demo.gui.tank.empty") + : fluid.getHoverName(), + Component.translatable( + "demo.gui.tank.contents", fluid.getAmount(), storage.getTankCapacity(0)), + Component.translatable("demo.gui.tank.accepts"), + Component.translatable("demo.gui.tank.controls") + ); + } + + @Override + public boolean interact(ScreenClick click) { + // Called on the server after the menu validates the action. + if (!(click.menu().blockEntity instanceof MyTankHost host)) return false; + + var carried = click.menu().getCarried(); + if (carried.isEmpty() || FluidUtil.getFluidHandler(carried).isEmpty()) return false; + + var inventory = click.player().getCapability(Capabilities.ItemHandler.ENTITY); + if (inventory == null) return false; + + var storage = storage(host); + int limit = click.shift() ? Integer.MAX_VALUE : 1000; + var result = click.button() == 0 + ? FluidUtil.tryEmptyContainerAndStow( + carried, storage, inventory, limit, click.player(), true) + : FluidUtil.tryFillContainerAndStow( + carried, storage, inventory, limit, click.player(), true); + + if (result.isSuccess()) { + click.menu().setCarried(result.getResult()); + } + return true; + } +} +``` + +Left-click empties the cursor container into the tank; right-click fills it from the +tank. A normal click transfers up to 1,000 mB, and Shift removes that amount limit. +The helpers handle one cursor container per click, including stowing its result from +a stack of containers. Actual transfer amounts depend on the container's capabilities +and your storage's rules; an indivisible bucket cannot transfer a partial bucket. + +The server reads the live cursor and storage, rather than trusting client-supplied +fluid amounts. The storage's change callback handles persistence; MMDLib samples the +new copied `FluidStack` and capacity for the open menu. No custom packet or value codec +is needed. Replacing the transfer helper calls with your own storage operations is +also possible; keep validation and mutation on the server. + +Returning no `ResourceBinding` opts out of automatic matching to MMDLib fluid resource +groups. The host validates the custom tank index instead. The element stores only its +definition, not a block entity or live handler. Its current constructor adds no extra +configuration beyond the inherited fields; if you add fields later, include them in +the element's deterministic equality/hash/text representation for screen fingerprints. + +## 3. Declare and register the XML tag + +`MyElements.java` is common code and contains no client references: + +```java +package com.example.demo; + +import javax.xml.namespace.QName; +import zone.moddev.mc.lib.gui.ElementType; +import zone.moddev.mc.lib.gui.load.ScreenAttribute; + +public final class MyElements { + public static final ElementType FLUID_TANK = + ElementType.xml( + MyFluidTankElement.class, + new QName("https://demo.example/gui", "fluid-tank"), + xml -> new MyFluidTankElement(xml.common(), xml.integer("tank")) + ) + .attribute("tank", ScreenAttribute.required("xs:nonNegativeInteger")) + .build(); + + private MyElements() {} + + public static void register() { + FLUID_TANK.register(); + } +} +``` + +Call `MyElements.register()` **once from your existing mod constructor, on both +physical sides**, before MMDLib freezes XML declarations during loading. Do not defer +it to client setup. `ElementType` forwards to the existing `ScreenTags` registry. +The namespace URI identifies your XML vocabulary; it is not fetched from the internet. + +## 4. Attach client-only rendering + +Place `MyClientElements.java` in a client package. The event subscriber below registers +automatically for mod ID `demo`; do not also call it from common code. + +```java +package com.example.demo.client; + +import com.example.demo.MyElements; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import zone.moddev.mc.lib.gui.client.ElementClient; + +@EventBusSubscriber(modid = "demo", bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) +public final class MyClientElements { + private MyClientElements() {} + + @SubscribeEvent + public static void setup(FMLClientSetupEvent event) { + event.enqueueWork(() -> ElementClient.of(MyElements.FLUID_TANK) + .renderer((element, context) -> { + var g = context.graphics(); + var r = context.bounds(); + int border = context.hovered() ? 0xffffcc55 : 0xff555555; + + g.fill(r.x(), r.y(), r.right(), r.bottom(), border); + g.fill(r.x() + 1, r.y() + 1, + r.right() - 1, r.bottom() - 1, 0xff181818); + + int filled = (int) Math.round((r.height() - 4) * context.ratio()); + g.fill(r.x() + 2, r.bottom() - 2 - filled, + r.right() - 2, r.bottom() - 2, 0xff36bcd4); + }) + .register()); + } +} +``` + +This replaces the inherited glass gauge with a solid cyan bar and a gold hover border. +`context.ratio()` calls the element's inherited fluid amount/capacity calculation using +synchronized values. The renderer never reads the machine's live storage. + +No `.behavior(...)` attachment is needed: ordinary inherited/default click handling +already sends validated clicks to `interact()`. For custom dragging, scrolling or +keyboard input, attach a fresh behavior factory with `.behavior(MyTankBehavior::new)`. +Omitted behaviors and rendering passes preserve normal inheritance. Rendering and input +still use the existing registries; `ElementClient` only bundles the registrations. + +## 5. Add the screen and translations + +Create **`src/main/resources/mmdlib/screen/machine.xml`** in the `demo` mod: + +```xml + + + +``` + +The file produces screen ID `demo:machine`, which must match the registered +block-entity type ID. It lives under the mod's root `mmdlib/screen/`, not under +`assets/` or `data/`. The `demo:` prefix selects your XML namespace; `tank="0"` +selects `MyTankHost.uiTank(0)`. This custom tag has a fixed vertical length of 54 +pixels from its constructor. It needs no `value`, `max` or `interaction` attributes. + +Add **`src/main/resources/assets/demo/lang/en_us.json`** (merge with existing keys): + +```json +{ + "demo.gui.machine": "Coolant Machine", + "demo.gui.tank.title": "Coolant", + "demo.gui.tank.empty": "Empty", + "demo.gui.tank.contents": "%s / %s mB", + "demo.gui.tank.accepts": "Accepts water only", + "demo.gui.tank.controls": "Left: empty container. Right: fill container. Shift: remove transfer limit." +} +``` + +## Check it in your mod + +1. Open the registered machine's screen and verify the custom cyan gauge and tooltip. +2. Pick up a water bucket and left-click the gauge. The tank gains 1,000 mB and the + cursor/container result updates. Right-click with an empty bucket to retrieve it. +3. Try a lava bucket: storage refuses it. Check full-tank and empty-tank cases too. +4. Save and reopen the world; the tank retains its contents through the NBT hooks. +5. Open the screen from two clients: both should receive the server's updated contents. + +Continue with the [element declaration and rendering API](gui-screens.md) for additional +render passes, parent tooltip rows, custom actions and XML container contracts. diff --git a/docs/gui-screens.md b/docs/gui-screens.md index b89b5394..171d44a0 100644 --- a/docs/gui-screens.md +++ b/docs/gui-screens.md @@ -12,8 +12,11 @@ embedded editor without adding per-filter XML to each screen. | Piece | Where | State | |---|---|---| -| Schema | `mmdlib/schema/screen.xsd` (in MMDLib's jar) | Done | +| Element declarations | `gui.ElementType`, `gui.client.ElementClient`; built-ins in `gui.load.BuiltinElements` and `gui.client.BuiltinElementClients` | Implemented | +| XML tag contracts | `gui.load.ScreenTags`, `ScreenTag`, `ScreenAttribute`; `screen.xsd` supplies built-in declarations/shared types | Implemented | | Typed model | `gui.model`: `Screen`, `Element` (one record per XML element), `Common`, `Text`, `ValueRef` | Done | +| Tank component | `gui.model.elements.FluidTankElement`: binding, snapshots, tooltip and container interaction | Done | +| Energy component | `gui.model.elements.EnergyGaugeElement`: binding, snapshots and default tooltip | Done | | Reading and checks | `gui.load.ScreenReader`: hardened SAX parser (no DOCTYPE, no external entities), schema validation, then the checks below | Done | | Getter binding | `gui.load.ScreenBinder`: `#method` / `Class#method` resolved to method handles, return kinds checked per element | Done | | Discovery | `gui.load.ScreenLoader`, called at startup; results in `gui.Screens` | Done | @@ -21,22 +24,29 @@ embedded editor without adding per-filter XML to each screen. | Layout | `gui.layout.ScreenLayout`: positions in GUI pixels; tab fit checked at load | Done | | Menu and opening | `gui.runtime.MachineMenu`, `ScreenRuntime`, `BlockEvents.openMenu` | Implemented | | Value sync | `gui.runtime.ScreenNetwork`, `ScreenValues`, `ScreenSnapshots` | Implemented | +| Network registration | `gui.runtime.network.NetworkRegistry`; feature-owned codecs and handlers | Implemented | | Client rendering | `gui.client.MachineScreen`, `GaugeRenderer`, `ScreenText` | Implemented | | Optional JEI | `client.jei.MachineJei`, loaded only by the JEI plugin | Implemented | Checks made at load, beyond the schema: unique `id`s and slot indexes; `shows`, `hover-shows` and `side-config groups` name existing elements; buttons have a tooltip and text or an icon (generic processor controls supply their own defaults); a -`-fallback` attribute has its key; `max` has a `value`; gauges, meters and progress indicators -have both; checkboxes and radio groups have a `value`; info pop-outs have text and -warning/error pop-outs a `value`; `spacer` sits directly in a box; at most one -`player-inventory`, and none with `inventory="none"`; the block-entity class exists and is a +`-fallback` attribute has its key; `max` has a `value`; display gauges, meters and progress indicators +have both; fluid-tank components own their values and require a tank binding; +checkboxes and radio groups have a `value`; info pop-outs have text and +warning/error pop-outs a `value`; `spacer` sits directly in a box; at most one of each +player inventory/armour/offhand section, and none with `inventory="none"`; the block-entity class exists and is a `BlockEntity`; every getter exists, is public, and returns a kind the element can show; side tabs and pop-out tabs fit their edge. -Getter return kinds: whole numbers (`byte` to `long`, boxes, `BigInteger`), decimals (`float`, -`double`), a declared `Number`, `boolean`, `FluidStack` (fluid gauges), `Component` and -collections of components (warning and error pop-outs). +Element-local limits are checked by the elements during both XML loading and Java screen binding: +`SlotElement.validate()` restricts indexes to 0..4095, including item, ghost and custom slots; +`SideConfigElement.validate()` permits at most 682 groups. `ScreenRuntime.validate()` checks +screen-wide transport limits and registered callback references, without element-type branches. + +Built-in getter types: whole numbers (`byte` to `long`, boxes, `BigInteger`), decimals (`float`, +`double`), a declared `Number`, `boolean`, `FluidStack` (fluid gauges), `ItemStack`, `Component` +and collections of components (warning and error pop-outs). Mods can register additional types. Screens are declared in **XML**, in keeping with MMDLib's data-driven core. Each file is loaded at startup into a typed screen model and validated there, with the file and line number in any @@ -45,6 +55,599 @@ autocompletion in their editors. ## Wiring a machine +### Registering getter value types + +`gui.value.ValueTypeRegistry.GLOBAL` owns the getter and wire type catalog. Register common-side +`ValueType` definitions during mod construction, before screens load and freeze the registry. +`ValueTypes` supplies the built-ins through the same API. For example: + +```java +public record Reading(int amount) {} + +public static final ValueType READING = ValueType.builder( + ResourceLocation.parse("example:reading"), Reading.class, + () -> StreamCodec.of( + (buffer, value) -> buffer.writeInt(value.amount()), + buffer -> new Reading(buffer.readInt())), + 4) + .version(1) + .description("a machine reading") + .validate(value -> value.amount() >= 0) + .immutable() + .build(); + +// In the mod constructor, on both physical sides: +ValueTypeRegistry.GLOBAL.register(READING); + +// On a custom Element (or pass Set.of(READING) to WidgetType): +public Set> valueKinds(WidgetTypes widgets) { return Set.of(READING); } +``` + +Import `ValueType`, `ValueTypeRegistry` and `ValueTypes` from `gui.value`; codecs use +`StreamCodec`. An XML getter returning `Reading` now binds without +changes to the loader, menu or screen. Renderers receive the decoded value through +`access.value(element, false)`. `valueKinds` and `maximumKinds` contain registered descriptors; +the former `BoundValue.Kind` enum has been removed. + +Each type owns Java aliases (`javaTypes`), canonical normalization (`normalize`), runtime +validation, copying, equality and a bounded codec. Exact Java registrations win, followed by the +most specific registered superclass/interface; ambiguous matches and duplicate IDs/Java bindings +are rejected. Use `subtypes(false)` for exact Java bindings only (as the built-in declared `Number` +type does). The declared getter's descriptor normalizes and snapshots its result. Null means +unavailable and requires no type registration. Standard whole-number bindings normalize to +`BigInteger`; decimals normalize to finite `Double` values. + +Snapshots default to a codec round trip to detach mutable data. Override `copy` only with a deep +copy, or use `immutable()` for genuinely immutable values. Use `equality` when the value's ordinary +`equals` does not compare contents. Copy results still pass validation; transport always enforces +the byte limit. Built-in item/fluid copies do not require serialization or registry access. +The codec supplier is lazy so registration does not initialize Minecraft registries prematurely. +The maximum is 1 MiB per value; declare the smallest positive limit your codec needs. +Built-in text collections additionally allow at most 4096 components. + +Deltas and action-result corrections use the same registered codecs. Each non-null wire value +carries its namespaced type ID, positive schema version and bounded payload. Unknown IDs, version +mismatches, excess bytes, trailing bytes and invalid decoded values are rejected. Type declarations +enter the menu fingerprint in deterministic ID order. Bump the type's version when changing its +codec or semantics, and register matching definitions on the client and server. The core screen +protocol is now 19. Internal typed-state envelopes use a wire-only registration, excluded from +getter binding. + +`gui.model` holds the open `Element` interface, `Screen`, and shared data types such as +`Common`, `Text`, `ValueRef`, `Interaction` and `ResourceBinding`. + +`gui.model.containers` holds composable containers and layout elements: `BoxElement`, +`GroupElement`, `ColouredGroupElement`, `ContextElement`, `ViewElement`, `InventoryElement`, +`InventoryControlsElement`, `RadioGroupElement`, `SideTabElement`, and the placement elements +`SpacerElement`, `PlayerInventoryElement`, `PlayerArmourElement` and `PlayerOffhandElement`. +The player sections share `PlayerSlotsElement`. `BoxElement` represents both `hbox` and `vbox`. + +`gui.model.elements` holds leaf controls and displays, including slots, buttons, sprites, +fluid/energy gauges and popouts. `gui.layout` holds geometry and orchestration helpers such as +`Rect`, `TextMeasure`, `ElementLayouts`, `LayoutContext` and `ScreenLayout`; these are not +elements in the screen model. Java imports use these packages; XML names remain unchanged. + +Every built-in element is a standalone `*Element` class: for example, `ButtonElement`, +`BoxElement`, `CustomElement`, `ProgressElement` and `PopoutElement`. `Element` contains only +the common contract and sizing constants. Former nested references such as `Element.Button` +become `ButtonElement`; shared enums, `ViewRef` and `ElementSnapshot` live in `gui.model`. +The progress direction enum is `ProgressDirection`. XML names and attributes stay the same. + +Built-in elements own their measurement and child placement too. `ScreenLayout` composes the +outer panel, contributed footers and registered tabs, then delegates to `measure` and `childLayout`. +`stretchX`, `stretchY` and `stretchToPanel` opt into the available space; `spacer` opts into +leftover main-axis space in a box. `prepareLayout(LayoutContext)` supplies context-dependent +metadata before measurement, such as reserving the back-button header in alternate views. + +`Element.layoutContribution(context)` optionally returns a fresh `LayoutContribution` for each +layout. The context supplies the screen, measurement context and resolved drawer definitions. +Plans run in element traversal order. They can claim body roots for custom placement, request +centred footer space, and place their elements through generic `Panel` operations in `finish`. +`shiftBelow` moves already placed roots and pending footer allocations; `grow` increases panel +height. `beforeOffsets` observes final placement after every plan finishes; all elements then receive +authored offsets. `resolveOffsets` can align associated +elements, and `regions()` publishes final named rectangles. Duplicate root claims and region IDs +are rejected. Multiple contributions compose without a registry or element-type checks in +`ScreenLayout`; keep mutable plan state out of the element definition. + +Consumers use `layout.region(id)`. Player inventory publishes `PlayerInventoryElement.INVENTORY` +and `TOOLBAR`; these replace `layout.inventory()` and `layout.inventoryColors()`. Default slot +geometry and dimensions live in `PlayerInventoryElement.slot`, `WIDTH` and `HEIGHT`, replacing +the former `ScreenLayout.playerSlot` and `INVENTORY_*` aliases. Item coordinates come from +`SlotElement.itemPosition(bounds)`, including custom item sizes. Default native item, ghost and +player-slot factories honor this hook for direct `SlotElement` implementations as well as subclasses. +Drawer tab defaults live in +`DrawerPlacement`; drawer content insets live in `SideDrawerLayout`. `AutomationInventoryElement` +owns the addon/filter slot preset used by automation menus and layout. `InventoryElement` only +provides the reusable named inventory/grid; player sections use their three dedicated containers. + +`ScreenHost.screenAutomationInventory(group)` supplies a fresh `AutomationInventoryElement` per +menu/group. Its slot-factory constructor accepts `IntFunction`; the factory +runs once per local index and retains the returned element identities. Defaults are extensible +`AutomationSlotElement`s with `AUXILIARY` roles. Custom slots may override the native factory, +`itemPosition` and `role`, including direct `SlotElement` implementations. Keep all eight real, +non-player slots and their local indexes: addon/filter storage semantics remain feature-owned. +`AutomationMenu` also accepts an explicit inventory factory for custom component construction. + +Override `drawerSlotBounds`, `filterToggleBounds` or `contentHeight` on the automation inventory to +customize presentation. Native creation/repositioning, drawn frames, filter-toggle hit boxes and +drawer height consume that same inventory instance; item coordinates always use the slot's +`itemPosition`. The static `AutomationLayout` helpers describe the default preset. Keep definitions +consistent on client and server. These hooks use the existing menu component and renderer paths. + + +`Element` and `SlotElement` are open interfaces: mods can implement them in their own packages +and include those instances in a Java-built `Screen`. A custom element implements `common()` +and `measure(TextMeasure)`; containers also expose `children()` and `childLayout(bounds, text)` +with child rectangles relative to the container. Existing `validate`, `snapshot`, `interactive`, +`interact` and tooltip hooks apply to these elements too. All concrete built-in elements are +inheritable classes, including containers, item/ghost slots and `FilterControl` implementations. +Their existing constructors and accessor names remain available; configuration fields are final +and child collections are copied. Mods can extend a built-in and override only the relevant hooks. +Subclasses inherit their base type's renderer and behavior unless a more specific registration exists. +The converted classes retain structural equality within an exact runtime type. If a subclass adds +configuration fields, include them in `equals`, `hashCode` and a deterministic `toString`; screen +fingerprints include concrete element class names and model text. Keep per-screen mutable state in +registered behaviors. Non-element data records such as slot appearances and sprite layers remain records. + +Elements also own value presentation, independently of synchronization: + +- `displayValue(value)` converts a synchronized snapshot for display; its default returns the value. +- `displayText(value, detailed)` formats current or maximum values. The default formats the converted + value numerically, abbreviating large integers unless detailed output is requested. +- `fillRatio(value, maximum)` supplies a finite fraction between zero and one. Its default computes + a clamped numeric ratio of the converted values, preserving large integer/decimal precision. + +Fluid tanks and generic fluid gauges convert `FluidStack` snapshots into amounts themselves. +Shared tooltips call `displayText`; gauge, meter, sprite and widget rendering call `fillRatio`. +No presentation registry is needed. Override conversion for custom value objects or override the +formatting/fill hook directly. These hooks receive synchronized snapshots and must not mutate them. +The numeric utility `DisplayValues` contains only reusable formatting/arithmetic defaults. + +`GaugeElement.fill(value)` and `FluidTankElement.fill(value)` select a common-side `GaugeFill`. +Override these methods to return `GaugeFill.solid(rgb)`, `GaugeFill.fluid(stack)`, or +`GaugeFill.fluid(stack, argbTint)` while inheriting the existing renderer. Selection is independent +of `fillRatio`: a numeric snapshot can select a fluid texture, and a tank can select a solid colour. +Default gauges use their configured solid colour first, otherwise the snapshot fluid, otherwise +grey; tanks default to the snapshot fluid. Fluid fills copy their contents on construction and +access. The renderer resolves textures/default tint and draws the selected fill; it does not +interpret raw snapshot types. No additional registration is needed. + +```java +public class ReserveTankElement extends FluidTankElement { + public ReserveTankElement(Common common, int tank) { + super(common, tank, Orientation.VERTICAL, 54); + } + + @Override + public double fillRatio(Object value, Object maximum) { + // For example, visualize the square root of the normal fraction. + return Math.sqrt(super.fillRatio(value, maximum)); + } + + @Override + public GaugeFill fill(Object value) { + return GaugeFill.solid(0x5599cc); + } +} +``` + +This subclass retains the tank's snapshots, tooltip, container interaction and registered renderer. +Override `appendTooltip` to add custom rows, using `displayText` for any values you include. + +For a complete worked example, see [a custom fluid element](custom-fluid-element.md): mod-owned +storage and persistence, server clicks, client rendering, common/XML registration and screen resources. + +For a new element, declare its common definition once and attach its client implementation +separately. `ElementType` is an immutable declaration, not another registry. Its optional XML +contracts register into `ScreenTags`; `ElementClient` forwards drawing/input attachments into +`ElementRenderers` and `ElementBehaviors`. + +```java +// Common code: MyFluidTankElement(Common, int) is your mod's element class. +public static final ElementType MY_TANK = + ElementType.xml( + MyFluidTankElement.class, + new QName("https://example.org/gui", "fluid-tank"), + xml -> new MyFluidTankElement(xml.common(), xml.integer("tank"))) + .attribute("tank", ScreenAttribute.required("xs:nonNegativeInteger")) + .build(); + +// Mod construction, on both physical sides: +MY_TANK.register(); +``` + +```java +// Client setup only; keep this code in a client class. +ElementClient.of(MY_TANK) + .renderer(MyTankRenderer::render) + .register(); +``` + +Imports: `gui.ElementType`, `gui.client.ElementClient`, `gui.load.ScreenAttribute`, and +`javax.xml.namespace.QName`. Add `.behavior(MyTankBehavior::new)` when custom client input is needed; +ordinary tank clicks already use inherited/default behavior. Omitted render passes and behaviors +install nothing and preserve the existing most-specific inheritance rules. Factories still create +fresh behavior instances per screen. Missing drawing is checked when the screen is validated. + +Use `ElementType.of(MyElement.class)` for a Java-only element or an abstract/interface family. +`ElementType.xml(name, existingScreenTag)` wraps an existing tag contract. `withXml(name, tag)` returns +a new immutable declaration with another XML alias, leaving the original unchanged. The builder +exposes `groups(...)`, `children(group, min, max)` and `children(QName, min, max)` using the existing +schema contracts. Document roots, data children and template directives remain ordinary `ScreenTag` +registrations because they are not UI elements. + +Client attachments support `.overlay(...)`, `.foreground(...)`, `.top(...)` and +`.renderer(pass, clip, renderer)` for explicit clipping. Register the renderer object itself to retain +its `validate` hook. Duplicate attachments/registrations fail; intentional replacements still use the +existing registries' `replace` APIs. Common registration accepts an explicit `ScreenTags`, and client +registration accepts explicit `ElementRenderers`/`ElementBehaviors` for isolated tooling or tests. + +The common descriptor has no client-class references: use `ElementClient.of(MY_TANK)`, not a common +`MY_TANK.client()` method. Built-ins follow the same split. `BuiltinElements` owns their shared common +declarations and XML parsers; `BuiltinElementClients` pairs their drawing and input attachments. +Existing XML schemas, aliases and lower-level registration APIs remain supported. The legacy `gauge` +tag can construct two unrelated element classes, so its XML declaration is separate from the typed +`GAUGE`/`FLUID_TANK` client attachments. + +Register drawing during client setup with +`ElementRenderers.GLOBAL.register(MyElement.class, (element, context) -> { ... })`. +Built-ins use this same registry: `MachineScreen` has no element-specific drawing switch. +The context contains GUI bounds, synchronized values, mouse/frame inputs, and read-only +`context.access()` presentation services (font/text, element lookup, parent, bounds, typed +element state and displayed slot contents). It exposes neither the machine nor the menu. Treat synchronized +values as immutable; slot contents/hints supplied by the access object are copies. No client +classes are needed in the common element implementation. + +An exact registration wins; otherwise the most specific registered superclass/interface wins. +Thus custom `SlotElement` and `ButtonControl` implementations inherit shared drawing, and sprite +subclasses inherit `SpriteRenderer`. Register the concrete mod class to override its inherited +drawing. Unrelated matching interfaces are an error until an exact or more specific registration +resolves the ambiguity; selection never depends on registration order. Duplicate registrations +are rejected. Use `replace` to intentionally replace an existing registration for that exact type. +Missing background renderers and ambiguous matches are checked when a screen is created. +A renderer can also override `Renderer.validate(element)` to check client dependencies; the +resolved registration for each pass owns that check, including inherited and replaced registrations. +Throw `IllegalArgumentException` with a useful message; validation adds the element's original XML +source and line. The built-in custom-widget adapter checks its widget factory through this hook. +`MachineScreen` and `ElementFragment` share this validation for every rendering pass. Fragments +validate before creating behaviors; their host can override `source(element)` for XML diagnostics +(the default is the host ID). `ElementRenderers` also validates loaded screens at load completion; +`WidgetRenderers` only freezes its own factory registrations. + +```java +// In client setup; MyElement remains a common-side model class. +ElementRenderers.GLOBAL.register(MyElement.class, (element, context) -> { + var r = context.bounds(); + context.graphics().fill(r.x(), r.y(), r.right(), r.bottom(), 0xff228855); +}); +``` + +There are four passes, each with independent registration and inheritance: + +| Registration | Draw order | Default clipping | +|---|---|---| +| `register` | Element backgrounds in tree order | Element bounds | +| `registerOverlay` | After all element backgrounds, for outlines | Element bounds | +| `registerForeground` | After vanilla container rendering, for popout bodies | Screen viewport | +| `registerTop` | Above foreground content, for tabs | Element bounds | + +Optional passes can be omitted. Pure layout containers explicitly register no-op backgrounds. +`register(pass, type, clip, renderer)` supports an explicit clip policy; side-config highlights, +for example, use viewport clipping because their target is another element. Each call balances +the registry's pose/scissor scope and resets drawing colour; renderers must balance any additional +pose/scissor changes of their own. `context.elapsedTicks()` counts game ticks since the screen's +first frame, including partial ticks, for local animation. Renderers may be shared across screens; +keep per-screen editing/open/scroll state in the appropriate client instance. + +Custom slots inherit shared slot rendering and the default vanilla adapter, or override +`createSlot` for their own adapter. Ghost behavior follows `ghost()`, with overridable +`selection`, `read` and `accept` hooks. XML-defined extensions continue to use the registered +`` / `WidgetTypes` API. + +Client behavior is registered independently from drawing: + +```java +ElementBehaviors.GLOBAL.register(MyElement.class, MyElementBehavior::new); +``` + +The factory receives `(element, ElementAccess)` and must create a fresh `ElementBehavior` for +each element in each open screen. Registration uses the same exact-type/most-specific-base +rules as renderers, including explicit `replace`. Keep focus, drafts, drag state and local +selection in that instance. Unregistered elements use `DefaultElementBehavior`, which handles +common interactions, recipe links and click callbacks. Extend it and override its protected +`click` hook to retain those defaults, or implement `ElementBehavior` directly for full control. + +`ElementInput` routes mouse, keyboard, focus, capture, visibility, resize and removal for every +element. Return `CAPTURE` to receive drag/release outside the element; hiding or resizing cancels +the capture without committing. `ElementAccess` supplies current bounds, synchronized values, +presentation services and typed action submission. It exposes no block entity or raw menu. +Navigation, inline side configuration, radio controls and slot behavior use this same registry. +Real inventory gestures still pass through vanilla slots. Existing `WidgetInstance` factories +are adapted to the same lifecycle, preserving the XML custom-widget API. + +Screens and fragments both call `ElementInput.click(x, y, button)` after updating visibility. +It checks the viewport and visits visible hits in reverse traversal order; `PASS` continues to +the next hit. Unhandled presses clear focus and permit native fallback. An existing capture +consumes further presses even outside the viewport until its release or cancellation. Hosts only +handle overlays first and native fallback afterward. Scrolling uses the same viewport predicate. + +Hover tracking also belongs to `ElementInput`. Screens and fragments select the last visible +element under the pointer in traversal order, matching their topmost tooltip target. A fragment +also requires the pointer to be inside its viewport. `hoverChanged(true/false)` runs only when the +target changes, with the previous target's leave before the next target's enter. Repeated frames +do not repeat callbacks; geometry, visibility and clipping changes recheck a stationary pointer. +Removal sends the final leave before disposing behaviors. Focus/capture cancellation alone does +not change hover, and a captured drag's release still belongs to its original control. + +Hosts sample `hover(x, y)` each frame and forward actual movement through `mouseMoved(x, y)`. +`ElementFragment.Host.hoverChanged` receives the same transitions; machine-backed hosts use +`ElementHover.dispatch` to supply the menu context for declarative `on-hover` callbacks. The filter +drawer provides this bridge. `ElementHover.preview` resolves `hover-shows` for both screens and +fragments, with fragment previews retaining viewport clipping. No additional registry is needed. + +Radio options share the `OptionSelection` parent behavior capability for drawing and input. +`selected(option)` supplies the selected state; `activate(option, button, modifiers)` owns the +action and returns its input result. `RadioGroupBehavior` resolves membership by element identity, +reads the synchronized group index and submits the group's ordinary click action. It does not +predict a selection before the server updates the value. An option's explicit click bindings +take precedence over parent activation. Mods can register a behavior implementing `OptionSelection` +for their own group types, using their own state and typed actions; radio rendering/input requires +no concrete parent class. Return `PASS` for unknown options and `CONSUME` for handled options, +including when a pending action prevents another submission. + +`ElementBehavior` contains input, lifecycle and tooltip hooks only. Specialized presentation is +optional: `ValueLabelProvider` supplies a tooltip value label, `SideConfigPresentation` supplies +the inline side control's selection/modes, and `WidgetRenderer` supplies custom-widget drawing. +Renderers request these through `context.access().capability(element, Contract.class)`. +Mods can define their own interfaces; a behavior implements them directly, or an adapter implements +`BehaviorCapabilities` to forward requests. The existing widget adapter forwards these contracts. + +Tooltips belong to the element. Override `Element.appendTooltip(TooltipContext, List)` +to supply its rows. The default calls `context.appendDefaults(rows)`, which includes server rows, +the authored tooltip (or Shift variant), value substitutions, the Shift hint and `valueTooltip`. +Call it before adding rows to retain those defaults, or omit it to replace them. Styled components +retain their formatting when split into multiple lines. + +Containers can override `appendChildTooltip` to add context when a descendant is hovered: + +```java +@Override +public void appendChildTooltip(TooltipContext context, List rows) { + rows.add(context.text(new Text("screen.example.input_group", Optional.of("Inputs")))); +} +``` + +Composition visits the hovered element first, then its parent, then each ancestor to the root. +At each level the element hook runs before the corresponding registered behavior hook. +`ElementBehavior.appendTooltip` and `appendChildTooltip` support client-local state; the default +own-tooltip hook also calls existing `tooltip(mouseX, mouseY)` overrides. A parent's ordinary +tooltip is **not** repeated automatically; `appendChildTooltip` does nothing unless overridden. + +`TooltipContext.target()` always identifies the hovered element; `element()` identifies the +current contributor, and `child()` is the immediate child along the path (empty at the target). +It exposes mouse coordinates, modifiers, synchronized values and translated text, without machine +storage. `serverLines()` returns the contributor's own server rows; a parent can append these +explicitly in its child hook. `appendDefaults` never invokes other hooks or walks parents. + +`ElementTooltips` composes the rows for machine screens, JEI recipe areas and embedded fragments. +The screen draws the result once. Filled native slots retain vanilla item details and append the +same composed rows; empty slots use the ordinary element tooltip. Server-dependent content still +comes from `Element.tooltip(ScreenHost, carried)`, sampled per viewer by `TooltipMenuComponent` +through the registered `mmdlib:tooltips` component field. Its sparse map permits up to 4096 element +entries and 4096 rows per entry, within a total 65536-byte encoded field bound. Fragments without +a server tooltip source return no server rows. Core screen protocol 20 requires matching versions. + +Group captions and highlighting use that same mechanism. A behavior can implement +`GroupLabelProvider.groupLabel()` to supply an optional `Text`, and +`HighlightPresentation.highlight(graphics, bounds, request)` to draw its own outline or another +shape. Register the behavior with `ElementBehaviors.GLOBAL` as above; the element need not inherit +from a built-in group. Both capabilities also work through `BehaviorCapabilities` adapters. + +Consumers call `ElementRenderAccess.groupLabel(element)` and +`highlight(graphics, element, ElementHighlight)`. `MachineScreen.presentation()` exposes the same +read-only services to drawers and integrations. Without a caption provider, the label falls back +to the element's common tooltip; an explicit empty caption remains empty. Without a highlight +provider, a rectangular outline uses the request's colour and outset. Highlights skip hidden +targets and run with viewport clipping and isolated pose/colour state. + +`ElementHighlight` distinguishes persistent `DECORATION`, `EMPHASIS` of an existing decoration, +and a control's `SELECTION` marker. Factory methods supply the usual zero/one-pixel outsets; +providers may reuse `request.outline(bounds)` or `request.render(graphics, bounds)`. +`GroupBehavior` supplies panel captions. `ColouredGroupBehavior` suppresses duplicate persistent +decoration, emphasizes its own coloured outline, and retains the requesting control's selection +style. The inline side-config renderer and side-config drawer depend only on these contracts; +they do not recognize concrete group classes or construct temporary group wrappers. + +Declare server actions in the common element's `actions()` list using `ElementAction`. +Its bounded codec and handler work like the `WidgetAction` example below, but the handler's +context contains any `Element`. Call `access.submit(action, payload)` from the client behavior; +`actionResult(accepted)` runs after authoritative values are applied. Include +`ElementActions.CLICK` when overriding `actions()` if common interactions/callbacks should +remain available. The menu owns player/session/distance/visibility checks, replay protection, +rate limits and acknowledgement transport; the element's handler validates its own payload and +performs its operation. Built-in navigation, back, side-mode cycling and ghost selection all +declare their actions this way. Existing widget factories and action APIs remain supported; +client and server must use matching network protocol versions. + +For additional synchronized data, implement `gui.sync.SynchronizedElement` and return stable, +ordered `ElementSync` declarations from `synchronizedValues()`. Each declaration owns its ID, +bounded `StreamCodec`, initial value and validation. For example: + +```java +public static final ElementSync SELECTED = ElementSync.field( + ResourceLocation.parse("example:selected"), + StreamCodec.of((buffer, value) -> buffer.writeVarInt(value), buffer -> buffer.readVarInt()), + 5, element -> 0) + .validate((element, value) -> value >= 0 && value < 3) + .affectsLayout() + .build(); + +public List> synchronizedValues() { return List.of(SELECTED); } +``` + +Without a sampler, a field retains per-menu state. A server action updates it through +`context.menu().elementStates().set(context.element(), SELECTED, selection)` after validating +the operation. Use `.sample((source, previous) -> ...)` for machine-derived data; it runs only +on the server and receives the element, machine and viewer. Ordinary `value`/`max` getters +keep their existing shared machine/tick cache. Renderers read +`context.access().state().get(element, SELECTED)`; behaviors use `access.presentation().state()`. + +`ElementStates` owns per-menu storage, wire addresses, byte comparisons and transport validation. +Values are serialized defensively; reads cannot mutate stored data. Batches validate before +application, and `affectsLayout()` invalidates layout/visibility once per changed batch. +`SelectiveContainer` lets a container choose visible children from this state, and +`NavigableElement` supplies optional named navigation. `ContextElement` owns selected-view state; +`SideConfigElement` owns its side-mode codec and server sampling. Shared snapshot/menu code +contains no context or side-mode serialization branches. + +Any element can declare navigation through `Optional navigation()`. For example, a +custom control can return `Optional.of(new Navigation(new ViewRef("panel", "settings")))`. +`ButtonElement.shows` supplies this same binding. `Element` validates its destination and includes +`Navigation.NAVIGATE` in its default actions; controls that override `actions()` must retain that +action when using navigation. The server derives the destination from the element definition, +not from a client-provided view name, and ordinary visibility/session/action checks still apply. + +The destination implements `NavigableElement`: `defaultSelection()`, `hasSelection(name)`, +`selected(state)` and `select(states, name)`. It owns its synchronized fields, selection rules and +layout. Selecting an already-selected target returns to its default selection. Both server +selection and client prediction use the same `Navigation` calculation, with no button/context +casts. `ContextElement` implements this contract for its views; mod destinations can use another +representation. + +`DefaultElementBehavior` handles navigation clicks, prediction and acknowledgment/timeout +correction for ordinary elements; no navigation-specific behavior registration is needed for a +plain custom control. Custom behaviors extending it should delegate unhandled clicks to +`super.click(...)` and acknowledgments to `super.actionResult(...)`. `ButtonBehavior` adds only +button tooltip labels. `NavigableBehavior` is registered for destination implementations and owns +remembered-view restoration; `ContextBehavior` adds its back arrow and Escape behavior. +`NavigationMemory` restores through a visible control declaring the matching binding and typed +action, waits for server confirmation, and never edits destination state directly during restore. +Custom lifecycle implementations can reuse these services. `ButtonElement.NAVIGATE` is an alias +of the shared action for existing callers; the wire ID and click payload remain unchanged. + +Limits are 16 fields per element, 64 KiB per field, and 256 KiB including envelope accounting +per screen. Declaration IDs, order, bounds and layout flags must match on both sides and enter +the screen fingerprint. Field codecs and semantics must also match between mod versions. +Typed state travels in ordinary deltas and action acknowledgments, including rejections, before +`actionResult`. Local prediction never authorizes a server mutation; timeouts restore the last +confirmed typed state. Local-only focus, hover and selection need no synchronized field. + +Element definitions also own their load-time contracts: `validate()` checks local configuration, +`validateScreen(ScreenValidationContext)` checks references and shared constraints, and +`valueKinds(widgets)` / `maximumKinds()` constrain getter return types. The context exposes +`screen()` and `unique(domain, key, duplicateMessage)`: namespaced claims let built-in and mod +elements reject repeated bindings without adding branches to shared validation. Claims live only +for one validation pass. `SlotElement` claims inventory/local-index pairs; `PlayerSlotsElement` +owns section uniqueness and screen-inventory checks. Player-backed `SlotElement`s claim player indices. +`ValueContract.require` checks required +value/max bindings. These contracts apply to both XML and Java-built screens, with source/line +diagnostics. `ScreenValidation` keeps common tree checks such as duplicate IDs and fill/spacer +relationships; `ScreenBinder` resolves getters against the element's declared kinds. + +Runtime host validation calls `validateHost(ScreenValidationContext, ScreenHost)` with a fresh +context before constructing slots. Slots validate their named storage bounds through this hook; +custom elements can validate host services and claim bindings through the same path. The default +contextual hooks delegate to existing `validateScreen(Screen)` and `validateHost(ScreenHost)` +overrides. When overriding a contextual hook, call the inherited implementation to retain those +checks (including shared slot rules when implementing `SlotElement`). + +`InventoryElement` contains `ItemSlotElement` and `GhostSlotElement` children. Its `storage` +name selects an independent handler from `ScreenHost.screenInventories()`; child indexes are +local to that handler. For example, both inventories below have their own slot zero: + +```xml + + + + + + + +``` + +`SlotElement.Appearance` owns the shared background sprite, tint, empty icon, icon atlas, +item size (4–32 pixels, default 16), and ghost opacity (0–1, default 0.42). XML uses +`background`, `tint`, `icon`, `icon-atlas`, `item-size`, and `ghost-alpha`. Without an atlas, +icons are GUI sprites; equipment slots use vanilla's block atlas. Real slots can show a +remembered ghost through `screenSlotHint(inventory, slot)` when empty. Editable ghost slots +use `screenGhost(inventory, slot)` and `setScreenGhost(player, inventory, slot, filter)`; +left-click copies one carried item, right-click clears, and neither consumes an item. + +The same classes provide player equipment and addon slots. `InventoryElement` owns grid +placement and menu assembly; `ItemSlotElement` owns machine adapters and reservation-aware +clicks, while `PlayerSlotElement` owns equipment rules; `SlotElement` owns ghost and reservation presentation. Client +renderers supply drawing primitives. The menu retains vanilla coordination, synchronization, +packet checks and routing between inventories. + +Returning `ItemInventories` from `screenItems()` exposes its independent handlers automatically. +Custom hosts can override `screenInventories()`. Use named `ItemInventories.Address` values in +`screenItemDestinations(stack)` and `screenRecipeItems(category)` for shift-click and JEI routing. +JEI both checks eligibility and resolves inputs through `screenRecipeItems`; a host implementing +that named API does not need to implement the legacy `screenRecipeSlots` method. +Hosts may override `screenInventoryBinding(inventory, index, standalone)` for custom wrappers or +logical-to-physical mappings. It returns an `InventoryBinding` containing the physical handler, +physical slot index, legacy index (`-1` for none), and named address aliases. Validation uses this +same hook; logical XML indexes need not equal physical indexes. Keep handler identities stable: +assembly uses handler identity/local index to reject duplicate physical slots and share client +mirrors. Different slots cannot claim the same named address. Default resolution preserves +`ItemInventories` and `ItemStore` behavior; assembly does not inspect those storage types. + +Custom containers can implement `gui.model.InventoryScope` and return their storage name from +`inventory()`. `InventoryElement` implements this same contract. `InventoryScope.owner(screen, element)` +finds the nearest enclosing scope by element identity, through ordinary layout wrappers; nested +scopes override outer scopes. Body, tabs and implicit sections all participate. `InventoryScope.name` +returns the scoped name, or `items` for legacy unscoped slots. Binding, validation, group associations +and generated-machine highlight compilation use this lookup. These helpers replace the former +`InventoryElement.parent`/static `inventory` helpers. Keep names and children stable on both sides; +custom containers still own their normal layout, rendering and XML registration. + +`SlotElement.hint(host, inventory)` supplies the server-sampled remembered item shown in an empty +slot. The default calls `host.screenSlotHint` for machine slots and returns empty for player slots +or an absent host. Mods may override it for either kind of slot; the host argument can be null. +Player slots receive the name `player`. Every registered native slot with an element participates, +including contributed drawer slots outside the screen tree. Contributors can pass a logical inventory +name to `MenuSlots.Registrar.add(slot, element, role, inventory)`; the three-argument overload defaults +to `player` for player-backed elements and `items` otherwise. Sampling and lookup copy the stacks. +The existing `HINTS` field synchronizes native menu indexes (schema version 2, up to 32,767 slots, +within the existing 64 KiB field limit). Complete empty snapshots clear old hints. Schema versions +participate in the menu fingerprint, so both sides must match. No new registry is needed. +`AutomationSlotElement` defaults to no hint; override `hint` to customize addon/filter previews. +The side configuration drawer uses the same synchronized hints. Hint items never become stored contents. + +`screenLegacySlotCount()` reserves the legacy numbering range (default: `screenItems().getSlots()`). +Bindings with legacy indexes retain those menu indexes; assembly resolves all bindings before +allocating other slots, so later legacy indexes cannot collide with earlier automatic allocations. +`screenSlotAddresses(inventory, index, standalone)` supplies default logical aliases and aliases +for ghost slots, which do not have physical bindings. Override the binding's returned addresses +for custom real-slot routing. Both menu sides must produce matching addresses and numbering. + +`SlotElement.role()` supplies the native transfer role for both machine and player slots. +`MACHINE` remains the default for machine storage; inactive numbering placeholders retain it too. +Roles supply the defaults for reservation participation and shift-click routing. Override +`reservationOverlay(entry)` to choose whether the native bridge calls the element's overlay drawing; +`renderOverlay` owns its contents. Override `reservationSync(entry)` independently to select slots +whose plans participate in synchronization. Hiding an overlay does not disable reservation rules. +Synchronization requires a `SlotAdapter`; a plain native slot can still opt into custom overlay +rendering. Both hooks default to machine roles (drawing also requires a `SlotAdapter`). Keep sync +eligibility stable and consistent on both menu sides. + +Override `quickMoveTargets(SlotTransferContext)` to supply ordered native destination slots, or +return an empty list to refuse shift-click routing. The context exposes the source entry, all entries +in native registration order, a defensive stack copy, and lazy `machineDestinations()` from the +registered menu contributors. Default routing sends player storage to those destinations; other +sources target player storage in reverse registration order. Native contributions without an element +retain that same default policy. Treat the context's native slots as read-only when choosing targets. + +`MenuSlots` filters foreign, self, inactive and incompatible targets, removes duplicates by identity, +and merges existing stacks before filling empty slots, preserving the supplied order within each +pass. Source pickup checks and adapter-specific reserved extraction remain enforced by the shared +executor. These hooks require no new registry and retain the existing adapters and renderers. + +Legacy standalone `` elements and integer routing methods still use the combined `items` +view by default. `menu.slot(element)` and `menu.slotIndex(element)` resolve a local UI slot to its native +menu slot; a local index is not necessarily a menu index. + Implement `gui.runtime.ScreenHost` on the block entity. `screenItems()` exposes its player-facing modifiable inventory (`IItemHandlerModifiable`); using `ItemStorage` preserves output restrictions and item filters. The menu adds real slots by the XML indexes, then the optional player inventory. Upgrade slots additionally limit @@ -71,6 +674,22 @@ contents don't need to be synced. Return the `ItemStorage` itself, or a handler/wrapper implementing `ReservedSlots`, including `plan(slot)`, so both menu sides receive and enforce the same reservations. +`gui.runtime.slot.ReservationMenuComponent` owns reservation synchronization. It is registered as +`BuiltinMenuComponents.RESERVATIONS` with the typed `PLANS` field (`mmdlib:reservation_plans`). +The field contains a complete map from native menu slot index to `SlotPlan`, sampled from eligible +slots implementing `SlotAdapter`, including slots contributed by mods. By default, only machine +roles participate; elements can override `reservationSync(entry)` to opt other roles in or out. +Contributions without an element retain the machine-role default. Hidden underlying storage is not +sampled. Overlay visibility is independent of synchronization eligibility. + +The slot subsystem owns the bounded codec (64 KiB per snapshot), including input counts and output +item components. Registry allocation supplies the field's transport address; native slot indexes +are only keys inside its payload. Equal plan contents produce no delta. On a client update, +`SlotAdapter.applyPlan` refreshes eligible slots; `SlotPlan.NONE` and omitted entries clear previous +plans and insertion predictions. Server sampling never calls client adapters. `MachineMenu` and +`ScreenValues` no longer allocate reservation keys or serialize reservation-specific values. +Use the normal component snapshot path; `PLAN_KEYS` and `reservationSnapshot()` are removed. + When the machine has reserved items in a slot (see [capabilities](capabilities.md#reservations)), the menu keeps them from being taken or replaced: @@ -80,15 +699,15 @@ the menu keeps them from being taken or replaced: - Double-clicking a slot with a plan doesn't collect from elsewhere. Double-clicking another slot still takes the planned slot's free part. -Machine slots show reservations at the top right. Both reservation and held quantities use +By default, machine slots show reservations at the top right. Both reservation and held quantities use 87.5% of vanilla's font size: `-2` means two items reserved for input, and `+1` means space reserved for one output. The held quantity stays at the bottom right, using the same size even when no reservation is present. -An empty planned output shows its item model at 50% +An empty planned output shows its item model at the slot's configured ghost opacity with `+1` at the top, without creating a real stack. Mixed consume/produce plans show both signed counts on the top row. Tooltips provide reservation counts and the planned output's name. Clearing the plan removes the badge and ghost. Player inventory, equipment and filter -slots are unaffected. +slots are unaffected by default; custom elements can opt into overlays using the hooks above. Chance outputs show their reserved capacity, not a guarantee that the product will be rolled. No extra packet or XML binding is needed; `MachineMenu.reservationPlan(index)` exposes the existing synchronized snapshot for real machine slots. @@ -98,18 +717,43 @@ values, so clicks don't flicker. Bars and numbers still come from the getters na feature's own client sync (see [block entities](block-entities.md#machine-features)) doesn't replace them. -`screenDestinations` controls shift-click routing into the machine; its default tries all storage -indexes, subject to their filters. `screenRecipeSlots` explicitly names recipe inputs in recipe -order; its default disables JEI transfer. Both methods must work on the client as well as the -server. JEI uses its standard transfer handler with these slots, including its server validation. +`screenItemDestinations` controls shift-click routing into the machine. `screenRecipeItems` names +recipe inputs in recipe order; an empty result disables JEI transfer. The named methods adapt the +legacy integer methods `screenDestinations` and `screenRecipeSlots` for hosts with combined storage. +Routing methods must work on the client as well as the server. JEI uses its standard transfer +handler with these slots, including its server validation. Only visible normal slots participate in recipe transfer. Category IDs must also occur in the screen's `jei-recipes` attributes. Ghost filters are separate from the real inventory. Override `setScreenGhost(player, index, filter)` to validate and persist a filter, and `screenGhost(index)` to expose its authoritative value. The -menu sends at most one item as the filter; an empty stack clears it. Defaults reject changes and +ghost element normalizes the filter to one item; an empty stack clears it. Defaults reject changes and show empty filters. No real items move through this path. +By default, ghost clicks and JEI ingredient drops submit `GhostSlotElement.SELECT_GHOST` through +the typed element-action channel. `SlotElement.selectionAction()` selects the `ElementAction`; +return empty to opt out. The default `actions()` declares that action automatically. If overriding +`actions()` too, include the same action instance: the behavior checks declaration identity before +submission. `acceptsSelection(button, stack)` controls client eligibility, and `selection(button, +carried)` prepares the payload. Both receive detached stacks. The action handler remains responsible +for authoritative server validation. The default ghost `accept(host, inventory, player, stack)` calls +`selection(0, stack)` on the original slot element again before writing to the host; overrides work +for direct `SlotElement` implementations and subclasses. Make normalization idempotent because it +runs during client preparation and server acceptance. The default keeps one item, or clears an empty +selection. Override `accept` for different authoritative rules. Submitted and stored stacks are copied; +`read(host, inventory)` returns a detached ghost value. + +`selectionIngredient(typeId, ingredient)` converts a detached viewer ingredient into an optional +item selection. The default accepts `mmdlib:item`; mods can recognize other registered ingredient +IDs and convert them without changing the shared behavior. Unsupported inputs return empty. +`SlotBehavior` rechecks action declaration, eligibility and pending state when accepting a drop. + `MachineScreen.submitElementAction` supplies the shared client +submission path, including pending-request handling. The server dispatcher checks menu/session, +request order, reach and visibility before decoding. The ghost action delegates to the inventory +component; `InventoryElement.Session` resolves the named inventory and rejects inactive, +unregistered or non-ghost slots. The old `ScreenNetwork.Ghost` packet and `MachineMenu.ghost` +entry point are removed. Core screen protocol 20 requires matching client/server versions. + Bind the block's `block-right-click` event to `mmdlib:open_menu`. This handler first looks for an XML screen matching the clicked block entity's type; otherwise it uses the block's own vanilla menu provider. It consumes matching clicks on both sides and opens only on the server. A click @@ -158,11 +802,398 @@ For a side-configuration-only drawer, return `false` from ends below its face controls. Combine this with `inventory="none"` to omit player inventory and equipment drawers, as in the companion Creative Power Source. +## Client drawer classes + +`gui.client.Drawer` is the public base for client drawers. Its constructor takes the owning +screen, a registered `DrawerDefinition`, an `IconSprite` and an RGB colour. Subclasses choose +their appearance and own their content layout, input, tooltips and local state. `IconSprite.gui` +uses a GUI sprite ID; `atlas` uses an atlas sprite and `item` preserves a live item model. A custom +icon can also implement the drawing callback. The base draws the shared tab/frame. + +The built-ins are `SideConfigDrawer` (previously `AutomationDrawer`), `RedstoneDrawer`, +`StatsDrawer`, `ContainerDrawer` (backpack or Curios equipment), and `PopoutDrawer` (XML info, +warning and error popouts). Each interprets and restores its own `Drawer.Selection` preference. +`DrawerMemory` only stores connection-scoped preferences and retries restoration. Server-owned +inventory, configuration and acknowledgments remain in their menu/network classes. + +All drawers come from registrations, including built-ins and XML popouts. Register a common-side +provider during mod construction and its client factory during client setup: + +```java +// Common setup; use immutable definition/feature presence, not live storage values. +var type = ResourceLocation.parse("example:diagnostics"); +DrawerRegistry.GLOBAL.register(type, context -> List.of( + new DrawerDefinition(type, "example:diagnostics", + DrawerPlacement.top(Edge.RIGHT, 15)))); + +// Client setup only. The constructor calls super(screen, definition, icon, colour). +DrawerFactories.GLOBAL.register(type, DiagnosticsDrawer::new); +``` + +`gui.drawer.DrawerRegistry` providers return zero or more definitions for a screen/host. Definitions +declare stable IDs, edge, top/bottom anchor, order, tab dimensions, overlap, gap, optional model +element and inventory-toolbar width. Smaller order values sit closest to their anchor. Built-in +top orders are side configuration 0, redstone 10, XML side tabs 20, XML popouts 30 and statistics 40. +Backpack and Curios tabs use bottom orders 0 and 10. Equal orders use provider ID order, then the +provider's list order, so mod loading order does not affect geometry. IDs must be unique per edge. + +`DrawerTabs` packs tabs generically, and `ScreenLayout` reserves the required panel height and +toolbar space. Top tab positions stay reserved when hidden, keeping XML/vanilla slot geometry +stable; unavailable bottom drawers collapse out of the stack. Bottom tabs respect viewport and +overlay insets while keeping clear of the top stack. A viewport too short to fit the stack needs +a larger window or smaller GUI scale. Drawers use the inherited `tabBounds()` for their allocated +position and own the body geometry. Inventory drawers expose `contentLayout()` for integrations; +the content-layout helper accepts an allocated tab instead of guessing where other drawers sit. + +Every interactive definition needs a factory returning a fresh drawer for that screen. Duplicate +registrations fail; `replace` explicitly replaces an existing provider/factory. Missing factories, +duplicate identities and unclaimed model tabs fail explicitly. A layout-only definition can +place a persistent side tab without creating an interactive drawer. Mods can associate any custom +element with a tab by registering a provider; neither layout nor dispatch needs a new type branch. +Keep registrations and provider results stable while menus are open. Common definitions enter +the client/server opening fingerprint; client factories never load on a dedicated server. + +Override `open` and `close`, then whichever render/input/lifecycle hooks the drawer needs. +Use `drawFrame` for shared framing and override `drawIcon` for badges. The screen forwards events +through `Drawers`. Bounds returned by `exclusions` are also reserved for recipe-browser overlays. +Direct `screen.drawers().add(...)` has been replaced by the registry path above. + +Drawer bodies follow the definition's edge as well as its tab. `SideDrawerLayout.bodyX` and +`room` provide shared horizontal geometry; inventory bodies also check available viewport space. +Server-controlled drawers implement `gui.runtime.component.DrawerSession` on their menu component. +After authorizing an open request, call `menu.components().activate(this)`: registered sessions on +the same edge close through `closeServer()`. Sessions identify their registered drawer type, which +must occupy only one edge. Controllers do not name sibling drawers. + +`gui.runtime.MenuRequests` provides optional bookkeeping primitives for custom menu controllers, +without another registry or a shared payload protocol. Keep one `Client(timeoutTicks)` and one +`Server(perTick)` per controller/session, accessed on the game thread. `Client.begin()` returns a +positive request ID, or zero while pending/after sequence exhaustion. IDs never wrap or get reused. +`tick()` releases an expired request and reports the timeout; the controller chooses recovery. +Use `acknowledgeExact` for exact replies or `acknowledgeThrough` for cumulative snapshots (which +also advance the local sequence). Apply revision/session validation before acknowledging. +On the server, authorize first, then `receive(request)` rejects replays and consumes fresh IDs. +Call `allow(gameTime)` once per fresh request to apply the allowance. Feature rejection and rate +limiting still consume the request ID, allowing authoritative replies to acknowledge it. + +The built-in controllers retain their own policies: + +| Controller | Timeout | Recovery | Acknowledgment | Requests per tick | +|---|---:|---|---|---:| +| Automation | 100 ticks | Unlock | Exact | 8 | +| Redstone | 100 ticks | Send sync | Cumulative | 16 | +| Equipped containers | 20 ticks | Send sync, retain inventory lock | Cumulative | 32 | + +Controllers still own revisions, attachment generations, inventory locks, action validation, +state application and packet transport. These primitives do not replace `ElementActionSession`, +which supports multiple independently pending element actions and typed state correction. + +Local drawers call `openLocal()` before opening to close siblings. Each drawer decides whether +pending requests should delay opening: Statistics waits, while Info remains accessible during +a pending item transfer. Override `closeLocal()` to close local presentation when a server-controlled +sibling activates. Server-controlled drawers call `activated()` after accepting an open request; +their server handles mutually exclusive authoritative state. Use `remember(open, selection)` for +accepted user intent, and override `selection`/`restore` for preference persistence. Returning +`false` from `restore` waits for another snapshot or acknowledgment; `pending()` protects an +unconfirmed preference when the screen closes. `close()` itself should not record user intent +because it is also used when another drawer opens. + +`PopoutElement.colour()` supplies the RGB tab/body colour, and +`content(value, resolveText)` supplies ordered, unwrapped `Component` paragraphs. Empty components +represent blank rows. The defaults retain info/warning/error colours, translated fixed-text +emphasis and styled live text/list entries. Subclasses can override both hooks (and `valueKinds` +for other synchronized value types) while inheriting the existing drawer and renderers; no new +registration is needed. `PopoutDrawer` only wraps the supplied content and owns placement, +scrolling and open/close behavior. + +XML popouts still draw through `ElementRenderers`, whose registered adapter delegates to +`PopoutDrawer`. The generic `drawerPresentation(element, Contract.class)` bridge lets a drawer +expose read-only presentation to its element renderer. Mods can replace drawing passes as before. +`MachineScreen` holds no +drawer-specific open, scroll, layout or restoration logic. + +`ElementFragment.Host.state()` supplies the state used by renderers, behaviors and layout. +`Host.navigate(target, selection)` applies host-owned navigation/prediction, while `Host.submit` +retains action transport and authorization. The fragment forwards only declared actions from +visible, non-pending elements. After applying an authoritative result, call +`fragment.actionResult(element, accepted)` to notify its behavior. + +Fragments run element layout preparation and use `SelectiveContainer.childVisible` for every +ancestor, composed with `Host.visible(element)` and viewport clipping. Rendering, input and +tooltips share this visibility. State changes remeasure the tree and cancel focus/capture when +an element becomes hidden; unchanged bounds preserve interaction. `refresh()` reconciles state +explicitly; drawing, input, ticking, placement and height queries also refresh automatically. +Behavior initialization occurs on first placement, when the viewport is known. A fragment keeps +its element identities and behavior instances; replace it if the actual tree changes. + +Item-provided filter editors also use the element registries. `ElementFragment` hosts an independent +element tree inside a clipped viewport with the shared focus/capture lifecycle. Its host supplies +values and optional behavior services through `ElementAccess.service`; rendering access stays +read-only. See [filter panels](inventory-automation.md#item-provided-filter-panels) for control +registration, validation and the existing typed-widget adapter. + +### Registered network contributors + +`gui.runtime.network.NetworkRegistry.GLOBAL` collects feature-owned play payloads. Register on +both logical sides during mod construction, before NeoForge's payload-registration event: + +```java +NetworkRegistry.GLOBAL.register(ResourceLocation.parse("example:diagnostics"), "1", network -> { + network.playToServer(DiagnosticRequest.TYPE, DiagnosticRequest.CODEC, DiagnosticsNetwork::request); + network.playToClient(DiagnosticReply.TYPE, DiagnosticReply.CODEC, DiagnosticsNetwork::reply); +}); +``` + +The contributor supplies its protocol version; each payload supplies its NeoForge type, bounded +stream codec and handler. The callback runs immediately and its registrar closes when it returns. +Contributor IDs and payload IDs must be unique across this registry, including payloads declared +in opposite directions. A failed declaration registers nothing. Startup freezes the registry and +installs contributors in ID order; registering after that is an error. + +`BuiltinNetworks` declares the built-in contributors through the same API. `ScreenNetwork`, +`AutomationNetwork`, `RedstoneNetwork` and `ContainerDrawerNetwork` each own their version, +payloads, codecs and handlers. `ScreenRuntime` only installs the registry; the generic screen +network has no list of feature networks. NeoForge continues to negotiate versions and dispatch +payloads on its normal handler thread. Handlers remain responsible for menu/session identity, +authorization, revisions and rate limits. Each feature declares its own version; the core screen +protocol is 19, and automation, redstone and container drawers remain at version 16. + +### Registered menu components and native slots + +`gui.runtime.component.MenuComponentRegistry.GLOBAL` creates fresh state for each menu. Register +on both logical sides during mod construction, before opening screens: + +```java +public static final MenuComponentKey DIAGNOSTICS = + new MenuComponentKey<>(ResourceLocation.parse("example:diagnostics"), DiagnosticsMenu.class); + +MenuComponentRegistry.GLOBAL.register(DIAGNOSTICS, 50, context -> + supportsDiagnostics(context.host()) ? new DiagnosticsMenu(context) : null); + +var diagnostics = menu.components().get(DIAGNOSTICS); // null when unavailable +``` + +Factories receive the menu, player inventory, optional host and logical side. They construct their +own state; other components become available after all factories finish. A factory may return +`null` when its feature is absent. Registration order is the numeric order followed by ID; duplicate +IDs fail, and `replace` deliberately replaces an existing typed registration. Keep availability +and slot ordering identical on client and server. Registration IDs, types, order and synchronized +field schemas enter the menu opening fingerprint (core protocol 20). Keep factories and drawer declarations consistent when disabling +a built-in feature. + +`MenuComponent` receives client ticks, before/after interaction, before/after native broadcast and +removal hooks. Optional contracts supply additional behavior: + +| Contract | Responsibility | +|---|---| +| `MenuSlotContributor` | Add native slots, position them and supply ordered shift-click destinations | +| `MenuSnapshot` | Sample registered typed fields and receive validated state changes | +| `DrawerSession` | Close server state when another registered session on the same edge opens | +| `InventoryAccess` | Resolve legacy/named item addresses and handle inventory hints/ghost edits | + +Declare synchronized fields alongside the component registration. Each `ComponentSync` provides +a namespaced ID, schema version, `StreamCodec`, byte limit, initial-value +supplier and optional validator: + +```java +public static final ComponentSync COUNT = new ComponentSync<>( + ResourceLocation.parse("example:count"), 1, + StreamCodec.of((buffer, value) -> buffer.writeVarInt(value), buffer -> buffer.readVarInt()), + 5, () -> 0, value -> value >= 0); + +MenuComponentRegistry.GLOBAL.register(DIAGNOSTICS, 50, context -> + supportsDiagnostics(context.host()) ? new DiagnosticsMenu(context) : null, COUNT); + +// On DiagnosticsMenu implements MenuSnapshot: +@Override public void snapshot(ComponentSnapshot values) { + values.put(COUNT, sampleServerCount()); +} + +int displayedCount = menu.components().state(diagnostics, COUNT); +``` + +The registration owns its field namespace: two components may use the same field ID without +colliding. Fields receive stable transport addresses ordered by component ID and field ID; omitted +components retain their reserved addresses. Components never choose integer wire keys. A writer +accepts only the exact field definitions declared for its component, once per sampling pass. +`replace` replaces the field declarations too; supply the replacement's complete field list. +Bump a field's schema version when its codec or meaning changes, so incompatible peers fail the +opening fingerprint check. + +Each field is limited to 64 KiB, with a 256 KiB encoded-state packet budget shared with element +state. A registry may declare at most 4,096 component fields. Codecs must also bound collection +counts before allocating or reading them. Updates validate the complete component batch before +committing it; reads decode independent copies. Optional `applied(ComponentStateView values)` runs +once per changed component after all values commit, on both server sampling and client updates. +It can cache decoded presentation data. Initial values come from the supplier without a callback. +Inventory hints, statistics and reservation plans use this same registered path. + +`MenuComponent.presentationRoots()` supplies stable per-menu element trees outside the XML screen. +Return matching trees in the same order on client and server. Tooltip synchronization includes those +roots and descendants, followed by any remaining contributed native slot elements, deduplicated by +identity. The existing `TOOLTIPS.ROWS` field uses schema v2 and permits up to 32,767 element addresses +within its existing 64 KiB budget; both sides must have matching schemas. This is a presentation/ +tooltip contribution, not an automatic declaration of element actions or synchronized element fields. +`ElementFragment.Host` exposes `contents`, `hint`, `reservation` and `serverTooltip` for such hosts; +fragments never sample server storage themselves. No additional registry is required. + + +All contributors run `addSlots` before any run `finishSlots`; the built-in inventory component +adds player slots in the second phase. Use `MenuSlots.Registrar` so the menu knows each slot's +element (optional) and role. With the default routing policy, `PLAYER_STORAGE` receives extraction +from machine, auxiliary and equipment slots; insertion from player storage uses contributor destinations, ordered by +`transferPriority`, topping up matching stacks before empty slots. The native adapter still checks +activity, pickup and placement permissions. Storage lookups follow actual menu indexes even if a +mod contributes slots before the built-in inventory. + +Built-ins use these same contracts. Access them through `BuiltinMenuComponents` keys instead of +the removed `menu.automation`, `menu.redstone`, `menu.containerDrawer` and `menu.curiosDrawer` fields. +Statistics use `menu.components().require(BuiltinMenuComponents.STATS).pages()`: each page supplies +a stable timescale ID, selector label and formatted rows. + +`gui.runtime.slot.MenuSlots` owns native assembly and transfer execution; slot elements choose routing targets. Optional `SlotAdapter` +hooks own reservation plans, interaction cleanup and specialized extraction. Item slot adapters +extract only the available portion of a reserved stack. `NativeSlotRenderer` adapts native drawing, +scaling, highlights, hit bounds and item-tooltip routing to `SlotElement`; ghost and reservation +presentation still comes from the element. For filled slots, `SlotElement.nativeTooltip(stack, vanilla)` +selects the native item rows: retain, edit, replace, or return an empty list to suppress them. Its stack +and row components are detached copies; copy the list before adding/removing entries. The bridge then +appends the element's composed tooltip, including all ancestor rows, so replacing native rows never +removes parent context. The default returns vanilla rows unchanged. Empty slots use the ordinary +element tooltip. The bridge checks the registered +native slot's element identity, so unrelated hovered elements retain their own tooltips. +`MachineScreen` only forwards vanilla callbacks to that adapter. + +Drawers can claim an occupied native slot's tooltip through `Drawer.nativeTooltip(slot, vanilla, +mouseX, mouseY)`. Return empty to leave it to the screen, or composed rows when the drawer owns it. +`SideConfigDrawer` uses the same native bridge and an `ElementFragment` rooted in the selected +`AutomationInventoryElement`. Registered slot and inventory renderers run in all four passes, and +registered behaviors contribute tooltip rows. The fragment supplies copied contents/hints, +reservations and synchronized server rows. Empty slots use ordinary element/parent composition; +occupied slots apply `nativeTooltip` first and preserve all composed parent rows afterward. +The default compatible-addon caption belongs to `AutomationInventoryElement.appendChildTooltip`. +In this fragment, a slot's value is its copied contents and the inventory's value is its group-name +component; custom tooltip hooks can read them through `TooltipContext.value(element, false)`. +Override that hook to customize inventory context; each slot still controls its own rows. +`drawerSlotBounds` also drives the inventory's fragment layout, keeping native and rendered positions +aligned. Hidden filter slots are excluded, and closing/switching the drawer removes old behaviors. + +Backpack and equipment pages use `ContainerInventoryElement` with `ContainerSlotElement` children. +`ContainerDrawer.createInventory(Page)` is a protected factory: subclass the drawer through the existing +`DrawerFactories.replace` API to supply your inventory and slot subclasses. The inventory accepts an +index-to-slot factory, preserving absolute container indexes; it owns `cellBounds`, label/header bounds, +and `drawerValue`. The page supplies detached provider metadata and body-relative cell rectangles. +These virtual cells do not create vanilla slots or change container request validation. +`SlotElement.cellHitBounds(frame)` supplies the same geometry to hover highlighting, clicks, +drag-target selection and tooltips. Its default applies `itemPosition`, `hitBounds` and vanilla's +one-pixel tolerance; an override returns the final bounds. The drawer viewport still clips hits. + +`ContainerSlotElement` inherits slot appearance, item positioning, native bounds, rendering and +`nativeTooltip` hooks. Its registered renderer draws the frame, empty icon, real item/decorations, +placeholder and selection overlay. Register a renderer for your subclass to replace those defaults; +all four passes use the existing element registry. `Display` values hold copied contents and visual +drag selection; maximum values default to null. Slot and inventory tooltip hooks run through +`ElementFragment`. Filled cells prepend the slot's `nativeTooltip` rows, then append element and all +parent rows, including behavior contributions. Suppressing native rows preserves parent context. +Item tooltip images and NeoForge item-tooltip context are retained. + +`ContainerSlotElement.description()` supplies the default availability, label, placeholder, tooltip +rows and empty icon. Override it to replace the provider description; an explicitly supplied +`Appearance` retains its own icon. Each individual presentation hook remains overridable. +`SlotElement.cellLabel()` supplies an optional label; `ContainerInventoryElement.cellLabel(slot)` +and `headings()` can customize labels and headings without replacing the renderer. + +Container fragments route hover, clicks, scrolling, keyboard/text input, key releases and pointer +capture through the existing `ElementBehaviors` registrations. A consumed click suppresses the +container gesture; `PASS` leaves the validated drawer fallback in charge. Cell hits use +`cellHitBounds` for both routes, clipped to the drawer body. Drawer controls retain click priority. +Capture receives dragging/releases outside the body. Closing, replacing a page, resizing or receiving +a new revision cancels interaction; pending locks cancel focus/capture and block input independently +of visible contents/tooltips. A cancelled press's release is consumed without committing. +`ElementFragment.Host.hitBounds` and `inputEnabled` expose the same geometry/lock policies to other +fragment hosts. Virtual cells do not register server actions: custom local behaviours cannot bypass +the existing item-transfer protocol or its cursor, session, revision and gesture validation. + +Equipment providers supply `EquippedContainers.Source.presentation` and `selectionKey` separately +from the live capability's topology key. `Presentation` owns title, icon, colour, column headings and +header/tab instructions; `SlotInfo` owns labels, tooltip rows and unavailable placeholders. Components +are copied on construction and access. The Curios adapter supplies its own translations, fallback +labels and sprites; the generic drawer does not derive them from Curios identifiers or cosmetic flags. +The three-argument `Source` constructor retains the live item name and standard container presentation. +Container snapshots now use protocol 19, with bounded text, row counts, policy settings and gesture instructions. + +Each source accepts a `ContainerPolicy`. Construct it with `Settings`, or subclass it and override +`settings()` for provider-dependent choices. `STORAGE` and `EQUIPMENT` are the default presets. +Settings supply body width, columns, row limits, cell placement/stride/size, footer space, initial +page size, alignment, allowed gestures and source-item protection. The server samples settings into +`Presentation.policy`; the client consumes that snapshot. Policy changes advance the interaction +revision and realign an incompatible page. The source's policy is authoritative; changing client +presentation cannot enable a forbidden action. Curios explicitly supplies the equipment preset. +Slot descriptions work with either drawer identity. + +`ContainerPolicy.instructions(Settings)` receives the same sampled settings and supplies copied, +bounded gesture rows. Defaults independently describe enabled distribution, collection and hotbar +swapping, so partial policies still show their supported actions. Override this method for mod text; +`Presentation.gestureRows()` carries the result to the client. A drawer subclass may override +`gestureInstructions()` for client presentation. Instructions never authorize gestures. + +`ContainerDrawerLayout` lays out the supplied settings; the boolean convenience overloads only select +a preset. Inventory header/label bounds follow the policy's cell positions and remain overridable. +Mod layouts must fit the bounded page size and preserve their declared page alignment. + +Container-to-player gestures use `MachineScreen.nativeSlotAt`, including custom hit bounds and vanilla +tolerance. Pending inventory guards use the geometry-only overload to block slots even while their +native interaction is disabled. This never authorizes a transfer. Cursor, source, revision, page, +request and drag validation remain independent of presentation overrides. `ContainerDrawerMenu.targets()` +provides the menu-owned `ContainerTargets` context for registered player hits, target encoding and +copied stack lookup. Server distribution uses its live-source resolver. The ordinary 36-slot player +storage range, hotbar range and wire limits remain validation constraints in the runtime. + +Override `AutomationInventoryElement.slotVisible(slot, Presentation)` to select visible slots. +The read-only context supplies copied group labels and local-slot contents, plus filter support. +Both native activation and fragment rendering use this policy; filter-row height and controls follow +it too. Open/selected-group checks remain in the menu. Pending requests independently disable native +interaction, so slots do not disappear while a request is in flight. Visibility does not override +storage acceptance or server authorization. Keep the policy consistent on both menu sides. + +Override `drawerValue(element, maximum, Presentation)` for renderer and tooltip values, including +maximums. Defaults expose copied slot contents or the group label and return null for maximums. +The drawer delegates without selecting values by element type. Render contents and hints remain +separate services, so custom display values do not change the actual inventory. + +`MachineScreen.nativeSlotAt(x, y)` resolves the first active native slot in screen coordinates using +its overridable `hitBounds`, vanilla's one-pixel tolerance and menu order. Drawer click fallback and +native-tooltip ownership use this hit result instead of slot frames. Enlarged/offset hits can reach +vanilla interaction; shrunken hits leave the surrounding frame to the drawer. Empty-slot tooltips +compose against the hit element, and an occupied slot suppresses the fragment tooltip only on a +native hit. Drawer controls/editor regions retain priority, and pending slots cannot receive hits. + + +`ItemSlotElement.MenuSlot` and `PlayerSlotElement.PlayerMenuSlot` are subclassable with protected +constructors. Override the element's slot factory to return your adapter subclass and refine its +rules while retaining reservation handling or vanilla equipment behavior. A custom client storage +can implement `api.capabilities.ReservationReceiver.setPlan` to receive synchronized plans through +the default item adapter; implement `ReservedSlots` for the corresponding reservation rules. +There is no dependency on the built-in `ClientMenuItems` implementation in that adapter. + + +Slot elements also own native presentation policy. `renderNative()` controls whether the bridge +calls vanilla's slot drawing (default: true except for ghosts). `nativeBounds(vanillaBounds)` chooses +the destination rectangle; the bridge translates and scales vanilla's 16x16 drawing to it and restores +the graphics pose afterward. `highlightBounds` and `hitBounds` default to that rectangle but can be +overridden independently, including offsets and different widths/heights. All rectangles use panel +coordinates, with the original native slot rectangle supplied as input. Default bounds use the +configured item size. Native hit routing retains active-slot checks and vanilla hover tolerance; +non-slot hit tests retain their original bounds. Use the existing element rendering contract for +custom artwork, and disable native drawing when that artwork replaces the item. No new registration +is needed for these policy hooks. + + ## Statistics drawer The left Statistics drawer shows sections supplied by machine parts through -`api.stats.StatsProvider`. The menu knows only this interface, not concrete storage or processor -classes. Return the contributing parts from `ScreenHost.screenStats()` in display order: +`api.stats.StatsProvider`. `StatsMenuComponent` samples this interface without knowing concrete +storage or processor classes. Return the contributing parts from `ScreenHost.screenStats()` in display order: ```java @Override public List screenStats() { @@ -176,16 +1207,62 @@ classes. Return the contributing parts from `ScreenHost.screenStats()` in displa ``` Return stable part instances so the menu can retain counter history. Repeated references to the -same part are sampled once. Providers run only on the server, once per 20 game ticks per menu; -unit changes are local and use the existing synchronized values channel. +same part are sampled once. Providers run only on the server, once per 20 game ticks per host, +shared by its viewers; page selection is local and uses the synchronized component field. A provider returns `Statistics`: a translated section title, rows with stable IDs, and optional -energy-demand contributions. Rows describe a cumulative `Counter`, an exact `Rate`, explanatory -`Text`, or aggregate `MaximumUse`. Each row declares its label, unit, precision and whether it -scales with time. `row.whenUsed(resource)` hides it unless a provider reports positive -energy demand for that exact resource. The drawer handles sampling and formatting. Custom parts can implement this -interface without changing the menu or renderer. Each section supports at most 64 distinct rows; -the combined wire snapshot is limited to 4,096 lines. Empty sections add no title or rows. +energy-demand contributions. Built-in values are cumulative `Counter`, exact `Rate`, explanatory +`Text`, and aggregate `MaximumUse`. Each row declares its label, unit, precision and whether it +scales with time. `row.whenUsed(resource)` hides it unless a provider reports positive demand for +that exact resource. Empty sections add no title or rows. + +`Statistics.Value` is an open interface. A value owns `sample(StatsSamplingContext, previous)`, +returning a `Statistics.Sample` whose `format(StatsFormatContext)` produces a styled `Component`. +Sampling runs once per update, regardless of the number of timescales. Formatting must use the +sampled data without changing history or reading live storage. No statistic registration, renderer +or custom network codec is needed: + +```java +record Percentage(double fraction) implements Statistics.Value { + @Override public Statistics.Sample sample(StatsSamplingContext context, + Statistics.Sample previous) { + long percent = Math.round(fraction * 100); + return format -> Component.literal(percent + "%"); + } +} +``` + +Stateful values can reuse `previous`; it is scoped to provider identity and row ID. It is null on +first appearance, after removal, when game time moves backward, or when the value class or its +`samplingKey()` changes. Return an immutable configuration key (for example the counter window), +excluding the current reading. Fresh value descriptions and reordered rows retain history. The +sampling context exposes game time and aggregated resource demand; the formatting context exposes +the timescale, precision and units, plus `ticks()` and `number(...)` helpers. Numeric conversion +and rolling history helpers live in `api.stats.StatsNumbers` and `CounterRate`. + +Hosts supply ordered timescale data through `screenStatsTimescales()`. Defaults are tick, second +and minute. Add or replace entries without registration: + +```java +@Override public List screenStatsTimescales() { + var scales = new ArrayList<>(StatsTimescale.DEFAULTS); + scales.add(new StatsTimescale(ResourceLocation.parse("example:hour"), 72_000, + Component.translatable("example.stats.per_hour"), + Component.translatable("example.stats.hour_unit"))); + return List.copyOf(scales); +} +``` + +Timescales have a stable namespaced ID, positive tick duration, selector label and rate-suffix unit. +A host may return up to 16 uniquely identified entries, including an empty list. The drawer retains +the selected ID across reordering and falls back to the first remaining page when it disappears; +empty lists have no selection and recover when pages return. Left/right clicks cycle forward/back. + +`ScreenStats` only coordinates providers, sample lifecycle, combined demand and section assembly. +`StatsMenuComponent.PAGES` synchronizes bounded `StatsPage(id, label, lines)` values; the client +never needs the custom value implementation. The field schema is version 2, reflected in menu +fingerprints. Each section supports at most 64 distinct rows, each page at most 4,096 lines, and +the encoded field at most 64 KiB. `PAGES`/`pages()` replace the fixed `LINES`/`lines(unit)` API. Built-in providers: @@ -224,11 +1301,83 @@ as completions. Rates use game time at 20 ticks per second, not wall-clock time. shows a dash until enough time has elapsed. RF values are grouped integers; cycles have exactly two decimal places. For example: 24,000 RF/min and 12.00 cycles/min. -The compact header selector cycles Per tick / Per second / Per minute; right-click reverses. +The compact header selector cycles the host-provided pages, defaulting to Per tick / Per second / +Per minute; right-click reverses. Labels and styled rows come from the synchronized pages. Info, Statistics and equipment drawers replace each other on the left. Right-side automation and redstone drawers remain independent. Statistics scrolls when needed, reserves its bounds from JEI, closes on Escape, and remembers its open state for the connection. +## Registering XML tags + +Every screen tag, including built-ins, resolves through `ScreenTags`. Each registration supplies +its factory, XML attribute contract, allowed children and placement groups. `ScreenReader` handles +secure parsing and delegates construction to the registry. The runtime XSD is compiled from those +registrations: adding a tag also adds its validation contract, without editing MMDLib's schema. +The bundled `screen.xsd` remains the built-in type library and an editor reference for built-ins. + +Register during mod construction, on both physical sides, before screen loading freezes the +registry. For example, assuming your mod defines a `TemperatureElement`: + +```java +ScreenTags.GLOBAL.register(new QName("https://example.org/gui", "temperature"), + ScreenTag.element(TemperatureElement.class, xml -> new TemperatureElement( + xml.common(), xml.size("width", 1), xml.size("height", 1), xml.attribute("unit"))) + .attribute("width", ScreenAttribute.defaulted("pixels", "32")) + .attribute("height", ScreenAttribute.defaulted("pixels", "18")) + .attribute("unit", ScreenAttribute.choices("celsius", "kelvin").withDefault("celsius")) + .build()); +``` + +Imports are `javax.xml.namespace.QName` and `gui.load.ScreenTags`, `ScreenTag`, `ScreenAttribute`. +Use the tag inside ordinary built-in containers: + +```xml + + + + + +``` + +Tag identity is the namespace URI plus local name; prefixes are arbitrary. A URI identifies a +namespace and is never downloaded. `register("my-tag", definition)` uses the standard screen +namespace. Prefer a mod-owned namespace to avoid collisions. Duplicate registrations fail; +`replace(QName, definition)` explicitly replaces both the factory and XML contract before freeze. + +`ScreenTag.element` includes common attributes and membership in the `widget` placement group. +`ScreenAttribute.required(type)`, `optional(type)` and `defaulted(type, value)` accept standard +XSD types such as `xs:string`, `xs:int`, `xs:boolean`, or shared types such as `pixels`, `length`, +`color`, `resourceId` and `translationKey`. Enum choices are supported directly. The model's +validation/value contracts still apply after construction. + +For containers, add `.children("widget", 0, -1)` and construct children with `xml.children()`. +The count `-1` means unbounded. Each child goes through the registry, including nested mod tags. +`.children(QName, min, max)` accepts one exact child tag. Register configuration-only children +with `ScreenTag.data(DataRecord.class, factory)` and read them with `xml.children(DataRecord.class)`; +they do not need to be UI elements. Mods can define their own child groups by assigning tags +`.group("example_options")` and accepting that group in their parent contract. + +Built-in placement groups include `widget`, `slots`, `buttons`, `views`, `radios`, `controls` +and `dock`. `.group(...)` adds membership; `.groups(...)` replaces the default memberships. +For example, an XML factory returning a custom `SlotElement` can join `slots` to work inside +``. Register additional inventory-row controls in `buttons`. Context/view and radio +containers still expect their model types when parsing children; group membership does not +bypass those type checks. +Root `dock` membership places the resulting element in `Screen.tabs()`. + +`ScreenXml` supplies the current node/source, shared `common()`, text/ID/colour/size helpers, +translation-key expansion, recursive child construction and `error(message)` with file/line. +Keep client drawing and local interaction in `ElementRenderers` and `ElementBehaviors` as above. + +Namespaced tags survive inheritance, fills, generated content and embedded machine screens. +`ScreenReader.Node` retains the namespace; generators can use its namespace-bearing constructor. +For tools/tests, use `ScreenTags.builtins()` and the explicit-registry overloads of `ScreenReader.read` +or `ScreenLoader.read`. Unknown tags/attributes, invalid values and illegal child placement remain +errors. DOCTYPE, external entities and XInclude remain disabled; schema imports resolve only to +registered, in-memory contracts. + ## Registering custom widget types A consuming mod can add a leaf widget without replacing MMDLib's screen or menu. Register its @@ -239,10 +1388,10 @@ import java.util.Set; import net.minecraft.resources.ResourceLocation; import zone.moddev.mc.lib.gui.WidgetType; import zone.moddev.mc.lib.gui.WidgetTypes; -import zone.moddev.mc.lib.gui.load.BoundValue.Kind; +import zone.moddev.mc.lib.gui.value.ValueTypes; WidgetTypes.GLOBAL.register(ResourceLocation.parse("example:temperature"), - new WidgetType(Set.of(Kind.WHOLE, Kind.DECIMAL, Kind.NUMBER), widget -> { + new WidgetType(Set.of(ValueTypes.WHOLE, ValueTypes.DECIMAL, ValueTypes.NUMBER), widget -> { if (widget.common().value().isEmpty() || widget.common().max().isEmpty()) throw new IllegalArgumentException("Temperature needs value and max"); if (!widget.properties().isEmpty()) @@ -347,7 +1496,7 @@ mouse, scroll, key, character, tick, focus and lifecycle methods. Its click resu Only a captured press receives mouse drag/release callbacks. Releasing an inventory drag over a widget leaves the release with the inventory, regardless of which widget is under the pointer. -Tab and Shift+Tab move between visible focusable custom widgets in document order. While one is +Tab and Shift+Tab move between visible focusable element behaviors in document order. While one is focused, inventory shortcuts are consumed even when its `keyPressed` returns false, so typing E, Q or a number cannot close the screen or move a slot. `charTyped` is separate from `keyPressed`. Escape first cancels capture; otherwise it is offered to the focused instance. If unhandled, it @@ -386,17 +1535,17 @@ var setLimit = new WidgetAction(ResourceLocation.parse("example:set_lim }); WidgetTypes.GLOBAL.register(ResourceLocation.parse("example:limit_slider"), - new WidgetType(Set.of(Kind.WHOLE), widget -> { + new WidgetType(Set.of(ValueTypes.WHOLE), widget -> { if (widget.common().value().isEmpty()) throw new IllegalArgumentException("Limit slider needs value"); }, List.of(setLimit))); ``` The relevant imports are `gui.WidgetAction`, `gui.WidgetType`, `gui.WidgetTypes`, -`gui.load.BoundValue.Kind`, `net.minecraft.network.codec.StreamCodec`, and the ordinary Java +`gui.value.ValueTypes`, `net.minecraft.network.codec.StreamCodec`, and the ordinary Java collections. Codecs use `RegistryFriendlyByteBuf`, so structured records and registry-aware values are supported. Register matching action IDs and codecs on both sides. Each action declares -a maximum encoded size of 1–4096 bytes. The envelope is bounded before the mod codec runs, the +a maximum encoded size of 1–4096 bytes. The envelope is bounded before the mod codec runs, the encoder uses a bounded buffer, and trailing bytes are refused. **Codecs must additionally bound their own string and collection lengths before allocating.** @@ -406,14 +1555,14 @@ Codec runtime exceptions (including buffer overflow) are logged and return false a packet or marking the widget pending. Do not submit every render frame. For sliders, commit on release; throttle continuous controls yourself. -The server checks menu ownership, interaction distance, visibility, element type, allowed action, +The server checks menu ownership, interaction distance, visibility, the element's allowed action, menu-session token and increasing request number before dispatch. At most eight typed actions per menu per server tick are processed; excess edits receive an immediate rejection using the last sent values, without invoking their codec, getters or handler. Replayed requests are dropped. Handler exceptions are logged and rejected, but MMDLib cannot undo a handler's partial mutations. -The response includes accept/reject and fresh authoritative `value`/`max` snapshots after the -handler (or the last sent values for rate-limited edits). They are applied before +The response includes accept/reject, fresh authoritative `value`/`max` snapshots and typed element +state after the handler (cached values/state for rate-limited edits). They are applied before `WidgetInstance.actionResult(boolean)` runs and update the server's delta baseline. The next ordinary snapshot also refreshes those values, so a late acknowledgment ignored after timeout cannot leave the client stale. Ordinary snapshot sync continues for every viewer, including changes to other @@ -425,13 +1574,26 @@ or never applied the edit. The timer follows the player's open menu even while a covers it; result callbacks are delivered when the machine screen resumes. Responses for old sessions or obsolete requests are ignored. +`gui.runtime.ElementActionSession` owns this per-menu lifecycle: request IDs, pending/results, +timeout recovery, replay/rate checks, acknowledgment baselines and next-broadcast correction keys. +`MachineMenu` delegates action operations and supplies player/menu/distance authorization plus fresh +snapshots. Removing the menu closes the action session, clears pending/results and refuses later +requests or acknowledgments. Covering its screen leaves the menu session active. + +Direct integrations use `sessionId()`, `prepareElementAction`, `elementPending`, +`receiveElementResult`, `takeElementResult` and `tickElementActions`. `tickClientActions` also ticks +registered menu components. The older widget-named methods are forwarding aliases; the existing +`ScreenNetwork.Widget`/`WidgetResult` envelopes and wire IDs are retained. No action-session registry +or additional registration is needed. + The companion Settings view demonstrates all three cases: a named machine, an energy-reserve slider that holds processing below the chosen charge percentage, and a Run/Pause selector whose payload contains both lane index and state. The server restricts that selector to the lane named in its XML property. These settings persist with the machine. The graphical smoke run checks focus, typing, cancellation, resizing, view changes, release outside bounds and server rejection. -The expanded menu protocol is version **9**, including a session token in menu-opening data. +The expanded menu protocol is version **14**, including a session token and registered drawer +definitions in menu-opening validation, plus typed element state in deltas and acknowledgments. Client and server must use matching MMDLib builds. ## Files and IDs @@ -651,8 +1813,22 @@ boots, with the offhand slot beside the hotbar. Empty equipment slots show vanil armor uses NeoForge's equipment checks, a one-item limit, and vanilla binding-curse protection. The complete inventory panel is 184 by 94 GUI pixels, including its toolbar row. There is no inventory label: the toolbar shares the helmet's row, directly above the three storage rows. -Write `` only to place it somewhere else, or set `inventory="none"` on -`` to leave out the inventory and equipment together. +Write `` to move the inventory and its default equipment column, or place +the three sections separately with ``, `` and +``. Each section may appear once; omitted sections retain their default placement. +Set `inventory="none"` on `` to leave out all three sections. + +```xml + + + + + +``` + +Here the armour column is on the left and the offhand is independently placed on the right. +Containers and offsets work normally. When equipment is explicitly placed elsewhere, the inventory +section is 162 pixels wide; omitted equipment reserves the default right-hand column. Shift-clicking equipment moves it into the ordinary inventory/hotbar. Machine outputs also go there; they do not automatically equip armor or fill the offhand. Shift-clicking ordinary @@ -661,13 +1837,82 @@ only the ordinary inventory and hotbar, leaving equipped items alone. Optional `` goes after the main body and before tabs/popouts. Its button children form a right-aligned row above the player inventory (for example manual pause -toggles). Automation's reusable `InventoryColorButtonBar` shares that row on the left, with +toggles). Automation uses the reusable `ColourButtonBar` on the left, with scrollable overflow. Both sides align with the nine-column storage grid, leaving the equipment column clear. Flat controls fit in the inventory's existing top row; taller controls add only the extra height they need. Use ordinary button declarations inside it; no spacer or coordinates are needed. This element requires a player inventory and is available to every screen, not just companion machines. +Elements own group associations through `Element.associatedGroups(screen, groups)`. The default +matches the element ID against each `ElementGroup`'s explicit anchors, or matches an unanchored +group's resource type/index to `resourceBinding()`. `ResourceBinding.groupType()` maps unqualified +kinds such as `fluids` to `mmdlib:fluids`; mod kinds can use namespaced IDs directly, without a registry. +`SlotElement` additionally matches unanchored item groups by named inventory and local slot index; +unscoped slots use the group's `legacyIndices`. The three-argument `ElementGroup.Members` constructor +uses the same indices for both address spaces; generated machine metadata supplies them separately. +Explicit anchors always take precedence over resource/member fallback. +`ElementGroup` carries read-only IDs, anchors and optional resource-member metadata, without +exposing storage or automation operations. Mods can override the hook directly, including in +subclasses, and combine their associations with `super.associatedGroups(...)`. +`Element.highlightedGroups(screen, groups)` defaults to those same associations. Override it to +opt out of highlighting or choose a different set while retaining the ordering associations. +The drawer and generated-machine highlight compilation both use `GroupHighlights.of` to resolve +these hooks. Unknown group IDs are ignored, duplicates are removed per element, and targets retain +document order. A selected container can override `replacesChildHighlight(screen, descendant, group, +groups)` to represent a descendant's highlight for that group. Only actual ancestor targets can +suppress descendants; an unnamed container cannot suppress named targets during generated binding +compilation. `ColouredGroupElement` represents slots in its child inventory scope by default, leaving +nested scopes independent. Override its association or replacement hooks to change that policy. +Registered `HighlightPresentation` still owns drawing, visibility filtering and clipping. + +`GroupOrder.of` walks every element in document order (including inactive views), keeps the first +occurrence of each declared ID, then appends unassociated groups in declaration order. Unknown +IDs are ignored; declared IDs must be unique. It accepts an association function and has no slot +or resource-type checks. `ColourButtonBar` takes ordered `Entry(id, colour)` values and a +selection callback; it owns drawing, overflow, hit testing and selection visibility. Its RGB +colours and IDs can represent any caller's data. `SideConfigDrawer` supplies the metadata and +maps the resulting order back to stable automation indices. No additional registry is involved. + +`PlayerInventoryElement` owns the 36 storage/hotbar slots; `PlayerArmourElement` owns the four +armour slots; `PlayerOffhandElement` owns the offhand. Their shared `PlayerSlotsElement` base exposes +ordinary `PlayerSlotElement` children, inheriting item-slot rendering. `PlayerSlotElement` owns +player-slot creation, empty equipment icons, armour acceptance/stack limits, equip callbacks and +binding-curse pickup restrictions; `ItemSlotElement` owns machine-storage interactions. +`MachineMenu.playerSlotElements()` identifies player backing through `playerInventory()`, independently +of transfer roles, and retains native registration order. + +`SlotElement.playerInventory()` opts any implementation into player-slot assembly. Override +`createPlayerSlot(inventory, bounds, visible)`, `role()` and `playerSlotOrder()` to control the native +adapter, transfer routing and ordering. Lower order values come first; equal values retain tree +traversal order. `PlayerSlotElement` supplies vanilla equipment/storage roles and the usual main +inventory, hotbar, armour, offhand order. Subclasses may override each hook independently. +Custom player slots can appear in ordinary containers without a standard player-inventory region; +no registry is needed. Keep these definitions deterministic on client and server, and include +additional configuration in the element's model equality/text used by screen fingerprints. +All three containers accept `(Common, IntFunction)` constructors. The factory +runs once per standard player index and must return a player-backed slot with that index. Its +stable children participate in traversal, validation, layout, equality and screen fingerprints. +For example, `new PlayerArmourElement(common, MyArmourSlot::new)` keeps the standard armour layout +and uses the custom slots' appearance, native factory, role and ordering hooks. Constructors do not +call overridable methods on a partly initialized section. Custom `PlayerSlotsElement` subclasses +can instead pass a collection of already-created slot children to its protected constructor and +supply their own positioning. + +All three containers are extensible. Default sections are stable implicit model roots, included +in screen traversal, validation and fingerprints. `MachineScreen` uses the same rendering and +behavior traversal for every element, without a separate player-slot pass. + +Inventory placement owns the toolbar policy, automatic footer and extra space for tall controls. +`ScreenLayout` composes the measured regions and supplies generic placement operations. +Offsets on an explicit inventory or its parents move native slots, toolbar and controls together. +Implicit equipment follows the inventory's visibility; explicitly placed sections follow their +own parents. Player slots declare their backing inventory, so machine storage validation and +group-membership fallback do not treat them as machine slots. Native slot numbering and transfer +roles remain stable regardless of section placement. +`PlayerInventoryElement.slot` describes the default arrangement; +use `layout.bounds(slotElement)` for independently placed sections. + ## Layout - **Boxes.** `vbox` stacks children top to bottom and `hbox` left to right, so text of any @@ -714,7 +1959,7 @@ requires a player inventory and is available to every screen, not just companion boolean (`false` = 0, `true` = 1) or whole-number `value`. Optional `value-labels` supplies translated names for `{value}` in the tooltip. When both lists are present their lengths must match. Missing or out-of-range values display the first state. These buttons keep the standard - 20×20 icon-button size by default. Set `flat="true"` for a 14×14 control with a 12×12 + 20×20 icon-button size by default. Set `flat="true"` for a 14×14 control with a 12×12 icon, a uniform one-pixel dark border and a flat grey background that lightens on hover. Flat buttons do not take text captions. An `hbox` with a leading `spacer` places them against its right edge. @@ -747,12 +1992,26 @@ instance to call and is rejected at load. Indexed references support generated gauges for multiple tanks without one Java getter per tank. For example, `value="#getTankFluid(0)" max="#getTankCapacity(0)"`. -Getters run only on the server, each tick while the screen is open. The client never calls -them; it only receives the values. +Use `` for a complete tank component. The host only +exposes its `FluidStorage` through `ScreenHost.screenFluids()`; XML-defined machines provide +this automatically. `FluidTankElement` owns binding validation, fluid/capacity snapshots, +tooltips and container transfers. It needs no value getters or machine-specific callbacks. +It rejects explicit `value`, `max` and `interaction` attributes so display and interaction +cannot accidentally target different tanks. `` is an alias +for the same component; a plain `` remains a general display with getter bindings. + +With a fluid container on the cursor, **left-click fills the tank** and **right-click drains +it into the container**. Input and manual tanks can be drained by players. Insertion retains +fluid/recipe filters and rejects output tanks; both directions respect reservations. Each +click handles one container, including partial modded containers. Stacked results go into the +player inventory (or drop when full); creative containers remain unchanged. + +Getters and element-owned snapshots run only on the server, each tick while the screen is open. +The client only receives their values. -Values name Java getters, not storage handles. The `energy-gauge` convenience widget defaults -to `#getEnergy` and `#getEnergyCapacity`; the block entity must expose these public getters, -or the screen must override `value` and `max` to name its own. +Explicit `value` and `max` attributes name Java getters. Resource components own their snapshots: +`fluid-tank` reads `ScreenHost.screenFluids()` and `energy-gauge` reads `ScreenHost.screenEnergy()`. +Neither component needs value/capacity getters on the block entity. The loader checks at startup that each method exists, belongs to the right class, and returns a supported type. A fluid gauge's getter returns the fluid itself (its type and amount); energy @@ -765,11 +2024,12 @@ and some values (ProjectE's EMC, for one) exceed even a Java `long`. | Attribute | Runs on | Purpose | |---|---|---| | `id` | client | Names the element so others can refer to it (`hover-shows`, `side-config groups`); unique within the screen | -| `value`, `max` | server → client | What the element shows (see [Values](#values)) | +| `value`, `max` | server → client | What the element shows (see [Values](#values)) | | `tooltip` | client | Translation key; `{value}` and `{max}` are filled in, with large numbers shortened (`40.2 E`) | | `tooltip-shift` | client | Detailed tooltip shown while Shift is held; a "Shift for more" hint is added automatically | | `on-click`, `on-right-click` | server | Handler IDs from extension files; the handler receives the player, menu, element, and held modifier keys | | `on-shift-click`, `on-shift-right-click` | server | Optional: used instead of the plain handler while Shift is held | +| `interaction`, `interaction-target` | server | Reusable registered behavior and its screen-authored target; supported on every element except inventory slots | | `on-hover` | client only | Handler run when the mouse enters and leaves the element; display only, it cannot change game state | | `hover-shows` | client | ID of an element to show while hovering, without any handler | | `offset-x`, `offset-y` | client | Pixel nudge after layout | @@ -777,6 +2037,51 @@ and some values (ProjectE's EMC, for one) exceed even a Java `long`. Clicks are sent to the server through the menu's button channel, and the server checks the player, menu, and element before running the handler. Hover never reaches the server. +### Reusable element interactions + +An interaction belongs to the element, regardless of whether Java, XML, or a generator created +the screen. For example, a label can use the same fluid transfer behavior as a gauge: + +```xml +