Core Concepts
The model MultiLib uses to describe and detect a structure. The coordinate system and symbol rules below are the contract the matcher relies on — read them before designing structures.
What is a definition?
A definition (MultiblockDefinition) is an immutable description of:
- an id (
ResourceLocation) — how the structure is referenced (recipe viewers, JSON, wrench,MultiLib.getDefinition); - symbols — single characters mapped to
BlockIngredients; - layers — horizontal slices described with those symbols (or a procedural
PatternProvider, or ashapeless()flood-fill — see Advanced Features); - a core and/or activation symbol;
- rotation rules (
RotationModeand/orallowRotation(...)); - a formation mode;
- callbacks (
onFormed,onBroken,onTick,onAmbient) and an optionalvalidator.
You never construct one directly — assemble it with MultiblockBuilder (via MultiLib.define(id)) and finish with .build().
Symbols and BlockIngredient
A symbol is a char bound to a BlockIngredient via .key(char, BlockIngredient) (or .key(char, Block)). Symbols are global to the definition — the same character means the same ingredient on every layer.
BlockIngredient is more than "one exact block" (full reference):
| Factory | Matches |
|---|---|
BlockIngredient.of(Block) |
Exactly one block |
BlockIngredient.ofState(Block).require(property, value)…build() |
A block with specific properties |
BlockIngredient.tag(TagKey<Block>) |
Any block in a tag |
BlockIngredient.anyOf(ingredient…) |
Any of several ingredients |
BlockIngredient.predicate(Predicate<BlockState>) |
Arbitrary logic |
BlockIngredient.any() |
Anything, including air |
Space (' ') is reserved: it means "no constraint" and is never a symbol. A character that appears in a layer but was never registered with .key(...) is silently treated as a space — layer strings are not validated against the symbol map, so typos pass unnoticed.
Core and activation symbols
Two roles, often the same symbol:
- Core (
.core(char)) — the structure's main block, usually a controller. Anchors the ghost overlay, auto-place, wrench diagnostics, and (withAbstractMultiblockControllerBE) where state lives. - Activation (
.activation(char)) — the symbol whose placement triggers an automatic check..core(char)also sets activation to the same symbol unless activation was set explicitly, so the common "placing the controller completes the structure" case needs only.core(...).
Split them when the last-placed block and the logical controller aren't the same cell. matchesActivationOrCore(BlockState) is what the wrench and periodic validation use to decide whether a block belongs to a definition's trigger set.
Layers and the coordinate system
Each .layer(String... rows) call adds one horizontal (Y) slice:
- Row order → Z. First string = lowest Z, increasing per row.
- Character order → X. Leftmost char = lowest X.
.layer(...)call order → Y, top to bottom. First call = top, last call = bottom.
.layer("PPP", // top → relY = 0
" P ",
" G ")
.layer("POP", // bottom → relY = -1
" P ",
" G ")
Each layer is centered independently: center column = row.length() / 2, center row = layer.size() / 2 (integer division). All rows in one call should be equal length — width is taken from the first row.
The origin (ctx.instance().getOrigin()) is the world position of the top-layer center cell, in whatever orientation matched.
Formation modes
| Mode | Automatic (placement) | Wrench |
|---|---|---|
AUTOMATIC |
✅ | ❌ |
WRENCH |
❌ | ✅ |
AUTOMATIC_AND_WRENCH |
✅ | ✅ |
FormationMode is an extensible registry, not a plain enum (FormationMode.register(id, allowsAutomatic, allowsWrench)), so a mod can define its own trigger semantics as long as its code decides when to call BlockActivationHandler.triggerFormationAt(level, pos).
A "wrench" is any Item implementing IMultiblockWrench. MultiLib ships no wrench item — implement the interface on your own tool (ExampleWrenchItem is a reference).
Registration and lookup
.build():
- Validates the definition (geometry present; core-symbol consistency; constraints like
unique()/surfaceOnly()— see Pattern Design Guide). - Constructs the immutable
MultiblockDefinition. - Registers it in
MultiblockRegistry, indexed by its symbols' candidate blocks — so a placement only checks definitions that could involve the placed block.
If validation fails, registration is skipped and an error is logged; .build() still returns the object, but it's never matched. Use .buildWithoutRegistering() to skip the registry entirely (e.g. tests).
When the same block is a valid core/activation symbol for more than one definition, higher priority(...) wins by default; a per-position override can pin a specific definition to a specific spot — see Ambiguity & Preferences.
Activation flow
- A block is placed (
BlockEvent.EntityPlaceEvent, server side). BlockActivationHandlerfinds definitions listing that block as a candidate, keeping those whose formation mode allows automatic triggering and whose activation symbol matches.PatternMatcher.matches(...)(shaped/shapeless/functional) searches around the position in every allowed orientation — see Rotation & Matching.- On a match: a
validator, if set, runs first and can veto (ValidationResult.Invalid). Otherwise a cancellableMultiblockFormedEventfires; if not cancelled, aMultiblockInstanceis created and stored in the world'sWorldMultiblockTracker(aSavedData, persistent across restarts),onFormedcallbacks run, the controller'sonStructureFormed(...)fires, and each part implementingIMultiblockPartgetsonJoinedStructure(...). - Breaking any block of a tracked instance mirrors this:
MultiblockBrokenEvent,onBrokencallbacks,onStructureBroken(...), each part'sonLeftStructure(), then removal from the tracker. onTick/onAmbientcallbacks run viaWorldMultiblockTracker.tick(...)(driven byLevelTickEvent.Post) — every tick, or every N ticks respectively.- A controller with
setValidationInterval(ticks)periodically re-validates its structure and can attempt formation while unformed.
The controller block-entity pattern
For structures with state (running/idle/error, a menu, per-tick logic), extend AbstractMultiblockControllerBE for the core's block entity and AbstractMultiblockControllerBlock for its block. You get:
MultiblockStatetracking (UNFORMED/IDLE/RUNNING/ERROR, extensible);- automatic NBT persistence of state and instance id;
onFormed/onBroken/onStateChanged/serverTickhooks;- automatic model-hiding if the definition uses
.model(...)(Master-Dummy model); - an
openMenu(...)hook reachable only onceisFormed().
See Block Entity Abstractions and the ExampleControllerBE/ExampleControllerBlock reference classes.