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

Adds rng_seed: Option<u64> to ObserverSnapshot. The WRONG button (#507) captures
inputs.jsonl and seed.txt for replay, but seed.txt was writing "unavailable"
because the server did not include the RNG seed in ObserverSnapshot.

Changes:
- bridge/types.rs: PROTOCOL_VERSION 9→10, rng_seed field with serde(default,
  skip_serializing_if = "Option::is_none") for backward compatibility
- perception/observer/mod.rs: inject Res<SimRng> into compute_observer_snapshot,
  populate rng_seed: Some(rng.seed()) each tick
- All test files: add rng_seed: None to ObserverSnapshot constructors
- tests/serialization.rs: bump protocol_version_constant assertion 9→10
- Regenerate msgpack fixtures and golden file for protocol v10

Completes the WRONG button capture loop: replays can now fully reproduce
observed bugs with the exact RNG seed from the capture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 14:15:34 +01:00
co-authored by Claude Sonnet 4.6
parent 3248603838
commit 6a7dc915de
16 changed files with 11 additions and 3 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ pub fn compute_visibility_geometry(
///
/// System ordering: after compute_visibility_geometry + compute_nearby_interactions,
/// before advance_tick.
#[allow(clippy::type_complexity)]
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn compute_observer_snapshot(
time: Res<SimulationTime>,
geometry: Res<VisibilityGeometry>,
+1
View File
@@ -61,6 +61,7 @@ fn snapshot_roundtrip_over_unix_socket() {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
rng_seed: None,
};
bridge
+1
View File
@@ -47,6 +47,7 @@ fn snapshot_roundtrip_over_tcp() {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
rng_seed: None,
};
bridge
+2
View File
@@ -37,6 +37,7 @@ fn fixture_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
rng_seed: None,
}
}
@@ -208,6 +209,7 @@ fn generate_msgpack_fixtures() {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
rng_seed: None,
};
write_fixture(
"snapshot_v2_full",
+2 -1
View File
@@ -64,9 +64,10 @@
"player_facing": "North",
"player_inventory": [],
"player_stance": "Sprint",
"rng_seed": 42,
"scan_events": [],
"tick": 8,
"version": 9,
"version": 10,
"visible_tiles": [
{
"tile_kind": "Wall",
+4 -1
View File
@@ -26,6 +26,7 @@ fn test_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
rng_seed: None,
}
}
@@ -254,6 +255,7 @@ fn snapshot_v2_fields_roundtrip() {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
rng_seed: None,
};
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
@@ -308,7 +310,7 @@ fn protocol_version_constant_matches_snapshot() {
let snapshot = test_snapshot(0, vec![]);
assert_eq!(snapshot.version, PROTOCOL_VERSION);
assert_eq!(
PROTOCOL_VERSION, 9,
PROTOCOL_VERSION, 10,
"bump this assertion when protocol version changes"
);
}
@@ -348,6 +350,7 @@ fn all_facing_direction_variants_roundtrip() {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
rng_seed: None,
};
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");