One commit for two tickets whose changes share the bridge/plugin plumbing files. T-1169 connects the three dormant feature-name pieces: atlas_feature_names populated at regen (17,891 rows — 15,190 mountain, 2,701 river — via populate_atlas_feature_names mirroring the city-names importer; systems.db regenerated, stamp fresh), attach_feature_names wired into the cascade's Topography block with name pools threaded DB-free through AnalyzeBody (D-225 pattern) and assignments stored on Layer1Output/BodyWorldState for future consumers, and a FeatureNamesRequest/Response read proxy as the bridge's 7th tagged envelope (D-236 pattern, both SimBridge impls). Client label DRAW is deliberately NOT here — implementation proved both river and mountain labels need a wire-carried position (the pool is position-free; course polylines aren't correlated with the named attractors by construction) — deferred to T-1195's single design pass. cascade_layer1 golden re-pinned (additive feature_names field). T-1159 retires the legacy u32 granularity field fully shadowed by window_granularity_v2: AtlasLayerRequest.window_granularity, DistrictWindowLayer.granularity echo, the u32::MAX sentinel, and resolve_window_granularity are gone server-side; client encode paths and the caller-less atlas_window_cache legacy key component dropped; msgpack fixtures regenerated; the T-1150 aliasing regression test now drives through the surviving enum field. The district_window carrier itself survives byte-compatible per D-255(c). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
758 lines
26 KiB
Rust
758 lines
26 KiB
Rust
//! 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<VisibleEntity>) -> 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<PlayerInput> 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(),
|
||
);
|
||
}
|