//! Generate MessagePack fixture files for cross-language testing (D-030 Layer 1). //! Run with: cargo test --test gen_fixtures -- --ignored use settled_reach_server::atlas::body_world_state::{DrainageBasin, RiverNetwork}; use settled_reach_server::atlas::layer1::Layer1Output; use settled_reach_server::atlas::layer_proxy::{ AtlasLayerResponse, AtlasLayerStatus, DistrictWindowLayer, QuarterFootprintEntry, QuarterFootprintLayer, RegionGridLayer, RoadGraphEdge, RoadGraphLayer, RoadGraphNode, SettlementEntry, SettlementLayer, SettlementSizeClass, WindowGranularity, REGION_TEMP_NONE_DC, }; use settled_reach_server::atlas::region_profile::{SeasonPhase, WeatherState}; use settled_reach_server::atlas::road_graph::RoadNodeKind; use settled_reach_server::bridge::types::*; use settled_reach_server::simulation::generator::{ AttractorType, DistrictType, GeographicAttractor, MaintenanceAuthority, SubBiomeVariant, ZoningType, }; use settled_reach_server::simulation::poi::PoiCategory; use settled_reach_server::simulation::time::{DayPhase, TickRate}; use std::collections::BTreeMap; 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 snapshot for fixtures (D-192: no version field) fn fixture_snapshot(tick: u64, entities: Vec) -> ObserverSnapshot { ObserverSnapshot { 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![], follow_state: None, character_pressure: None, rng_seed: None, poi_list: vec![], examine_result: None, player_knowledge: None, save_result: None, triangle_crisis_events: vec![], state_hash: None, debug_response: None, sim_errors: vec![], current_ticker: None, settings_response: None, economy_snapshot: None, bookmark_catalog: 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, tell_state: None, }], ); 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, tell_state: None, }], ); 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, tell_state: None, }, VisibleEntity { entity_id: 2, x: 5.0, y: 10.0, z: 0, kind: EntityKind::Npc, visibility: VisibilitySector::Peripheral, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, tell_state: None, }, VisibleEntity { entity_id: 3, x: 15.5, y: 3.0, z: 1, kind: EntityKind::Object, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, tell_state: None, }, VisibleEntity { entity_id: 4, x: 0.0, y: 0.0, z: -1, kind: EntityKind::Terrain, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, tell_state: None, }, ], ); 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 { 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, tell_state: None, }], 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![], follow_state: None, character_pressure: None, rng_seed: None, poi_list: vec![], examine_result: None, player_knowledge: None, save_result: None, triangle_crisis_events: vec![], state_hash: None, debug_response: None, sim_errors: vec![], current_ticker: None, settings_response: None, economy_snapshot: None, bookmark_catalog: 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(), ); // PlayerInput: DialogueResponse (#539) let input_dialogue_response = PlayerInput { tick: 300, action: PlayerAction::DialogueResponse { target_entity_id: 42, response_id: "kael-davan_d_001".to_string(), }, }; write_fixture( "input_dialogue_response", &rmp_serde::to_vec_named(&input_dialogue_response).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()); } // === #271 fixtures: named fixtures for cross-language Layer 1 testing === // snapshot_minimal: tick=0, one Player entity, all optionals absent let snapshot_minimal = fixture_snapshot( 0, vec![VisibleEntity { entity_id: 1, x: 0.0, y: 0.0, z: 0, kind: EntityKind::Player, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, tell_state: None, }], ); write_fixture( "snapshot_minimal", &rmp_serde::to_vec_named(&snapshot_minimal).unwrap(), ); // snapshot_full: tick=42, monologue + dialogue + inventory + POIs + KG dump let snapshot_full = ObserverSnapshot { tick: 42, game_time: GameTime { day: 3, time_of_day: 840, day_phase: DayPhase::Evening, tick_rate: TickRate::Full, }, player_facing: FacingDirection::East, player_stance: MovementStance::Walk, player_inventory: vec![InventoryItem { item_id: 7, name: "Forged Customs Cert".to_string(), slot: 0, }], entities: vec![VisibleEntity { entity_id: 1, x: 10.0, y: 10.0, z: 0, kind: EntityKind::Player, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, tell_state: None, }], visible_tiles: vec![], nearby_interactions: vec![], current_monologue: Some(MonologueEvent { id: "test_monologue_001".to_string(), text: "Something feels off about this place.".to_string(), duration_seconds: 4.0, }), pending_recognitions: vec![PendingRecognitionWire { entity_id: 7, x: 12.0, y: 8.0, z: 0, remaining_ticks: 3, total_delay_ticks: 10, }], dialogue_response: Some(DialogueResponseEvent { line_id: "kael_d_001".to_string(), text: "We need to talk about the shipment.".to_string(), speaker_entity_id: 99, speaker_color_index: 2, speaker_name: "Kael".to_string(), }), blocked_entities: vec![5, 6], scan_events: vec![], sound_events: vec![], follow_state: None, character_pressure: None, rng_seed: Some(0xDEADBEEF), poi_list: vec![PoiWire { poi_id: "docking_bay_7".to_string(), name: "Docking Bay 7".to_string(), x: 50, y: 30, z: 0, category: PoiCategory::Location, }], examine_result: Some(ExamineResultWire { entity_id: 42, text: "A smuggler, probably. The way they hold themselves.".to_string(), confidence: KnowledgeConfidence::KnowsOf, }), save_result: None, player_knowledge: Some(PlayerKnowledgeWire { entities: vec![KnownEntityWire { entity_id: 99, name: "Kael".to_string(), confidence: KnowledgeConfidence::KnowsDetails, source: "DirectObservation".to_string(), state: KnowledgeState::Active, relationship: RelationshipState::Known, last_observed_tick: 40, }], facts: vec![KnownFactWire { fact_id: "poi.docking_bay_7".to_string(), confidence: KnowledgeConfidence::KnowsOf, source: "DirectObservation".to_string(), state: KnowledgeState::Active, acquired_tick: 10, }], }), triangle_crisis_events: vec![], state_hash: None, debug_response: None, sim_errors: vec![], current_ticker: None, settings_response: None, economy_snapshot: None, bookmark_catalog: None, }; write_fixture( "snapshot_full", &rmp_serde::to_vec_named(&snapshot_full).unwrap(), ); // player_input_move: tick=1, MoveNorth let input_move = PlayerInput { tick: 1, action: PlayerAction::MoveNorth, }; write_fixture( "player_input_move", &rmp_serde::to_vec_named(&input_move).unwrap(), ); // player_input_interact: tick=2, Interact { target: 99, verb: "Talk" } let input_interact = PlayerInput { tick: 2, action: PlayerAction::Interact { target_entity_id: Some(99), verb: Some("Talk".to_string()), }, }; write_fixture( "player_input_interact", &rmp_serde::to_vec_named(&input_interact).unwrap(), ); // malformed: intentionally truncated bytes — tests error handling in both Rust and GDScript // 0x82 = fixmap with 2 entries, 0xa4 = fixstr of length 4 — incomplete map, no key/value follows write_fixture("malformed", &[0x82u8, 0xa4u8]); // === 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()); } } /// Generate a snapshot fixture with a populated `BookmarkCatalog`. /// /// Client (#618) uses this to validate GDScript MessagePack decode against /// real `rmp_serde` output — field order and string encoding may diverge /// from GDScript-constructed data. #[test] #[ignore] // Run manually: cargo test --test gen_fixtures -- --ignored fn generate_snapshot_with_bookmark_catalog() { let catalog = BookmarkCatalog { bookmarks: vec![BookmarkWire { id: "tycoon".into(), title: "Tycoon".into(), subtitle: "Small business owner on the make.".into(), flavor: String::new(), default_location: "GJ 35".into(), allowed_locations: vec!["GJ 35".into()], allowed_locations_cultures: vec!["frontier_industrial".into()], career: CareerKindWire::Tycoon, starting_capital_tractus: 5_000, }], }; let mut snapshot = fixture_snapshot(0, vec![]); snapshot.bookmark_catalog = Some(catalog); let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize snapshot_with_bookmark_catalog"); write_fixture("snapshot_with_bookmark_catalog", &bytes); } /// Atlas layer-stream responses (#969, D-225) — the client decodes these to /// render the per-layer Atlas overlays (#960). Covers Ready (with a small /// Layer1Output), Pending, and NotFound. #[test] #[ignore] // Run manually: cargo test --test gen_fixtures -- --ignored fn generate_atlas_layer_response_fixtures() { let layer1 = Layer1Output { body_id: "GJ1c".into(), river_network: RiverNetwork { river_cells: vec![(12, 58), (12, 59)], confluences: vec![], mouths: vec![(12, 58)], river_class: vec![1, 2], river_downstream: vec![2, 8], // 2=E direction; 8=MOUTH sentinel river_seaward: vec![(0, 0), (12, 60)], // meaningful only for the MOUTH entry }, drainage_basins: vec![DrainageBasin { basin_id: 1, boundary: vec![(0, 0), (0, 10), (10, 10), (10, 0)], area_pct: 0.42, territorial_status: settled_reach_server::simulation::generator::TerritorialStatus::FrontierUnclaimed, }], attractors: vec![GeographicAttractor { position: (12, 58), attractor_type: AttractorType::CoastalAccess, strength: 90, sub_biome: SubBiomeVariant::CoastalLowland, terrain_modification_cost: 170, water_bearing: 90, }], grid_w: 512, grid_h: 256, survey_basin_dirs: std::collections::BTreeMap::new(), feature_names: vec![], }; // T-960 §1/§2: a small populated RoadGraphLayer + SettlementLayer, one // settlement (a capital) connected to one waypoint-free short edge. let road_graph = RoadGraphLayer { nodes: vec![ RoadGraphNode { position: (12, 58), kind: RoadNodeKind::Settlement, city_id: Some(1), }, RoadGraphNode { position: (20, 70), kind: RoadNodeKind::Settlement, city_id: Some(2), }, ], edges: vec![RoadGraphEdge { from: 0, to: 1, path: vec![(12, 58), (16, 64), (20, 70)], maintenance: MaintenanceAuthority::Administrative, is_rail: false, named_route_id: None, }], }; let settlements = SettlementLayer { settlements: vec![ SettlementEntry { city_id: 1, name: "Port Aldren".into(), position: (12, 58), size_class: SettlementSizeClass::Major, is_capital: true, is_port: true, }, SettlementEntry { city_id: 2, name: "Farmstead Rell".into(), position: (20, 70), size_class: SettlementSizeClass::Minor, is_capital: false, is_port: false, }, ], }; // T-1113: a small populated RegionGridLayer — a 2×1 covering grid, one // temperate region and one airless-style region (mean_temp_dc sentinel), // mirroring the values exercised by // `build_region_grid_encodes_dense_quantized_climate` in layer_proxy.rs. let region_grid = RegionGridLayer { cols: 2, rows: 1, season: vec![SeasonPhase::Summer as u8, SeasonPhase::Winter as u8], weather: vec![WeatherState::Clear as u8, WeatherState::Snow as u8], mean_temp_dc: vec![ 123, settled_reach_server::atlas::layer_proxy::REGION_TEMP_NONE_DC, ], moisture_q: vec![80, 5], }; // T-1119 (D-226 T-1112 amendment): a populated QuarterFootprintLayer // covering both fixture settlements — city_id 1 (Port Aldren, a dense // Commercial-dominant quarter with landmarks/corridors) and city_id 2 // (Farmstead Rell, a sparse Residential-dominant quarter with neither), // so the fixture exercises both a "rich" entry and a "minimal" entry // rather than only one shape. let quarter_footprints = QuarterFootprintLayer { entries: BTreeMap::from([ ( 1, QuarterFootprintEntry { city_id: 1, density_avg_pct: 62, dominant_district_type: DistrictType::Commercial, dominant_zoning: ZoningType::Commercial, landmark_count: 3, corridor_count: 4, }, ), ( 2, QuarterFootprintEntry { city_id: 2, density_avg_pct: 18, dominant_district_type: DistrictType::Residential, dominant_zoning: ZoningType::Residential, landmark_count: 0, corridor_count: 1, }, ), ]), }; let ready = AtlasLayerResponse { body_id: "GJ1c".into(), status: AtlasLayerStatus::Ready, layer1: Some(layer1), district_grid: None, road_graph: Some(road_graph), settlements: Some(settlements), region_grid: Some(region_grid), district_window: None, quarter_footprints: Some(quarter_footprints), }; write_fixture( "atlas_response_ready", &rmp_serde::to_vec_named(&ready).unwrap(), ); let pending = AtlasLayerResponse { body_id: "GJ1c".into(), status: AtlasLayerStatus::Pending, layer1: None, district_grid: None, road_graph: None, settlements: None, region_grid: None, district_window: None, quarter_footprints: None, }; write_fixture( "atlas_response_pending", &rmp_serde::to_vec_named(&pending).unwrap(), ); let not_found = AtlasLayerResponse { body_id: "ghost".into(), status: AtlasLayerStatus::NotFound, layer1: None, district_grid: None, road_graph: None, settlements: None, region_grid: None, district_window: None, quarter_footprints: None, }; write_fixture( "atlas_response_not_found", &rmp_serde::to_vec_named(¬_found).unwrap(), ); // D-226 T-1124 amendment, T-1137: a Ready response carrying a populated // district_window — the windowed-family field, distinct from the five // whole-body layers above. A small n=2 window keeps the fixture readable // while exercising every field (including the REGION_TEMP_NONE_DC // sentinel and VegetationClass::Marine = 6, both non-negotiable per the // amendment §3). let window = DistrictWindowLayer { center: (10, -5), n: 2, granularity_v2: WindowGranularity::District, min_wl_m: 0, morphology: vec![0, 8, 14, 16], // OpenOcean, AlluvialPlain, Alpine, Wetland elev_q: vec![0, 45, 98, 60], temp_dc: vec![205, 150, REGION_TEMP_NONE_DC, 80], // 20.5°C, 15.0°C, airless sentinel, 8.0°C moisture_q: vec![90, 55, 0, 100], vegetation: vec![6, 3, 0, 5], // Marine, Forest, Absent, RiparianThicket glaciation: vec![0, 0, 4, 1], // None, None, IceCap, Light courses: vec![], }; let ready_with_window = AtlasLayerResponse { body_id: "GJ1c".into(), status: AtlasLayerStatus::Ready, layer1: None, district_grid: None, road_graph: None, settlements: None, region_grid: None, district_window: Some(window), quarter_footprints: None, }; write_fixture( "atlas_response_ready_with_window", &rmp_serde::to_vec_named(&ready_with_window).unwrap(), ); }