feat(simulation): error handling and recovery — panic supervision, state hash, structured errors (#85)

Protocol v17: add state_hash (desync detection) and sim_errors
(structured error reporting) to ObserverSnapshot. Add SimError,
SimErrorKind, SimErrorBuffer types. Wrap main loop app.update() in
catch_unwind — on panic, send a final SimError snapshot before exit.
Report recoverable deserialization errors to client via SimErrorBuffer.
Compute per-tick state hash from player position + NPC count + tick.
Update all test fixtures and golden files for protocol v17.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 17:46:27 +01:00
co-authored by Claude Opus 4.6
parent c2cae1f618
commit d31ac1cc70
23 changed files with 557 additions and 7 deletions
+25
View File
@@ -9,6 +9,8 @@
use bevy_ecs::prelude::*;
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use crate::bridge::types::*;
use crate::knowledge::graph::filter_by_access;
use crate::knowledge::types::{AccessRule, KnowledgeState};
@@ -103,6 +105,8 @@ pub fn compute_observer_snapshot(
mut crisis_queue: ResMut<TriangleCrisisEventQueue>,
sim_rng: Option<Res<SimRng>>,
pressure_query: Query<&crate::simulation::pressure::CharacterPressure, With<PlayerCharacter>>,
error_buffer: Option<ResMut<SimErrorBuffer>>,
npc_count_query: Query<Entity, With<crate::npc::Npc>>,
) {
let Ok((
observer_entity,
@@ -396,6 +400,25 @@ pub fn compute_observer_snapshot(
.map(TriangleCrisisEventWire::from)
.collect();
// Compute state hash for desync detection (#85).
// Hash inputs: player position (x, y, z), NPC count, tick number.
// Uses DefaultHasher for speed — not cryptographic, just comparison.
let state_hash = {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
time.tick.hash(&mut hasher);
observer_pos.x.hash(&mut hasher);
observer_pos.y.hash(&mut hasher);
observer_pos.z.hash(&mut hasher);
let npc_count = npc_count_query.iter().count() as u64;
npc_count.hash(&mut hasher);
Some(hasher.finish())
};
// Drain sim errors collected this tick (#85)
let sim_errors = error_buffer
.map(|mut buf| buf.drain())
.unwrap_or_default();
buffer.snapshot = Some(ObserverSnapshot {
version: crate::bridge::types::PROTOCOL_VERSION,
tick: time.tick,
@@ -424,6 +447,8 @@ pub fn compute_observer_snapshot(
player_knowledge,
save_result,
triangle_crisis_events,
state_hash,
sim_errors,
});
}