@@ -131,11 +131,117 @@ The block delegates `newBlockEntity` and `getTicker` to `binding.prepared()`, as
131131it, ` DropsContents ` is never called and nothing reports the missing hook. It reads ` binding.type() ` only after registration (for example
132132when 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
136239The separately packaged [ machine companion] ( machine-companion.md ) provides playable examples of
137240the 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+
139245A ` FeatureHost ` runs a machine's ` MachineFeature ` s, such as a ` RecipeProcessor ` , and saves each
140246under its key. The block entity ticks it from ` serverTick ` with ` features.tick(level) ` , and saves
141247and 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
166272cycle 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
169276screen; ` time() ` is 0 when idle or paused.
170277
171278On ` /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
181288status. It also releases the running cycle's reservations, so the items it locked can be taken
182289out. This is the trap for players: an unpowered machine keeps its inputs and outputs locked, and
183290the 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
186296recipe again if it still can. Progress is kept only if that recipe still exists, can reserve its
187297inputs 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
191302can share that inventory and reserve the same slot, provided enough unreserved inputs and output
192303space remain; each lane holds only its own share of the slot's plan.
193304
@@ -217,7 +328,8 @@ given back.
217328Custom blocks can call ` DropsContents.onRemove(state, level, pos, replacement) ` before their
218329superclass 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
221333should 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
249363A feature controls what clients get with two methods. ` clientState() ` is the state to send, by
250364default what ` state() ` saves; a feature overrides it for a moment clients shouldn't see.
0 commit comments