Skip to content

Machine recipe types: datapack loading, lookup, datagen and JEI - #83

Merged
dshadowwolf merged 5 commits into
master-2.0-1.21.1-neofrom
machine-recipes
Nov 2, 2026
Merged

dshadowwolf merged 5 commits into
master-2.0-1.21.1-neofrom
machine-recipes

Conversation

@faceofcat

@faceofcat faceofcat commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

What this is

Phase 2 of the machine module, after #81 and #82. Mods can now register machine recipe types, and their recipe files load, reload and sync like any other recipe. There's still no processor, and mmdlib:crushing is unchanged. MMDLib registers no machine recipe type of its own.

Recipe types.

  • MachineRecipeTypes.register adds a MachineRecipeType through the mod's own DeferredRegisters. This is the only new class that imports NeoForge.
  • MachineRecipe, MachineIngredient, MachineProduct and FluidAmount get STREAM_CODECs, and the type's serializer is built from CODEC and STREAM_CODEC.
  • MachineRecipeEntry wraps a MachineRecipe as a vanilla Recipe. It matches through RecipeMatcher.check and stays out of the recipe book.
  • MachineRecipeInput exposes a MachineInventory to vanilla. It's never empty, so fluid-only and energy-only recipes still get asked.

Lookup and datagen.

  • MachineRecipeLookup.find finds the runnable recipe. It tries the last recipe by ID first, so /reload is picked up.
  • MachineRecipeLookup.explain reports the recipe closest to running and why it can't, for screens and debugging. It's empty when the machine holds nothing any recipe uses.
  • MachineRecipeBuilder writes recipes from datagen.

JEI. A generic MachineRecipeCategory covers every registered machine recipe type, sized for its largest recipe. Tags show source fluids only, and recipes that can never run are left out.

Review fixes

Notes from #82 (045844a):

  1. Tests in the main build path: no change needed. build already skips check; the end of gradle/verification.gradle detaches it. ./gradlew build --dry-run lists only jar, assemble and build from the build and verification tasks, with no test, runGameTestServer or verify*. Those run on ./gradlew check, which is what CI calls.
  2. Release-jar guard: fixed. It refused everything under data/mmdlib/structure/. It now refuses only data/mmdlib/structure/empty.nbt, so MMDLib can ship structures.
  3. Absolute coordinates: fixed. blocksPlace and bucketsPlaceAndRecover now use helper.absolutePos. MachineSmoke.check had the same problem, so it changed too. The old grid wouldn't fit in the 5×5×5 structure, so the first two place one block or fluid at a time on the same spot.
  4. CI action versions: checked, no change. The tags actions/checkout@v7, actions/setup-java@v6, gradle/actions/setup-gradle@v6 and actions/upload-artifact@v7 all exist in those repos (gradle/actions holds setup-gradle), and the CI run on Test infrastructure: JUnit 5, game tests and CI #82 used them.
  5. Stale vanilla extension docs: no change. Not touched here, as you said to keep an eye on them.

Notes from this PR:

  • Output room ignoring consumed inputs: fixed in its own commit. check now counts what the recipe's inputs take from a group, using the same claims as consume. It works on copies, because a rolled-back transaction would run onChange on every check.
  • Nits and explain: in the commit after that. See my comment below for what was deferred, and why.
  • explain false positive: fixed in its own commit. An empty machine no longer reports a missing input of a recipe nobody started.

Review notes

Testing

  • ./gradlew check passes locally: 52 unit tests and all 18 game tests. Six game tests are new, covering registration, loading with a broken file skipped, lookup, explain, a network round trip, and builder output matching the file.
  • The room fix has its own regression test. It failed before the fix, on gold fits where the iron was.
  • I didn't run the GUI smoke test locally. The CI job on this PR covers it, including the JEI category checks for hidden recipes and source fluids, and it uploads the screenshot.

🤖 Generated with Claude Code

https://claude.ai/code/session_018zpP6ZrFjaGe477FvWAZKo

faceofcat and others added 2 commits November 2, 2026 18:45
…tagen and JEI

Phase 2 of the machine module. Mods register a MachineRecipeType through
their own DeferredRegisters (MachineRecipeTypes.register); its recipe
files load, reload and sync like any recipe.

