Aufzeichnung und Wiederholung von Sitzungen
Eine Aufzeichnung hält den Startzustand und die Befehle jedes Takts fest. Wiederholen ergibt exakt dieselbe Welt; verify meldet, wenn das Ende von der aufgezeichneten Prüfsumme abweicht. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ pub mod inventory;
|
||||
pub mod map;
|
||||
pub mod mapgen;
|
||||
pub mod player;
|
||||
pub mod replay;
|
||||
pub mod rng;
|
||||
pub mod snapshot;
|
||||
pub mod world;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//! Recording of a session: the starting state and every command, tick by
|
||||
//! tick. Replaying a recording reproduces the session exactly.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::codec::{DecodeError, Reader, Writer};
|
||||
use crate::command::Command;
|
||||
use crate::snapshot::SnapshotError;
|
||||
use crate::world::World;
|
||||
|
||||
const MAGIC: [u8; 4] = *b"GRWR";
|
||||
/// Version of the recording format.
|
||||
pub const RECORDING_VERSION: u32 = 1;
|
||||
|
||||
/// Reason a recording could not be read or replayed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RecordingError {
|
||||
/// The bytes do not start with the recording marker.
|
||||
NotARecording,
|
||||
/// The recording was written by an incompatible version of the game.
|
||||
UnsupportedVersion(u32),
|
||||
/// The recording is damaged.
|
||||
Corrupt(DecodeError),
|
||||
/// The starting state inside the recording could not be restored.
|
||||
BadStart(SnapshotError),
|
||||
/// Replaying did not end in the recorded state.
|
||||
Diverged { expected: u64, actual: u64 },
|
||||
}
|
||||
|
||||
impl fmt::Display for RecordingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RecordingError::NotARecording => write!(f, "not a recording"),
|
||||
RecordingError::UnsupportedVersion(version) => {
|
||||
write!(f, "unsupported recording version {version}")
|
||||
}
|
||||
RecordingError::Corrupt(error) => write!(f, "damaged recording: {error}"),
|
||||
RecordingError::BadStart(error) => write!(f, "unusable starting state: {error}"),
|
||||
RecordingError::Diverged { expected, actual } => write!(
|
||||
f,
|
||||
"replay ended in state {actual:016x} instead of {expected:016x}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RecordingError {}
|
||||
|
||||
impl From<DecodeError> for RecordingError {
|
||||
fn from(error: DecodeError) -> Self {
|
||||
RecordingError::Corrupt(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// The starting state of a session and the commands of every tick after it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Recording {
|
||||
start: Vec<u8>,
|
||||
tick_count: u32,
|
||||
/// Ticks that had commands, as (tick index, commands), in rising order.
|
||||
busy_ticks: Vec<(u32, Vec<Command>)>,
|
||||
}
|
||||
|
||||
impl Recording {
|
||||
/// Starts a recording at the current state of `world`.
|
||||
pub fn start(world: &World) -> Self {
|
||||
Self {
|
||||
start: world.to_snapshot(),
|
||||
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]) {
|
||||
if !commands.is_empty() {
|
||||
self.busy_ticks.push((self.tick_count, commands.to_vec()));
|
||||
}
|
||||
self.tick_count += 1;
|
||||
}
|
||||
|
||||
/// Number of recorded ticks.
|
||||
pub fn tick_count(&self) -> u32 {
|
||||
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)?;
|
||||
let mut busy = self.busy_ticks.iter().peekable();
|
||||
for index in 0..self.tick_count {
|
||||
match busy.peek() {
|
||||
Some((tick, commands)) if *tick == index => {
|
||||
world.step(commands);
|
||||
busy.next();
|
||||
}
|
||||
_ => {
|
||||
world.step(&[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
/// Encodes the recording together with the state that replaying it must
|
||||
/// reach. `end` is the world after the last recorded tick.
|
||||
pub fn to_bytes(&self, end: &World) -> Vec<u8> {
|
||||
let mut w = Writer::new();
|
||||
w.raw(&MAGIC);
|
||||
w.u32(RECORDING_VERSION);
|
||||
w.u64(end.state_hash());
|
||||
w.bytes(&self.start);
|
||||
w.u32(self.tick_count);
|
||||
w.u32(u32::try_from(self.busy_ticks.len()).expect("busy tick count fits in u32"));
|
||||
for (tick, commands) in &self.busy_ticks {
|
||||
w.u32(*tick);
|
||||
w.u16(u16::try_from(commands.len()).expect("command count fits in u16"));
|
||||
for command in commands {
|
||||
command.encode(&mut w);
|
||||
}
|
||||
}
|
||||
w.into_bytes()
|
||||
}
|
||||
|
||||
/// Decodes a recording and the state hash that replaying it must reach.
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<(Self, u64), RecordingError> {
|
||||
let mut r = Reader::new(bytes);
|
||||
let magic = r
|
||||
.raw(MAGIC.len())
|
||||
.map_err(|_| RecordingError::NotARecording)?;
|
||||
if magic != MAGIC.as_slice() {
|
||||
return Err(RecordingError::NotARecording);
|
||||
}
|
||||
let version = r.u32()?;
|
||||
if version != RECORDING_VERSION {
|
||||
return Err(RecordingError::UnsupportedVersion(version));
|
||||
}
|
||||
let expected_hash = r.u64()?;
|
||||
let start = r.bytes()?.to_vec();
|
||||
let tick_count = r.u32()?;
|
||||
let busy_count = r.u32()?;
|
||||
let mut busy_ticks = Vec::new();
|
||||
let mut first_free_tick = 0;
|
||||
for _ in 0..busy_count {
|
||||
let tick = r.u32()?;
|
||||
if tick < first_free_tick || tick >= tick_count {
|
||||
return Err(DecodeError::InvalidValue("tick index").into());
|
||||
}
|
||||
first_free_tick = tick + 1;
|
||||
let command_count = r.u16()?;
|
||||
if command_count == 0 {
|
||||
return Err(DecodeError::InvalidValue("command count").into());
|
||||
}
|
||||
let mut commands = Vec::new();
|
||||
for _ in 0..command_count {
|
||||
commands.push(Command::decode(&mut r)?);
|
||||
}
|
||||
busy_ticks.push((tick, commands));
|
||||
}
|
||||
r.finish()?;
|
||||
Ok((
|
||||
Self {
|
||||
start,
|
||||
tick_count,
|
||||
busy_ticks,
|
||||
},
|
||||
expected_hash,
|
||||
))
|
||||
}
|
||||
|
||||
/// Replays `bytes` and checks that the recorded final state is reached.
|
||||
pub fn verify(bytes: &[u8]) -> Result<World, RecordingError> {
|
||||
let (recording, expected) = Self::from_bytes(bytes)?;
|
||||
let world = recording.replay()?;
|
||||
let actual = world.state_hash();
|
||||
if actual == expected {
|
||||
Ok(world)
|
||||
} else {
|
||||
Err(RecordingError::Diverged { expected, actual })
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user