//! Generate MessagePack fixture files for cross-language testing (D-030 Layer 1). //! Run with: cargo test --test gen_fixtures -- --ignored use settled_reach_server::bridge::types::*; use settled_reach_server::simulation::time::{DayPhase, TickRate}; use std::fs; use std::path::Path; fn write_fixture(name: &str, bytes: &[u8]) { // Write directly into the Godot project's test fixtures (single source of truth) let dir = Path::new("../client/tests/fixtures/msgpack"); fs::create_dir_all(dir).expect("create fixture dir"); let path = dir.join(format!("{}.msgpack", name)); fs::write(&path, bytes).expect("write fixture"); eprintln!("Wrote {} ({} bytes)", path.display(), bytes.len()); } /// Helper to create a minimal v2 snapshot for fixtures fn fixture_snapshot(tick: u64, entities: Vec) -> ObserverSnapshot { ObserverSnapshot { version: PROTOCOL_VERSION, tick, game_time: GameTime { day: 0, time_of_day: 0, day_phase: DayPhase::Morning, tick_rate: TickRate::Full, }, player_facing: FacingDirection::North, player_stance: MovementStance::default(), player_inventory: vec![], entities, visible_tiles: vec![], nearby_interactions: vec![], current_monologue: None, pending_recognitions: vec![], dialogue_response: None, blocked_entities: vec![], scan_events: vec![], sound_events: vec![], conversation_events: vec![], conversation_ended: vec![], rng_seed: None, } } #[test] #[ignore] // Run manually: cargo test --test gen_fixtures -- --ignored fn generate_msgpack_fixtures() { // Snapshot with one NPC entity let snapshot = fixture_snapshot( 42, vec![VisibleEntity { entity_id: 1, x: 10.0, y: 20.0, z: 0, kind: EntityKind::Npc, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }], ); write_fixture( "snapshot_one_npc", &rmp_serde::to_vec_named(&snapshot).unwrap(), ); // Empty snapshot let empty = fixture_snapshot(0, vec![]); write_fixture("snapshot_empty", &rmp_serde::to_vec_named(&empty).unwrap()); // PlayerInput: MoveNorth let input_north = PlayerInput { tick: 100, action: PlayerAction::MoveNorth, }; write_fixture( "input_move_north", &rmp_serde::to_vec_named(&input_north).unwrap(), ); // PlayerInput: UsePerceptionMode let input_perception = PlayerInput { tick: 200, action: PlayerAction::UsePerceptionMode("thermal".to_string()), }; write_fixture( "input_perception_mode", &rmp_serde::to_vec_named(&input_perception).unwrap(), ); // Snapshot with Player entity let snapshot_player = fixture_snapshot( 1, vec![VisibleEntity { entity_id: 100, x: 16.5, y: 16.5, z: 0, kind: EntityKind::Player, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }], ); write_fixture( "snapshot_player", &rmp_serde::to_vec_named(&snapshot_player).unwrap(), ); // Snapshot with multiple entities and all EntityKind variants let snapshot_multi = fixture_snapshot( 999, vec![ VisibleEntity { entity_id: 1, x: 16.5, y: 16.5, z: 0, kind: EntityKind::Player, visibility: VisibilitySector::Forward, relationship: RelationshipState::Known, observation: EntityVisibility::Visible, }, VisibleEntity { entity_id: 2, x: 5.0, y: 10.0, z: 0, kind: EntityKind::Npc, visibility: VisibilitySector::Peripheral, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }, VisibleEntity { entity_id: 3, x: 15.5, y: 3.0, z: 1, kind: EntityKind::Object, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }, VisibleEntity { entity_id: 4, x: 0.0, y: 0.0, z: -1, kind: EntityKind::Terrain, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }, ], ); write_fixture( "snapshot_multi_entity", &rmp_serde::to_vec_named(&snapshot_multi).unwrap(), ); // v2 snapshot with visible_tiles and game_time populated let snapshot_v2_full = ObserverSnapshot { version: PROTOCOL_VERSION, tick: 500, game_time: GameTime { day: 1, time_of_day: 720, day_phase: DayPhase::Evening, tick_rate: TickRate::Full, }, player_facing: FacingDirection::Southeast, player_stance: MovementStance::default(), player_inventory: vec![], entities: vec![VisibleEntity { entity_id: 1, x: 10.5, y: 10.5, z: 0, kind: EntityKind::Player, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }], visible_tiles: vec![ VisibleTile { x: 10, y: 10, z: 0, visibility: VisibilitySector::Forward, tile_kind: TileKind::Floor, zone_id: Some(1), }, VisibleTile { x: 11, y: 10, z: 0, visibility: VisibilitySector::Peripheral, tile_kind: TileKind::Floor, zone_id: Some(1), }, VisibleTile { x: 10, y: 9, z: 0, visibility: VisibilitySector::Forward, tile_kind: TileKind::Floor, zone_id: None, }, ], nearby_interactions: vec![], current_monologue: None, pending_recognitions: vec![], dialogue_response: None, blocked_entities: vec![], scan_events: vec![], sound_events: vec![], conversation_events: vec![], conversation_ended: vec![], rng_seed: None, }; write_fixture( "snapshot_v2_full", &rmp_serde::to_vec_named(&snapshot_v2_full).unwrap(), ); // Batch input: Vec with two actions (D-030 Layer 1 bidirectional symmetry) let input_batch = vec![ PlayerInput { tick: 0, action: PlayerAction::MoveNorth, }, PlayerInput { tick: 0, action: PlayerAction::Interact { target_entity_id: None, verb: None, }, }, ]; write_fixture( "input_batch_two", &rmp_serde::to_vec_named(&input_batch).unwrap(), ); // Diagonal movement fixtures (clockwise: NE, SE, SW, NW) for (name, action) in [ ("input_move_northeast", PlayerAction::MoveNortheast), ("input_move_southeast", PlayerAction::MoveSoutheast), ("input_move_southwest", PlayerAction::MoveSouthwest), ("input_move_northwest", PlayerAction::MoveNorthwest), ] { let input = PlayerInput { tick: 100, action }; write_fixture(name, &rmp_serde::to_vec_named(&input).unwrap()); } // === Boundary value fixtures (#472) === // 14 raw integer values at encoding format boundaries (Appendix C). // These are Rust-encoded MessagePack that GDScript must decode correctly. // Covers every encoding format transition and the int16/int32 asymmetry zones. let boundary_raw: [(u64, &str); 14] = [ // pos fixint boundaries (0, "boundary_raw_0"), (127, "boundary_raw_127"), // uint 8 boundaries (128, "boundary_raw_128"), (255, "boundary_raw_255"), // int16/uint16 asymmetry zone (GDScript: int_16, Rust: uint_16) (256, "boundary_raw_256"), (32767, "boundary_raw_32767"), // uint 16 boundaries (32768, "boundary_raw_32768"), (65535, "boundary_raw_65535"), // int32/uint32 asymmetry zone (GDScript: int_32, Rust: uint_32) (65536, "boundary_raw_65536"), (2147483647, "boundary_raw_2147483647"), // uint 32 boundaries (2147483648, "boundary_raw_2147483648"), (4294967295, "boundary_raw_4294967295"), // int 64 boundaries (4294967296, "boundary_raw_4294967296"), (u64::MAX >> 1, "boundary_raw_i64_max"), // 2^63-1 = i64::MAX ]; for (value, name) in &boundary_raw { // Encode as u64 (matches how entity_id/tick are encoded in snapshots) let bytes = rmp_serde::to_vec(value).expect("encode boundary value"); write_fixture(name, &bytes); } // 5 snapshot fixtures at boundary tick values. // Tests that GDScript can decode full ObserverSnapshot structs when the tick // field crosses encoding format boundaries. let boundary_snapshots: [(u64, &str); 5] = [ (0, "snapshot_boundary_tick_0"), // pos fixint (127, "snapshot_boundary_tick_127"), // pos fixint max (32767, "snapshot_boundary_tick_32767"), // int16/uint16 asymmetry (2147483647, "snapshot_boundary_tick_2b31m1"), // int32/uint32 asymmetry (4294967296, "snapshot_boundary_tick_2b32"), // int64 minimum ]; for (tick, name) in &boundary_snapshots { let snapshot = fixture_snapshot(*tick, vec![]); write_fixture(name, &rmp_serde::to_vec_named(&snapshot).unwrap()); } }