Replace string-matching disconnect detection with explicit BridgeError::Disconnected variant. Add TcpBridge::accept_on(listener) that takes a pre-bound TcpListener, eliminating the 100ms sleep hack in TCP tests. Send errors now also trigger ServerRunning=false. Add trace logging to generate_snapshot for entity count visibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
171 lines
4.9 KiB
Rust
171 lines
4.9 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 from ECS state
|
|
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,
|
|
});
|
|
}
|
|
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<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),
|
|
generate_snapshot
|
|
.after(crate::simulation::movement::validate_movement)
|
|
.before(crate::simulation::time::advance_tick),
|
|
send_bridge_snapshot.after(generate_snapshot),
|
|
),
|
|
);
|
|
tracing::debug!("BridgePlugin initialized");
|
|
}
|
|
}
|