Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Block transactions

Current contract for bulk block edits on the server: the one native submission API every block writer lowers to. Design: Enki B §2 (docs/superpowers/specs/2026-09-27-enki-b-mod-platform-contract-design.md) (§2.11 for non-chunk sections, and its “As built” issues 16–25). Source: crates/manifold-server-state/src/transaction/, crates/manifold-server-state/src/block_world_apply.rs (single edits), crates/manifold-world/src/{chunk_cell,chunk_map,world_space}.rs, crates/manifold-net-protocol/src/chunk_edit.rs.

The rule

A block write goes through transaction::submit as a TxnRequest: an actor (Player, ModSelf or the server-only Rule), an optional owner, options and one or more sections. Console /fill and /setblock, the block-world handler’s decided edits and Talos’s use-block (navigation) already do. A scheduled system, which holds the world shared, declares add_resource_write::<TxnEngine>() and submits with TxnEngine::submit on the engine it reaches through that write: the same checks and queue as transaction::submit, one code path, except that a player actor needs the world’s connections and is refused there (Other("player-actor-needs-world")). Such worlds install the engine up front with transaction::install, which is idempotent (an installed engine, its queue and configuration stay as they are); submit installs it on first use. A section is either the engine’s BlockOps (fill, set, revert, reapply, expect) or a program’s own TxnSection. ECS state that must change atomically with blocks (an item consumed by a placement, loot deposited by a break) changes in a non-chunk section of the same transaction, never in a follow-up.

Single-block edits

The handler keeps the block-world decision ABI (take-edit-requests, queue-edit, reject-edit, plus take-results). The orchestrator lends the handler (the first mod importing block-world) the world, shared, for its server-tick systems: ModOrchestrator::tick_server opens the loan before the schedule walk and closes it before the write-back, and the calls reach the server through the BlockWorldPort resource ServerSession::new installs (block_world_apply::install_port). queue-edit calls block_world_apply::submit_decided_edit during the guest call, which submits the edit as a one-cell transaction and returns its batch id: actor and owner the requesting player, requester Requester::Handler(<handler load-order index>), no undo record, Replication::BlockUpdates, and a section that applies within the validating slot. Handler edits validate first in the slot, so an edit decided this tick commits this tick and is broadcast as BlockUpdate with it. Locks, reservations, nodes (build.edit, which every player holds by default), water intake, the commit log, lighting and hierarchy apply as to any transaction.

  • An edit that writes the value already there commits with changed_cells: 0 and sends nothing; the chunk is not dirtied.
  • A refusal is sent to the requesting client as BlockEditRejected in the same tick, under the engine.edit.rejected.* keys (block_world_apply::rejection_text). The handler’s own reject-edit takes the same path as TxnReject::Protected(reason).
  • queue-edit fails at once only with the shared submit-error: malformed (a value wider than a block id) and actor-unknown (the peer is not a connected player), both also sent to the client, and other("not-handler") (a later importer) or other("outside-server-tick") (no loan: outside a server-tick system, or no port installed).
  • Each edit’s TxnResult waits in the handler’s results ring. The guest drains it with take-results (the txn-results payload, postcard Vec<TxnResult>; SDK: manifold_mod_substrate::block_world::take_results); native code with block_world_apply::take_block_edit_results(world, handler). Results of edits queued in tick N arrive from tick N + 1’s call. Feedback such as a break or place sound belongs to Committed results with changed_cells > 0 (block_world::committed_changes), matched to queue-edit’s batch id.
  • The loan is shared, so other per-call world views (Daedalus G’s physics window) can be held alongside it: the port writes only the transaction engine and TickEvents, through VoxelWorld::structural (transaction::submit_in, take_results_in). Executor parity harnesses lend a scratch world instead (BlockWorldRecord::record, RECORDING_PORT).

