Skip to content

Commit f4841ac

Browse files
authored
Merge pull request #96 from MinecraftModDevelopmentMods/codex/architectural_sanity
Add shared powered machines, inherited screens and inventory safeguards
2 parents ff9e6c4 + f0d1af4 commit f4841ac

71 files changed

Lines changed: 4409 additions & 659 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/block-entities.md‎

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,117 @@ The block delegates `newBlockEntity` and `getTicker` to `binding.prepared()`, as
131131
it, `DropsContents` is never called and nothing reports the missing hook. It reads `binding.type()` only after registration (for example
132132
when a block entity is created), never in its constructor.
133133

134+
## BasePoweredMachine
135+
136+
`zone.moddev.mc.lib.machine.neoforge.BasePoweredMachine` is an optional abstract `BlockEntity`
137+
for code-defined machines. It implements `ScreenHost`, `DropsContents` and `Clearable`. It owns
138+
`energy` and `features`, exposes their screen/statistics bindings, and supplies ticking, persistence,
139+
feature synchronization and common warnings. It does not register blocks, capabilities, recipes
140+
or a recovery item for the consuming mod.
141+
142+
The default constructor configures 100,000 FE capacity, 1,000 FE input and no external output.
143+
The overload `(type, pos, state, capacity, maxInput, maxOutput)` accepts different values, including
144+
an output-only generator. The owning machine's energy operations bypass external transfer rates.
145+
146+
Define items/tanks and resource groups in the subclass, then connect them once:
147+
148+
```java
149+
// In a BasePoweredMachine subclass constructor, after defining the three ResourceGroups:
150+
resources = new MachineResources(this, this::facing)
151+
.items(inputGroup, items)
152+
.items(outputGroup, items)
153+
.energy(energyGroup, energy)
154+
.recipeInputs(MyRecipes.PRESSING, "input")
155+
.build();
156+
features.add(new RecipeProcessor("work", MyRecipes.PRESSING, resources.recipes(),
157+
Component.translatable("example.press.processor")));
158+
inventory = poweredInventory(resources.automation())
159+
.items("items", items)
160+
.fluids("fluids", fluids);
161+
ScreenInventoryValidation.validate(this);
162+
163+
// Subclass methods; inventory is the configured MachineInventoryLifecycle field.
164+
@Override public ItemStorage screenItems() { return items; }
165+
@Override protected MachineInventoryLifecycle machineInventory() { return inventory; }
166+
@Override protected void recoverResources(Level level, BlockPos pos, CompoundTag payload) {
167+
// Consumer-owned item that durably retains the complete payload:
168+
Containers.dropItemStack(level, pos.getX() + .5, pos.getY() + .5, pos.getZ() + .5,
169+
MyRecoveryItem.create(payload));
170+
}
171+
```
172+
173+
Group IDs in this fragment are `input`, `output` and `energy`; recipe files must use the matching
174+
keys. See [resource definitions and filtering](inventory-automation.md#shared-machine-resource-definitions).
175+
`MyRecipes` and `MyRecoveryItem` are consumer implementations, not library registrations.
176+
177+
The base automatically registers its own buffer under the save key `energy`, even when the
178+
subclass returns a directly constructed lifecycle instead of using `poweredInventory`. A lifecycle
179+
must belong to this entity and use its `features` and `energy`; conflicting energy bindings are
180+
rejected. Without a lifecycle, the base saves/restores features and energy only. Custom items and
181+
tanks must be registered with the lifecycle to participate in persistence, clearing and dropping.
182+
183+
| Base behavior | Subclass extension |
184+
|---|---|
185+
| `getEnergy()`, `getEnergyCapacity()` | Backed by the owned buffer |
186+
| `getProgress()`, `getTime()`, `getPaused()` | First registered recipe processor; zero/false if none |
187+
| `screenFeatures()`, `screenStats()` | Owned host; statistics include energy and all feature statistics providers |
188+
| `screenAutomation()` | Lifecycle's automation inventory, when configured |
189+
| `getWarnings()` | Redstone processing gate plus `STALLED`, `BLOCKED`, `HELD` processor warnings, using each processor's display name; append custom messages to `super.getWarnings()` |
190+
| `screenRedstone()` | Opt in with a configured `RedstoneControl`; base reads it before processing and updates outputs afterward |
191+
| `processingAllowed()` | Additional processing gate, default true |
192+
| `pausePolicy()` | Default `PROCESSING_ONLY`; controls redstone gating of processing and automation transfers |
193+
| `beforeMachineTick()`, `afterMachineTick()` | Work around the common processing/transfer tick, such as generator refilling and output |
194+
| `saveMachineData()`, `loadMachineData()` | Additional persistent metadata, after the common lifecycle; never used for client-only packets |
195+
| `recoverResources()` | Required durable handoff for displaced storage and transfer escrow |
196+
197+
The final no-argument `serverTick()` is server-only. Register its block ticker with
198+
`(level, pos, state, entity) -> entity.serverTick()`. Custom before/after tick hooks run even when
199+
the processing gate is held, so gate any custom work explicitly if necessary. Manual processor
200+
pause remains independent of the redstone transfer policy.
201+
202+
`MachineInventoryLifecycle` resets features before loading storage, then restores automation and
203+
features, so reservations are rebuilt against loaded contents. It settles owed energy refunds even
204+
when transfers are paused. `clearContent()` destructively clears registered contents, recovery,
205+
automation and feature state. `dropContents()` recovers displaced contents and transfer escrow,
206+
releases plans and drops ordinary items. Normal tank contents and stored power are not converted
207+
to recovery payloads on destruction. Custom blocks still need the `DropsContents.onRemove` hook.
208+
209+
Screen XML extends `mmdlib:base_powered_machine`; `SCREEN_TEMPLATE` exposes that ID in Java.
210+
The final `openScreen(player)` opens the concrete type's screen, not the abstract template.
211+
See [screen inheritance](gui-screens.md#inherited-screens) for the shared energy display,
212+
processor control, info text and placeholders. The mod still supplies a screen under each type ID.
213+
214+
### Additional client state
215+
216+
The base's final update packet/tag methods always include feature synchronization. Extend them
217+
through these protected hooks instead of overriding the packet methods:
218+
219+
```java
220+
@Override protected void saveClientData(CompoundTag tag, HolderLookup.Provider registries) {
221+
tag.putInt("visual_mode", visualMode);
222+
}
223+
@Override protected void loadClientData(CompoundTag tag, HolderLookup.Provider registries) {
224+
visualMode = tag.getInt("visual_mode");
225+
}
226+
// After a server-side change that should reach tracking clients:
227+
syncToClient();
228+
```
229+
230+
Custom data occupies a separate `mmdlib_machine` compound; it cannot replace the root feature or
231+
storage fields. Features restore first, followed by `loadClientData`. Older feature-only packets
232+
leave custom client state alone. World saves never invoke these hooks, and packet loads never
233+
invoke `loadMachineData` or reload inventories. Persist any value that must survive a world reload
234+
separately in the machine-data hooks and call `setChanged()` when changing it; `syncToClient()`
235+
only sends current state. Exact screen values still use the menu's getter synchronization.
236+
134237
## Machine features
135238

136239
The separately packaged [machine companion](machine-companion.md) provides playable examples of
137240
the processor, a one-slot converter, shared lanes and a creative power source.
138241

242+
The manual wiring below is for a plain `BlockEntity`. When extending `BasePoweredMachine`, use its
243+
owned `features` and lifecycle instead of redeclaring the host or overriding final save/tick methods.
244+
139245
A `FeatureHost` runs a machine's `MachineFeature`s, such as a `RecipeProcessor`, and saves each
140246
under its key. The block entity ticks it from `serverTick` with `features.tick(level)`, and saves
141247
and loads it through a `DynamicOps`, after the storages, since a processor checks the loaded
@@ -164,8 +270,9 @@ BlockEntityTicker<CrusherEntity> ticker =
164270

165271
`status()` is one of: `IDLE`, no cycle is running; `RUNNING`, a cycle is running; `STALLED`, a
166272
cycle is waiting for energy and keeps its progress and reservations; `BLOCKED`, a cycle has run its
167-
time but cannot finish, say an input was taken, and retries each tick; and `PAUSED`, held by an
168-
interrupt. `progress()` and `time()` give the ticks done and the ticks the cycle takes, for a
273+
time but cannot finish, say an input was taken, and retries each tick; `HELD`, suspended by a host
274+
gate with progress and reservations retained; and `PAUSED`, held by a manual interrupt.
275+
`progress()` and `time()` give the ticks done and the ticks the cycle takes, for a
169276
screen; `time()` is 0 when idle or paused.
170277

171278
On `/reload` a running cycle finishes as planned, and the next cycle uses the new recipes.
@@ -181,13 +288,17 @@ until `resume()`; `paused()` says whether it is held. It works without power and
181288
status. It also releases the running cycle's reservations, so the items it locked can be taken
182289
out. This is the trap for players: an unpowered machine keeps its inputs and outputs locked, and
183290
the only ways to get them back are an interrupt or breaking the block. A mod should give the
184-
player a button, an `Element.Button` in the screen XML with a `gui-click` handler (see
185-
[GUI screens](gui-screens.md)) that calls `interrupt()` or `resume()`. Resuming reserves the paused
291+
player a generic `<processor-button>` (see [GUI screens](gui-screens.md#generic-processor-control)).
292+
`pause()`, `resume()` and `setPaused(boolean)` expose the same processor operations. For actions
293+
on a hosted processor, use `FeatureHost.setProcessorPaused(key, paused)`: it marks changes dirty
294+
and synchronizes immediately, even while host processing is held. Direct processor calls require
295+
the caller to arrange persistence/sync. Resuming reserves the paused
186296
recipe again if it still can. Progress is kept only if that recipe still exists, can reserve its
187297
inputs and output space again, and has the same `time`; otherwise progress resets.
188298

189-
**Groups.** The processor reaches storage through the inventory's group keys. Groups in one
190-
inventory must not overlap: use one key for each set of slots or tanks. Multiple processor lanes
299+
**Groups.** The processor reaches storage through the inventory's group keys. Physical groups in
300+
`MachineResources` cannot overlap; recipe aliases may expose the same storage through alternate
301+
keys. Avoid describing the same physical input/output twice within one recipe. Multiple processor lanes
191302
can share that inventory and reserve the same slot, provided enough unreserved inputs and output
192303
space remain; each lane holds only its own share of the slot's plan.
193304

@@ -217,7 +328,8 @@ given back.
217328
Custom blocks can call `DropsContents.onRemove(state, level, pos, replacement)` before their
218329
superclass removal hook to use the same server-only, different-block guard.
219330

220-
**Syncing to clients.** Nothing is sent to clients unless the block entity opts in. A feature that
331+
**Syncing to clients.** `BasePoweredMachine` wires this automatically. A plain block entity must
332+
opt in. A feature that
221333
should be readable on the client, for a renderer, particles or a tooltip, returns true from
222334
`syncsToClient()`; `RecipeProcessor` does. The block entity then wires `FeatureSync`:
223335

@@ -243,8 +355,10 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries)
243355
}
244356
```
245357

246-
The host sends at most once every 10 ticks (`FeatureHost.SYNC_INTERVAL`), and always sends the
247-
latest change. Storages are not part of the update.
358+
Tick-driven feature changes are throttled to one send every 10 ticks (`FeatureHost.SYNC_INTERVAL`),
359+
with the latest change retained. Hosted manual pause changes synchronize immediately. Storages
360+
are not part of the feature update. `features.tick(level, false)` still calls each feature's
361+
`suspended(host)` hook, allowing processors to publish `HELD` rather than leaving stale running state.
248362

249363
A feature controls what clients get with two methods. `clientState()` is the state to send, by
250364
default what `state()` saves; a feature overrides it for a moment clients shouldn't see.

‎docs/capabilities.md‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,38 @@ What a plan does to the calls on the storage:
6868
Plans are not saved by the storages. A feature saves its own cycle and puts the plans back when it
6969
loads, so load the storages first.
7070

71+
## Stable storage identities and recovery
72+
73+
Assign durable identities with `items.slotId(index, "input")` and `fluids.tankId(index, "coolant")`.
74+
IDs must be nonblank and unique within that storage. They are save identities, separate from
75+
numeric recipe/menu indexes and from resource-group or XML element IDs.
76+
77+
Saves include `SlotId` / `TankId` where configured. Loading matches identified entries by ID;
78+
an unknown ID never falls back to its former numeric position. Saves without identities load by
79+
index, or through the explicit `deserializeNBT(registries, tag, IntUnaryOperator)` migration
80+
overload. Select that mapping by your machine's save version; it applies only to legacy entries.
81+
Keep slot/tank IDs stable when reordering a definition.
82+
83+
Storage keeps its configured shape. Removed slots/tanks, overflow after reducing limits/capacity,
84+
incompatible colliding contents and undecodable entries are retained in raw `Recovery` data,
85+
instead of being discarded. Subsequent loads retry recovery entries, allowing returning IDs or
86+
registrations to make them usable again. Loading restores existing contents without treating them
87+
as a new insertion through recipe filters.
88+
89+
`recoveryContents()` returns a snapshot. `recover(Consumer<CompoundTag>)` hands displaced contents
90+
to a durable consumer-owned sink, clearing them only after the callback succeeds. `StorageRecovery`
91+
uses the same `resources` envelope as automation escrow, with `mmdlib:items` / `mmdlib:fluids`
92+
payloads. MMDLib does not register a recovery item; the companion demonstrates a Recovery Capsule.
93+
Use `MachineInventoryLifecycle` or [BasePoweredMachine](block-entities.md#basepoweredmachine) to
94+
coordinate storage, reservations, saving, clearing and this handoff. Explicit `clear()` destroys
95+
both ordinary and recovery contents; stop/reset owning features first.
96+
97+
External admission rules can be combined with `restrict(predicate, indexes...)`. Recipe-linked
98+
input rules are installed by [MachineResources](inventory-automation.md#shared-machine-resource-definitions).
99+
They supplement existing filters and reservations. Machine products use `insertResult` for items
100+
or `fillResult` for fluids to bypass external admission rules; fluid products still obey the
101+
tank's structural filter and capacity. Restoring a saved item explicitly can use `restoreStack`.
102+
71103
## Example
72104

73105
```java
@@ -126,10 +158,11 @@ is checked every tick, keep a NeoForge `BlockCapabilityCache` instead.
126158
the block entity returns them from `ScreenHost.screenSides()`. A view still takes one mode per
127159
face for the whole storage, so the mod's `sides` function decides how groups combine. When
128160
groups differ by `Role`, take insert from the input group and extract from the output group.
129-
A per-slot mode in the views, which would remove that step, is not implemented.
161+
Per-slot/per-tank mode functions are also supported. Prefer `MachineResources` and
162+
`automation.mode(type, index, side)` for independent groups sharing a storage.
130163

131-
- `ItemStorage` keeps the slot count given to its constructor. Loading a save that has more
132-
slots drops the stacks that no longer fit. `FluidStorage` loads its tanks by index.
164+
- `ItemStorage` keeps its configured slot count and `FluidStorage` its configured tanks. Entries
165+
that cannot be restored remain in recovery; see stable identities above.
133166
- `EnergyBuffer` stores up to `Long.MAX_VALUE`. Screens show the full `long` through a getter
134167
such as `#getEnergy` (`stored()`), while other mods see at most `Integer.MAX_VALUE`.
135168
- The runtime smoke test covers roles, filters, limits, faces, out-of-range indices, transfer,

0 commit comments

Comments
 (0)