//! Pinned values for everything that must not change unnoticed. //! //! 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::{Map, Pos, Terrain, Tile}; use grovewright_core::player::PlayerId; use grovewright_core::protocol::{Action, Refusal, ToGuest, ToHost, PROTOCOL_VERSION}; use grovewright_core::replay::Recording; use grovewright_core::rules::{ end_hash, frozen_offline_script, frozen_script, rules_id, RULES_SEED, }; use grovewright_core::world::World; /// 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(RULES_SEED).to_snapshot()); assert_eq!( 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" ); } /// Covers the rules of joining, leaving and digging, the shortening of names /// and the encoding of players. #[test] fn the_frozen_session_ends_in_the_same_state() { assert_eq!( end_hash(frozen_script), 0xc304_43df_fc74_dcfc, "the rules or the snapshot format changed, and with them the rules \ id; if saves are affected, raise SNAPSHOT_VERSION in \ core/src/snapshot.rs; then update this value" ); } /// Covers the encoding of a player who is offline. #[test] fn the_frozen_offline_session_ends_in_the_same_state() { assert_eq!( end_hash(frozen_offline_script), 0x904f_10e1_7c52_8be4, "the rules or the snapshot format changed, and with them the rules \ id; if saves are affected, raise SNAPSHOT_VERSION in \ core/src/snapshot.rs; then update this value" ); } /// Covers how the identity of the rules is combined from the end states of /// the frozen scripts. #[test] fn the_rules_id_is_unchanged() { assert_eq!( rules_id(), 0x04f1_1b1e_16c5_feff, "the rules id changed: builds with other rules can no longer play \ together, and older recordings report other rules; if the tests \ above fail as well, deal with those first; then update this value" ); } /// Covers the recording header and layout and the wire format of commands. #[test] fn the_recording_format_is_unchanged() { let mut world = World::new(RULES_SEED); let mut recording = Recording::start(&world); for commands in frozen_script(&world) { recording.step(&mut world, &commands); } assert_eq!( fnv1a64(&recording.to_bytes(&world)), 0xf203_b229_8dad_7c53, "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" ); } /// Covers every message between host and guests, built from literal values. #[test] fn the_protocol_format_is_unchanged() { let to_host = [ ToHost::Hello { rules: 0x0102_0304_0506_0708, key: [3; 16], name: "Ada".to_owned(), }, ToHost::Act(Action::Dig { pos: Pos::new(-2, 9), }), ToHost::Diverged { tick: 40, hash: 77 }, ]; let to_guest = [ ToGuest::World { snapshot: vec![5, 6, 7], }, ToGuest::Refused(Refusal::WorldFull), ToGuest::Tick { tick: 12, commands: vec![Command::Dig { player: PlayerId(1), pos: Pos::new(3, 4), }], check: Some(99), }, ToGuest::Tick { tick: 13, commands: vec![], check: None, }, ]; let mut bytes = Vec::new(); for message in &to_host { bytes.extend(message.encode()); } for message in &to_guest { bytes.extend(message.encode()); } assert_eq!( fnv1a64(&bytes), 0xc263_69c0_f892_247b, "the messages between host and guests changed; raise \ PROTOCOL_VERSION in core/src/protocol.rs; then update this value. \ The start of a greeting and the refusal must never change, see \ the_greeting_start_and_the_refusal_never_change" ); } /// Covers the promise that games of every version can still tell each other /// that they do not fit: a greeting starts with its tag and the version, and /// a refusal is its tag and the code of its reason. Unlike every other part /// of the messages, these stay the same when PROTOCOL_VERSION is raised. #[test] fn the_greeting_start_and_the_refusal_never_change() { let hello = ToHost::Hello { rules: 1, key: [3; 16], name: "Ada".to_owned(), } .encode(); let mut start = vec![1]; start.extend(PROTOCOL_VERSION.to_le_bytes()); assert_eq!( hello[..start.len()], start[..], "a greeting starts with the tag 1 and the version; this never \ changes, not even with a new PROTOCOL_VERSION" ); let refusals = [ (Refusal::OtherProtocol, 1), (Refusal::OtherRules, 2), (Refusal::WorldFull, 3), (Refusal::TooManyPlayers, 4), (Refusal::AlreadyPlaying, 5), ]; for (refusal, code) in refusals { assert_eq!( ToGuest::Refused(refusal).encode(), vec![2, code], "a refusal is the tag 2 and the code of its reason; this never \ changes, not even with a new PROTOCOL_VERSION" ); } }