- STREAM_CODECs for MachineRecipe, MachineIngredient, MachineProduct and
  FluidAmount; the type's serializer is built from CODEC and STREAM_CODEC.
- MachineRecipeEntry wraps a MachineRecipe as a vanilla Recipe; it
  matches through RecipeMatcher.check and stays out of the recipe book.
- MachineRecipeInput exposes a MachineInventory to vanilla and is never
  empty, so fluid- or energy-only recipes are still asked.
- MachineRecipeLookup finds the runnable recipe, trying the last one by ID
  so /reload is picked up.
- MachineRecipeBuilder writes recipes from datagen.
- JEI: a generic MachineRecipeCategory for every registered machine
  recipe type, sized for its largest recipe.

mmdlib:crushing is unchanged; moving it is left for a separate decision.

Tests: five game tests over mmdlib_smoke:grinding and its recipe files
(registration, loading with a broken file skipped, lookup, network round
trip, builder output equal to the file); the GUI smoke test checks the
JEI category and saves a screenshot, which CI now uploads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zpP6ZrFjaGe477FvWAZKo
Review notes from #82.

- blocksPlace, bucketsPlaceAndRecover and MachineSmoke.check used absolute
  world coordinates. They now take a position from helper.absolutePos, so
  they stay inside their own structure. The first two used a spaced grid,
  which doesn't fit in the 5x5x5 structure, so they place one block or
  fluid at a time on the same spot.
- verifyReleaseClasses refused everything under data/mmdlib/structure/,
  which would fail the build if MMDLib ever ships a structure. It now
  refuses only the test structure, empty.nbt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zpP6ZrFjaGe477FvWAZKo
@dshadowwolf

Copy link
Copy Markdown
Collaborator

Single fix needed: output room ignores what the recipe consumes from the same group

