From 5af765de5317193a67d8d9307db0dc30501b7219 Mon Sep 17 00:00:00 2001 From: Face o' Cat Date: Sat, 31 Oct 2026 23:04:41 +0200 Subject: [PATCH 01/21] test: match definition-loader messages on Windows paths Duplicate-definition messages name file-system paths, which print with backslashes on Windows, so the fragment check failed there. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018zpP6ZrFjaGe477FvWAZKo --- .../moddev/mc/lib/common/builders/DefinitionLoaderTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/builderTest/java/zone/moddev/mc/lib/common/builders/DefinitionLoaderTest.java b/src/builderTest/java/zone/moddev/mc/lib/common/builders/DefinitionLoaderTest.java index 867a60e..e5d7b60 100644 --- a/src/builderTest/java/zone/moddev/mc/lib/common/builders/DefinitionLoaderTest.java +++ b/src/builderTest/java/zone/moddev/mc/lib/common/builders/DefinitionLoaderTest.java @@ -291,7 +291,9 @@ private static void expectFailure(Runnable action, String... fragments) { try { action.run(); } catch (IllegalArgumentException ex) { - for (var fragment : fragments) require(ex.getMessage().contains(fragment)); + // Messages name file-system paths, which use backslashes on Windows. + var message = ex.getMessage().replace('\\', '/'); + for (var fragment : fragments) require(message.contains(fragment)); return; } throw new AssertionError("Bad definitions accepted"); From f74216561b0f6fe66f294aa30db0a21c6a085718 Mon Sep 17 00:00:00 2001 From: Face o' Cat Date: Sat, 31 Oct 2026 23:04:42 +0200 Subject: [PATCH 02/21] docs: machine module design and current machine docs Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018zpP6ZrFjaGe477FvWAZKo --- .gitignore | 2 + CLAUDE.md | 32 + docs/capabilities.md | 11 +- docs/gui-screens.md | 19 +- docs/machine-module/README.md | 21 + docs/machine-module/comparison.md | 150 +++++ docs/machine-module/data-only-machines.md | 632 ++++++++++++++++++ docs/machine-module/machine-ui.md | 359 ++++++++++ docs/machine-module/rebuild-plan.md | 342 ++++++++++ docs/machine-module/recipes.md | 281 ++++++++ docs/machine-module/stage-1-plan.md | 443 ++++++++++++ docs/machines.md | 187 ++++++ docs/mod-author-guide.md | 3 +- docs/roadmap.md | 6 + .../moddev/mc/lib/gui/runtime/ScreenHost.java | 9 + 15 files changed, 2486 insertions(+), 11 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/machine-module/README.md create mode 100644 docs/machine-module/comparison.md create mode 100644 docs/machine-module/data-only-machines.md create mode 100644 docs/machine-module/machine-ui.md create mode 100644 docs/machine-module/rebuild-plan.md create mode 100644 docs/machine-module/recipes.md create mode 100644 docs/machine-module/stage-1-plan.md create mode 100644 docs/machines.md diff --git a/.gitignore b/.gitignore index 43e9f6c..6003c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ # Local tools (google-java-format for ./gradlew format) /tools/*.jar + +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3b8d546 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,32 @@ +# MMDLib + +Data-driven material and machine library for **NeoForge 1.21.1 / Java 21**. Package root +`zone.moddev.mc.lib`. Docs are in `docs/`; start with +[docs/mod-author-guide.md](docs/mod-author-guide.md). + +## Machine-related docs + +| Topic | Doc | Code | +|---|---|---| +| How storage, capabilities, screens and recipes fit together | [machines](docs/machines.md) | | +| Block-entity definitions, handles, `mmdlib:machine` | [block entities](docs/block-entities.md) | `api`, `common.runtime` | +| `ItemStorage`, `FluidStorage`, `EnergyBuffer`, `SideConfig`, `MachineCapabilities`, `Transfer` | [capabilities](docs/capabilities.md) | `api.capabilities` | +| XML screens, `ScreenHost`, menu, sync, JEI | [GUI screens](docs/gui-screens.md), [sprites](docs/gui-sprites.md), [gauges](docs/gauge-textures.md) | `gui.*`, `client.jei` | +| `gui-click` / `gui-hover` and other handlers | [method events](docs/method-events.md) | | +| `mmdlib:crushing` | [runtime behaviours](docs/runtime-behaviors.md) | `common.recipes` | +| Proposed machine module: ingredients, recipes, transactions, JSON-only machines, 26.x/Fabric plan | [machine module](docs/machine-module/README.md) | not built | + +## Facts that are easy to get wrong + +- MMDLib has no machine-ingredient or machine-recipe abstraction; recipes are vanilla `Recipe` + types with vanilla `Ingredient`s, and processing is the mod's own ticker. Everything under + `docs/machine-module/` is a design; none of its types exist yet. +- Every face exposes every slot with the same numbering; `IoMode` and `Role` only allow or refuse + operations. Out-of-range indexes are refused, not thrown. +- The block entity bypasses roles and rates (`insertResult`, `tank(i)`, `consume`); automation + goes through `view(...)`. +- Screens show values only through public getters named in the XML; the client never calls them. +- `ScreenHost.screenSides()` keys are the element ids in ``; combining several + group `SideConfig`s into one capability view is the mod's job. +- Format with `./gradlew format` (needs `tools/gjf.jar`); `./gradlew check` runs the regression + checks. diff --git a/docs/capabilities.md b/docs/capabilities.md index e0843c9..643821a 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -90,10 +90,13 @@ is checked every tick, keep a NeoForge `BlockCapabilityCache` instead. ## Notes -- **Planned: side configuration per resource group.** The GUI design configures sides per - group of slots or tanks (item input, item output, a tank, energy), Industrial Foregoing - style. Today a `SideConfig` applies to a whole storage; views will gain a per-slot (and - per-tank) mode so each group can have its own `SideConfig` without changing slot numbering. +- **Side configuration per resource group.** The screen's `` view edits one + `SideConfig` per group (item input, item output, a tank, energy), Industrial Foregoing style; + the block entity returns them from `ScreenHost.screenSides()`. A view still takes one mode per + face for the whole storage, so the mod's `sides` function decides how groups combine. When + groups differ by `Role`, take insert from the input group and extract from the output group; + see [machines](machines.md#side-configuration-with-several-groups-on-one-storage). A per-slot + mode in the views, which would remove that step, is not implemented. - `ItemStorage` keeps the slot count given to its constructor. Loading a save that has more slots drops the stacks that no longer fit. `FluidStorage` loads its tanks by index. diff --git a/docs/gui-screens.md b/docs/gui-screens.md index 7a3e0e8..3bfa109 100644 --- a/docs/gui-screens.md +++ b/docs/gui-screens.md @@ -71,8 +71,14 @@ show empty filters. No real items move through this path. Bind the block's `block-right-click` event to `mmdlib:open_menu`. This handler first looks for an XML screen matching the clicked block entity's type; otherwise it uses the block's own vanilla -menu provider. It consumes matching clicks on both sides and opens only on the server. A direct -code caller may use `ScreenRuntime.open(player, blockEntity)`. +menu provider. It consumes matching clicks on both sides and opens only on the server. A click +holding a fluid container on a face that has a fluid handler is left alone, so the block's own +item use can fill or empty the tank; MMDLib's `MaterialEntityBlock` does not do that itself (see +[machines](machines.md#buckets-and-fluid-containers)). A direct code caller may use +`ScreenRuntime.open(player, blockEntity)`. + +`screenSides()` keys must equal the element `id`s named in ``; a group +with no entry in the map is listed, but shows no modes and ignores clicks. Click extension methods use event type `gui-click` and signature `public static void method(ScreenClick click)`. The context supplies the player, menu, element, mouse button, Shift/Control/Alt state, and selected @@ -160,10 +166,11 @@ a compact meter, a few controls, an upgrade tab, and a side-configuration view: - - - - + + + + + diff --git a/docs/machine-module/README.md b/docs/machine-module/README.md new file mode 100644 index 0000000..e1eb8c3 --- /dev/null +++ b/docs/machine-module/README.md @@ -0,0 +1,21 @@ +# Machine module (proposed) + +> **Status: design only, nothing here is implemented.** Today's machine support is described in +> [machines](../machines.md), [capability wrappers](../capabilities.md) and +> [GUI screens](../gui-screens.md). + +A planned layer on top of MMDLib's storages and screens: machine ingredients and recipes that +cover items, fluids and energy, transactions, a processing loop, and later machines defined +entirely in JSON. It rebuilds the idea of Tesla-Core-Lib's unfinished `net.modcrafters.mclib` +and targets NeoForge 1.21.1, NeoForge 26.1 and Fabric 26.3. + +Read in this order: + +| Page | What it covers | +|---|---| +| [Comparison](comparison.md) | Tesla-Core-Lib and MMDLib side by side as the code stands; what is worth taking from each | +| [Rebuild plan](rebuild-plan.md) | Decisions, the core/vanilla/loader layering, core interfaces, per-build differences, Stonecutter layout, phases 1–8 | +| [Recipes](recipes.md) | How ingredients, groups, matching, the processor and transactions work, with a crusher end to end | +| [Data-only machines](data-only-machines.md) | Ports, targets, actions, the plan → check → commit → apply cycle, four complete JSON machines, phases 9–14 | +| [Machine UI](machine-ui.md) | The screen pieces those machines need, face config with face cards, internals (parts in sockets that change stats), phases 15–18 | +| [Stage 1 plan](stage-1-plan.md) | Step-by-step implementation plan for the first build: internals without GUI, and phase 1 (ingredients, groups, transactions, matcher) | diff --git a/docs/machine-module/comparison.md b/docs/machine-module/comparison.md new file mode 100644 index 0000000..ee18ded --- /dev/null +++ b/docs/machine-module/comparison.md @@ -0,0 +1,150 @@ +# Tesla-Core-Lib and MMDLib compared + +> **Status: reference.** Describes the code as it stands in Tesla-Core-Lib (branch `1.12`, commit +> `b418bea`) and MMDLib (branch `master-2.0-1.21.1-neo`, commit `9083780`), 31 October 2026. + +Both libraries give a mod the machine plumbing: storage, automation access, a GUI and recipe +inputs. They come from different eras and make opposite choices in several places. + +| | Tesla-Core-Lib | MMDLib | +|---|---|---| +| Branch | `1.12` | `master-2.0-1.21.1-neo` | +| Platform | Minecraft 1.12.2, Forge, Kotlin | Minecraft 1.21.1, NeoForge, Java 21 | +| Shape | Machine base classes (`SidedTileEntity` and up), code-built GUI pieces, multi-mod energy adapters, plus `net.modcrafters.mclib`, an unfinished 2017 rewrite holding the ingredients/inventory system | Composable storages (`ItemStorage`, `FluidStorage`, `EnergyBuffer`) with per-face views, XML-defined screens with a generic menu, and data-defined machines | + +Tesla-Core-Lib's own docs: `Tesla-Core-Lib/docs/gui.md` and +`Tesla-Core-Lib/docs/mclib-ingredients-inventory.md`. + +## In short + +- **The GUI moved from code to data.** Tesla-Core-Lib builds every screen from Kotlin pieces at + fixed pixel positions. MMDLib reads an XML file with boxes and groups, checks it at startup, + and binds displayed values to getters on the block entity. +- **Faces stopped renumbering slots.** In Tesla-Core-Lib a face exposes only the slots of colours + enabled on it, so slot numbers differ per face. In MMDLib every face shows every slot, and the + face mode only allows or refuses operations. +- **The ingredient abstraction exists in only one of them, and is unfinished.** mclib defines one + ingredient type covering items, fluids and energy, with amount-aware matching. Its item + extraction and recipe matching are stubs and nothing uses it. MMDLib has no such layer: recipes + use vanilla `Ingredient`. +- **Tesla-Core-Lib ships more machine behaviour.** Pause, redstone control, lockable inventories, + bucket fill/drain from the GUI and multi-mod energy units are built in. MMDLib leaves those to + the mod and provides the slots, widgets and handler hooks for them. + +## Side by side + +### GUI + +| Aspect | Tesla-Core-Lib | MMDLib | +|---|---|---| +| How a screen is defined | Kotlin: the tile returns a list of `IGuiContainerPiece`s and a list of slots, both built from its inventories | XML under `mmdlib/screen/`, validated against an XSD and checked at startup with file and line in errors | +| Layout | Absolute pixels in a 198 × 184 panel; UVs hard-coded into one texture | `vbox`/`hbox`, groups, `fill` lengths, slot units (`3.5s`); nine-slice sprites | +| Showing values | Pieces read the client-side tile; each value must be a named NBT sync part with a sync level | Getters named in the XML run on the server each tick; only changed values are sent, with no size limit (`BigInteger`) | +| Buttons and actions | Piece sends an NBT message type; tile handles it in `processClientMessage` | `on-click` names a `gui-click` handler; the server checks player, menu and element first | +| Navigation | Side drawers on the left edge; the side configurator replaces the player inventory while open | Context areas with swappable views, side tabs, info/warning/error pop-outs | +| Shift-click routing | First machine slot that accepts the stack, in slot order | `ScreenHost.screenDestinations(stack)` gives the order per stack | +| JEI | None | Built in: ghost filters, recipe click areas, "+" transfer, exclusion areas; optional | +| Text | Translation keys built in code | Every text attribute is a key; optional `-fallback`; pop-out markup | + +### Inventory and automation + +| Aspect | Tesla-Core-Lib | MMDLib | +|---|---|---| +| Item storage | One `ColoredItemHandler` per group, merged by `SidedItemHandler` into consecutive slot numbers | One `ItemStorage` with a `Role`, filter and limit per slot | +| Per-slot rules | Input and output filter lambdas per inventory | `STORAGE`, `INPUT`, `OUTPUT`, `MANUAL` roles; the machine bypasses them with `insertResult` | +| Side configuration unit | A dye colour per inventory or tank, on or off per face | A `SideConfig` per group, four modes per face (disabled, input, output, both) | +| What a face exposes | Only slots of enabled colours, renumbered from 0 | Every slot, same numbering; out-of-range indexes refused, not thrown | +| Several groups on one face | Automatic: each colour's slots appear when that colour is on | The mod's job: the capability lambda combines group modes ([machines](../machines.md#side-configuration-with-several-groups-on-one-storage)) | +| Locked / filtered slots | `LockableItemHandler`: lock remembers current items as filters, ghost-rendered | Ghost slots; the block entity stores and validates filters via `ScreenHost` | +| Upgrades | Addon inventory; addons can add capabilities (`ICapabilityAddon`) | One-item upgrade slots in a side tab; meaning belongs to the mod | +| Pause and redstone control | Built in: pause drawer and redstone toggle on every machine | The mod's job: checkbox and radio widgets exist; no handler ships | + +### Fluids and energy + +| Aspect | Tesla-Core-Lib | MMDLib | +|---|---|---| +| Tanks | `ColoredFluidHandler` per tank; `FluidTankType` input/output/both; each tank its own colour | `FluidStorage`: several tanks, one handler, a role and filter per tank | +| Buckets | Built in: click a tank in the GUI to fill or drain; a bucket slot pair; right-click with a bucket | Half: `open_menu` steps aside for fluid containers, but nothing fills the tank; the mod's block must | +| Energy | `long` storage; adapters for Tesla, Forge Energy, RF, MJ, Mekanism; GUI unit switch | `EnergyBuffer`, `long` inside, Forge Energy outside (clamped to `int`) | +| Moving things to neighbours | Per-machine code | `Transfer`: push/pull items, fluids, energy | + +### Recipes and ingredients + +| Aspect | Tesla-Core-Lib | MMDLib | +|---|---|---| +| Machine ingredient type | Unfinished: `IMachineIngredient` for items, fluids, energy; amount-aware matching | None: vanilla `Ingredient` | +| Machine inventories for recipes | Stub: `IMachineInventory`; item extraction always returns nothing | None: the storages themselves | +| Recipe registry | Unused: Forge registry per recipe type with find/remove helpers; input matching is empty | Vanilla datapack recipe types; ships `mmdlib:crushing` | +| JSON crafting extensions | `fluid_container` and `ore_dict_ex` ingredients, `ore_dict` condition | Tags and NeoForge ingredient types cover this | + +## What one face sees + +The same machine in both libraries: two inputs, one output, one upgrade. The bottom face is set +to output only, and automation asks the bottom face for its slots. + +| Machine slot | Tesla-Core-Lib (orange colour on bottom) | MMDLib (output group: bottom = output) | +|---|---|---| +| input | not exposed | slot 0, insert refused | +| input | not exposed | slot 1, insert refused | +| output | slot 0, extract | slot 2, extract | +| upgrade | not exposed | slot 3, manual only | + +In Tesla-Core-Lib the face has one slot, numbered 0. Turn on the blue (input) colour there too +and the output becomes slot 2, so an automation program written against slot 0 now points at an +input. In MMDLib the face always has four slots numbered 0 to 3; the face mode refuses inserts +and the slot roles refuse the rest. Changing the configuration never moves a slot. + +## mclib, the ingredients and inventory system + +Tesla-Core-Lib's "new" system is the `net.modcrafters.mclib` package. Its idea is worth knowing, +because MMDLib has nothing in its place. + +As designed: + +- A recipe lists inputs per inventory key: `"input"`, `"water"`, `"energy"`. +- The machine wraps its item handlers, tanks and energy as `IMachineInventory`s with the same keys. +- Each slot reads as an `IMachineIngredient`; items, fluids, fluid-in-container and energy all + match each other through one call. +- Matching takes an amount rule: exact, enough, or ignore size. +- Items can be written as strings: `modid:path$meta#count`, `__ore:ingotCopper#3`, wildcards like `*gear*`. + +```kotlin +val required = FluidIngredient(FluidStack(WATER, 1000)) +val available = tank.fluid?.asIngredient() ?: NoIngredient +required.isEnough(available) // 1000 <= tank amount +``` + +As it stands: + +| Part | State | +|---|---| +| Energy storage | Works; the only part Tesla-Core-Lib itself uses | +| Ingredient matching between items, fluids and energy | Works | +| String parsing, ore and registry-mask ingredients | Works | +| Fluid extraction | Partial: ignores the slot; no item-to-fluid extraction | +| Item extraction | Stub: always returns nothing | +| Recipe input `isMatch` | Stub: returns `Unit`; shaped input is empty | +| Feature sync | Broken: sent but never received; ticking work cannot be overridden | + +Ten defects in total; the full list is in `Tesla-Core-Lib/docs/mclib-ingredients-inventory.md`, +and [the rebuild plan](rebuild-plan.md#mclibs-defects-and-why-they-dont-come-back) says how each is avoided. + +## Worth taking, worth leaving + +- **Take: one ingredient type across resources.** Matching items, fluids, fluid containers and + energy through one interface with an amount rule would give MMDLib machines a common + recipe-input layer. It needs writing fresh; the 1.12 code is a sketch. See + [the rebuild plan](rebuild-plan.md). +- **Take: tank interaction from the screen.** Clicking a fluid gauge with a held bucket to fill or + drain it, as `FluidTankPiece` does. MMDLib now lets buckets through `open_menu` but has no code + that fills the tank. +- **Take: stock machine controls.** Pause and redstone control are on every Tesla-Core-Lib + machine. A shipped MMDLib handler would save each mod writing its own. +- **Take: a per-slot face mode.** Tesla-Core-Lib's colours apply to their own slots + automatically. MMDLib's group configs need the mod to combine them in the capability lambda; a + per-slot mode in the views would remove that step. +- **Leave: renumbering slots per face.** It breaks remote-access mods when a player changes the + configuration. MMDLib's fixed numbering is the better rule. +- **Leave: hard-coded atlas positions.** Every Tesla-Core-Lib piece blits fixed UVs from one + texture, so retexturing means keeping those coordinates. MMDLib's named, nine-sliced sprites + avoid that. diff --git a/docs/machine-module/data-only-machines.md b/docs/machine-module/data-only-machines.md new file mode 100644 index 0000000..ceef58a --- /dev/null +++ b/docs/machine-module/data-only-machines.md @@ -0,0 +1,632 @@ +# Machine module: data-only machines + +> **Status: proposal, not built.** Builds on the [rebuild plan](rebuild-plan.md) and +> [recipes](recipes.md): `MachineTx`, slot groups, the three ingredient kinds and the per-loader +> adapters all stay, and everything here sits on top of them. JSON keys are proposed names; +> `//` lines in JSON blocks are annotations, not part of the file. + +The aim is a mod like Industrial Foregoing written as JSON. A machine is one file. Each of its +operations picks things in the world to work on, works out what it will take and give, and then +does all of it at once. Recipes stop naming slots and start naming **ports**, so the same recipe +works whether wheat comes from the machine's own slots, a chest above it, or a teammate's hotbar. + +## Three questions, one operation + +Every machine behaviour answers three questions: where does stuff come from and go to, what in +the world is being worked on, and what happens to it. Keep those apart and an operation is just +the glue. + +```mermaid +flowchart LR + subgraph Ports["Ports: where stuff comes from and goes"] + p1[Own slot groups] + p2[Neighbors on each face] + p3[Target's inventory] + p4[Items / XP on the ground] + p5[World fluids] + p6[Linked machines] + end + subgraph Op["Operation, every N ticks"] + plan[Plan: pick targets, compute amounts, roll loot] + check[Check: reserve inputs and output space] + commit[Commit: take inputs, store outputs] + apply[Apply: change the world] + plan --> check --> commit --> apply + end + subgraph Targets["Targets in the area"] + t1[Entities: EntityPredicate] + t2[Blocks: BlockPredicate] + t3[Players: team + opt-in] + t4[Positions: grid, empty ground] + t5[Items and XP on the ground] + end + subgraph Actions["Actions on each target"] + a1[breed · shear · milk · kill] + a2[harvest · place · bonemeal] + a3[use_item_on · run_function] + a4[set_var · sound · effect] + end + Targets -- select, filter --> plan + Ports -- reserve --> check + commit -- take, store --> Ports + apply -- perform --> Actions +``` + +All four steps run inside one tick call. "Target's inventory" is how a player or villager becomes +a source: the port points at whatever the operation is currently working on. + +| Word | Means | Lives in | +|---|---|---| +| Port | A name an operation consumes from or produces into, bound to an ordered list of sources | `ports` | +| Source | One concrete place: a slot group, a neighbor face, the target's hotbar, a tank | inside a port | +| Target | The thing an operation works on this run: an entity, block, player, position or item on the ground | `operation.target` | +| Action | A registered, two-step effect: check it can happen, then do it | `operation.act` | +| Operation | One repeating job: target + when + consume + act + produce + drops | `operations` | +| Variable | A synced, saved value the machine owns; actions write it, conditions and screens read it | `variables` | +| Modifier | A number that comes from installed upgrades: speed, range, looting | `modifiers` | + +## Ports + +A port is an ordered list of sources. The transaction walks them in order and may split one input +across several. Ports work both ways, so the same mechanism that takes food from a teammate can +top up their torches or take their cobblestone away. + +```json +"ports": { + "feed": ["feed", { "neighbors": "up" }, { "target_inventory": "main", "keep": 16 }], + "output": ["output", { "neighbors": "down" }], + "hotbar": [{ "target_inventory": "hotbar" }] +} +``` + +Example: an operation consumes `feed: 12 × wheat`. + +| Order | Source | Holds | Takes | +|---|---|---|---| +| 1 | Own slots `feed` | 4 | 4 | +| 2 | Chest above (`neighbors: up`) | 30 | 8 | +| 3 | Teammate in range, `keep: 16` | 40 | 0, not reached | + +If all three together came up short, nothing would be taken from any of them and the operation +would stall. + +| Source | Way | How it's reached | Rules | +|---|---|---|---| +| Slot group (`"feed"`) | in / out | The machine's own storage | Filters apply; roles don't, as for any internal access | +| `neighbors` | in / out | Capability per face (NeoForge), `BlockApiLookup` (Fabric) | The neighbor's own rules apply. `"*"` means all faces, in a fixed order | +| `target_inventory` | in / out | Player inventory wrapper; entity capability (NeoForge) or `EntityApiLookup` (Fabric); vanilla `InventoryCarrier` for villagers | Slots: `main`, `hotbar`, `armor`, `offhand`. Optional `keep`. Players only through a consenting player target | +| `ground` | in | Item entities inside the area | Same items a `pick_up` action would see | +| `xp` | in / out | XP orbs in the area, or the target player's levels | Conversion rate set on the port, e.g. 20 mB per point | +| `world_fluid` | in / out | Source blocks in the area | Whole buckets only (1000 mB). Placing respects claims | +| `linked` | in / out | Positions stored on a link-card item in a slot | Same dimension, loaded chunks only. The linked machine's own ports are not followed | + +## Targets + +The area belongs to the machine: a box or sphere anchored in front of it, on top of it, or +centered. A range upgrade grows it. Blocks are found by a cursor that walks the area a few +positions per tick, so a large farm costs the same per tick as a small one. Entities are found +with one box query per run. Positions in unloaded chunks are skipped, never loaded. + +```json +"area": { + "shape": "box", "size": [9, 1, 9], "anchor": "front", + "grow": { "upgrade": "example:range", "per_level": [1, 0, 1], "max_level": 2 } +}, +"operations": { + "harvest": { + "target": { + "blocks": { "blocks": "#minecraft:crops", "state": { "age": "7" } }, + "pick": "cursor", "scan_per_tick": 16, + "limit": 1 + } + } +} +``` + +The `blocks` object is vanilla's `BlockPredicate`, the same format advancements use. + +| Key | Values | Use it for | +|---|---|---| +| `pick` | `nearest`, `random`, `round_robin`, `all`, `first` | Entities and players. `round_robin` spreads milking across a herd | +| | `cursor` | Blocks and positions; walks the area across ticks (a snake, row by row) | +| | `pair_same_type` | Breeding: two adults of one type that can both breed | +| `limit` | number | Targets per run. Costs marked `"per": "target"` scale with it | +| `filter` | list of conditions | Checks the predicate can't express, like `mmdlib:can_breed` | +| `cooldown_per_target` | ticks | Kept in the machine's memory by UUID or position, not on the entity. Pruned when targets leave | +| `expand` | `connected`, `also`, `max`, `order` | Turns one hit into a group: a whole tree, a vein, a sugar-cane column | + +## Plan, check, commit, apply + +You can roll back an inventory but not a sheared sheep. So everything that can fail happens +before anything is changed, and the world is touched last. Loot is rolled during the plan, so the +check knows the exact drops instead of guessing. + +```mermaid +flowchart LR + P[Plan
pick targets, evaluate $vars
roll loot now] --> C[Check
reserve inputs through ports
reserve space for drops] + C --> M[Commit
take inputs, store outputs
one MachineTx] + M --> A[Apply
run actions on targets] + A --> D([Done: progress resets]) + P -. nothing to do .-> I([Idle]) + C -. inputs short .-> S([Stalled]) + C -. output full .-> B([Blocked]) + M -. exception .-> R([Rolled back: nothing applied yet]) +``` + +Plan to Apply happens inside one tick call of one machine, so nothing else changes the world in +between. States are per operation: a ranch can be Blocked on milk while shearing keeps running. +When the loader only reveals drops at apply time (NeoForge 1.21.1's `IShearable`, +`use_item_on`), the plan reserves the most that action can produce instead. + +## Actions + +An action is a registered type with a codec, a check and an apply, like vanilla's loot functions. +Mods add their own the same way. World changes go through a stand-in player that represents the +machine's owner, so claim and protection mods can refuse them through the loader's normal break, +place and attack events. + +| Action | Target | Check (plan) | Apply | Drops | +|---|---|---|---|---| +| `mmdlib:breed` | 2 animals | adult, not in love, can breed | set in love, owner as cause | none | +| `mmdlib:shear` | entity | `readyForShearing()` | mark sheared, play sound | shearing loot, rolled in plan | +| `mmdlib:harvest` | block | matches, owner may break it | remove without drops, break particles | `Block.getDrops` with the tool, rolled in plan | +| `mmdlib:place` | position | replaceable, block can survive there | place as owner | none | +| `mmdlib:kill` | entity | never a player; not protected by filters | damage as owner with weapon and looting | entity loot, captured by the loader adapter | +| `mmdlib:pick_up` | item or XP on ground | still there | remove the entity | the stack or XP | +| `mmdlib:use_item_on` | entity or block | item on the mod's allow-list | stand-in player right-clicks with the item | whatever it leaves behind; space reserved for the worst case | +| `mmdlib:apply_effect` | entity or player | effect not already stronger | add `MobEffectInstance` | none | +| `mmdlib:set_var`, `add_var` | none | always | write a machine variable | none | +| `mmdlib:play_sound`, `particles` | any | always | client feedback | none | +| `mmdlib:run_function` | any | function exists | runs a datapack `.mcfunction` at the target | none | + +`use_item_on` alone covers milking, bonemealing, tilling, sowing, dyeing and bucketing fish, but +it is also where duplication bugs live. Each mod lists the items it allows; nothing is allowed by +default. + +## Context variables and expressions + +A feeder needs *this animal's* food, a mob crusher's cost should grow with the mob's health, and +a restocker takes only what's missing. Any number or item in an operation may be a `$` reference +or a short expression. The grammar is deliberately small: numbers, variables, `+ - * /`, +comparisons, `and` / `or` / `not`. No loops, no strings, no calls. + +| Reference | Value | Example | +|---|---|---| +| `$target` | The current target. Fields: `.max_health`, `.health`, `.type`, `.pos` | `"200 + 2 * $target.max_health"` | +| `$targets.count` | How many targets this run selected | a head count | +| `$consumed.` | The stack or fluid actually taken, with `.count` | place the sapling that was taken | +| `$next.` | What would be taken next, without taking it | check the next sapling can be placed | +| `$rule` | The current ghost slot in a `for_each`: `.item`, `.count` | "keep 32 torches in the hotbar" | +| `$` | A machine variable | `"$adults < $max_adults"` | +| `$` | A modifier from upgrades | `"looting": "$looting"` | +| `let` | Named values computed in the plan, in order | `"missing": { "type": "mmdlib:missing", ... }` | + +Context-dependent ingredients are ordinary ingredient types that read the target: +`mmdlib:target_food` matches whatever the current animal eats, and drops come from the target's +loot table. They plug into `ItemPart` as the ingredient, so `AmountMatch` and ports work unchanged. + +## The machine definition + +MMDLib's existing block-entity definitions already create the block. The new file adds structure +and behaviour. Storage sizes and variables have to exist when the game registers capabilities and +save data, so they load at startup. Everything about behaviour is datapack content and reloads +with `/reload`. + +| Section | Produces | Loads from | +|---|---|---| +| `groups` | Item, fluid and energy storages, exposed as capabilities | startup | +| `variables` | Synced, saved values for screens and conditions | startup | +| `area` | The box or sphere the machine reaches, grown by upgrades | datapack | +| `modifiers` | Speed, cost, range and looting from upgrade items | datapack | +| `ports` | The ordered sources behind each port | datapack | +| `operations` | The work list: target, when, consume, act, produce | datapack | +| `screen` | The XML screen, bound to `$variables` and `#getters` | resource pack | + +[Machine UI](machine-ui.md#three-ways-to-change-a-machine-one-set-of-stats) folds `modifiers` +into named stats fed by upgrades, engine parts and face cards, and adds `faces` and `internals` +sections; the examples below still use `modifiers` and fixed `neighbors` for brevity. + +The startup part lives with the block-entity definition; the datapack part under +`data//mmdlib/machine/`. A reload that changes the datapack part restarts each operation's +progress but never touches stored items. + +Screens gain values bound to machine variables with `$`, next to today's `#getter` bindings, plus +built-in handlers to edit them. Element names are today's: + +```xml + + + +