feat(engine): add rng_seed to ObserverSnapshot for deterministic replay (#527)

Add rng_seed: Option<u64> to ObserverSnapshot (protocol v10). Populated
from SimRng state each tick. Completes the WRONG button capture loop —
seed.txt now writes a valid u64 instead of "unavailable", enabling
deterministic replay from bug reports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 13:34:14 +01:00
co-authored by Claude Opus 4.6
parent 0f5a990f82
commit 9ea9fd2a74
3 changed files with 12 additions and 1 deletions
+2
View File
@@ -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"));
+7 -1
View File
@@ -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<u64>,
/// 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<u64>,
}
/// Game time data for client display (D-031)
+3
View File
@@ -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<SnapshotBuffer>,
sim_rng: Option<Res<SimRng>>,
) {
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()),
});
}