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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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");