// 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, BridgeError>; } /// BridgeResource: Bevy Resource wrapper for SimBridge trait object #[derive(Resource)] pub struct BridgeResource { inner: Box, } 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, BridgeError> { self.inner.receive_inputs() } } /// Generate ObserverSnapshot from ECS state pub fn generate_snapshot( time: Res, entities: Query<( Entity, &crate::simulation::movement::TilePosition, Option<&crate::simulation::movement::PlayerCharacter>, Option<&crate::npc::Npc>, )>, mut buffer: ResMut, ) { 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, }); } tracing::trace!( "generate_snapshot: tick={}, entities={}", time.tick, visible.len() ); buffer.snapshot = Some(ObserverSnapshot { tick: time.tick, entities: visible, }); } /// Receive inputs from bridge and push to InputQueue pub fn receive_bridge_inputs( bridge: Option>, mut input_queue: ResMut, mut running: ResMut, ) { 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>, mut buffer: ResMut, mut running: ResMut, ) { 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::() .init_resource::() .add_systems( Update, ( receive_bridge_inputs.before(crate::simulation::input::process_player_input), generate_snapshot .after(crate::simulation::movement::validate_movement) .before(crate::simulation::time::advance_tick), send_bridge_snapshot.after(generate_snapshot), ), ); tracing::debug!("BridgePlugin initialized"); } }