ecsly_replay

Record simulation history for an ecsly world and seek forward or backward to any tick or opaque event key. Built for inspector time bars and harness tooling where agent commands are not deterministic but recorded outcomes are.

Install

dart pub add ecsly_replay ecsly_serialization

Design

  • Identity: PersistentId and WorldSnapshot from ecsly_serialization — no parallel id scheme.
  • Log: append-only ReplayEntry { tick, seq, kind, payload, eventKey? } ordered by (tick, seq) with an eventKey → position index.
  • Commands: each entry carries a full serialized outcome snapshot; replay applies it via reconcile restore (no inverse deltas, no re-running systems).
  • Inputs: optional second kind for deterministic games (host supplies InputReplayer).
  • Checkpoints: serialized world snapshots on a cadence; invalidated when history is truncated.
  • Seek: nearest checkpoint at or before the target, then replay forward. Checkpoints reconcile into the live world by default (transient entities survive with TransientPolicy.keep). Use TransientPolicy.despawn to drop non-persistent entities after each seek. CheckpointRestoreMode.spawn is the explicit oracle. Use seek, seekToTick, seekToEventKey, or seekTo.
  • No Flutter UI in this package.

Plugin

ReplayPlugin installs the whole record-and-seek state into a world in one call — log, checkpoint store, recorder, and a player bound to that world, reachable from systems and tooling as ReplayStateResource:

world.addPlugin(ReplayPlugin(
  header: ReplayLogHeader(
    formatVersion: replayLogFormatVersion,
    fingerprints: ReplayFingerprints(
      buildFingerprint: myBuildId,
      schemaFingerprint: 'schema-$schemaVersion',
    ),
  ),
));

final state = world.getResource<ReplayStateResource>();
state.recorder.recordCommand(
  tick: tick,
  outcome: captureWorldSnapshot(world),
  eventKey: harnessEncodedKey,
);
state.recorder.maybeCheckpoint(world, tick);
state.player.seekToEventKey(harnessEncodedKey);

The standalone classes below remain the API for hosts that wire the pieces themselves (multiple worlds, external logs, custom players).

Manual wiring

import 'package:ecsly_replay/ecsly_replay.dart';
import 'package:ecsly_serialization/ecsly_serialization.dart';

final log = ReplayLog(
  header: ReplayLogHeader(
    formatVersion: replayLogFormatVersion,
    fingerprints: ReplayFingerprints(
      buildFingerprint: myBuildId,
      schemaFingerprint: 'schema-$schemaVersion',
    ),
  ),
);
final checkpoints = CheckpointStore(cadence: 30);
final recorder = ReplayRecorder(log: log, checkpoints: checkpoints);

// After each command:
recorder.recordCommand(
  tick: tick,
  outcome: captureWorldSnapshot(world),
  eventKey: harnessEncodedKey,
);
recorder.maybeCheckpoint(world, tick);

final player = ReplayPlayer(world: world, log: log, checkpoints: checkpoints);
player.seekToEventKey(harnessEncodedKey);

See DX_FAQ.md for usage notes and DESIGN_FAQ.md for rationale.

Dependencies

  • ecsly (core runtime)
  • ecsly_serialization (snapshots, reconcile restore)

What was borrowed

Concepts (not code paths) from internal ecs_replay_tools: log ordering rules, checkpoint cadence/invalidation, cold-vs-hot determinism checks, and build/schema fingerprints in the log header.

Libraries

ecsly_replay
Record and seek deterministic replay positions for ecsly worlds.