From ba867564a8d92051afa3d0ad334493a3fa3ebe10 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 12 Feb 2026 21:22:34 +0100 Subject: [PATCH] fix(server): add error logging, protocol version, and architecture docs - Replace silent Entity::to_bits() fallbacks with tracing::error in observer.rs and interaction.rs (makes unregistered entities loud) - Add PROTOCOL_VERSION constant to types.rs, use in observer snapshot - Document single-observer assumption on NearbyInteractionBuffer - Document proximity-only (no LOS) limitation on compute_nearby_interactions Co-Authored-By: Claude Opus 4.6 --- server/src/bridge/types.rs | 9 +++++++++ server/src/perception/observer.rs | 7 +++++-- server/src/simulation/interaction.rs | 14 ++++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/server/src/bridge/types.rs b/server/src/bridge/types.rs index f73637c6d..06e9cb311 100644 --- a/server/src/bridge/types.rs +++ b/server/src/bridge/types.rs @@ -8,6 +8,15 @@ use serde::{Deserialize, Serialize}; pub use crate::knowledge::types::{EntityVisibility, KnowledgeConfidence, RelationshipState}; pub use crate::simulation::time::{DayPhase, TickRate}; +/// Wire protocol version for ObserverSnapshot. +/// +/// Versioning strategy: flat struct + serde defaults for field evolution. +/// Client and server are co-versioned (subprocess IPC per D-020), so protocol +/// 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 = 4; + /// The ONLY data structure crossing the client-server boundary (D-020) /// Contains all information visible to the observer at a given tick. /// diff --git a/server/src/perception/observer.rs b/server/src/perception/observer.rs index 9e21cf567..057475059 100644 --- a/server/src/perception/observer.rs +++ b/server/src/perception/observer.rs @@ -118,7 +118,10 @@ pub fn compute_observer_snapshot( let wire_id = registry .to_stable(entity) .map(|sid| sid.0) - .unwrap_or_else(|| entity.to_bits()); + .unwrap_or_else(|| { + tracing::error!(?entity, "entity visible but not in EntityRegistry"); + entity.to_bits() + }); visible_entity_bits.insert(wire_id); entities.push(VisibleEntity { entity_id: wire_id, @@ -160,7 +163,7 @@ pub fn compute_observer_snapshot( // Step 8: Assemble snapshot (v4: added nearby_interactions) buffer.snapshot = Some(ObserverSnapshot { - version: 4, + version: crate::bridge::types::PROTOCOL_VERSION, tick: time.tick, game_time, player_facing: facing, diff --git a/server/src/simulation/interaction.rs b/server/src/simulation/interaction.rs index f304eae87..c9b2c12ba 100644 --- a/server/src/simulation/interaction.rs +++ b/server/src/simulation/interaction.rs @@ -20,8 +20,12 @@ pub(crate) const MID_RANGE: u32 = 5; pub struct Interactable; /// Compute nearby interactions for the player character. -/// For each visible entity in range, determines available verbs sorted by priority. +/// For each entity in range, determines available verbs sorted by priority. /// Results are written to the NearbyInteractionBuffer for inclusion in ObserverSnapshot. +/// +/// NOTE: Checks proximity only, not line-of-sight. The client filters +/// interaction prompts against visible entities. Server-side LOS filtering +/// is deferred until the interaction system can read the observer's visible set. #[allow(clippy::type_complexity)] pub fn compute_nearby_interactions( player_query: Query<(&TilePosition, &KnowledgeGraph), With>, @@ -130,7 +134,10 @@ pub fn compute_nearby_interactions( let wire_id = registry .to_stable(entity) .map(|sid| sid.0) - .unwrap_or_else(|| entity.to_bits()); + .unwrap_or_else(|| { + tracing::error!(?entity, "entity in interaction range but not in EntityRegistry"); + entity.to_bits() + }); buffer.interactions.push(NearbyInteraction { entity_id: wire_id, @@ -148,6 +155,9 @@ pub fn compute_nearby_interactions( /// Buffer for nearby interaction results, consumed by snapshot generation. /// Field is private — use `take()` to drain results into the snapshot. +/// +/// Global Resource — single-observer assumption (v0.1). D-009 multiplayer +/// will refactor the entire observer + interaction pipeline to per-entity. #[derive(Resource, Debug, Default)] pub struct NearbyInteractionBuffer { interactions: Vec,