Engine SDK
The deterministic region engine as an API: what a batch is, what a quorum proves, and what a mod may touch.
Mirrored from docs/engine/SDK.md.Source last changed Edit docs/engine/SDK.md
Writing a Nodera rule pack
A rule pack is a mod's deterministic contribution to Nodera's validated lane: extra blocks, and
the code that decides what they do. Packs are how a modded world stays delegable instead of
falling back to the vanilla lane the moment a non-vanilla block appears
(LIMITATIONS.md L-21).
The shape of a pack
public final class ExampleMachines implements RulePack {
public static final int MACHINE = 1000; // ids >= 1000 belong to packs
public static final int MACHINE_LIT = 1001;
@Override public String namespace() { return "examplemod"; }
@Override public List<PackPaletteEntry> paletteEntries() {
return List.of(new PackPaletteEntry(MACHINE, "machine"),
new PackPaletteEntry(MACHINE_LIT, "machine_lit"));
}
/** Bump this on ANY behavioural change. It is the pack's version, not its build number. */
@Override public long semanticFingerprint() { return 0x0001L; }
@Override public Optional<PackRules> rules() { return Optional.of(new MachineRules()); }
}
A pack that only contributes ids can stop there — rules() defaults to empty, and the pack still
carries an identity.
What the platform guarantees you
- Your ids are yours. The base palette is frozen below
RulePackRegistry.PACK_ID_FLOOR(1000), and registration refuses a duplicate id across packs. Ownership is therefore a function, andPackDelegatingRuleSetroutes an action to yourPackRulesonly when the block it targets is one you declared. You cannot intercept a vanilla block, and no other pack can shadow yours. - Installation order never matters. The registry sorts packs by namespace, so per-tick execution order is identical on every peer no matter what order the mod loader happened to use.
- A version mismatch is a refusal, not a corruption. Every pack's
(namespace, entries, semanticFingerprint)is folded intoRulePackRegistry.combinedFingerprint, which is the number each committee member pins. A member missing your pack — or running a different build of it — computes a different fingerprint and the engine refuses to validate with them. A packless registry is deliberately identical to no SDK at all, so merely shipping the SDK does not fork the network.
What you must guarantee
PackRules.validate / apply / tick must be pure functions of their arguments:
| Rule | Why |
|---|---|
| No clocks, no IO, no network | Replicas run your code at different wall-clock times |
| No shared mutable statics | Two replicas would see different state |
| No floating-point in anything reaching hashed state | JVM float math is not reproducible across hardware, so every continuous quantity in hashed state is Q32.32 fixed point (envelope A-5) |
Only the supplied DeterministicRandom |
Any other source diverges immediately |
| Iterate in a canonical order | HashMap iteration order is not a contract |
Write blocks through MutableRegionState.setBlock |
Direct writes bypass the mutation buffer |
This is not sandboxed, and the failure mode is honest rather than silent. A pack that breaks determinism does not corrupt the world: the members running it compute a different root, their votes stop matching, and the committee refuses them. But it does break your pack's players, so the burden is yours.
Mutating the world from your own thread
Do not. Off-thread direct writes into a delegated region are refused by MutationGuard with an
AsyncWriteException naming this page — deliberately a loud, actionable error rather than a silent
block-or-convert, because a silently-dropped write is a bug you find days later in a desync report.
The legal path is AsyncActionGate.submit(envelope): it accepts signed actions from any thread
into a bounded FIFO, and the server thread drains it once per tick into the validated lane.
Asynchrony ends at the gate; determinism begins at the drain. Per-submitter FIFO order is preserved,
so your own actions stay in the order you submitted them.
// From your executor, worker pool, or event callback — any thread:
asyncGate.submit(signedEnvelope);
Registering
Register at startup, before the registry is frozen:
RulePackRegistry registry = new RulePackRegistry();
registry.register(new ExampleMachines());
registry.freeze();
RegionEngine engine = new FlatWorldRegionEngine(
FlatWorldRules.RULES_VERSION, registry, hashService);
The engine derives the expected fingerprint from the registry it is going to run, so it can never be handed a number that disagrees with its own behaviour.
Testing your pack
The bar a pack should meet before shipping is the one the built-in rules meet: run the same actions
through two independently-constructed registries and assert the resulting StateRoots are equal.
PackRuleExecutionTest is the worked example — replica-identical roots, a pack rejection leaving the
world untouched, and canonical tick ordering across reversed installation order.