Files
settled-reach/server/src/bridge/mod.rs
T
jpmschweitzerandClaude Opus 4.6 e205b38938 feat(simulation): integrate observer visibility query (#112)
Replace unfiltered generate_snapshot with compute_observer_snapshot
that combines shadowcasting + vision cone to send only visible
entities and tiles. Enforces information asymmetry (D-011): NPCs
behind walls or in the blind spot are excluded from the snapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:21:07 +01:00

187 lines
5.5 KiB
Rust

// Bridge module - Client-server communication
// Implements D-020 subprocess/IPC architecture
// MessagePack serialization for Rust<->Godot communication
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::IntoScheduleConfigs;
pub mod framing;
pub mod local;
pub mod tcp;
pub mod types;
pub use types::*;
/// Error type for bridge operations
#[derive(Debug, thiserror::Error)]
pub enum BridgeError {
#[error("serialization error: {0}")]
Serialization(#[from] rmp_serde::encode::Error),
#[error("deserialization error: {0}")]
Deserialization(#[from] rmp_serde::decode::Error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("transport error: {0}")]
Transport(String),
#[error("client disconnected")]
Disconnected,
}
/// Abstracts transport layer (D-020)
/// Implemented by LocalBridge (stdio) and future NetworkBridge
pub trait SimBridge: Send + Sync {
/// Send an observer snapshot to the client
fn send_snapshot(&self, snapshot: &ObserverSnapshot) -> Result<(), BridgeError>;
/// Receive player inputs from the client
fn receive_inputs(&self) -> Result<Vec<PlayerInput>, BridgeError>;
}
/// BridgeResource: Bevy Resource wrapper for SimBridge trait object
#[derive(Resource)]
pub struct BridgeResource {
inner: Box<dyn SimBridge>,
}
impl BridgeResource {
pub fn new(bridge: impl SimBridge + 'static) -> Self {
Self {
inner: Box::new(bridge),
}
}
pub fn send_snapshot(&self, snapshot: &ObserverSnapshot) -> Result<(), BridgeError> {
self.inner.send_snapshot(snapshot)
}
pub fn receive_inputs(&self) -> Result<Vec<PlayerInput>, BridgeError> {
self.inner.receive_inputs()
}
}
/// Generate ObserverSnapshot v2 from ECS state.
/// Pre-visibility version: sends ALL entities (no LOS filtering yet).
/// Will be replaced by perception::observer::compute_observer_snapshot in #112.
pub fn generate_snapshot(
time: Res<crate::simulation::time::SimulationTime>,
entities: Query<(
Entity,
&crate::simulation::movement::TilePosition,
Option<&crate::simulation::movement::PlayerCharacter>,
Option<&crate::npc::Npc>,
)>,
mut buffer: ResMut<SnapshotBuffer>,
) {
let mut visible = Vec::new();
for (entity, pos, is_player, is_npc) in entities.iter() {
let (x, y, z) = pos.to_render_coords();
let kind = if is_player.is_some() {
EntityKind::Player
} else if is_npc.is_some() {
EntityKind::Npc
} else {
EntityKind::Object
};
visible.push(VisibleEntity {
entity_id: entity.to_bits(),
x,
y,
z,
kind,
visibility: VisibilitySector::Forward,
});
}
let game_time = GameTime {
day: time.day(),
time_of_day: time.time_of_day_minutes(),
day_phase: time.day_phase(),
paused: time.paused,
};
tracing::trace!(
"generate_snapshot: tick={}, entities={}",
time.tick,
visible.len()
);
buffer.snapshot = Some(ObserverSnapshot {
version: 2,
tick: time.tick,
game_time,
player_facing: FacingDirection::default(),
entities: visible,
visible_tiles: Vec::new(), // Empty until #112 adds LOS filtering
});
}
/// Receive inputs from bridge and push to InputQueue
pub fn receive_bridge_inputs(
bridge: Option<Res<BridgeResource>>,
mut input_queue: ResMut<crate::simulation::input::InputQueue>,
mut running: ResMut<ServerRunning>,
) {
let Some(bridge) = bridge else { return };
match bridge.receive_inputs() {
Ok(inputs) => {
for input in inputs {
input_queue.push(input);
}
}
Err(BridgeError::Disconnected) => {
tracing::info!("Client disconnected, shutting down");
running.0 = false;
}
Err(e) => {
tracing::error!("Bridge receive error: {}", e);
}
}
}
/// Send snapshot from buffer to bridge
pub fn send_bridge_snapshot(
bridge: Option<Res<BridgeResource>>,
mut buffer: ResMut<SnapshotBuffer>,
mut running: ResMut<ServerRunning>,
) {
let Some(bridge) = bridge else { return };
if let Some(snapshot) = buffer.snapshot.take() {
if let Err(e) = bridge.send_snapshot(&snapshot) {
tracing::error!("Bridge send error: {}", e);
running.0 = false;
}
}
}
/// Server running flag resource
#[derive(Resource, Debug, Clone)]
pub struct ServerRunning(pub bool);
impl Default for ServerRunning {
fn default() -> Self {
Self(true)
}
}
/// Bridge plugin for client-server communication
/// Abstracts transport layer (LocalBridge/NetworkBridge)
pub struct BridgePlugin;
impl Plugin for BridgePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SnapshotBuffer>()
.init_resource::<ServerRunning>()
.add_systems(
Update,
(
receive_bridge_inputs.before(crate::simulation::input::process_player_input),
crate::perception::observer::compute_observer_snapshot
.after(crate::simulation::movement::validate_movement)
.before(crate::simulation::time::advance_tick),
send_bridge_snapshot
.after(crate::perception::observer::compute_observer_snapshot),
),
);
tracing::debug!("BridgePlugin initialized");
}
}