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 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 21:22:34 +01:00
co-authored by Claude Opus 4.6
parent e2f2f4e590
commit ba867564a8
3 changed files with 26 additions and 4 deletions
+9
View File
@@ -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.
///
+5 -2
View File
@@ -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,
+12 -2
View File
@@ -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<PlayerCharacter>>,
@@ -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<NearbyInteraction>,