Skip to content

Recording and playback

A recording moves through a small number of states, and the useful mental model is that pausing is reversible until you commit:

  1. Arm. RewindRecorder.start(level, cfg, host) begins journaling the level. Play continues normally.
  2. Pause. Vanilla ticking freezes, players lock in place, and everyone sees a PAUSED title plus a boss-bar scrub timeline showing position, total, and the nearest marker.
  3. Scrub. The host seeks to any earlier tick. The world, players, blocks, entities and registered slices all snap to that moment. They can keep scrubbing as much as they like.
  4. Commit or cancel. Commit accepts the scrubbed position and truncates the future; cancel returns to the live head as though nothing happened.
  5. Resume. Ordinary live play continues from wherever step 4 left things.
  6. Stop. At match end the host is asked whether to save a durable record.
GameHost.of("duel", "Duel")
.rewind(RewindConfig.fromConfig())
.onRoundStart(r -> MatchRewind.bind(r.level(), match)); // r.recording() exists here

A host asked to .rewind(cfg) records every match. It arms after the arena is built and stops before it comes down, so the block journal (non-evictable by design) never holds the arena’s own blocks; it freezes the match’s phase clock and callbacks while the recording is paused or scrubbing; and it rewinds the round itself with the world: phase, clock, who is still in, arena size, with a player rewound to before their elimination released from spectating. Do not call RewindRecorder.start yourself on a host.

Build the arena first, then arm. Every block write in a recorded level is journalled and the journal is never evicted, so arming before the build puts the whole arena in it for the life of the match.

Recording rec = RewindRecorder.start(instanceLevel, RewindConfig.fromConfig(), hostUuid);

The two-argument form omits the host and is fine for automated recordings that nobody will drive by hand. RewindConfig.fromConfig() reads the server’s conduit.toml; construct a RewindConfig directly if one game needs a different window from the server default.

Useful lookups:

RewindRecorder.get(level) // the Recording on a level, or null
RewindRecorder.isFrozen(level) // paused or scrubbing, gate your tick on this
RewindRecorder.anyRecording() // cheap volatile read, any level
RewindRecorder.anyFrozen() // cheap volatile read, any level
RewindRecorder.active() // every live Recording
RewindRecorder.stop(rec)

All gated behind the conduit.rewind.control permission.

/conduit rewind arm | stop
/conduit rewind pause | resume
/conduit rewind back <seconds> | step <frames> | to <marker|seconds>
/conduit rewind commit | cancel
/conduit rewind mark <label>
/conduit rewind status
/conduit rewind save [name] | discard (at match end)
/conduit rewind list | load <id>

mark is worth using habitually while filming. A labelled marker is much easier to scrub to than a remembered timestamp, and markers are what a saved tape’s timeline is built from, so they are also how you seek inside a replay months later.

RecordingStartedEvent RecordingStoppedEvent
GamePausedEvent GameResumedEvent
RewindBeganEvent RewindCommittedEvent

GamePaused/GameResumed are the pair you want for suspending your own logic. RewindBegan/RewindCommitted bracket an actual scrub, which is the right place to hang anything that should happen once per do-over rather than once per pause.

A restore is must-happen work. Rewind lives in instance dimensions, whose chunks become prime unload candidates the moment players are back in the hub, so a restore frequently targets chunks that are no longer resident.

Rather than loading them on the tick thread, a restore derives the diff’s chunk set up front and requests them through the non-blocking entry point, firing the tick they arrive. When everything is already resident it applies immediately, exactly as before.

Two consequences worth knowing:

  • Scrub targets that pile up while chunks load coalesce to the latest. Drag the seek bar across ten notches quickly and you get the tenth, not all ten in sequence.
  • After 15 seconds the apply proceeds with blocking loads and a warning. Writes are never dropped. Play never resumes into un-restored geometry.