use grovewright_core::codec::DecodeError; 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::snapshot::SnapshotError; use grovewright_core::world::World; const ADA: PlayerId = PlayerId(0); /// Commands of tick `tick` in a short scripted session. fn commands_for(world: &World, tick: u32) -> Vec { if tick == 0 { vec![Command::Join { key: [1; 16], name: "Ada".to_owned(), }] } else if tick.is_multiple_of(7) { vec![Command::Dig { player: ADA, pos: outcrop_origin(world.map()), }] } else { vec![] } } /// Plays 200 ticks from a fresh world and records them. fn recorded_session() -> (Recording, World) { let mut world = World::new(11); let mut recording = Recording::start(&world); for tick in 0..200 { let commands = commands_for(&world, tick); 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(); assert_eq!(recording.tick_count(), 200); assert_eq!(world.player(ADA).unwrap().inventory.count(ITEM_STONE), 1); assert_eq!(recording.replay(), Ok(world)); } #[test] fn a_recording_survives_encoding() { let (recording, world) = recorded_session(); let bytes = recording.to_bytes(&world); let (decoded, expected_hash) = Recording::from_bytes(&bytes).unwrap(); assert_eq!(decoded, recording); assert_eq!(expected_hash, world.state_hash()); } #[test] fn verify_accepts_a_faithful_recording() { let (recording, world) = recorded_session(); assert_eq!(Recording::verify(&recording.to_bytes(&world)), Ok(world)); } #[test] 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; match Recording::verify(&bytes) { Err(RecordingError::Diverged { expected, actual }) => { assert_eq!(actual, world.state_hash()); assert_ne!(expected, actual); } other => panic!("expected a divergence, got {other:?}"), } } #[test] fn a_recording_can_start_in_the_middle_of_a_session() { let mut world = World::new(11); for tick in 0..50 { let commands = commands_for(&world, tick); world.step(&commands); } let mut recording = Recording::start(&world); for tick in 50..120 { let commands = commands_for(&world, tick); recording.step(&mut world, &commands); } assert_eq!(recording.tick_count(), 70); assert_eq!(recording.end_tick(), 120); // Decoding derives the start tick from the stored start world. The // decoded recording must end at the same tick and equal the original. let (decoded, _) = Recording::from_bytes(&recording.to_bytes(&world)).unwrap(); assert_eq!(decoded.end_tick(), 120); assert_eq!(decoded, recording); assert_eq!(recording.replay(), Ok(world)); } #[test] fn an_empty_recording_replays_to_its_starting_state() { let world = World::new(3); let recording = Recording::start(&world); assert_eq!(recording.tick_count(), 0); 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(); let bytes = recording.to_bytes(&world); assert_eq!( Recording::from_bytes(b""), Err(RecordingError::NotARecording) ); assert_eq!( Recording::from_bytes(&world.to_snapshot()), Err(RecordingError::NotARecording) ); let mut other_version = bytes.clone(); other_version[4..8].copy_from_slice(&(RECORDING_VERSION + 1).to_le_bytes()); assert_eq!( Recording::from_bytes(&other_version), Err(RecordingError::UnsupportedVersion(RECORDING_VERSION + 1)) ); assert_eq!( Recording::from_bytes(&bytes[..bytes.len() - 1]), Err(RecordingError::Corrupt(DecodeError::UnexpectedEnd)) ); let mut padded = bytes.clone(); padded.push(0); assert_eq!( Recording::from_bytes(&padded), 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)) ); }