Skip to content

API Multiblocks

Piggo41 edited this page Oct 17, 2026 · 3 revisions

Multiblocks

Lua API / Multiblocks

Declare block patterns with a controller, or discover controllerless structures from their ordinary member blocks.

Managed controllerless structures can activate automatically or when a configured item is used. Definitions without a controller or discovery remain available for pure checks.

Formation preserves member blocks, metadata, tile data, inventories and appearances.

Registry

Function Behavior
betamoon.multiblocks:add(definition) Declare during initialization/reload; returns a definition reference.
betamoon.multiblocks:get(key) Reference, or nil if absent.
betamoon.multiblocks:getRequired(key) Reference; raises an error if absent.
definition:getKey() Canonical namespaced key, with content type multiblock.

References expire when their definition is replaced or its script disappears. A failed declaration publication

retains the previous working definition. Reacquire references after a successful reload.

Declaration

Field Meaning
key Required namespaced content key.
discovery Optional controllerless discovery; excludes controller.
activation Optional activation policy; requires discovery, defaults to automatic.
canActivate, onActivated, onDeactivated Optional activation callbacks.
controller Optional resolved block reference. Each block can bind one automatic definition.
facing Fixed north, east, south or west. Required for controllers without declared horizontal facing state.
layers Dense list of layers, each a dense list of equal-width row strings.
cells Alternative sparse list of {x, y, z, symbol} offsets. Excludes layers and repeatLayers.
palette Symbol-to-rule table. Ordinary rules use resolved block references.
mirror Default false. Allow mirrored checks; automatic matching tries unmirrored first at each repeat size.
exclusive Default false. Reject sharing required non-air cells with another formed structure.
constraints Optional count or allSame constraints, at most 64.
captures Named selections of symbols for getMatches(name). Names cannot shadow symbols.
repeatLayers Optional bounded repeated layer section; see below.
data Optional structured data schema, without entity references.
onFormed, onInvalidated, onSuspended, onResumed Optional functions taking a multiblock context.

Layers run bottom to top. Rows run front to back; characters run left to right. Authored front is north,

right is east, and back is south. Exactly one @ anchors the pattern at the check/controller coordinates.

The anchor need not be centered. With a controller, @ checks that block automatically. Standalone

definitions require palette["@"].

. requires actual air (block ID zero). ? ignores the position completely, including chunk availability

and membership. Other symbols must be single printable ASCII characters with palette rules. Spaces,

undefined symbols, ragged rows and duplicate anchors are declaration errors, identified by layer/row/column.

Horizontal rotation transforms local offsets clockwise: north (x,y,z), east (-z,y,x), south

(-x,y,-z), west (z,y,-x). Mirroring reverses local X before rotation. Automatic controllers derive

orientation from their declared horizontal facing state unless a fixed facing is supplied.

Sparse offsets are integers; @ must be at (0,0,0). Positions cannot repeat, even for ignored cells.

Their bounding dimensions have the same limits as layers. Sparse issue layer/row/column values are the

authored Y/Z/X offsets rather than string indices.

Palette rules

Rule Matches
Resolved block reference That block identity, any metadata.
{block = reference} Same identity rule.
{block = reference, damage = integer} Literal metadata 0–15.
{block = reference, state = fields} Declared semantic states; horizontal facing values rotate/mirror with the pattern.
{air = true} Actual air.
{anyOf = rules} Boolean OR of 1–32 rules.
{allOf = rules} Boolean AND of 1–32 rules.
{notRule = rule} Negation at a loaded, readable position.

Use exactly one main rule form. damage and state require block; together they both must match.

Raw metadata stays literal. References are resolved at declaration time. Unknown fields, cyclic rules,

more than 16 levels of nesting, or more than 256 nodes in a rule are rejected.

Constraints and captures

A count constraint has cells (a string selecting symbols), rule, optional min (default 0),

max (default 32768), and label (default the selected symbols). Each selected coordinate counts once

when its rule passes. Constraints all must pass; counts do not alter the meaning of Boolean rules.

Alternatively, use cells, optional label, and allSame = "block", "metadata" or "state:<field>".

Selected coordinates must have identical values; a missing named state fails the constraint.

allSame cannot combine with rule, min or max.

captures maps names to symbol-selection strings. getMatches returns coordinates selected by a symbol

or capture only while the structure is verified formed. Ignored cells cannot be selected.

There are at most 64 capture names, each 1–64 characters, and selections contain 1–94 symbols.

Repeatable layers

repeatLayers = {first = integer, last = integer, min = integer, max = integer} repeats one contiguous

inclusive, 1-based Y-layer section. Counts are 0–32, with min <= max. The anchor must lie outside it.

All expanded dimensions and required-cell counts must stay within limits.