Comments:

  • Lookup can't say why nothing ran.
  • Each find() runs the full max-flow plus an output simulation for every recipe of the type. This is rather expensive
  • Minor data-first gap: Recipe types can only be declared in Java
    Design Nits:
  • JEI fluid inputs from a tag show the tag's whole contents. #minecraft:water includes flowing_water, so JEI cycles through flowing water. Filtering to source fluids would fix it.
  • A recipe whose item tag is empty (an optional mod's dust, say) still shows in JEI with a blank slot.
  • MachineRecipeBuilder.build() only rejects time == 0. A negative time isn't caught until encoding.
  • outputFluid has no chance overload, unlike outputItem.
  • percent() formats with Locale.ROOT but seconds() uses the default locale.

faceofcat and others added 2 commits November 2, 2026 19:31
RecipeMatcher.check tested output room against the inventory as it stood,
before the recipe's own inputs were gone. A recipe whose input and output
share a group couldn't start: 1 iron in a one-slot group returned NO_ROOM
for its gold, and a full tank refused a recipe that drains it first.

check now keeps what each group's inputs claim and passes it to a new
fits(list, taken) on ItemGroup and FluidGroup, which subtracts it from its
copies. The claims are built the same way as in consume, so the check
matches the slots consume really takes from. Nothing is extracted or rolled
back, because that would run the storage's onChange on every recipe check.

machineRecipeLookup expected a full tank to block a recipe that drains that
tank, so it now blocks the item output instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zpP6ZrFjaGe477FvWAZKo
Review notes from #83.

- MachineRecipeLookup.explain returns the recipe closest to running and
  its MatchResult, for screens and debugging: one that can run, then
  NO_ROOM, then MISSING_INPUT, then a recipe the machine doesn't suit. It
  checks every recipe, so it's for when the answer will be shown, and
  doesn't add to the cost of find.
- JEI: fluid inputs from a tag show source fluids, so #minecraft:water no
  longer cycles through flowing_water. Recipes that can never run, such as
  one whose input is an empty tag from an optional mod, are left out.
- JEI: the energy, time and chance text all format with Locale.ROOT.
- MachineRecipeBuilder.build refuses a time below 1, not just 0, instead of
  failing when the recipe is encoded.
- MachineRecipeBuilder.outputFluid has a chance overload, like outputItem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zpP6ZrFjaGe477FvWAZKo
@faceofcat

Copy link
Copy Markdown
Collaborator Author

Checked each of these against the code. The fixes are pushed and CI is green.

Output room ignoring consumed inputs: fixed in 7f4d3d6. check now counts what the recipe's inputs take from a group, using the same claims as consume. It works on copies through a new fits(list, taken), because a rolled-back transaction would run onChange on every check. The regression test failed before the fix. The fix also showed that machineRecipeLookup expected the bug: it assumed a full tank blocks a recipe that drains that tank, so that test now blocks the item output instead.

Lookup can't say why nothing ran: added in 9703786. lookup.explain(level, inventory) returns the recipe closest to running and its MatchResult: one that can run, then NO_ROOM, then MISSING_INPUT, then a recipe the machine doesn't suit. It's on demand, so it adds nothing to find.

find() cost: partly agree, left for the processor. A machine with nothing to run does check every recipe on each find, and Ingredient.test scans the ingredient's item list per slot, so that cost is real. It isn't the full max-flow and output simulation for every recipe, though: each check stops at its first missing input, and room is only tested once the inputs pass. Nothing calls find yet and I haven't measured it. I'd have the processor search again only after an inventory change or /reload, which removes most of the idle cost.

Recipe types in Java only: agreed, deferred. Registries freeze before datapacks load, so this would take a mod-root definition file like mmdlib/material/. I think it belongs with the JSON-only machines work. Say if you'd rather have it sooner.

Nits: all fixed in 9703786.

  • JEI fluid tags now show source fluids only.
  • Recipes that can never run are hidden from JEI, including empty fluid sets. For empty item tags, one correction: NeoForge substitutes a barrier named "Empty Tag: ...", so from the code the slot looks like it would show a barrier rather than a blank. I didn't look at it in game. hasNoItems() catches that case either way.
  • build() refuses any time below 1.
  • outputFluid has a chance overload.
  • percent() and seconds() both use Locale.ROOT now, and so does the energy line, which had the same inconsistency.

Heads-up: reservations in the processor

Fixing the room check opens a new problem, so here's the plan before it gets built. A one-slot machine can now start iron to gold, but its slot is empty mid-cycle. A hopper can refill it, the gold then has nowhere to go, and the machine stays stuck if nothing ever empties that slot.

The plan for the processor PR is that a running recipe reserves what it needs:

  • Inputs stay in their slots and tanks, reserved, and are only taken when the cycle finishes. Output space is reserved too, so a recipe that starts can always finish.
  • Reserved items are locked. A slot of 50 iron with 1 reserved gives up 49. Hoppers and menus respect this, and space reserved for a product counts as occupied when inserting.
  • This changes my answer to your Machine module stage 1: internals and recipe core #81 question about breaking a machine mid-cycle. Items and fluids are still in the inventory and drop or go with it as usual, so only energy already spent is lost. On Machine module stage 1: internals and recipe core #81 I said fluids would be lost too.
  • Two lanes sharing output slots can't collide, because each sees what the other reserved.
  • An interrupt button pauses the machine and clears its reservations, so locked items can be recovered when there's no power. Resuming keeps progress only if the same recipe can reserve everything again.

Three routes get past extractItem and would need fixing in MachineMenu: shift-click, clicking with a different item, and the number key swap. I'd also make a small separate PR first for the client menu handler, which today is a blank ItemStackHandler that doesn't know slot roles, filters or limits, so it should mispredict placement until the server corrects it.

If you see a problem with any of this, I'd like to hear it before the processor gets written.

explain reported the first recipe's missing input whatever the machine
held, so an empty machine could say "missing raw aluminum". Reported on
#83.

A missing input now only counts when the machine holds some other item or
fluid input of that recipe. Energy doesn't count, as a powered machine with
nothing in it hasn't begun on any recipe. When nothing qualifies, explain
is empty, so a screen can say so. A recipe with NO_GROUP or WRONG_KIND is
only returned when the machine suits none of them, as a setup problem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zpP6ZrFjaGe477FvWAZKo
@dshadowwolf
dshadowwolf merged commit de3b45f into master-2.0-1.21.1-neo Nov 2, 2026
3 checks passed
@faceofcat
faceofcat deleted the machine-recipes branch November 2, 2026 18:31
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