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

Scoped whole-stack rollback for internals mods

Proposal for #1407; this note does not authorize an implementation. TreeFall needs to retain a whole stack while trying a step and put it back if that step fails. The issue does not establish a need to keep snapshots across callbacks or ticks.

Decision for review

Prefer a transaction-scoped, same-place savepoint over a long-lived snapshot handle. Inside an existing server HostTransaction, an internals mod asks the host to capture the exact stack at a supported place and receives an opaque value usable only with that transaction and place. It can try the step and explicitly restore the captured stack before the transaction closes. Closing or aborting the transaction invalidates the savepoint. A decoded or copied guest token must not extend its authority: the host checks the live transaction and captured place on every use. At most one savepoint may be live in an entire root transaction and its nested transactions, including reentrant callbacks; a second capture returns AlreadyCaptured. A successful restore consumes it, so another capture can then succeed; closing its owning transaction also releases it. This bounds retention without a persistent per-mod registry, release API, principal binding, or cross-tick quota policy. The savepoint is not a serialized ItemStack and grants no access to engine-owned data.

This serves the stated same-step use if the capture, attempted mutation and decision all occur in one transaction. It does not serve a mod that must keep the stack over a later tick or callback. The real lifetime of TreeFall's transaction must be confirmed before building; confirm its actual call boundary before implementation; if it needs the longer lifetime, bring that use and its resource/ownership requirements back for design rather than silently widening this contract.

Capture and restore preserve the complete engine stack, including opaque wire-backed fields, unknown component and runtime data, count, and the empty state. Never reconstruct the engine stack through its public API projection. The handle promises neither Copy semantics nor free or hidden full-stack clones; copying guest token bytes duplicates neither the retained value nor its authority. The host identifies the original place by its level, holder incarnation and slot/hand identity, not a bare entity number. Ordinary changes to the stack in that same place are exactly what restore is meant to undo; a replaced holder, changed selected slot, stale identity or different destination must instead refuse. The host validates the captured value against the destination's current constraints before writing; if it no longer fits, it returns InvalidSnapshot without changing the journal rather than truncating or partially restoring it. Reusing a closed transaction or restoring to a different place is a refusal, not a best-effort write.

Native panic/unwind, wasm trap and world close must run the owning transaction's abort/teardown: discard pending restore writes, release the retained engine value and permanently invalidate the token. Cleanup must also work when teardown occurs with a live savepoint, without an explicit restore.

Boundary and named refusals

For initial review, support only a player's selected main-hand hotbar slot, identified as that player's concrete ItemPlace::PlayerSlot at capture, using the transaction's already-journaled player inventory. Confirm TreeFall actually uses that place before committing to the boundary. Refuse other places, including equipment and offhand, until their transaction coverage and the use case are established. Explicitly refuse mod-owned ItemPlace::Mod, overriding item-capability providers, and nested places. A provider's readable API stack need not contain the complete backing value, and its existing insert/extract interface does not promise an atomic, generation-checked whole-stack replacement. Treating it as game-owned could overwrite a different provider lifetime or discard data. A named refusal is safer than expanding PlaceProvider or ResourceHandler for every mod before the use requires it.

Use distinguishable, wire-safe results rather than a false success: UnsupportedPlace (including a provider-owned backing), ReadOnly, StalePlace, ClosedTransaction, InvalidSnapshot, and AlreadyCaptured for the single-savepoint bound. Final names are part of API review. Failed capture or restore changes neither stack nor journal; failed restore leaves the savepoint live until its transaction closes. A mod may handle a refusal by aborting the step; there is no fallback to the underlying game slot.

Transaction and pending-item overlay

A savepoint is a value within the existing item-place transaction, not a second rollback journal. Capture reads the transaction's current view, including prior pending writes; restore stages a write in that same journal. A nested transaction's accepted changes remain subject to the outer transaction, and an outer abort discards them. Restore cannot commit the transaction, undo unrelated effects, or make immediate host writes transactional by itself.

#2846 is separately designing a block-entity-tick journal and a reads-own-writes overlay for dropped-item writes and removal; it is not implemented by this proposal. Where that overlay applies, capture must read its effective pending stack and restore must stage into the same overlay, with its level/incarnation and stale-item checks. If no common transaction/overlay view exists for a place, refuse it until one does. Do not add another dropped-item rollback mechanism or claim that effects outside the #2846 boundary become atomic.

Evidence required before landing

  • Establish TreeFall's actual snapshot/restore call and transaction lifetime; if it crosses the proposed boundary, revise the design first.
  • Exercise native calls against real Game storage and real wasm-to-Game calls against the same supported place: exact full-stack and empty-stack restoration, opaque wire-field preservation, prior pending writes, nested commit/outer abort, and no partial change on refusal. Both targets must have identical contracts, including refusals and consumption on successful restore.
  • Cover native panic/unwind, wasm trap and world close while a savepoint is live: pending restore writes are discarded, the retained value is released and copied tokens remain permanently invalid.
  • Cover moved/replaced holders, slot changes, same numeric ID with a new incarnation, another level, closed transactions, repeated capture, read-only and provider-owned places; prove provider refusal never writes through to game storage.
  • Compose with the accepted #2846 overlay design and test reads-own-writes, stale item and rejected block-entity writeback once that contract exists. No claim of #2846 implementation is made here.