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:
@@ -91,14 +91,20 @@ impl Default for HandshakeState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive inputs from bridge and push to InputQueue
|
||||
/// Receive inputs from bridge and push to InputQueue.
|
||||
/// Protocol errors (malformed input) are recoverable: the frame is skipped
|
||||
/// and a SimError is pushed to the SimErrorBuffer for client reporting (#85).
|
||||
pub fn receive_bridge_inputs(
|
||||
bridge: Option<Res<BridgeResource>>,
|
||||
mut input_queue: ResMut<crate::simulation::input::InputQueue>,
|
||||
mut running: ResMut<ServerRunning>,
|
||||
handshake: Res<HandshakeState>,
|
||||
mut error_buffer: ResMut<SimErrorBuffer>,
|
||||
time: Option<Res<crate::simulation::time::SimulationTime>>,
|
||||
) {
|
||||
let Some(bridge) = bridge else { return };
|
||||
let current_tick = time.as_ref().map(|t| t.tick).unwrap_or(0);
|
||||
|
||||
match bridge.receive_inputs() {
|
||||
Ok(inputs) => {
|
||||
if !inputs.is_empty() && *handshake == HandshakeState::Pending {
|
||||
@@ -134,8 +140,22 @@ pub fn receive_bridge_inputs(
|
||||
running.0 = false;
|
||||
}
|
||||
Err(BridgeError::DeserializationWithDump(ref msg)) => {
|
||||
// Recoverable: skip this frame's input, don't shut down
|
||||
// Recoverable: skip this frame's input, report to client (#85)
|
||||
tracing::error!("Skipping malformed input frame: {}", msg);
|
||||
error_buffer.push(SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: format!("Malformed input frame: {}", msg),
|
||||
tick: current_tick,
|
||||
});
|
||||
}
|
||||
Err(ref e @ BridgeError::Deserialization(_)) => {
|
||||
// Recoverable deserialization error without dump
|
||||
tracing::error!("Skipping malformed input: {}", e);
|
||||
error_buffer.push(SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: format!("Deserialization error: {}", e),
|
||||
tick: current_tick,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Bridge receive error: {}", e);
|
||||
@@ -191,6 +211,7 @@ impl Plugin for BridgePlugin {
|
||||
app.init_resource::<SnapshotBuffer>()
|
||||
.init_resource::<ServerRunning>()
|
||||
.init_resource::<HandshakeState>()
|
||||
.init_resource::<SimErrorBuffer>()
|
||||
.init_resource::<crate::perception::query::VisibilityGeometry>()
|
||||
.init_resource::<crate::perception::query::ActivePerceptionMode>()
|
||||
.add_systems(
|
||||
|
||||
Reference in New Issue
Block a user