Skip to content

Let the Attuned Crafting Interface enable and disable individual recipes - #221

Open
rubensworks wants to merge 2 commits into
master-1.21-ltsfrom
claude/attuned-crafting-recipe-list-x0ogkw
Open

Let the Attuned Crafting Interface enable and disable individual recipes#221
rubensworks wants to merge 2 commits into
master-1.21-ltsfrom
claude/attuned-crafting-recipe-list-x0ogkw

Conversation

@rubensworks

@rubensworks rubensworks commented Aug 31, 2026

Copy link
Copy Markdown
Member

Closes #162, Closes #219

The Attuned Crafting Interface exposes every recipe of its target machine, with no way to leave any of them out. Pointing one at a crafting table hands the network thousands of recipes, and a crafting job may pick one the player never wanted automated.

This gives the part a gui of its own that lists all recipes of its target, with the output icon and name of each. Clicking a row toggles it; Enable all, Disable all and Invert apply to whatever the search field currently matches, which is what makes a machine with thousands of recipes workable (search minecraft: → disable all). The settings gui, which used to be the part's main gui, moved behind a button. Nothing was added to the normal crafting interface, whose recipes are hand-picked already.

How disabled recipes are stored

A blacklist of recipe keys on the part state: an interface with nothing disabled costs no extra NBT, a pack update that adds recipes does not silently disable them, and no migration is needed.

A RecipeKey is the recipe id where IRecipeDefinition#getRecipeId gives one, and the full structural serialization otherwise. Keys are never resolved back into recipes for storage, since RecipeDefinition#fromRecipeId throws once a recipe is gone — an unresolvable key has to stay opaque so a pack update cannot silently re-enable something the player disabled.

Keeping the network's recipe index honest

getRecipes() now returns the filtered list and getAllRecipes() the full one. Every toggle updates the filtered list and then tells the network about exactly the recipes that changed, through add/removeCraftingInterfaceRecipe.

This matters because CraftingNetwork#removeCraftingInterface unregisters an interface by iterating over getRecipes(). Had that set ever changed without the network being told, the disabled recipes would have leaked into the recipe index permanently. GameTestsAttunedRecipes#testAttunedDisabledRecipeDoesNotLeakOnUnregister covers that invariant by moving an interface with a disabled recipe to another crafting channel and asserting the channel-independent index matches what the interface exposes.

Client sync and toggling

Recipes reach the client through the gui data buffer at open time. Recipes backed by a built-in recipe are sent by id and resolved from the client's own recipe manager, so a crafting table costs tens of kilobytes rather than a full structural dump; only machine-specific recipes, of which there are tens rather than thousands, are transferred structurally.

Sorting by localized output name happens client-side so the order follows the client's language, tiebroken on the recipe id for a stable order across sessions. Because the client's order therefore does not match the server's, toggles are keyed by RecipeKey rather than by list position — which also keeps the gui correct if the server re-reads its recipes while it is open. Bulk actions send the server-side indexes of the filtered recipes plus a version of the recipe list, and the server ignores them if it re-read its recipes in the meantime. Both payloads carry a sequence number, without which ContainerExtended#setValue would swallow a repeated identical toggle (disable X → enable all → disable X).

Search matches the output name, the mod id and the recipe id, against lowercase strings precomputed once at gui open.

Dependency bump

CommonCapabilities 2.9.122.11.5. Recipe ids on IRecipeDefinition arrived in 2.11.4; without them every key would have to be a full structural dump, which neither fits in the part NBT nor in the gui data packet at this scale.

Testing

  • ./gradlew test — unit tests for RecipeKey round-trips (id form, structural form, and the missing-recipe case) and for sort-comparator tiebreak stability.
  • ./gradlew runGameTestServer — all 56 pass, including 7 new ones:
    • the index shrinks on disable and grows back on enable;
    • the job planner stops selecting a disabled recipe;
    • no recipe leaks into the index when the interface unregisters;
    • disabled keys persist across a state save/load, including keys of recipes that no longer exist;
    • a disabled recipe is not crafted while an enabled one still is;
    • every recipe survives the trip through the gui data buffer, with its disabled state, its searchable strings and its server index;
    • the gui's value-notifier payloads apply server-side — single toggles, all three bulk actions, a repeated identical toggle, and the two cases the server refuses (stale recipe-list version, out-of-range indexes).
  • The gui itself was driven in a dev client with clientdevbridge-cli: opening it on a crafting table, toggling rows, searching, all three bulk actions scoped to the filter, scrolling, the settings round-trip, and re-opening to confirm each change reached the server.

The screen class stays untested, as every other screen in this repo is.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR

claude added 2 commits August 31, 2026 19:30
The Attuned Crafting Interface exposes every recipe of its target machine, with
no way to leave any of them out. Pointing one at a crafting table therefore hands
the network thousands of recipes, and a crafting job may pick one the player
never wanted to be automated.

This gives the part a gui of its own that lists all recipes of its target, with
the output icon and name of each. Clicking a row toggles it, and "Enable all",
"Disable all" and "Invert" apply to whatever the search field currently matches,
which is what makes a machine with thousands of recipes workable. The settings
gui, which used to be the part's main gui, moved behind a button.

Disabled recipes are stored as a blacklist of recipe keys on the part state, so
an interface that has nothing disabled costs no extra NBT, and a pack update that
adds recipes does not silently disable them. A recipe key is the recipe id where
there is one, and the full structural serialization otherwise. Keys are never
resolved back into recipes for storage, since that throws once a recipe is gone,
and an unresolvable key must stay opaque so that a pack update cannot silently
re-enable something the player disabled.

The part state now exposes the filtered recipes through getRecipes, and the full
list through getAllRecipes for the gui. Every toggle updates the filtered list
and then tells the network about exactly the recipes that changed, because the
network unregisters a crafting interface by iterating over the recipes it
exposes: had that set changed silently, the disabled recipes would have leaked
into the network's recipe index for good. A game test covers that invariant.

Recipes reach the client through the gui data buffer at open time. Ones that come
from a built-in recipe are sent by id and resolved from the client's own recipe
manager, so a crafting table costs a few tens of kilobytes rather than a full
structural dump. Sorting by output name happens client-side, so the order follows
the client's language, and toggles are therefore keyed by recipe key rather than
by list position. Bulk actions send the server-side indexes of the filtered
recipes along with a version of the recipe list, and the server ignores them if
it re-read its recipes in the meantime. Both payloads carry a sequence number,
without which the value notifier would swallow a repeated identical toggle.

This needs the recipe ids that CommonCapabilities 2.11.4 added, so its version is
bumped along with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR
Coveralls flagged the previous commit for dropping coverage: the recipe list
container was the largest new class and had nothing exercising it, since this
repo has no precedent for testing containers at all.

Two game tests now drive it the way a player does. The first writes the part's
gui data and constructs the container from that buffer, which covers the whole
client-sync encoding: recipes sent by id and resolved back through the recipe
manager, the disabled keys, the search filter, and the server index that bulk
actions are keyed on. The second feeds the container the value-notifier payloads
the gui sends, covering single toggles, all three bulk actions, a repeated
identical toggle, and the two cases the server refuses: indexes from a stale
recipe list and indexes out of range. Opening the container through the part's
own menu provider is covered too.

The value ids of both payloads are exposed so a test can address them, and the
gui-side recipe reader is public for the same reason.

The screen itself stays untested, as every other screen in this repo is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants