Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Declaring a machine's menu data

A mod block entity has two different kinds of storage: external item access uses transactional Parts, while internal slots use the handle methods below; integer menu data is read with BlockEntityRef::data_slots and changed on a server Host. Menu data is transient: creation and loading both start with an empty data vector, even when the saved payload still contains the machine's progress.

State-aware creation

Declare the block entity type and its owning blocks during registration. To make creation depend on block properties, answer stateful_block_entity with true for that block. The existing block_entity hook then receives the actual placed state, not just the block identity. Read its properties through the Host's state registry and supply the initial saved compound. The registered declaration owns the entity type; an unanswered state-aware creation makes no entity.

Creation runs inside the block write, after placement side effects, on servers and clients. Changing properties within the same block preserves an existing entity even when a fresh instance of the new state would have no entity.

Size the data vector

Call entity.set_data_slots(host, initial_values) with the complete initial vector when preparing a newly created or loaded machine. For a progress display, use one element initialized from the progress value in its saved payload. This is the live-handle replacement for assigning a mutable tick payload's data field. Do not reset the vector to its initial values on every tick.

The method accepts at most the game's mod-menu data-slot limit (currently 256). It returns Ok(true) when it changes the vector, Ok(false) for identical values, or BlockEntityDataError::TooManySlots with the submitted count and limit. It never silently clamps a vector. BlockEntityDataError::Unavailable means the handle cannot reach a writable mod holder in the server level currently in place.

Update individual values

After sizing, call entity.set_data_slot(host, index, value) for scalar updates. This method does not resize: an absent index returns Ok(false). Both methods use the same write target, journal and setChanged path. Nested transactions retain their ordinary behavior; an aborted outer transaction restores the vector that existed before its first write, including writes committed by children. The normal menu broadcast delivers committed changes to viewers.

The public handle resolves one entity's lifecycle generation as well as its level and position. Removal, replacement and chunk reload make it stale; in-place data edits preserve it. It is not a borrowed-payload API.

Internal items versus automation

For a configured mod container, item_slot(host, slot) snapshots an internal slot; set_item_slot(host, tx, slot, stack) replaces it within an existing server transaction. Any held mod block-entity handle can be used; there is no caller ownership check. This deliberately bypasses inputs, outputs and face policy, so a recipe can consume its input and produce its output atomically, including output slots that automation cannot insert into. Use the ITEMS capability for external insertion; its policy is unchanged. Vanilla containers are unavailable here.

Indices use the current container's declared count. Lazy slots read empty; saved excess slots are neither exposed nor truncated. None clears, identical values return Ok(false), and real changes send Changed at root commit. Nested abort restores ordinary before-images, including committed-child writes. Invalid indices, unknown/air/zero-count stacks and counts above the registered or component-overridden maximum return BlockEntityItemError, never a partial write or silent count clamp. A stale handle refuses a replacement rather than accessing its container; acquire a new handle for the replacement.

One-shot client effects

To tell clients about something that happened once, such as a burst of particles, queue fields instead of writing them to the saved nbt. In block_entity_tick, or another event with a mutable entity, native and wasm mods alike call event.entity.set_client_update_fields(fields), then host.block_entity_sync(event.pos). A later call replaces the batch; an empty compound clears it.

The next block change that reaches a viewer puts the fields over what block_entity_update_tag gives, or sends them alone without one, in one packet for every current viewer, and the batch is gone. A change nobody sees leaves it waiting, and neither a chunk sent to a player nor a resend of the blocks under a single player takes it. Removing or unloading the block entity drops it. It is never saved, cloned or compared, and it does not change the entity's revision.

A client merges the fields into its copy's nbt as any update field, and they stay there until sent again: put a counter in them to tell one effect from the next.

Retaining a handle

A BlockEntityRef names one live entity, not every entity later placed at its position. Removal, replacement and chunk reload invalidate it; reacquire with host.block_entity(level, position). In-place data edits and temporary tick storage preserve identity. Option reads return None for stale handles; writes return their error's Stale variant, never writing to a replacement. set_lock, set_name, merge_data, replace_data, set_changed and set_data_slot return Result<bool, BlockEntityError>: handle Err(BlockEntityError::Stale) by dropping or reacquiring the handle. Ok(false) means unchanged data or an unsupported operation, not a stale identity. Existing item and data-vector methods retain their specialized errors.

player.open_menu(host, entity) also returns Result<bool, BlockEntityError>: Ok(true) means opened, Ok(false) means a non-provider, cancelled open or unconnected/wrong-level player, and Err(BlockEntityError::Stale) means the entity no longer resolves. It never opens a replacement through an old handle.