Skip to content

Saved tapes and replays

A live rewind lasts as long as the match does. A tape is that recording written to disk, and a replay is that tape reopened later, in a world of its own, scrubbed from spectator.

/conduit rewind save [name] write the stopped recording as a tape
/conduit rewind list every saved recording, with sizes
/conduit rewind load <id> open one as a replay
/conduit rewind verify <id> read a tape back and report it
/conduit rewind delete <id> remove one

Inside a replay:

/conduit rewind replay pause | resume
/conduit rewind replay to <marker|seconds> | back <s> | step <frames>
/conduit rewind replay speed <mult>
/conduit rewind replay status
/conduit rewind replay close

Loading a tape does not put the match back. That match is over, its instance dimension was released, and the players have moved on. Restoring live play into that would not be a time machine, it would be a fork of a finished match into a world with no idea it is happening.

So a replay opens its own instance dimension, paints the arena as it stood at the first recorded tick, recreates the entities, and lets you scrub. It is read-only: replay commit exists purely to tell you why it will not work.

The scrub itself is not a second engine. It is the same seek the live rewind uses, pointed at a loaded tape instead of a live recording.

A tape is a directory, not a file, because a real recording outgrows a single NBT read before a block edit is counted: two minutes of eight players is 2,400 frames of poses.

<world>/conduit_rewind/<id>/
manifest.nbt summary, marker timeline, slice roster, frame-to-sequence map
palette.nbt block states and entity types, interned
baseline.nbt the arena as it stood at the first recorded tick
entities.nbt spawn NBT, state keyframes and transform samples per entity
segments/ frames, inventories, block edits and slice snapshots

The baseline costs nothing to capture. The journal already records (position, before, after) for every block write, so the first edit at any position carries what was there to begin with. A tape needs no separate before-picture of the arena, only the arithmetic to fold the journal.

A SnapshotSlice rewinds custom game state live by handing an object back to itself in the same JVM. A tape crosses to disk, so a slice needs two more methods to survive one:

public interface SnapshotSlice {
String id();
Object capture(ServerLevel level);
void restore(ServerLevel level, Object snapshot);
// Implement both to persist into a tape. Neither is required.
default CompoundTag write(Object snapshot, ServerLevel level) { return null; }
default Object read(CompoundTag tag, ServerLevel level) { return null; }
}

Both default to null, so every existing slice keeps working live. A slice that does not implement them is named in the tape as not persisted, and a replay says so rather than quietly showing zero. MatchRewind in conduit-arena implements the pair, so arena scores travel with the tape.

  • A replay is read-only. You cannot resume live play from a tape.
  • Entity behaviour does not replay. A replay shows recorded transforms, not re-simulated AI. Same limitation live rewind has.
  • A slice that does not implement write/read is absent from the replay and named as absent.
  • Registry drift across engine versions drops unknown effects, entities and blocks with a log line. A tape is not a save file and is not migrated.
  • Pose and entity coverage is bounded by the eviction floor. Block coverage is not, because the block journal never evicts.
  • Nothing prunes tapes. list shows the total on disk and delete is manual, because automatically deleting a saved match is not a decision the engine should make for you.

manifest.nbt carries a format version, and a version this engine does not know is refused with the version in the message rather than parsed best-effort. A half-parsed tape that restores the wrong arena is worse than one that will not open.

A recording saved before tapes existed is a flat <id>.nbt v0 record: a real summary and marker timeline, and never scrubbable. list marks it, load explains it, and nothing silently upgrades it.