Lifecycle

  1. Queue. submit checks framing and the actor and returns a batch id.
  2. Validate at B’s slot in ServerSession::step_tick, after the F3 flush, before the console drains; the handler’s single edits first, then other native requests, then guest transactions: nodes (requires, build.edit for players; every node goes through anim_contract::player_has_node and the permission catalogue), the cell cap (TxnConfig::max_cells, 262,144 in the slice), residency, locks, values, compare-and-set, reservations and water intake. All-or-nothing: a refusal writes nothing.
  3. Lock. Every touched chunk is locked (other transactions get RegionBusy, which a handler edit’s client sees as engine.edit.rejected.region_busy), pinned by a loader view centre from PIN_VIEW_BASE up, and held out of save batches until commit.
  4. Apply chunk by chunk inside the “world edits” budget (4 ms, up to 8 ms with tick headroom); a larger transaction continues on later ticks. Each chunk is one WorldSpace::apply_chunk_edit: one snapshot and one diff, one change-log entry, one dirty mark, emitter updates for emissive cells, one hierarchy invalidation and one ChunkEdits delta (a BlockUpdate per changed cell with Replication::BlockUpdates).
  5. Commit. Non-chunk sections run apply_once in submission order, the undo record (MUR1) is written if requested, locks and pins are released, the commit is logged in TxnCommitLog, and the requester’s results ring gets Committed. Each section’s finish hook runs in that tick for every outcome; a transaction it submits queues for the next slot.

Before a shutdown save (native shutdown, the browser worker’s shutdown and flush), hosts call transaction::settle, which applies every queued and in-flight transaction to completion: held chunks would otherwise miss the final save.

Non-chunk sections

A section that returns Some(AccessSet) from TxnSection::access is a non-chunk section: it reads the ECS while validating (ValidateCx::ecs(), a read-only EcsView) and writes it once at commit (apply_once(&mut ApplyOnceCx)), both only through that set (manifold_ecs::AccessSet, as a system declares; set_structural for spawn, despawn, insert, remove).

  • No ambient world: the contexts expose typed accessors that check the set. Undeclared access panics in debug builds with the scheduler’s §15 message; release skips the check. An exclusive set is refused at submit.
  • A transaction holding one is single-tick (validate, chunks, apply_once, commit in one slot; TooLarge above single_tick_max_cells), so nothing changes the ECS between its validation and its commit. apply_once cannot fail. Sections validate against the world before the transaction, so a check that depends on another section’s effect belongs in one section; queued transactions validate in turn against what earlier ones committed.
  • No chunks listed (an inventory move): no residency, locks, pins, previews, snapshots, change log or ChunkEdits, and no build.edit; it commits in the slot that validates it. Measured by txn_non_chunk_section (target ≤ 5 µs for three sections).
  • Structural changes apply at commit through the funnel with Cause::Transaction { batch, mod_index }; their lifecycle events reach observers at F4. A later transaction in the same slot sees them.
  • TxnCommitLog records every commit that changed a cell or ran a non-chunk section, with all section kinds (CommittedTxn::sections).
  • Undo records hold a section’s data only if its record() writes bytes. Container contents are never recorded (the items program’s sections enforce it).

Guarantees

  • Nothing is written per cell outside the chunk writer; capture, logging and replication cost is per chunk.
  • Unknown blocks keep their names through fill, revert and reapply.
  • Revert and reapply are compare-and-set over the record; any mismatch is a Conflict and nothing changes.
  • Clients receive one S2C::ChunkEdits entry per changed chunk they hold (uniform, box or lz4 runs) and remesh and relight the chunk once.
  • The browser worker runs the same engine through the same step_tick.

Not yet

Loading unloaded chunks (unloaded = load), reveal at commit, parallel apply, the per-connection edit backlog, the in-flight ChunkLoad race (edits for chunks a client does not hold yet are dropped; the chunk streams fresh), protections and reviews, per-player build.txn-cells.max, the single-edit rate limit and reach check (Enki B §3.4), and the guest block-transaction WIT interface (its commit should use the same world loan as queue-edit).

Tests

manifold-server-state unit tests transaction::tests, transaction::record, block_world_apply and console::world_edit_commands; manifold-host-core suite step_tick_block_edits; manifold-server suite txn_two_client, block_edit and block_world_roundtrip; the benches txn_bulk_section and txn_non_chunk_section.