Multiblock Library¶
How to add a multiblock structure from Kotlin. The gameplay side is in Multiblocks; the library's runtime contract — coordinates, IO helpers, hooks — is at the end of this page.
Two parts are involved:
| Part | Namespace | Package | Role |
|---|---|---|---|
MultiblockLibPart |
ra_lib_multiblock |
ra.multiblocklib |
the library: registry, assembly, rotation, validation, IO |
MultiblockModule |
ra_multiblock |
ra.modules.multiblock |
the Multiblock Base tiers and the concrete structures |
The library's public surface¶
ra.api.MultiblockLib:
| Reference | Path | Use |
|---|---|---|
register |
ra_lib_multiblock:register |
register a structure from storage ra:multiblock spec (add-ons) |
tryAssemble, tryTier |
try_assemble, try_tier |
called by the wrench on a base |
validateAll |
validate_all |
the periodic integrity pass |
disassemble |
disassemble |
teardown |
Io.at, Io.isBlock |
io/at, io/is_block |
reach a named IO position of the marker's structure |
init |
init |
called from ra:load |
Hooks.LOAD, TICK, VALIDATE, SETUP_TYPE, CHECK_STRUCTURE, ON_BREAK |
#ra_lib_multiblock:<name> |
the hook tags |
Kotlin structures use MultiblockSpec, multiblockSpec { } and Ra.multiblock(spec) from
ra.api instead of register (below).
The six hook tags are declared empty by the library (Registry.kt, declareHookTags) so its own
function #ra_lib_multiblock:* calls resolve whatever has hooked in. Structures add themselves
with pack.hook.
Adding a structure: a Kotlin spec¶
Structures written in Kotlin describe themselves with a MultiblockSpec (ra/api/MultiblockSpec.kt)
and are compiled when the pack is generated. Every structure the pack ships is one: the Rock
Metallic Drill (modules/multiblock/drills/RockMetallicDrill.kt) is the small worked example, the
Blast Forge (modules/multiblock/blastforge/BlastForge.kt) a 26-block one, and the Upgrade
Platform (modules/multiblock/upgradeplatform/UpgradePlatform.kt) a non-rotating one.
1. The spec, written once for a structure facing north, offsets relative to the base:
val SPEC = multiblockSpec("rock_metallic_drill", "Rock Metallic Drill", "copper") {
hint = "Need: barrel in front of the base, iron bars under the barrel, smooth stone under the base"
block(0, 0, 0, "minecraft:waxed_copper_block")
block(0, 0, -1, "minecraft:barrel")
block(0, -1, -1, "minecraft:iron_bars")
block(0, -1, 0, "minecraft:smooth_stone")
input("input_1", 0, 0, -1)
output("output_1", 0, 0, -1)
control("redstone_in", 0, 0, 1)
}
| Builder | Meaning |
|---|---|
multiblockSpec(id, name, tier) { … } |
id is the type ([a-z0-9_]); the marker gets ra.multiblock.<id> (spec.tag). name is shown by the goggles, tier is the base tier the wrench assembles it from |
hint |
what the wrench tells a player whose structure is incomplete |
block(x, y, z, match) |
one required block. match is a block id, a #tag, or an id with states (minecraft:furnace[lit=true]); NBT is not supported |
blocks(match, pos(x, y, z), …) |
the same block at several offsets |
box(xs, ys, zs, match) { except } |
every position of an inclusive box, minus those except rejects — a 3x3 floor is box(-1..1, -1..-1, -2..0, BRICKS) |
input, output, control(name, x, y, z) |
named IO positions |
rotates = false |
rotationally symmetric: one predicate, and the marker's facing is ignored when checking |
symmetricFacing |
the data.facing a non-rotating structure is assembled with ("north" by default, or "none") |
2. Register it from the part's register():
fun Ra.registerRockMetallicDrill() {
multiblock(SPEC)
pack.define(tickDispatch) { forEachMarker(SPEC.tag, tick) }
pack.hook(Ns.LIB_MULTIBLOCK, MultiblockLib.Hooks.TICK, tickDispatch)
}
Ra.multiblock only records the spec; the library compiles every recorded spec once all parts
have registered (registering from inside an afterAllParts block is an error).
3. Tick it, as above: a dispatcher hooked into #ra_lib_multiblock:tick selects the type's
markers and runs its logic. Hook #ra_lib_multiblock:on_break too if the type needs cleanup.
4. Use named IO, never facing-specific coordinates. The marker carries IO maps already rotated to its facing:
function(MultiblockLib.Io.isBlock, nbt { this["name"] = "redstone_in"; this["block"] = "minecraft:redstone_block" })
What the library generates¶
For every spec, from multiblocklib/spec/Compiled.kt:
| Output | Role |
|---|---|
ra_lib_multiblock:structure/<id>/<facing> predicate (structure/<id> when rotates = false) |
all_of of one location_check per block (offsetX/offsetY/offsetZ + block predicate). The offsets are rotated in Kotlin, so the whole pattern is one execute if predicate |
spec/<id>/validate, spec/<id>/accept/<facing> |
tries the facings' predicates; the winner sets #mb_result and writes facing, inputs, outputs, controls to storage as SNBT literals |
spec/<id>/check |
the periodic integrity check: the predicate of the marker's data.facing, stored into @s ra.multiblock |
spec/<id>/debug/<facing>, spec/<id>/debug |
the per-block report (below) |
spec/validate, spec/check_structure, spec/setup_type, spec/debug |
static dispatchers (one line per type, no macro, no registry walk), hooked after any structure's own hook and ahead of generic/* |
spec/try_tier, spec/tier/<tier> |
the compiled types of each tier, tried by try_tier before the storage registry |
spec/load |
called from init before #ra_lib_multiblock:load: writes types.<id> as one literal and appends the id to registry |
spec.facings, spec.blocksFacing(facing), spec.portsFacing(spec.inputs, facing) and
spec.predicate(facing) give the rotated data and the predicate id in Kotlin, for a custom check
or for positions a tick needs. The Blast Forge's tick uses MultiblockSpec.Pos.rotated(facing) to
write its furnace, barrel and control coordinates into one generated function per facing, so it
never macro-substitutes a position while running.
Debugging a structure¶
ra_lib_multiblock:spec/<id>/debug/<facing> checks each block of the spec turned to that facing,
at the current position, and tells @a[tag=ra.debug] which ones are wrong and what belongs there,
then <ok>/<total> blocks OK. It needs no marker, so it also explains a structure the wrench
refuses to assemble:
tag @s add ra.debug
execute positioned <base x y z> run function ra_lib_multiblock:spec/blast_forge/debug/north
As an assembled marker, spec/<id>/debug picks the marker's data.facing, and spec/debug picks
the type from the marker's tag:
execute as @e[type=marker,tag=ra.multiblock,limit=1,sort=nearest] at @s run function ra_lib_multiblock:spec/debug
validate_single runs spec/debug itself for a marker that fails its periodic check while a
player with ra.debug is online, just before the structure is taken apart.
The storage view stays¶
Everything that read storage ra:multiblock keeps working: types.<id> holds the authored spec
(id, name, tier, hint, blocks, inputs, outputs, controls, rotates when false)
plus facings.<f>.{blocks,inputs,outputs,controls} in exactly the shape build/* derives, and
compiled:1b. registry lists every type, compiled or not. The wrench, goggles, io/* and
add-ons see no difference.
Adding a structure from an add-on: ra_lib_multiblock:register¶
A data pack that cannot run Kotlin registers its structure as data, at runtime. This path is unchanged; the library rotates and checks these specs with its macro functions.
# from a function listed in #ra_lib_multiblock:load
data modify storage ra:multiblock spec set value {id:"my_drill",name:"My Drill",tier:"iron",hint:"...",blocks:[{x:0,y:0,z:0,match:"minecraft:iron_block"},{x:0,y:0,z:-1,match:"minecraft:barrel"}],inputs:[{name:"input_1",x:0,y:0,z:-1}],outputs:[],controls:[]}
function ra_lib_multiblock:register with storage ra:multiblock spec
| Field | Meaning |
|---|---|
id |
the type; the marker gets ra.multiblock.<id> |
name |
shown by the goggles |
tier |
the base tier the wrench assembles it from |
hint |
what the wrench tells a player whose structure is incomplete |
blocks |
required blocks; match is a block id, block state or #tag, passed to execute if block (NBT works here) |
inputs, outputs, controls |
named IO positions |
rotates |
optional; 0b for a rotationally symmetric structure |
register appends the id to registry and to registry_runtime. The library's build/*
functions derive the south, east and west variants of every registry_runtime type on load;
generic/validate, generic/setup_type and generic/check_structure validate, tag and check
them; try_tier walks registry_runtime after the compiled types. The registry is rebuilt from
scratch on every load, so a removed or edited type cannot linger.
Why generic/* is registered last¶
generic/check_structure, generic/setup_type and generic/validate cover every type nobody
else handled, so they must run after every structure's own hook in the same tag. Since the
multiblock library registers before the modules, it queues those three hooks with
ra.afterAllParts { … } (Architecture). The
compiled-spec dispatchers are queued the same way, just before them.
Structure-specific hooks¶
A shape a block list cannot express can still hook its own validate, setup_type and
check_structure into the tags with pack.hook; such hooks run before the library's spec/*
dispatchers and generic/*. None of the pack's structures needs one any more: the Blast Forge
and the Upgrade Platform, which used to carry hand-written per-facing tables and validators,
are compiled specs. Their markers kept the same data (facing, inputs, outputs,
controls; facing:"none" for the platform), so structures assembled before the change keep
running and validating.
Bases¶
The Multiblock Base is one block type with five tiers (copper, iron, gold, diamond,
netherite). BaseTier (modules/multiblock/blocks/BaseTier.kt) describes each tier once and
produces its item, give and placement data; Bases.kt registers them, marks tier and
tier_level read-only, and handles breaking. The wrench's plain-RMB on a base tries to assemble
(tools/wrench/WrenchMultiblock.kt).
Goggles¶
Register a readout per multiblock type with multiblockGoggles += TagRoute(markerTag, fn); Tools
builds ra:tools/goggles/draw_multiblock from it. Readouts use the multiblock billboard writers
in Goggles.Billboard (handle_multiblock, show_input, show_output, show_control) and
Goggles.Status.readStatusMultiblock.
Ticking order¶
ra_multiblock:tick:
- Break detection for every base tier (
Bases.checkBreak). - Every 40 ticks,
ra_lib_multiblock:validate_all. function #ra_lib_multiblock:tick: every structure's dispatcher, in hook order.
Runtime contract¶
What the generated ra_lib_multiblock functions promise, for anyone calling them from Kotlin or from an addon pack.
Coordinates¶
Offsets are relative to the base block, for a structure facing north, where
"in front of the base" is -Z. South, east and west are quarter turns about the
vertical axis — north ( x, y, z), south (-x, y, -z), east (-z, y, x), west
( z, y, -x) — computed in Kotlin for a compiled spec and by build/* for a
storage-registered one.
IO helpers¶
All of these take the marker as @s, positioned at the base, and resolve names
against the marker's own data.inputs / data.outputs / data.controls. They
work for any multiblock that went through setup_marker, compiled or
storage-registered — a tick function never needs to branch on facing.
| Function | Purpose |
|---|---|
io/at {name,run} |
Run a function positioned at the named IO block. |
io/insert {name,id,count,components} |
Insert an item into the named container. |
io/extract {name,id,count} |
Remove items; returns 1 only if the full amount was taken. |
io/peek {name} |
Copy the first stack to storage ra:multiblock io_item; returns 1 if present. |
io/count {name} |
Number of occupied slots. |
io/is_block {name,block} |
Test the block at that position; returns 1 on a match. |
For items with components, build storage ra:multiblock io_item yourself and
call io/at {name:"...",run:"ra_lib_multiblock:io/insert_here"} — that avoids
quoting a component compound through a macro argument.
Hooks¶
| Tag | When |
|---|---|
#ra_lib_multiblock:load |
Register storage-described types. Compiled specs need no entry. |
#ra_lib_multiblock:tick |
Per-type tick dispatch. |
#ra_lib_multiblock:validate |
Assembly. Compiled types are covered by spec/validate, registered ones by generic/validate. |
#ra_lib_multiblock:check_structure |
Periodic integrity check. Covered by spec/check_structure / generic/check_structure. |
#ra_lib_multiblock:setup_type |
Marker tagging. Covered by spec/setup_type / generic/setup_type. |
#ra_lib_multiblock:on_break |
Optional cleanup on disassembly. |
A compiled type needs only tick; a storage-registered one needs load and tick.