Skip to content

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

Merged
rubensworks merged 10 commits into
master-1.21-ltsfrom
claude/attuned-crafting-recipe-list-x0ogkw
Sep 4, 2026
Merged

Let the Attuned Crafting Interface enable and disable individual recipes#221
rubensworks merged 10 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 shows all recipes of its target as a grid of output icons, 9 × 6 = 54 at a time, with a green or red border per recipe. Clicking a cell toggles it, dragging over a run of cells toggles all of them, and hovering shows the output's tooltip, the recipe id and the inputs it requires. Enable, Disable and Invert apply to whatever the search field currently matches, which is what makes a machine with thousands of recipes workable (search minecraft: → Disable). 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.

The grid

Modelled on how Integrated Terminals shows crafting options. Vanilla slot metrics (18px pitch, 16px content) mean the grid lines up with the player inventory below it, so the gui grows by only one row of height over the plain list it replaces. Per cell: the output icon with its count, a white highlight under the mouse, and a green or red state border inset by a pixel so the slot's own grey bevel stays visible around it. The border draws before the output, so it never cuts through the icon or its stack count. Disabled cells additionally get a grey overlay in front of the output, since covering it is the point of that one.

The name no longer fits beside the icon, so it moves into the tooltip. Search already matches name, mod id and recipe id, which is how you find a specific recipe.

Dragging over a run of recipes

Holding the left mouse button and moving over the grid applies the state that the first recipe was set to, so a run of recipes goes off — or back on — in one motion, and the same gesture does both. Recipes the drag already passed over are skipped, so moving back and forth over one does not flip it repeatedly.

Two details keep it from feeling lossy: the whole path between two mouse events is walked over in 4px steps, so a fast swipe cannot skip cells it visually crossed, and the single-pixel gutter between two cells is ignored while dragging, so the drag is not interrupted by it. The tooltip is hidden for the duration, since it would cover exactly the cells being dragged over, and the click sound is rate-limited to one per 100ms so a full row does not machine-gun. Scrolling still works mid-drag, so a drag can span more than the six visible rows.

The hover tooltip

Inputs are shown as a grid of ingredient icons rather than a list of lines. Inputs differing only in quantity are merged and their quantities summed — a recipe taking five planks is one slot showing five, not five slots showing one — and inputs accepting alternatives (tags) cycle through them. This mirrors IntegratedTerminals' crafting-option tooltip; its implementation sits behind an IT-only capability, so the two small classes are reimplemented here against ItemStacks directly. Inputs are resolved only for the recipe actually under the mouse, since doing it for every shown recipe every frame would be far too slow.

The bulk action buttons carry tooltips too. Their labels are one word because three buttons that fit "Disable all" comfortably need more than the 162px the grid is wide — "Disable all" measured 51px inside a 52px button — so what they act on is stated in the tooltip, where it can also be said accurately: they apply to whatever the search matches, not to literally everything.

Escape returns to this gui

The settings and offsets guis are reached from here, so pressing escape in them goes back to this gui instead of closing everything.

Integrated Dynamics already routes its settings save button through PartHelpers#openContainerPart, so escape only has to trigger that same button action; it is scoped to the attuned interface, so the normal crafting interface keeps closing outright. Its offsets gui, however, is bound to a container type of Integrated Dynamics that a part cannot influence, so an offsets container and screen are registered here and handed out from getContainerProviderOffsets for this part.

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 grid 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).

A CyclopsCore bug this ran into

ContainerScreenScrolling computed the scrollbar's total rows with filteredCount / columns — integer division. Measured on this grid with 985 recipes: that gives 109 rows instead of 110, and scrolling fully to the bottom leaves recipes 981–984 unreachable. Fixed upstream in CyclopsMC/CyclopsCore#236, which is merged and released; this branch bumps to that release and drops the local workaround it carried in the meantime.

Dependency bumps

  • 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.
  • CyclopsCore 1.26.21.29.3, the first release carrying the scrolling fix above.

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 57 pass, including 8 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, and the grid fills every cell;
    • 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 part hands out the settings and offsets guis of this mod, which are the ones that can return to this gui.
  • The gui was driven in a dev client with clientdevbridge-cli: opening it on a crafting table (985 recipes), toggling cells, the hover tooltips, searching, all three bulk actions scoped to the filter, scrolling to the last partial row (985 % 9 = 4 cells, all reachable), and re-opening to confirm each change reached the server. For this round specifically: dragging over a full row disables all nine and dragging back enables them again, a drag with only two intermediate mouse events still catches every cell it crosses, a diagonal drag paints a clean diagonal band with no gaps, and escape returns to this gui from both the settings and the offsets gui while the normal crafting interface's settings gui still closes outright.
  • Checked against Integrated Terminals, by running its dev client against a local build of this branch: with an attuned interface and a storage terminal on one network, the terminal's crafting options listed both bookshelf recipes; dragging over them in the interface removed both from the terminal, and re-enabling them brought them back.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR

