Skip to content

Add a list-based crafting interface - #227

Open
rubensworks wants to merge 4 commits into
master-1.21-ltsfrom
feature/list-based-crafting-interface
Open

Add a list-based crafting interface#227
rubensworks wants to merge 4 commits into
master-1.21-ltsfrom
feature/list-based-crafting-interface

Conversation

@rubensworks

Copy link
Copy Markdown
Member

Closes #10.

Adds a List-Based Crafting Interface part: a single variable slot that accepts a ValueTypeList of ValueRecipe, and exposes every recipe in that list to the crafting network. This lets a whole recipe set be derived with logic (Machine Reader, recipesbyinput/recipesbyoutput, filtered lists) instead of being placed card by card, which is the limitation raised in the issue: nine slots per interface is not enough in large packs, and the attuned interface offers no control over which of the target's recipes are used.

Approach

The variable machinery that lived in PartTypeInterfaceCrafting moves into a new core/part/PartTypeInterfaceCraftingVariableBase, shared by both variants:

  • currentRecipes becomes one list of recipes per slot instead of a single recipe, with a cached flattened view for getRecipes() (it is called on every addCraftingInterface).
  • Value extraction moves behind extractRecipes(int slot, IValue value). The base keeps the type check, the EvaluationException to slot-message path, and the isValid loop honouring validateRecipesCraftingInterface / disableCraftingCheck.
  • NBT keys (variables, recipeSlotMessages, recipeSlotValidated, disableCraftingCheck) are unchanged, so existing worlds load as before.
  • PartVariableDrivenVariableContentsUpdatedEvent no longer hardcodes PartTypes.INTERFACE_CRAFTING as its part type.

PartTypeInterfaceCrafting reduces to a 9-slot part whose extractRecipes returns zero or one recipe. PartTypeInterfaceCraftingList is a 1-slot part over ValueTypes.LIST.

The container and screen are reused for both: the inventory size already travels in the GUI packet, the slot filter now comes from getPartType().getSlotValueType(), and the background texture is derived from the part name. No new menu type, GuiConfig or RegistryEntries holder. The settings GUI's two instanceof PartTypeInterfaceCrafting.State guards are widened to the shared base, so the list variant also gets the crafting-check checkbox.

Guards on list reading

Recipe lists are not necessarily cheap or finite, so extractRecipes follows the guard order used upstream in Operators and TunnelAspectWriteBuilders:

  • infinite lists (isInfinite()) are rejected with a slot message;
  • the element type must correspond to OBJECT_RECIPE, with a per-element check as well, since CATEGORY_ANY passes the list-level check;
  • elements are read with an explicit indexed loop rather than for-each, because ListFactoryIterator swallows per-element EvaluationException and substitutes defaults, which would hide errors instead of surfacing them in the slot;
  • the list is materialized once per reload, capped at maxCraftingInterfaceListRecipes (default 256). A Machine Reader's ValueTypeListProxyPositionedRecipes is an uncached positioned view, where each get(i) is a capability lookup plus an Iterables.get;
  • duplicates are removed. CraftingNetwork drops a recipe from its index as soon as one removal is requested for it, so the same recipe must never be registered twice from one interface. Not reachable with one recipe per slot; very reachable with a list.

Two related changes to how reloads reach the network:

  • update now patches only what actually changed between the old and new recipes of a slot, instead of removing all and re-adding all.
  • Reader-backed variables are invalidated on every reader tick (AspectReadBase.updateLazyAspectVariable.invalidate), and each invalidation triggers a reload. With a list that means re-materializing up to 256 recipes several times a second. Reloads of a list slot are therefore throttled to craftingInterfaceListMinReloadInterval (default 20 ticks). This only bounds how often invalidations are acted on; it adds no polling, and the regular interface is unaffected.

Config

Option Default
interfaceCraftingListBaseConsumption 10 energy per processing job, same as the attuned interface
maxCraftingInterfaceListRecipes 256 0 disables the cap
craftingInterfaceListMinReloadInterval 20 0 reloads on every invalidation

Recipe

Mirrors the attuned interface, with c:gems/diamond in place of the emeralds: two crafting interfaces and a crystalized chorus block in the middle row, diamonds above and below.

Tests

