diff --git a/server/src/bridge/text_renderer.rs b/server/src/bridge/text_renderer.rs index 79d746029..ea0929978 100644 --- a/server/src/bridge/text_renderer.rs +++ b/server/src/bridge/text_renderer.rs @@ -299,6 +299,7 @@ mod tests { dialogue_response: None, blocked_entities: vec![], scan_events: vec![], + rng_seed: None, } } @@ -422,6 +423,7 @@ mod tests { dialogue_response: None, blocked_entities: vec![], scan_events: vec![], + rng_seed: None, }; let text = format_snapshot_text(&snap); assert!(text.contains("Tick 0")); diff --git a/server/src/bridge/types.rs b/server/src/bridge/types.rs index dee8ed37d..7395e0505 100644 --- a/server/src/bridge/types.rs +++ b/server/src/bridge/types.rs @@ -15,7 +15,7 @@ pub use crate::simulation::time::{DayPhase, TickRate}; /// negotiation is unnecessary. Client should reject snapshots with version != /// PROTOCOL_VERSION. New fields use #[serde(default)] only during the migration /// period, then the default is removed once both sides are updated. -pub const PROTOCOL_VERSION: u8 = 9; +pub const PROTOCOL_VERSION: u8 = 10; /// The ONLY data structure crossing the client-server boundary (D-020) /// Contains all information visible to the observer at a given tick. @@ -28,6 +28,7 @@ pub const PROTOCOL_VERSION: u8 = 9; /// v7 adds: pending_recognitions (#423, D-060 cognitive delay). /// v8 adds: dialogue_response (#305, D-028 dialogue pipeline). /// v9 adds: blocked_entities (#514, debug field for LOS-blocked entities). +/// v10 adds: rng_seed (#527, deterministic replay — completes WRONG button loop). /// Future fields: ambient sound events, HUD state (D-020 expansion). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ObserverSnapshot { @@ -79,6 +80,11 @@ pub struct ObserverSnapshot { /// Sorted ascending for deterministic output. Client can safely ignore. #[serde(default)] pub blocked_entities: Vec, + /// RNG seed active at this tick for deterministic replay (#527). + /// The WRONG button writes this to seed.txt so replays reproduce observed bugs. + /// None when the RNG resource is unavailable (should not occur in practice). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rng_seed: Option, } /// Game time data for client display (D-031) diff --git a/server/src/perception/observer/mod.rs b/server/src/perception/observer/mod.rs index aa7887317..1b196310d 100644 --- a/server/src/perception/observer/mod.rs +++ b/server/src/perception/observer/mod.rs @@ -21,6 +21,7 @@ use crate::simulation::interaction::NearbyInteractionBuffer; use crate::simulation::inventory::{CarriedBy, InventorySlot, ItemName}; use crate::simulation::monologue::{MonologueBuffer, SprintAnomalyQueue}; use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; +use crate::simulation::rng::SimRng; use crate::simulation::stance::Stance; use crate::simulation::time::SimulationTime; @@ -81,6 +82,7 @@ pub fn compute_observer_snapshot( )>, inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, mut buffer: ResMut, + sim_rng: Option>, ) { let Ok(( observer_entity, @@ -215,6 +217,7 @@ pub fn compute_observer_snapshot( dialogue_response, blocked_entities, scan_events, + rng_seed: sim_rng.as_deref().map(|r| r.seed()), }); }