Speicherstand mit Prüfsumme und Prüfung beim Laden
Snapshots tragen jetzt eine Prüfsumme (Format 2). Beim Wiederherstellen werden Zustände abgelehnt, die die Regeln nie erzeugen: zu große Takte, mehr als vier Spieler online, doppelte Schlüssel, unmögliche Wartezeiten. World::load ersetzt den öffentlichen Aufruf von set_all_offline. Die festgeschriebenen Werte sind bewusst angepasst und um einen Offline-Spieler und das Kartenformat ergänzt. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -9,8 +9,9 @@ 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;
|
||||
/// Version of the recording format. Raise it whenever the encoded recording,
|
||||
/// including the snapshot inside it, changes.
|
||||
pub const RECORDING_VERSION: u32 = 2;
|
||||
|
||||
/// Reason a recording could not be read or replayed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
||||
+74
-8
@@ -1,18 +1,26 @@
|
||||
//! Complete, versioned encoding of a world. Saves, joining players and state
|
||||
//! checksums all use it.
|
||||
//!
|
||||
//! Layout: the marker `GRWS`, the format version, the encoded state, and an
|
||||
//! FNV-1a checksum over everything before it.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::codec::{DecodeError, Reader, Writer};
|
||||
use crate::hash::fnv1a64;
|
||||
use crate::map::Map;
|
||||
use crate::player::{Player, MAX_KNOWN_PLAYERS};
|
||||
use crate::world::World;
|
||||
use crate::player::{Player, MAX_KNOWN_PLAYERS, MAX_ONLINE_PLAYERS};
|
||||
use crate::world::{World, DIG_COOLDOWN_TICKS};
|
||||
|
||||
const MAGIC: [u8; 4] = *b"GRWS";
|
||||
/// Version of the snapshot format. Raise it whenever the encoded state
|
||||
/// changes.
|
||||
pub const SNAPSHOT_VERSION: u32 = 1;
|
||||
pub const SNAPSHOT_VERSION: u32 = 2;
|
||||
/// Length of the checksum at the end of a snapshot.
|
||||
const CHECKSUM_BYTES: usize = 8;
|
||||
/// Highest tick a restored world may have. No real session gets near it,
|
||||
/// and adding to it can never overflow.
|
||||
pub const MAX_RESTORABLE_TICK: u64 = u64::MAX / 2;
|
||||
|
||||
/// Reason a snapshot could not be restored.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -21,8 +29,13 @@ pub enum SnapshotError {
|
||||
NotASnapshot,
|
||||
/// The snapshot was written by an incompatible version of the game.
|
||||
UnsupportedVersion(u32),
|
||||
/// The checksum does not match: the bytes changed after they were
|
||||
/// written.
|
||||
ChecksumMismatch,
|
||||
/// The snapshot is damaged.
|
||||
Corrupt(DecodeError),
|
||||
/// The bytes decode, but describe a state the rules can never produce.
|
||||
Invalid(&'static str),
|
||||
}
|
||||
|
||||
impl fmt::Display for SnapshotError {
|
||||
@@ -32,7 +45,9 @@ impl fmt::Display for SnapshotError {
|
||||
SnapshotError::UnsupportedVersion(version) => {
|
||||
write!(f, "unsupported snapshot version {version}")
|
||||
}
|
||||
SnapshotError::ChecksumMismatch => write!(f, "snapshot checksum does not match"),
|
||||
SnapshotError::Corrupt(error) => write!(f, "damaged snapshot: {error}"),
|
||||
SnapshotError::Invalid(what) => write!(f, "impossible state in snapshot: {what}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,16 +80,20 @@ impl World {
|
||||
}
|
||||
|
||||
/// Encodes the complete world behind a header that names format and
|
||||
/// version.
|
||||
/// version, followed by a checksum.
|
||||
pub fn to_snapshot(&self) -> Vec<u8> {
|
||||
let mut w = Writer::new();
|
||||
w.raw(&MAGIC);
|
||||
w.u32(SNAPSHOT_VERSION);
|
||||
self.encode_state(&mut w);
|
||||
w.into_bytes()
|
||||
let mut bytes = w.into_bytes();
|
||||
let checksum = fnv1a64(&bytes);
|
||||
bytes.extend_from_slice(&checksum.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Restores a world from bytes written by [`World::to_snapshot`].
|
||||
/// Restores a world exactly as it was encoded, including which players
|
||||
/// were online. Joining players receive the host's world this way.
|
||||
pub fn from_snapshot(bytes: &[u8]) -> Result<World, SnapshotError> {
|
||||
let mut r = Reader::new(bytes);
|
||||
let magic = r
|
||||
@@ -87,6 +106,17 @@ impl World {
|
||||
if version != SNAPSHOT_VERSION {
|
||||
return Err(SnapshotError::UnsupportedVersion(version));
|
||||
}
|
||||
if r.remaining() < CHECKSUM_BYTES {
|
||||
return Err(DecodeError::UnexpectedEnd.into());
|
||||
}
|
||||
let (body, trailer) = bytes.split_at(bytes.len() - CHECKSUM_BYTES);
|
||||
let mut checksum = [0u8; CHECKSUM_BYTES];
|
||||
checksum.copy_from_slice(trailer);
|
||||
if fnv1a64(body) != u64::from_le_bytes(checksum) {
|
||||
return Err(SnapshotError::ChecksumMismatch);
|
||||
}
|
||||
|
||||
let mut r = Reader::new(&body[MAGIC.len() + 4..]);
|
||||
let seed = r.u64()?;
|
||||
let tick = r.u64()?;
|
||||
let map = Map::decode(&mut r)?;
|
||||
@@ -99,11 +129,47 @@ impl World {
|
||||
players.push(Player::decode(&mut r)?);
|
||||
}
|
||||
r.finish()?;
|
||||
Ok(World {
|
||||
|
||||
let world = World {
|
||||
seed,
|
||||
tick,
|
||||
map,
|
||||
players,
|
||||
})
|
||||
};
|
||||
world.check_restorable()?;
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
/// Restores a saved world for a new session. Like
|
||||
/// [`World::from_snapshot`], but every player starts offline, because
|
||||
/// nobody has joined the new session yet.
|
||||
pub fn load(bytes: &[u8]) -> Result<World, SnapshotError> {
|
||||
let mut world = World::from_snapshot(bytes)?;
|
||||
world.set_all_offline();
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
/// Rejects states that the rules can never produce, so that a damaged or
|
||||
/// crafted snapshot fails here instead of breaking a later tick.
|
||||
fn check_restorable(&self) -> Result<(), SnapshotError> {
|
||||
if self.tick > MAX_RESTORABLE_TICK {
|
||||
return Err(SnapshotError::Invalid("tick"));
|
||||
}
|
||||
let online = self.players.iter().filter(|player| player.online).count();
|
||||
if online > MAX_ONLINE_PLAYERS {
|
||||
return Err(SnapshotError::Invalid("online players"));
|
||||
}
|
||||
for (index, player) in self.players.iter().enumerate() {
|
||||
if self.players[..index]
|
||||
.iter()
|
||||
.any(|other| other.key == player.key)
|
||||
{
|
||||
return Err(SnapshotError::Invalid("duplicate player key"));
|
||||
}
|
||||
if player.next_action_tick > self.tick + DIG_COOLDOWN_TICKS {
|
||||
return Err(SnapshotError::Invalid("next action tick"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -95,9 +95,11 @@ impl World {
|
||||
.map(|index| PlayerId(index as u8))
|
||||
}
|
||||
|
||||
/// Marks every player as offline. Used after loading a save from disk,
|
||||
/// when nobody has joined yet.
|
||||
pub fn set_all_offline(&mut self) {
|
||||
/// Marks every player as offline. Only [`World::load`] uses it: a loaded
|
||||
/// save starts a new session, which nobody has joined yet. Called during
|
||||
/// a running session on one machine, it would split the worlds of the
|
||||
/// players apart.
|
||||
pub(crate) fn set_all_offline(&mut self) {
|
||||
for player in &mut self.players {
|
||||
player.online = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user