Matching prefers larger repeat counts. At each size it tries the authored orientation before its mirror

when mirroring is enabled. An unavailable preferred candidate stops the search; it is never assumed to

have failed so a smaller structure can form. Explicit checks can select a single options.repeats count.

World checks

ctx.world:checkMultiblock(definition, x, y, z, options?) returns a detached report without forming a

structure, creating an instance, changing blocks, or loading chunks. Coordinates are separate integers.

Options are facing, mirror (default false), detailed (default true), and repeats. A standalone check

requires options.facing unless its definition has fixed facing. A managed definition can derive facing

from a loaded anchor. Explicit mirror checks require mirror = true in the definition.

Report fields are key, anchor, facing, mirror, status, reason, checked, required, repeats,

width, height, depth, truncated, issues, and constraints.

  • status is complete, incomplete or unavailable. Unloaded cells never count as air.

  • Reasons include complete, cell_mismatch, constraint_mismatch, out_of_bounds, chunk_unavailable,

    diagnostic_budget, exclusive_conflict and controller_removed. Pending checks use checking.

  • Issues contain coordinates, layer, row, column, symbol, expected, and reason. Readable

    mismatches also include blockId, metadata, and declared state where available.

  • Constraint entries contain label, found, min, max, and passed.

  • Issue lists are capped at 64; truncated flags omitted issues or exhausted diagnostic work. Counts

    describe the reported candidate and completed work, not a promised total of all failures.

Remote-world checks are observations only. Automatic formation and gameplay callbacks target authoritative

singleplayer worlds. Remote diagnostics inspect available chunks; absent terrain remains unavailable.

Controller access

ctx.world:getMultiblock(x, y, z) returns a scoped controller instance or nil. Block and tile contexts

at managed controllers also expose optional ctx.multiblock. These handles expire with the callback,

world, controller replacement or definition reload.

Instance method/field Behavior
isFormed() Cached verified usability; immediately false after a relevant change or chunk unload.
getStatus() Detached lifecycle/report snapshot.
getMatches(symbolOrCapture) Detached coordinate list when formed; otherwise nil, reason. Unknown names raise an error.
requestCheck() Mutable callbacks only; queue a coalesced check.
data:get(name) / data:set(name, value) / data:snapshot() Persistent declared values, using structured-data validation.

Lifecycle status is checking, formed, unformed, suspended or removed. getStatus() additionally

includes checkStatus (last query result), isFormed, persistent identity, optional callbackError,

optional dataError and optional conflictingController coordinates. Last report fields may be stale

while checking; use isFormed() to gate machine work. Reading status does not perform a synchronous scan.

Lifecycle callbacks and persistence

Callback Trigger
onFormed(ctx) New or confirmed-unformed controller verifies complete.
onInvalidated(ctx) Previously usable geometry fails, or its controller is removed.
onSuspended(ctx) Previously usable geometry cannot be verified because required chunks are unavailable.
onResumed(ctx) Suspended/saved previously usable geometry verifies complete again.

Contexts contain x, y, z, key, declaring-script owner, reason, detached report, scoped world, multiblock, and

data. Removal notifications describe the old instance; its data can be read but cannot be written.

An explicit world lookup may observe a replacement controller as a distinct instance.

Rapid valid-to-valid replacements coalesce without invalidated/formed spam. Callbacks run at a world

tick boundary before world services. A callback altering its own structure immediately loses formed

usability and is checked at a later boundary. Lua failures are reported separately from physical validity.

Identity and declared custom data persist independently of controller tile inventories/data. Rejoining

revalidates live geometry; saved formation never makes a structure usable by itself. Chunk unload preserves

identity; observed controller removal/replacement creates a new one. Successful reload expires handles,

revalidates geometry and uses resumption for previously usable structures. Missing definitions leave

saved records dormant. Texture/model refresh does not re-form structures or reset gameplay data.

Lifecycle notifications are not crash-safe exactly-once reward transactions. Keep saved idempotence

markers for rewards. Formation itself performs no block conversion, inventory aggregation or model changes.

Limits and availability

Automatic recognition requires the Java agent and logical world ticks. It follows world pause behavior.

Patterns are at most 32 per axis and required world Y is 0–127. Script limits are 512 definitions per

owner, 4096 globally; at most one repeat section is supported.

Setting in config/betamoon.cfg Default Range
multiblocks.maxRequiredCells 4096 1–32768 per expanded candidate
multiblocks.visitsPerTick 65536 256–1048576 shared discovery/matching work
multiblocks.maxInstances 4096 1–65535 per active world
multiblocks.maxInstancesPerOwner 1024 1–65535 per script/world

Checks may span ticks under load; pending geometry stays unusable. Explicit diagnostics have a separate

