Repository navigation
Add multiblocks, dynamic machine visuals, and recipe controls - #105
Conversation
| transition(Status.READY, "ready"); | ||
| } | ||
|
|
||
| public boolean ready() { |
There was a problem hiding this comment.
ready() calls tick() on every call, and the port wrappers call ready() on every getSlots/getStackInSlot/insertItem/fill/receiveEnergy. So each capability access re-validates the whole structure and can dismantle the machine (removing blocks under REMOVE_OWNED) from inside another block's capability call.
Failure scenario: A pipe or hopper next to the socket calls getSlots(), then getStackInSlot(0), then insertItem() in one tick. That is three full matchesAll() passes over every member (blockstate and BE lookups, plus capability invalidation on status changes). If a member broke this tick, the first call runs dismantle(RECOVERY) and setBlock(AIR) on every member while the pipe is still in its transfer loop. Cost is O(members) per handler call, for structures of up to 4096 members.
| && controller.multiblock().contains(pos, link)) | ||
| instance = controller.multiblock(); | ||
| if (instance != null) | ||
| instance.dismantle(MultiblockInstance.Cause.RECOVERY); |
There was a problem hiding this comment.
onRemove dismantles with Cause.RECOVERY for every non-player removal. Cause.EXPLOSION (and INVALIDATED) are never produced, so listeners can't tell an explosion from bookkeeping recovery, and the demo listener only returns the machine item for SURVIVAL.
Failure scenario: A creeper blows up one part of a formed REMOVE_OWNED Multiblock Demo. onRemove runs dismantle(RECOVERY), which removes every member. The listener drops the contents but not the MultiblockDemo item, and the removed blocks have no loot. The whole machine is lost, while breaking the same part by hand in survival returns it.
| if (ghost.defaultItem() != null) { | ||
| if (!BuiltInRegistries.ITEM.containsKey(ghost.defaultItem())) | ||
| throw new IllegalArgumentException( | ||
| "Unknown ghost default item " + ghost.defaultItem()); |
There was a problem hiding this comment.
A ghost slot's default-item is only checked against the item registry inside the block entity constructor, which throws IllegalArgumentException. A typo in the XML surfaces as a block-entity creation failure in the world, not as a definition-load error.
Failure scenario: An XML has . The definition loads cleanly. Placing the machine, or loading a chunk that already contains one, throws from the DefinedMachineEntity constructor, so the BE fails to load, the machine breaks, or the server crashes. loadMachineData separately falls back to AIR for the same id, so the two paths also disagree.
| parts.forEach(part -> { | ||
| boolean valid = part instanceof MachineIngredient.ItemPart | ||
| ? items.contains(key) | ||
| ? items.contains(key) || ghostItems.contains(key) |
There was a problem hiding this comment.
Recipe validation accepts any ItemPart on a ghost group, but GhostItemGroup holds one sample per slot (free(slot) is always 0 or 1). An ingredient with count > 1 on a ghost group can never be claimed, so the recipe loads but can never run.
Failure scenario: A recipe declares {"items":"#c:plates","count":2} on ghost group 'pattern'. validateGroups accepts it. RecipeMatcher.check and CyclePlan.of always return MISSING_INPUT because the claim finds only 1, so the machine sits idle with no load-time diagnostic.
| costs.add(group.getKey() + ": " + fluid.amount() | ||
| + " mB required, " | ||
| + Math.max(0, fluid.amount() - returned) | ||
| + " mB consumed"); |
There was a problem hiding this comment.
The chooser's cost text ignores MachineIngredient.consumed(). A reusable fluid input (consumed: false) is still listed as 'N mB consumed'.
Failure scenario: A recipe has {"fluids":"minecraft:lava","amount":1000,"consumed":false}. The recipe selector row shows '...: 1000 mB required, 1000 mB consumed', but finish() never drains it. Players are told the recipe spends a bucket that it actually keeps.
| slots, groups, recipeType, processor, processorName, redstone, | ||
| screen, null); | ||
| } | ||
| /** Cube-front configuration retained for existing Java callers. */ |
There was a problem hiding this comment.
The old canonical MachineDefinition constructor (textures, power, fluids, ...) is gone. The overload documented as 'retained for existing Java callers' has a different signature (it also takes modelParts and faceTextures), so existing callers no longer compile or link.
Failure scenario: A 2.0.x consumer that builds MachineDefinition(id, checksum, name, hardness, mineable, requiresTool, textures, power, fluids, itemStorageId, slots, groups, recipeType, processor, processorName, redstone, screen) gets a compile error after updating, or NoSuchMethodError at runtime against the new jar.
| multiblock().tick(); | ||
| if (isRemoved()) | ||
| return; | ||
| boolean structureReady = structureReady(); |
There was a problem hiding this comment.
serverTick calls multiblock().tick() and then structureReady(), which calls multiblock().ready(), which calls tick() again. The full structure validation therefore runs twice per controller tick.
Failure scenario: For a formed multiblock, every server tick runs matchesAll() over all members twice (blockstate, BE and link lookups each time), on top of any extra passes triggered by port capability calls. That is double the steady-state cost for no benefit.
| // Returning to the same | ||
| // amount leaves a failed match unchanged; a refill must bypass the | ||
| // idle retry delay. | ||
| return buffer.stored(); |
There was a problem hiding this comment.
Using stored FE as the energy group's revision means any change in stored energy invalidates the failed-search cache. An idle machine whose buffer is charging re-runs the recipe lookup every tick instead of every SEARCH_RETRY_INTERVAL.
Failure scenario: An idle powered machine with no matching input sits on a generator supplying 10 FE/t into a 100k buffer. stored changes every tick, so inventory.unchanged(revisions) is always false, and lookup.find scans every recipe of the type each tick for about 10,000 ticks. This hits many machines at once on a shared grid.
| public static final Set<String> RESERVED = Set.of("automation", "features", | ||
| "redstone_control", "redstone_mode", "id", "components", | ||
| "mmdlib_features", "mmdlib_machine"); | ||
| "mmdlib_features", "mmdlib_machine", "mmdlib_ghost_items", "multiblock", |
There was a problem hiding this comment.
Adding 'multiblock', 'multiblock_view' and 'mmdlib_ghost_items' to RESERVED makes MachineDefinitions reject existing XML machines whose items or fluids storage id uses one of those names.
Failure scenario: An existing 2.0.1 XML machine with or loaded before this change. After it, definition loading fails with a reserved-key error and the machine disappears from the pack.
| features.reset(); | ||
| if (definition | ||
| .dismantling() == zone.moddev.mc.lib.machine.multiblock.MultiblockDefinition.Dismantling.LEAVE_BLOCKS) | ||
| return; |
There was a problem hiding this comment.
With a LEAVE_BLOCKS definition, the demo's dismantling listener returns before dropping its item storage, and MultiblockBlock has no DropsContents handling. The controller's contents are deleted when the controller is broken.
Failure scenario: A MultiblockDemoEntity is built with a LEAVE_BLOCKS definition (the constructor accepts one, and tests use this). A player breaks the controller while it holds items. dismantle runs the listener, which returns early, then vanilla removes the block and BE, and the stacks in 'items' never drop.
MMDLib consumers need reusable support for multi-block machines, state-driven appearances, richer recipe inputs, and physically unavailable resource/redstone faces. This adds those APIs and XML options, companion examples, documentation, and regression coverage in one delivery.
Changes
consumed: false) with reservation handling.Integration and compatibility
master-2.0-1.21.1-neothrough1ada8db(2.0.1 release preparation), including the updated formatting configuration.Validation
spotlessApply,spotlessCheck, andgit diff --checkagainst the target branch.