GameTestsItemsCraftList covers crafting the first and the last recipe of a list, re-indexing after the variable card is replaced, duplicate removal, rejection of a non-recipe list, and a Machine Reader-backed lazy list. The last one also covers the invalidation path specifically: it removes the reader's target block and asserts the interface drops its recipes, which can only happen through the variable's invalidation listener. GameTestsAdvancements gets positive and negative tests for the new advancement.

./gradlew build and ./gradlew runGameTestServer both pass (62 game tests). Also verified in a dev client with clientdevbridge-cli: the part renders on a cable, the new one-slot GUI lines up, the existing nine-slot GUI is unchanged, the settings GUI opens with the crafting-check checkbox, and the recipe and advancement resolve in game.

Notes for review

  • The two PNGs are placeholder art. part/interface_crafting_list.png is the base interface texture hue-rotated to cyan, and gui/part_interface_crafting_list.png is the existing background with the eight extra slot frames painted out. They are correctly sized and render fine, but they deserve real art before release.
  • No CHANGELOG entry: those blocks are generated per release by the ReleaseHelpers scripts, so writing one by hand would need an invented version and timestamp.
  • The new entry in root.json's rewards.recipes follows the existing sibling entries, which use integratedcrafting:part_interface_crafting… rather than the recipe ids (integratedcrafting:crafting/…). That looks like a pre-existing mismatch for all three interfaces; I kept the file uniform rather than fixing only the new line.
  • A stale recipe set is still possible where a variable never invalidates, for example a Machine Reader whose target gains recipes without the proxy's identity changing. Not addressed here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bn8brBLvFiJsfnBHpwf8ZN


Generated by Claude Code

A new crafting interface variant with a single variable slot that accepts a
list of recipes, so that a whole recipe set can be derived with logic instead
of placed card by card. Closes #10.

The variable machinery of the existing crafting interface is extracted into
PartTypeInterfaceCraftingVariableBase, with a per-slot recipe list instead of a
single recipe, so both variants share the evaluator, invalidation, validation
and slot messages. The container and screen are reused for both, since the
inventory size and the accepted value type are derived from the part type.

List reads are guarded: infinite lists are rejected, the number of recipes read
from a list is capped, duplicates are removed, and the network index is patched
with only what changed. Because reader-backed list variables are invalidated on
every reader tick, reloads of a list slot are throttled to a configurable
minimum interval.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bn8brBLvFiJsfnBHpwf8ZN
The single slot of the list interface sat in the top-left corner. The first
slot's x position is now derived from the slot count, which centers one slot
and leaves the nine-slot row exactly where it was.

"List-Based Crafting Interface" ran into the settings button in the gui title,
so it becomes "List Crafting Interface".

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

The list interface had its own config to throttle how often invalidations were
acted on. The part's update interval already governs this, so that config is
gone. The list interface now has its own minimum update interval config
instead, defaulting to 20 ticks, which is what the settings gui exposes.

Reload cost is cut so the interval is a preference rather than a necessity,
measured against a machine reader on a crafting table (987 recipes):

* Validation results are cached per part until it fully reloads, when the
  target may have changed. The cache is keyed by identity, as recipe handlers
  hand out the same instances on every read while hashing a recipe by value
  turned out to be expensive.
* A slot whose recipes did not change no longer syncs to the client, posts a
  contents-updated event, or touches the network recipe index.
* The network index diff uses sets rather than repeated list scans.

At the default cap of 256 recipes, a repeated reload goes from ~1.4 ms plus
network work to ~0.3 ms with none. Uncapped at 987 recipes, from ~9 ms to
~2.7 ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bn8brBLvFiJsfnBHpwf8ZN
The attuned crafting interface already exposes every recipe of its target with
no limit at all, and indexing those 987 recipes of a crafting table costs it
the same as it costs a list interface (~95-100 ms for a network remove+add in
both cases). Limiting lists to 256 was therefore inconsistent: it withheld
something the mod already allows elsewhere.

The limit now defaults to 4096, which no regular machine reaches. It stays as a
guard against a computed list that runs away, since a list is not bounded by a
real machine the way an attuned interface is. Set it to 0 for no limit.

The machine reader game test asserted against the limit, which made it pass
for the wrong reason once the limit exceeded the machine's recipe count. It now
asserts that the interface exposes exactly the recipes its target holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bn8brBLvFiJsfnBHpwf8ZN
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