Merge remote-tracking branch 'origin/ci'

# Conflicts:
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_0.msgpack
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_127.msgpack
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_2b31m1.msgpack
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_2b32.msgpack
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_32767.msgpack
#	client/tests/fixtures/msgpack/snapshot_empty.msgpack
#	client/tests/fixtures/msgpack/snapshot_multi_entity.msgpack
#	client/tests/fixtures/msgpack/snapshot_one_npc.msgpack
#	client/tests/fixtures/msgpack/snapshot_player.msgpack
#	client/tests/fixtures/msgpack/snapshot_v2_full.msgpack
#	server/Cargo.toml
#	server/src/bridge/text_renderer.rs
#	server/src/bridge/types.rs
#	server/src/perception/observer/mod.rs
#	server/src/simulation/path_follow.rs
#	server/tests/bridge_ipc.rs
#	server/tests/bridge_tcp.rs
#	server/tests/gen_fixtures.rs
#	server/tests/serialization.rs
This commit is contained in:
2026-02-19 15:35:56 +01:00
38 changed files with 307 additions and 50 deletions
+1
View File
@@ -62,6 +62,7 @@ fn snapshot_roundtrip_over_unix_socket() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
rng_seed: None,
};
bridge
+1
View File
@@ -48,6 +48,7 @@ fn snapshot_roundtrip_over_tcp() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
rng_seed: None,
};
bridge
+5 -5
View File
@@ -21,7 +21,9 @@ use std::time::Instant;
use settled_reach_server::bridge::types::*;
use settled_reach_server::bridge::BridgePlugin;
use settled_reach_server::knowledge::registry::{EntityRegistry, StableEntityId};
use settled_reach_server::knowledge::{KnowledgeConfidence, KnowledgeGraph, KnowledgePlugin, StableId};
use settled_reach_server::knowledge::{
KnowledgeConfidence, KnowledgeGraph, KnowledgePlugin, StableId,
};
use settled_reach_server::npc::{Contentment, Npc, NpcPlugin, ToleranceThreshold, Want, WantKind};
use settled_reach_server::simulation::interaction::Interactable;
use settled_reach_server::simulation::movement::TilePosition;
@@ -462,10 +464,8 @@ fn max_npc_pack_behavioral_regression() {
.clone();
let stress_kg = player_kg_snapshot(&stress_app, max_gauntlet_id);
let baseline_snap = baseline_snapshot
.expect("baseline Gauntlet should produce a snapshot");
let stress_snap = stress_snapshot
.expect("80-NPC stress run should produce a snapshot");
let baseline_snap = baseline_snapshot.expect("baseline Gauntlet should produce a snapshot");
let stress_snap = stress_snapshot.expect("80-NPC stress run should produce a snapshot");
// Tick index must match (same number of updates).
assert_eq!(
+13 -7
View File
@@ -349,10 +349,7 @@ fn t4_knowledge_graph_survives_room_transition() {
"T4 post: KG entry must persist after player moves to Hub"
);
assert_eq!(
world
.get::<KnowledgeGraph>(player)
.unwrap()
.entity_count(),
world.get::<KnowledgeGraph>(player).unwrap().entity_count(),
1,
"T4 post: exactly 1 KG entry after room transition"
);
@@ -542,7 +539,10 @@ fn t7_confrontation_verb_disappears_on_retreat_beyond_mid_range() {
"T7 close: NPC at distance 2 must appear in interaction buffer"
);
assert!(
interactions[0].verbs.iter().any(|v| v.kind == VerbKind::Talk),
interactions[0]
.verbs
.iter()
.any(|v| v.kind == VerbKind::Talk),
"T7 close: Talk must be available at CLOSE_RANGE (confrontation possible)"
);
@@ -599,7 +599,10 @@ fn t8_sprint_blocks_eavesdrop_then_careful_enables_accumulation() {
run_listening_system(&mut world);
}
assert_eq!(
world.get::<ListeningFocus>(player).unwrap().stationary_ticks,
world
.get::<ListeningFocus>(player)
.unwrap()
.stationary_ticks,
0,
"T8 sprint: Sprint must block stationary_ticks (50 ticks at sprint, still 0)"
);
@@ -613,7 +616,10 @@ fn t8_sprint_blocks_eavesdrop_then_careful_enables_accumulation() {
// resets stationary_ticks to 0, and updates last_position to alcove_pos.
run_listening_system(&mut world);
assert_eq!(
world.get::<ListeningFocus>(player).unwrap().stationary_ticks,
world
.get::<ListeningFocus>(player)
.unwrap()
.stationary_ticks,
0,
"T8 transition: movement tick must reset stationary_ticks to 0"
);
+2
View File
@@ -38,6 +38,7 @@ fn fixture_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
rng_seed: None,
}
}
@@ -210,6 +211,7 @@ fn generate_msgpack_fixtures() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
rng_seed: None,
};
write_fixture(
"snapshot_v2_full",
@@ -64,6 +64,7 @@
"player_facing": "North",
"player_inventory": [],
"player_stance": "Sprint",
"rng_seed": 42,
"scan_events": [],
"sound_events": [],
"tick": 8,
+80
View File
@@ -27,6 +27,7 @@ fn test_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
rng_seed: None,
}
}
@@ -256,6 +257,7 @@ fn snapshot_v2_fields_roundtrip() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
rng_seed: None,
};
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
@@ -350,7 +352,11 @@ fn all_facing_direction_variants_roundtrip() {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
<<<<<<< HEAD
sound_events: vec![],
=======
rng_seed: None,
>>>>>>> origin/ci
};
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
@@ -1187,6 +1193,80 @@ fn v8_payload_deserializes_into_v9_struct() {
);
}
/// rng_seed round-trips through MessagePack (#527).
/// Verifies Some(seed) survives the wire and None is omitted.
#[test]
fn rng_seed_roundtrip() {
let mut snapshot = test_snapshot(0, vec![]);
snapshot.rng_seed = Some(123456789);
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
assert_eq!(decoded.rng_seed, Some(123456789));
// None case: skip_serializing_if omits the field, default restores it
let mut snapshot_none = test_snapshot(0, vec![]);
snapshot_none.rng_seed = None;
let bytes_none = rmp_serde::to_vec_named(&snapshot_none).expect("serialize");
let decoded_none: ObserverSnapshot = rmp_serde::from_slice(&bytes_none).expect("deserialize");
assert_eq!(decoded_none.rng_seed, None);
}
/// v9 payloads (without rng_seed) must deserialize into the v10 struct
/// via #[serde(default)]. Guards backwards compat during migration (#527).
#[test]
fn v9_payload_deserializes_into_v10_struct() {
#[derive(serde::Serialize)]
struct ObserverSnapshotV9 {
version: u8,
tick: u64,
game_time: GameTime,
player_facing: FacingDirection,
player_stance: MovementStance,
player_inventory: Vec<InventoryItem>,
entities: Vec<VisibleEntity>,
visible_tiles: Vec<VisibleTile>,
nearby_interactions: Vec<NearbyInteraction>,
current_monologue: Option<MonologueEvent>,
pending_recognitions: Vec<PendingRecognitionWire>,
dialogue_response: Option<DialogueResponseEvent>,
blocked_entities: Vec<u64>,
scan_events: Vec<settled_reach_server::simulation::contraband::ScanEvent>,
}
let v9 = ObserverSnapshotV9 {
version: 9,
tick: 200,
game_time: GameTime {
day: 0,
time_of_day: 0,
day_phase: DayPhase::Morning,
tick_rate: TickRate::Full,
},
player_facing: FacingDirection::North,
player_stance: MovementStance::Walk,
player_inventory: vec![],
entities: vec![],
visible_tiles: vec![],
nearby_interactions: vec![],
current_monologue: None,
pending_recognitions: vec![],
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
};
let bytes = rmp_serde::to_vec_named(&v9).expect("serialize v9");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes)
.expect("v9 payload should deserialize into v10 struct via serde(default)");
assert_eq!(decoded.version, 9, "version field preserved from v9");
assert_eq!(decoded.tick, 200);
assert_eq!(
decoded.rng_seed, None,
"missing rng_seed should default to None"
);
}
/// NearbyInteraction.object_type round-trips through MessagePack (#422).
/// Verifies object_type=Some(Container) survives the wire.
#[test]