claude added 3 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
The recipe list showed five rows at a time, each a full-width row with an icon
and a name. On a crafting table that is five recipes out of a thousand, so
finding anything meant scrolling or relying entirely on search.

The recipes are now a grid of output icons, like the crafting options in
Integrated Terminals: 9 columns by 6 rows, 54 at a time instead of 5. The grid
uses vanilla slot metrics and lines up with the player inventory below it, so the
gui only grows by the one extra row of height. Each cell draws its output icon
with its count, a white highlight under the mouse, and a green or red border
inset to the cell's inner area, which leaves a gap between neighbouring cells
instead of merging their borders. Disabled cells are also dimmed. Both overlays
are drawn at a raised z, as the output is rendered as a 3D item and would
otherwise cover them.

The name no longer fits next to the icon, so it moves into the tooltip, which now
also shows the inputs the recipe requires as a grid of ingredient icons rather
than as a list of lines. Inputs that differ only in quantity are merged and their
quantities summed, so a recipe taking five planks is one slot showing five rather
than five slots showing one. Inputs that accept alternatives, such as tag-based
ones, cycle through them. This mirrors IntegratedTerminals' crafting option
tooltip; its implementation lives behind an IntegratedTerminals-only capability,
so the two small classes are reimplemented here against ItemStacks directly.
The inputs are only resolved for the recipe actually under the mouse, as doing it
for every shown recipe every frame would be far too slow.

ScrollingInventoryContainer already supports columns, so the container change is
just the column count. Its screen computes the scrollbar's total rows by integer
division though, which with nine columns leaves the last partial row unreachable,
so that is rounded up here until a CyclopsCore release carries the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR
…ttons

The green and red border was drawn over the cell's inner area at a raised z, so
it crossed the output icon and cut through the stack count in the corner. It now
outlines the cell itself rather than its contents, which puts it where the output
never reaches, so it can be drawn before the output instead of over it. Only the
overlay that greys out a disabled recipe still draws in front, since covering the
output is the whole point of it.

"Disable all" measured 51 pixels in a 52 pixel button, leaving half a pixel on
either side, and "Enable all" only a pixel and a half. Widening them is not an
option: three buttons that fit "Disable all" comfortably need more than the 162
pixels the grid is wide. The labels lose their "all" instead, at 33 to 35 pixels
for a comfortable eight either side, and what they act on moves into a tooltip —
where it can also be stated properly, as the actions apply to whatever the search
matches rather than to literally everything. The three are now spread over the
full width of the grid rather than bunched at its left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR
The border covered the slot's own grey bevel, so the cells read as blocks of
colour rather than as slots. Inset by a pixel it sits just inside that bevel,
which stays visible around it.

It still draws before the output, so it does not cut through the icon or its
count. The cost is that an output which fills its whole 16 by 16 now covers part
of the border, which the full-cell placement did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR
The settings and offsets guis of the attuned crafting interface are opened
from its recipes gui, so pressing escape in them now brings the player back
to that gui instead of closing everything.

Integrated Dynamics exposes this for the settings gui through the action of
its save button, but its offsets gui is bound to a container type of its own,
so an offsets container and screen are added here for the attuned interface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR
Holding the left mouse button and moving over the grid now applies the state
that the first recipe was set to, so that a run of recipes can be toggled in
one motion without clicking every one of them.

The whole path between two mouse events is walked over, so that recipes are
not skipped when the mouse moves quickly, and the tooltip is hidden while
dragging so that it does not cover the cells that are dragged over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR
CyclopsCore 1.29.3-1096 makes the scrolling container round its row count up
for multi-column grids, so the last partially filled row of the attuned
interface recipes no longer has to be made reachable here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9xTc8kXNRDppvXktL24aR
Comment thread CHANGELOG-1.21.1.md Outdated
@rubensworks
rubensworks merged commit de52573 into master-1.21-lts Sep 4, 2026
5 checks passed
@rubensworks
rubensworks deleted the claude/attuned-crafting-recipe-list-x0ogkw branch September 4, 2026 18:34
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