Sprint 37 dead-code sweep closing out two stale supersession chains: #877 (D-167, 2026-03-24): Removes HeritageRoot type alias and ZonePaletteModifier::Heritage variant from server/src/simulation/ generator.rs. The 7 abstract heritage roots were retired in favour of the corridor cultural system; these two stubs were the only remaining references. #878 (D-032 + cascade rule): Strips the entire CharacterArchetype (Smuggler/Detective) trace from the server. Per lead direction 2026-04-21 and the development cascade (CLAUDE.md), character/NPC/ verb-differentiation/monologue code is Phase 6 detail that should not exist in code yet. The running archetype trace was pre-cascade filler, not production — production is only the client's character- creation UI and insert screens (client follow-up in #882). Deleted: - CharacterArchetype enum + StartupMessage.character_archetype field - archetype_verb_label() + archetype branch of apply_phase2_verb_filter (D-057 character-verb differentiation — marked superseded) - MonologueState.character partitioning - Gauntlet archetype plumbing (setup_gauntlet no longer takes an archetype) - server/content/schemas/drama_module.schema.yaml (zero Rust consumers) - server/content/modules/tier1/smuggling_ring_v0_1.yaml - server/tests/archetype_monologue.rs (regression guard for the removed system) - server/tests/v01_integration_playthrough.rs (archetype-dependent) Decision updates: - decisions/content.md D-032 supersession rewritten to cite the cascade (v0.2 drop invalidated the prior D-117 framing). - decisions/content.md D-035 tag taxonomy: `character` enum footnote updated; field noted as unused, do not reintroduce without a confirmed Phase 6 design. - decisions/perception.md D-057: archetype-verb differentiation marked superseded. Also bundles the types.rs version-field removal from #874 since the file was already touched here. Full trace audit in docs/architecture/sprint-37-878-audit.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
532 lines
18 KiB
Rust
532 lines
18 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::bridge::types::*;
|
|
use settled_reach_server::simulation::poi::PoiCategory;
|
|
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 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);
|
|
}
|