262144-visit budget. Instance-cap exhaustion produces a warning and background discovery retries.

Overlaps are allowed by default. Under exclusivity an existing claim is retained, new completed candidates

use stable anchor ordering, and conflicting controllers become eligible when the claim is released.

Changes made by other mods may be detected with a delay when those mods bypass ordinary block updates.

Background checks revisit loaded structures; recovery time increases with loaded terrain, controller counts,

and lower configured processing limits. Ordinary block updates through Lua or normal gameplay invalidate usability immediately.

Controllerless discovery

Omit controller, supply palette["@"], and add discovery. @ is a mathematical reference point; it can require exactly the same block as every other frame cell. Players need no special block or facing. Equivalent symmetric placements share one instance with a stable canonical reference and facing.

Field Meaning
discovery.mode "automatic" (default) scans loaded chunks and reacts to member edits; "manual" requires item activation and creates instances only through explicit item activation.
discovery.cells Seed symbols or a capture name. Required for automatic discovery; rules must identify finite non-air block sets. Air-only or unrestricted negative seeds are rejected.
discovery.facings One to four distinct allowed horizontal directions. Defaults to fixed facing, or all four.

Existing completed structures are discovered when chunks load. Discovery is budgeted and may take several ticks. Required empty cells are watched too. Missing chunks suspend existing structures; discovery never loads them. Manual-mode instances receive the same validation, persistence, and cleanup after creation.

-- Fields inside a multiblock declaration; frame is a registered block reference:
layers = { { "@FF" }, { "FIF" }, { "FFF" } },
palette = { ["@"] = frame, F = frame, I = { air = true } },
captures = { frame = "@F", interior = "I" },
discovery = { cells = "frame" },
activation = { mode = "automatic" }

Activation

Formation and activation are separate. A complete item-activated structure is formed while waiting for its item. instance:isActive() also requires verified formation; it is false immediately after a relevant edit or unload. Controller-based instances use formation as their processing gate and do not use these activation policies.

Field Meaning
activation.mode "automatic" (default) or "item".
activation.item Required resolved item reference for item mode.
activation.damage Optional exact held-item damage, 0–32767.
activation.target.cells Required symbols/capture accepting item use.
activation.target.position "clicked" (default) uses the clicked member; "adjacent" uses the position next to its clicked face.
activation.cost.damage Positive durability cost for a damageable item.
activation.cost.consume Positive stack count, at most 64. Cannot combine with durability cost. Omit cost for a reusable item.
activation.fill Optional owned output, described below.
activation = {
    mode = "item", item = wand,
    target = { cells = "frame", position = "clicked" }
}

Built-in item activation runs after item-use-first and ordinary block interactions, before normal item use. A successful activation handles the click and applies its cost once. Clicking an already-active instance costs nothing. A normal mismatch or rejected eligibility check passes through to ordinary item use. Ambiguous matches, relevant unavailable chunks, exhausted work budgets, conflicts, or pending cleanup handle the click without a cost so native fallback cannot perform a different destructive action. Manual discovery mode disables built-in item routing.

Owned output

fill = { cells = "interior", block = surface, facingState = "facing" }

fill is a field of activation. cells selects symbols/capture; block is a resolved non-air block that differs from the original selected cells. Owned fills do not replace or create tile entities; activation fails before item cost if a selected original cell contains one. Optional facingState names an enum containing exactly north, east, south, and west on that output block. The engine binds the matched orientation to it.

The instance accepts only its own recorded output in place of the original requirements. Other structures cannot adopt it by block ID. Deactivation restores unchanged owned cells and preserves external replacements. Cleanup waits for unavailable chunks and blocks reactivation while pending. Breaking an owned panel deactivates item-mode structures even when the replacement is air. Automatic structures may activate again when their geometry is valid.

Activation callbacks

canActivate(ctx) returns a boolean using a read-only context. False or a callback error denies activation. It receives the proposed instance and report, with optional player, item snapshot, clicked position, target position and named face for item use. These extra fields are absent during automatic activation.

onFormed precedes onActivated. onActivated(ctx) observes a committed activation; callback failures are reported without repeating effects or charging the item again. onDeactivated(ctx) includes a reason, such as manual, output_changed, or a structural mismatch. On confirmed invalidation, deactivation occurs before onInvalidated. Unload/rejoin uses suspension/resumption without replaying activation. Incompatible definition replacement or removal cleans owned output without invoking callbacks from the unloaded definition.

Manual activation and deactivation

Function Behavior
instance:deactivate() Controllerless only. Preserve valid geometry, clean owned output and inhibit automatic reactivation.
instance:activate() Explicitly reactivate automatic controllerless instances after validation; item mode returns item_required.
ctx:activateMultiblock(definition, x, y, z, options?) Item context only. Validate an exact placement and the current actor/item before activating.

