M1b: Brücke und Hülle #6
+38
-5
@@ -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,
|
||||
},
|
||||
|
||||
@@ -123,8 +123,7 @@ fn a_recorded_session_replays_to_the_same_hash() {
|
||||
let mut world = World::new(8);
|
||||
let mut recording = Recording::start(&world);
|
||||
for commands in &script {
|
||||
recording.push_tick(commands);
|
||||
world.step(commands);
|
||||
recording.step(&mut world, commands);
|
||||
}
|
||||
let bytes = recording.to_bytes(&world);
|
||||
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 recording = Recording::start(&world);
|
||||
for commands in frozen_script(&world) {
|
||||
recording.push_tick(&commands);
|
||||
world.step(&commands);
|
||||
recording.step(&mut world, &commands);
|
||||
}
|
||||
assert_eq!(
|
||||
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::player::PlayerId;
|
||||
use grovewright_core::replay::{Recording, RecordingError, RECORDING_VERSION};
|
||||
use grovewright_core::snapshot::SnapshotError;
|
||||
use grovewright_core::world::World;
|
||||
|
||||
const ADA: PlayerId = PlayerId(0);
|
||||
@@ -31,12 +32,28 @@ fn recorded_session() -> (Recording, World) {
|
||||
let mut recording = Recording::start(&world);
|
||||
for tick in 0..200 {
|
||||
let commands = commands_for(&world, tick);
|
||||
recording.push_tick(&commands);
|
||||
world.step(&commands);
|
||||
recording.step(&mut world, &commands);
|
||||
}
|
||||
(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]
|
||||
fn replaying_reaches_the_same_state() {
|
||||
let (recording, world) = recorded_session();
|
||||
@@ -62,14 +79,14 @@ fn verify_accepts_a_faithful_recording() {
|
||||
|
||||
#[test]
|
||||
fn verify_reports_a_replay_that_ends_in_another_state() {
|
||||
let (recording, mut world) = recorded_session();
|
||||
// One tick more than was recorded: the claimed end state is unreachable.
|
||||
world.step(&[]);
|
||||
let bytes = recording.to_bytes(&world);
|
||||
let (recording, world) = recorded_session();
|
||||
let mut bytes = recording.to_bytes(&world);
|
||||
// The expected state hash follows the marker and the version.
|
||||
bytes[8] ^= 0x01;
|
||||
match Recording::verify(&bytes) {
|
||||
Err(RecordingError::Diverged { expected, actual }) => {
|
||||
assert_eq!(expected, world.state_hash());
|
||||
assert_ne!(actual, expected);
|
||||
assert_eq!(actual, world.state_hash());
|
||||
assert_ne!(expected, actual);
|
||||
}
|
||||
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);
|
||||
for tick in 50..120 {
|
||||
let commands = commands_for(&world, tick);
|
||||
recording.push_tick(&commands);
|
||||
world.step(&commands);
|
||||
recording.step(&mut world, &commands);
|
||||
}
|
||||
assert_eq!(recording.tick_count(), 70);
|
||||
assert_eq!(recording.end_tick(), 120);
|
||||
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));
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn damaged_recordings_are_rejected() {
|
||||
let (recording, world) = recorded_session();
|
||||
@@ -133,3 +167,16 @@ fn damaged_recordings_are_rejected() {
|
||||
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