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:
+38
-5
@@ -6,7 +6,7 @@ use std::fmt;
|
|||||||
use crate::codec::{DecodeError, Reader, Writer};
|
use crate::codec::{DecodeError, Reader, Writer};
|
||||||
use crate::command::Command;
|
use crate::command::Command;
|
||||||
use crate::snapshot::SnapshotError;
|
use crate::snapshot::SnapshotError;
|
||||||
use crate::world::World;
|
use crate::world::{Event, World};
|
||||||
|
|
||||||
const MAGIC: [u8; 4] = *b"GRWR";
|
const MAGIC: [u8; 4] = *b"GRWR";
|
||||||
/// Version of the recording format. Raise it whenever the encoded recording,
|
/// 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)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Recording {
|
pub struct Recording {
|
||||||
start: Vec<u8>,
|
start: Vec<u8>,
|
||||||
|
start_tick: u64,
|
||||||
tick_count: u32,
|
tick_count: u32,
|
||||||
/// Ticks that had commands, as (tick index, commands), in rising order.
|
/// Ticks that had commands, as (tick index, commands), in rising order.
|
||||||
busy_ticks: Vec<(u32, Vec<Command>)>,
|
busy_ticks: Vec<(u32, Vec<Command>)>,
|
||||||
@@ -67,18 +68,33 @@ impl Recording {
|
|||||||
pub fn start(world: &World) -> Self {
|
pub fn start(world: &World) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: world.to_snapshot(),
|
start: world.to_snapshot(),
|
||||||
|
start_tick: world.tick(),
|
||||||
tick_count: 0,
|
tick_count: 0,
|
||||||
busy_ticks: Vec::new(),
|
busy_ticks: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records the commands of one tick. Call it once for every
|
/// Advances `world` by one tick with `commands` and records them. This is
|
||||||
/// [`World::step`], with the same commands.
|
/// the only way to add to a recording, so it can never hold other
|
||||||
pub fn push_tick(&mut self, commands: &[Command]) {
|
/// 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() {
|
if !commands.is_empty() {
|
||||||
self.busy_ticks.push((self.tick_count, commands.to_vec()));
|
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.
|
/// Number of recorded ticks.
|
||||||
@@ -86,6 +102,11 @@ impl Recording {
|
|||||||
self.tick_count
|
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.
|
/// Restores the starting state and applies every recorded tick.
|
||||||
pub fn replay(&self) -> Result<World, RecordingError> {
|
pub fn replay(&self) -> Result<World, RecordingError> {
|
||||||
let mut world = World::from_snapshot(&self.start).map_err(RecordingError::BadStart)?;
|
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
|
/// Encodes the recording together with the state that replaying it must
|
||||||
/// reach. `end` is the world after the last recorded tick.
|
/// 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> {
|
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();
|
let mut w = Writer::new();
|
||||||
w.raw(&MAGIC);
|
w.raw(&MAGIC);
|
||||||
w.u32(RECORDING_VERSION);
|
w.u32(RECORDING_VERSION);
|
||||||
@@ -139,6 +168,9 @@ impl Recording {
|
|||||||
}
|
}
|
||||||
let expected_hash = r.u64()?;
|
let expected_hash = r.u64()?;
|
||||||
let start = r.bytes()?.to_vec();
|
let start = r.bytes()?.to_vec();
|
||||||
|
let start_tick = World::from_snapshot(&start)
|
||||||
|
.map_err(RecordingError::BadStart)?
|
||||||
|
.tick();
|
||||||
let tick_count = r.u32()?;
|
let tick_count = r.u32()?;
|
||||||
let busy_count = r.u32()?;
|
let busy_count = r.u32()?;
|
||||||
let mut busy_ticks = Vec::new();
|
let mut busy_ticks = Vec::new();
|
||||||
@@ -163,6 +195,7 @@ impl Recording {
|
|||||||
Ok((
|
Ok((
|
||||||
Self {
|
Self {
|
||||||
start,
|
start,
|
||||||
|
start_tick,
|
||||||
tick_count,
|
tick_count,
|
||||||
busy_ticks,
|
busy_ticks,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ fn a_recorded_session_replays_to_the_same_hash() {
|
|||||||
let mut world = World::new(8);
|
let mut world = World::new(8);
|
||||||
let mut recording = Recording::start(&world);
|
let mut recording = Recording::start(&world);
|
||||||
for commands in &script {
|
for commands in &script {
|
||||||
recording.push_tick(commands);
|
recording.step(&mut world, commands);
|
||||||
world.step(commands);
|
|
||||||
}
|
}
|
||||||
let bytes = recording.to_bytes(&world);
|
let bytes = recording.to_bytes(&world);
|
||||||
let replayed = Recording::verify(&bytes).expect("the recording replays cleanly");
|
let replayed = Recording::verify(&bytes).expect("the recording replays cleanly");
|
||||||
|
|||||||
@@ -194,8 +194,7 @@ fn the_recording_format_is_unchanged() {
|
|||||||
let mut world = World::new(SEED);
|
let mut world = World::new(SEED);
|
||||||
let mut recording = Recording::start(&world);
|
let mut recording = Recording::start(&world);
|
||||||
for commands in frozen_script(&world) {
|
for commands in frozen_script(&world) {
|
||||||
recording.push_tick(&commands);
|
recording.step(&mut world, &commands);
|
||||||
world.step(&commands);
|
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fnv1a64(&recording.to_bytes(&world)),
|
fnv1a64(&recording.to_bytes(&world)),
|
||||||
|
|||||||
+57
-10
@@ -4,6 +4,7 @@ use grovewright_core::inventory::ITEM_STONE;
|
|||||||
use grovewright_core::mapgen::outcrop_origin;
|
use grovewright_core::mapgen::outcrop_origin;
|
||||||
use grovewright_core::player::PlayerId;
|
use grovewright_core::player::PlayerId;
|
||||||
use grovewright_core::replay::{Recording, RecordingError, RECORDING_VERSION};
|
use grovewright_core::replay::{Recording, RecordingError, RECORDING_VERSION};
|
||||||
|
use grovewright_core::snapshot::SnapshotError;
|
||||||
use grovewright_core::world::World;
|
use grovewright_core::world::World;
|
||||||
|
|
||||||
const ADA: PlayerId = PlayerId(0);
|
const ADA: PlayerId = PlayerId(0);
|
||||||
@@ -31,12 +32,28 @@ fn recorded_session() -> (Recording, World) {
|
|||||||
let mut recording = Recording::start(&world);
|
let mut recording = Recording::start(&world);
|
||||||
for tick in 0..200 {
|
for tick in 0..200 {
|
||||||
let commands = commands_for(&world, tick);
|
let commands = commands_for(&world, tick);
|
||||||
recording.push_tick(&commands);
|
recording.step(&mut world, &commands);
|
||||||
world.step(&commands);
|
|
||||||
}
|
}
|
||||||
(recording, world)
|
(recording, world)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stepping_through_a_recording_gives_the_events_of_the_world() {
|
||||||
|
let mut plain = World::new(11);
|
||||||
|
let mut recorded = World::new(11);
|
||||||
|
let mut recording = Recording::start(&recorded);
|
||||||
|
for tick in 0..30 {
|
||||||
|
let commands = commands_for(&plain, tick);
|
||||||
|
assert_eq!(
|
||||||
|
recording.step(&mut recorded, &commands),
|
||||||
|
plain.step(&commands),
|
||||||
|
"tick {tick}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(recorded, plain);
|
||||||
|
assert_eq!(recording.end_tick(), 30);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn replaying_reaches_the_same_state() {
|
fn replaying_reaches_the_same_state() {
|
||||||
let (recording, world) = recorded_session();
|
let (recording, world) = recorded_session();
|
||||||
@@ -62,14 +79,14 @@ fn verify_accepts_a_faithful_recording() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_reports_a_replay_that_ends_in_another_state() {
|
fn verify_reports_a_replay_that_ends_in_another_state() {
|
||||||
let (recording, mut world) = recorded_session();
|
let (recording, world) = recorded_session();
|
||||||
// One tick more than was recorded: the claimed end state is unreachable.
|
let mut bytes = recording.to_bytes(&world);
|
||||||
world.step(&[]);
|
// The expected state hash follows the marker and the version.
|
||||||
let bytes = recording.to_bytes(&world);
|
bytes[8] ^= 0x01;
|
||||||
match Recording::verify(&bytes) {
|
match Recording::verify(&bytes) {
|
||||||
Err(RecordingError::Diverged { expected, actual }) => {
|
Err(RecordingError::Diverged { expected, actual }) => {
|
||||||
assert_eq!(expected, world.state_hash());
|
assert_eq!(actual, world.state_hash());
|
||||||
assert_ne!(actual, expected);
|
assert_ne!(expected, actual);
|
||||||
}
|
}
|
||||||
other => panic!("expected a divergence, got {other:?}"),
|
other => panic!("expected a divergence, got {other:?}"),
|
||||||
}
|
}
|
||||||
@@ -85,10 +102,10 @@ fn a_recording_can_start_in_the_middle_of_a_session() {
|
|||||||
let mut recording = Recording::start(&world);
|
let mut recording = Recording::start(&world);
|
||||||
for tick in 50..120 {
|
for tick in 50..120 {
|
||||||
let commands = commands_for(&world, tick);
|
let commands = commands_for(&world, tick);
|
||||||
recording.push_tick(&commands);
|
recording.step(&mut world, &commands);
|
||||||
world.step(&commands);
|
|
||||||
}
|
}
|
||||||
assert_eq!(recording.tick_count(), 70);
|
assert_eq!(recording.tick_count(), 70);
|
||||||
|
assert_eq!(recording.end_tick(), 120);
|
||||||
assert_eq!(recording.replay(), Ok(world));
|
assert_eq!(recording.replay(), Ok(world));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +117,23 @@ fn an_empty_recording_replays_to_its_starting_state() {
|
|||||||
assert_eq!(Recording::verify(&recording.to_bytes(&world)), Ok(world));
|
assert_eq!(Recording::verify(&recording.to_bytes(&world)), Ok(world));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "the world is not at the tick this recording has reached")]
|
||||||
|
fn a_recording_refuses_a_world_it_does_not_follow() {
|
||||||
|
let mut world = World::new(3);
|
||||||
|
let mut recording = Recording::start(&world);
|
||||||
|
world.step(&[]);
|
||||||
|
recording.step(&mut world, &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "the end state is not at the tick this recording has reached")]
|
||||||
|
fn a_recording_refuses_an_end_state_at_another_tick() {
|
||||||
|
let (recording, mut world) = recorded_session();
|
||||||
|
world.step(&[]);
|
||||||
|
recording.to_bytes(&world);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn damaged_recordings_are_rejected() {
|
fn damaged_recordings_are_rejected() {
|
||||||
let (recording, world) = recorded_session();
|
let (recording, world) = recorded_session();
|
||||||
@@ -133,3 +167,16 @@ fn damaged_recordings_are_rejected() {
|
|||||||
Err(RecordingError::Corrupt(DecodeError::TrailingBytes))
|
Err(RecordingError::Corrupt(DecodeError::TrailingBytes))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_recording_with_a_damaged_start_is_rejected() {
|
||||||
|
let (recording, world) = recorded_session();
|
||||||
|
let mut bytes = recording.to_bytes(&world);
|
||||||
|
// The snapshot starts after marker, version, expected hash and its own
|
||||||
|
// length: 4 + 4 + 8 + 4 bytes.
|
||||||
|
bytes[20 + 10] ^= 0x01;
|
||||||
|
assert_eq!(
|
||||||
|
Recording::from_bytes(&bytes),
|
||||||
|
Err(RecordingError::BadStart(SnapshotError::ChecksumMismatch))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user