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:
Steven Wroblewski
2026-10-01 10:17:03 +02:00
parent 8573a68901
commit ef728cd100
6 changed files with 324 additions and 35 deletions
+99 -8
View File
@@ -1,15 +1,16 @@
//! Pinned values for everything that must not change unnoticed.
//!
//! Every other test compares two computations inside the same build, so a
//! Most other tests compare two computations inside the same build, so a
//! change that affects both sides alike stays green. This file compares
//! against values recorded once. If one of them fails, rules or encodings
//! changed, on purpose or by accident. If saves or recordings are affected,
//! raise the version constant named in the failure message, then update the
//! value here deliberately.
use grovewright_core::codec::Writer;
use grovewright_core::command::Command;
use grovewright_core::hash::fnv1a64;
use grovewright_core::map::Pos;
use grovewright_core::map::{Map, Pos, Terrain, Tile};
use grovewright_core::mapgen::outcrop_origin;
use grovewright_core::player::PlayerId;
use grovewright_core::replay::Recording;
@@ -99,13 +100,55 @@ fn frozen_script(world: &World) -> Vec<Vec<Command>> {
ticks
}
/// Covers the map generator, the map encoding (terrain numbers, byte order of
/// width and height) and the snapshot header.
/// The commands of a second session of 30 ticks that ends with one player
/// offline, so that the encoding of offline players is pinned as well.
///
/// Never change or extend this script either.
fn frozen_offline_script(world: &World) -> Vec<Vec<Command>> {
let origin = outcrop_origin(world.map());
let scripted = [
(
0,
Command::Join {
key: [7; 16],
name: "Linus".to_owned(),
},
),
(
1,
Command::Join {
key: [8; 16],
name: "Margaret".to_owned(),
},
),
(
3,
Command::Dig {
player: PlayerId(1),
pos: origin,
},
),
(
10,
Command::Leave {
player: PlayerId(0),
},
),
];
let mut ticks = vec![Vec::new(); 30];
for (tick, command) in scripted {
ticks[tick].push(command);
}
ticks
}
/// Covers the map generator, the map encoding and the snapshot header and
/// checksum.
#[test]
fn the_starting_snapshot_is_unchanged() {
let actual = fnv1a64(&World::new(SEED).to_snapshot());
assert_eq!(
actual, 0x1b78_d996_1586_fbf1,
actual, 0x6812_937b_f2b2_19ae,
"the starting world or the snapshot format changed; if saves are \
affected, raise SNAPSHOT_VERSION in core/src/snapshot.rs; then \
update this value"
@@ -129,6 +172,22 @@ fn the_frozen_session_ends_in_the_same_state() {
);
}
/// Covers the encoding of a player who is offline.
#[test]
fn the_frozen_offline_session_ends_in_the_same_state() {
let mut world = World::new(SEED);
for commands in frozen_offline_script(&world) {
world.step(&commands);
}
assert_eq!(
world.state_hash(),
0x904f_10e1_7c52_8be4,
"the rules or the snapshot format changed; old recordings no longer \
verify; if saves are affected, raise SNAPSHOT_VERSION in \
core/src/snapshot.rs; then update this value"
);
}
/// Covers the recording header and layout and the wire format of commands.
#[test]
fn the_recording_format_is_unchanged() {
@@ -140,8 +199,40 @@ fn the_recording_format_is_unchanged() {
}
assert_eq!(
fnv1a64(&recording.to_bytes(&world)),
0x2312_5a7d_d8ef_5ae0,
"the recording or command format changed; raise RECORDING_VERSION in \
core/src/replay.rs; then update this value"
0x1c24_8cc7_6bc8_846a,
"the recording or command format changed; if other tests in this \
file fail as well, deal with those first, because a recording \
contains a snapshot and an end state; otherwise raise \
RECORDING_VERSION in core/src/replay.rs; then update this value"
);
}
/// Covers the byte layout of a map: width before height, little endian,
/// terrain numbers and damage. The map is not square, so a swap of width
/// and height shows.
#[test]
fn the_map_encoding_is_unchanged() {
let mut map = Map::filled(3, 2, Terrain::Ground);
map.set(Pos::new(1, 0), Tile::new(Terrain::Bedrock));
map.set(Pos::new(0, 1), Tile::new(Terrain::HardRock));
map.set(
Pos::new(2, 1),
Tile {
terrain: Terrain::SoftRock,
damage: 1,
},
);
let mut w = Writer::new();
map.encode(&mut w);
assert_eq!(
w.into_bytes(),
vec![
3, 0, 0, 0, // width
2, 0, 0, 0, // height
0, 0, 3, 0, 0, 0, // row 0: ground, bedrock, ground
2, 0, 0, 0, 1, 1, // row 1: hard rock, ground, soft rock hit once
],
"the map encoding changed; raise SNAPSHOT_VERSION in \
core/src/snapshot.rs; then update these bytes"
);
}