These return {activated, handled, reason, instance?}; the optional instance handle is scoped to the callback. activated is true only for a newly committed activation. Deactivation returns deactivated or already_inactive, with no automatic refund. Repeated deactivation does not repeat callbacks. Manual inhibition survives rechecks and rejoin; breaking and rebuilding a confirmed retired structure creates a new identity with normal activation rules.

Exact activation options: facing, mirror, repeats, plus optional clicked = {x,y,z} and face for a ray-traced onUse target. Named faces are preferred; indexes 0–5 are also accepted. Omitted placement options use the first allowed facing, no reflection, and the longest declared repeat variant. Use found-match values when available. The target must still belong to the declaration's activation capture. Additional Lua checks cannot bypass item costs, structural validation, claims or limits. A handled manual activation suppresses ordinary native fallback for that item use.

Status adds isActive, activationSaved (including suspended intent), and manuallyInhibited. Calls on controller-based instances return controller_managed.

Manual finding and membership

ctx.world:findMultiblocks(definition, options)

Pure search returning {matches, complete, reason, truncated}. Matches are detached reports with canonical anchor, facing, mirror, repeats, world bounds, and named captures of positions.

Option Meaning
bounds Required inclusive {min={x,y,z}, max={x,y,z}} bounds for candidate reference positions; volume 1–32768.
near Optional {x,y,z,cells} requiring that world position to belong to selected symbols/capture.
containedIn Optional inclusive bounds that must contain every required pattern cell.
facings Optional subset of the definition's allowed orientations. Equivalent placements return their canonical facing.
mirror Default definition mirror setting; true searches both reflections, false only unmirrored.
repeats Optional explicit declared count; omitted searches declared sizes.
maxMatches Default 64; 1–256.

Search never creates managed instances. complete is false on search_budget_exceeded, result_limit, or relevant chunk_unavailable; partial matches remain available. Require completeness before deciding uniqueness. An ordinary finished search with zero matches returns complete = true.

local result = ctx.world:findMultiblocks(structure, {
    near = { x = ctx.position.x, y = ctx.position.y, z = ctx.position.z, cells = "frame" },
    bounds = { min = { x = 0, y = 0, z = 0 }, max = { x = 15, y = 127, z = 15 } }
})
if result.complete and #result.matches == 1 then
    local match = result.matches[1]
    local outcome = ctx:activateMultiblock(structure, match.anchor.x, match.anchor.y, match.anchor.z, {
        facing = match.facing, mirror = match.mirror, repeats = match.repeats
    })
end

The example belongs in an item block-use callback. For ordinary onUse, obtain ctx:rayTrace() first and pass its position and face as activation options. Pure reports can be retained, but activation always checks the live world.

ctx.world:getMultiblocksAt(x, y, z, options?)

Returns known callback-scoped memberships in stable identity order, including required air and owned output cells. options.definition optionally filters by a definition reference. Returns an empty list when none are known; it does not trigger discovery or assume a single owner. Existing getMultiblock(x,y,z) remains controller lookup.

Work limits

Pure checks/searches share 65536 work units per callback; partial diagnostic results remain explicit. Seed transformations are limited to 65536 cells across variants/orientations. Managed activation requires requiredCells * (8 + 4 * constraintCount) within the smaller of 65536 and half the configured per-tick visit budget; declarations exceeding it explain how to reduce work or raise that budget. These limits bound validation and owned effects; repeated search calls do not reset the callback allowance. Recognition and pending cleanup are incremental. Structures remain unusable until a complete current verification succeeds.

Examples

Visual portal

Advanced 06: Visual Portal uses a 4x5 frame made entirely from Visual Portal Frame blocks (243), with optional corners and a 2x3 opening. No special controller position or placement direction is needed. Use flint and steel against an inner frame face to activate its owned, animated surface (244). Breaking a frame block or panel extinguishes it; repair and activate again. Cleanup preserves player replacements. Passing through never teleports.

Charcoal kiln

Advanced 12: Charcoal Kiln uses 24 brick blocks and an outward-facing controller. Layers run bottom to top; rows run front to back:

Layer Front Middle Back
Bottom BBB BBB BBB
Middle B@B B.B BBB
Top BBB B.B BBB

B is brick, . is air, and @ is the controller. The machine gates existing recipe processing with ctx.multiblock:isFormed(). Every vanilla log variant yields charcoal without fuel. Its two-slot GUI uses a vanilla flame at the furnace arrow position and a conditional warning tooltip. Full output pauses work; changed input or a failed structure resets progress. Tile inventory/data remain with the controller.

Clone this wiki locally