Regel-Kennung und Aufzeichnung Version 3

Die eingefrorenen Skripte ziehen von den festgeschriebenen Tests in den
Kern. Aus ihren Endzuständen leitet sich die Regel-Kennung ab, die sich
mit jeder Regel ändert, die ein Skript berührt. Aufzeichnungen tragen
sie und werden mit anderen Regeln abgelehnt, statt eine scheinbare
Abweichung zu melden. Fremde Aufzeichnungen dürfen höchstens 30 Tage
lang sein. Ein Test hält fest, dass zu lange Namen beim Dekodieren
abgelehnt werden.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-10-01 18:00:00 +02:00
parent 760bea36fd
commit f2e4c91082
6 changed files with 296 additions and 154 deletions
+55 -6
View File
@@ -3,11 +3,15 @@ use grovewright_core::command::Command;
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::replay::{Recording, RecordingError, MAX_RECORDING_TICKS, RECORDING_VERSION};
use grovewright_core::rules::rules_id;
use grovewright_core::snapshot::SnapshotError;
use grovewright_core::world::World;
const ADA: PlayerId = PlayerId(0);
/// Offset of the starting snapshot in an encoded recording: marker, version,
/// rules identity, expected hash and the snapshot's length come first.
const START: usize = 4 + 4 + 8 + 8 + 4;
/// Commands of tick `tick` in a short scripted session.
fn commands_for(world: &World, tick: u32) -> Vec<Command> {
@@ -81,8 +85,9 @@ fn verify_accepts_a_faithful_recording() {
fn verify_reports_a_replay_that_ends_in_another_state() {
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;
// The expected state hash follows the marker, the version and the rules
// identity.
bytes[16] ^= 0x01;
match Recording::verify(&bytes) {
Err(RecordingError::Diverged { expected, actual }) => {
assert_eq!(actual, world.state_hash());
@@ -179,11 +184,55 @@ fn damaged_recordings_are_rejected() {
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;
// The snapshot starts after marker, version, rules identity, expected
// hash and its own length: 4 + 4 + 8 + 8 + 4 bytes.
bytes[START + 10] ^= 0x01;
assert_eq!(
Recording::from_bytes(&bytes),
Err(RecordingError::BadStart(SnapshotError::ChecksumMismatch))
);
}
#[test]
fn a_recording_of_other_rules_is_refused_before_replaying() {
let (recording, world) = recorded_session();
let mut bytes = recording.to_bytes(&world);
// The rules identity follows the marker and the version.
bytes[8] ^= 0x01;
assert_eq!(
Recording::from_bytes(&bytes),
Err(RecordingError::OtherRules {
recorded: rules_id() ^ 0x01,
current: rules_id()
})
);
}
/// The encoding of `recording` with its tick count replaced by `ticks`. The
/// tick count follows the starting snapshot.
fn with_tick_count(recording: &Recording, world: &World, ticks: u32) -> Vec<u8> {
let mut bytes = recording.to_bytes(world);
let length = u32::from_le_bytes(bytes[START - 4..START].try_into().unwrap()) as usize;
let at = START + length;
bytes[at..at + 4].copy_from_slice(&ticks.to_le_bytes());
bytes
}
#[test]
fn a_recording_longer_than_thirty_days_is_refused() {
let world = World::new(3);
let recording = Recording::start(&world);
let (longest, _) =
Recording::from_bytes(&with_tick_count(&recording, &world, MAX_RECORDING_TICKS)).unwrap();
assert_eq!(longest.tick_count(), MAX_RECORDING_TICKS);
assert_eq!(
Recording::from_bytes(&with_tick_count(
&recording,
&world,
MAX_RECORDING_TICKS + 1
)),
Err(RecordingError::Corrupt(DecodeError::InvalidValue(
"tick count"
)))
);
}