KubeJS Integration
MultiLib exposes a KubeJS event group (MultiblockEvents) to create and modify definitions from .js scripts. Optional and reflection-gated: without the kubejs mod, none of it activates. If KubeJS is present but MultiLib was built without kubejs_version in gradle.properties, the compat.kubejs package is excluded and setup is skipped.
When scripts run
Both create and modify live in server_scripts, not startup_scripts. Like JSON multiblocks, they re-fire on every MultiblockDefinitionsReloadedEvent — initial load and every /reload — so iterating doesn't need a restart.
Order per reload (driven by KubeJSMultiblockSetup):
MultiblockEvents.create— each listener declares a builder viaevent.multiblock(id).- Every builder is built and swapped in via
MultiblockRegistry.replace(id, definition)(remove-then-register), so a script safely re-declares its own multiblock each reload. MultiblockEvents.modify— by now every Java/JSON/KubeJS definition is registered, soevent.modify(id, ...)can patch any of them.
Because setup runs from FMLCommonSetupEvent and these events don't fire until the first reload after that, all block/item registries — including KubeJS's own StartupEvents.registry(...) — are already closed. A script's key(symbol, block) never resolves to a not-yet-registered block.
Creating a multiblock
MultiblockEvents.create(event => {
event.multiblock('examplemod:my_altar')
.layer('PPP', ' P ', ' G ')
.layer('POP', ' P ', ' G ')
.key('P', 'minecraft:stone_bricks')
.key('O', 'examplemod:controller')
.key('G', 'minecraft:gold_block')
.core('O')
.formationMode('automatic_and_wrench')
.rotations('horizontal')
.onFormed(ctx => MultiblockUtils.playSound(ctx, 'minecraft:block.beacon.activate'))
})
event.multiblock(id) returns a normal MultiblockBuilder — the whole Java builder API is available from JS. Don't call .build() — the setup collects each returned builder and registers it for you, so /reload never throws "already registered". (The method is multiblock(...), not create(...), to avoid clashing with StartupEvents.registry's own event.create.)
A validation failure is reported to the server log and the KubeJS console with getLastValidationError(). Two create listeners declaring the same id: the second replaces the first, with a warning.
Registering a wrench
MultiblockEvents.create(event => {
event.wrench(Item.get('examplemod:my_wrench'))
})
The script-side equivalent of implementing IMultiblockWrench — for items made entirely in KubeJS, which can't implement a Java interface. See Wrench tool.
Modifying a multiblock
modify patches any registered definition — Java, JSON, or KubeJS — by id:
MultiblockEvents.modify(event => {
event.modify('examplemod:my_altar', builder => {
builder.key('G', 'minecraft:diamond_block')
builder.onBroken(ctx => MultiblockUtils.summon(ctx, 'minecraft:lightning_bolt'))
})
})
event.modify(id, callback) returns true on success, false (logged) when no definition exists under id or the rebuild fails validation. It snapshots via MultiblockBuilder#toBuilder(), applies your callback, rebuilds, and swaps in place — the original is untouched until the rebuild succeeds.
Everything flows through toBuilder(), so newer builder features are scriptable too: formedProperty(...), tier stats, and variants — e.g. .variant('tall', v => { v.layer('III'); v.layer('ILI') }) (Rhino converts the arrow function to the Consumer<VariantBuilder> parameter).
Declaring an assembly
MultiblockEvents.assembly(event => {
event.assembly('examplemod:steam_plant')
.role('core', 'examplemod:reactor_core')
.role('turbine', 'examplemod:turbine', 1, 8)
.proximity('core', 'turbine', 6)
.masterRole('core')
.breakPolicy('DEGRADE')
.aggregateStat('power', 'SUM')
})
Fires on the same reload cycle as create/modify, re-declared safely on every /reload. See Multiblock Assembly for the full role/connection/policy reference.
Wrench interaction events
MultiblockEvents.wrench(event => {
if (event.status === 'formation_failed') {
event.player?.tell(`Couldn't form: ${event.failureReason}`)
}
})
Fires on every wrench interaction, for any wrench (Java or event.wrench(...)-registered). MultiLib shows the player nothing on its own — scripts wanting feedback listen here.
event.status ∈ "not_a_multiblock", "already_formed", "mode_disallows_wrench", "formed", "formation_failed", "variant_changed". event.multiblockId is null only for "not_a_multiblock"; event.failureReason is set only for "formation_failed"; event.fromVariant/event.toVariant are set only for "variant_changed".
MultiblockUtils helpers
Script-friendly helpers bound globally as MultiblockUtils, translating Java concepts into string ids for use inside callbacks:
| Method | Behavior |
|---|---|
playSound(ctx, soundId[, volume, pitch]) |
Plays a sound at the context position (unknown id: warns, no-ops). |
summon(ctx, entityId) |
Spawns an entity at the context position (unknown/disabled type: warns, no-ops). |
Both take the shared MultiblockEventContext that onFormed/onBroken receive.
Limitations
- No script-side way to define a new
PatternProvidertype — the built-ins are usable, but a custom shape needs a JavaPatternProviderSerializer. modifyreplaces a definition wholesale via a rebuilt builder; there's no per-field read beyond the builder's own getters.