Aufzeichnung an den Weltschritt koppeln

Recording::step führt einen Takt aus und zeichnet ihn auf. Andere
Befehle als die ausgeführten können so nicht in eine Aufzeichnung
geraten. Aufzeichnung und Welt prüfen gegenseitig ihren Takt, und ein
beschädigter Startzustand fällt schon beim Einlesen auf.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-10-01 10:34:30 +02:00
parent ef728cd100
commit 5dacbf2222
4 changed files with 97 additions and 19 deletions
+38 -5
View File
@@ -6,7 +6,7 @@ use std::fmt;
use crate::codec::{DecodeError, Reader, Writer};
use crate::command::Command;
use crate::snapshot::SnapshotError;
use crate::world::World;
use crate::world::{Event, World};
const MAGIC: [u8; 4] = *b"GRWR";
/// Version of the recording format. Raise it whenever the encoded recording,
@@ -57,6 +57,7 @@ impl From<DecodeError> for RecordingError {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Recording {
start: Vec<u8>,
start_tick: u64,
tick_count: u32,
/// Ticks that had commands, as (tick index, commands), in rising order.
busy_ticks: Vec<(u32, Vec<Command>)>,
@@ -67,18 +68,33 @@ impl Recording {
pub fn start(world: &World) -> Self {
Self {
start: world.to_snapshot(),
start_tick: world.tick(),
tick_count: 0,
busy_ticks: Vec::new(),
}
}
/// Records the commands of one tick. Call it once for every
/// [`World::step`], with the same commands.
pub fn push_tick(&mut self, commands: &[Command]) {
/// Advances `world` by one tick with `commands` and records them. This is
/// the only way to add to a recording, so it can never hold other
/// commands than the ones the world executed.
///
/// # Panics
/// Panics if `world` is not at [`Recording::end_tick`], which means it is
/// not the world this recording follows.
pub fn step(&mut self, world: &mut World, commands: &[Command]) -> Vec<Event> {
assert_eq!(
world.tick(),
self.end_tick(),
"the world is not at the tick this recording has reached"
);
if !commands.is_empty() {
self.busy_ticks.push((self.tick_count, commands.to_vec()));
}
self.tick_count += 1;
self.tick_count = self
.tick_count
.checked_add(1)
.expect("a recording holds fewer than 2^32 ticks");
world.step(commands)
}
/// Number of recorded ticks.
@@ -86,6 +102,11 @@ impl Recording {
self.tick_count
}
/// Tick of the recorded world after the last recorded tick.
pub fn end_tick(&self) -> u64 {
self.start_tick + u64::from(self.tick_count)
}
/// Restores the starting state and applies every recorded tick.
pub fn replay(&self) -> Result<World, RecordingError> {
let mut world = World::from_snapshot(&self.start).map_err(RecordingError::BadStart)?;
@@ -106,7 +127,15 @@ impl Recording {
/// Encodes the recording together with the state that replaying it must
/// reach. `end` is the world after the last recorded tick.
///
/// # Panics
/// Panics if `end` is not at [`Recording::end_tick`].
pub fn to_bytes(&self, end: &World) -> Vec<u8> {
assert_eq!(
end.tick(),
self.end_tick(),
"the end state is not at the tick this recording has reached"
);
let mut w = Writer::new();
w.raw(&MAGIC);
w.u32(RECORDING_VERSION);
@@ -139,6 +168,9 @@ impl Recording {
}
let expected_hash = r.u64()?;
let start = r.bytes()?.to_vec();
let start_tick = World::from_snapshot(&start)
.map_err(RecordingError::BadStart)?
.tick();
let tick_count = r.u32()?;
let busy_count = r.u32()?;
let mut busy_ticks = Vec::new();
@@ -163,6 +195,7 @@ impl Recording {
Ok((
Self {
start,
start_tick,
tick_count,
busy_ticks,
},