feat(simulation): add input processing, snapshot gen, and game loop

Implements the full server-side tick pipeline:
- process_player_input drains InputQueue, converts PlayerActions to
  MoveIntent components or pause/unpause toggles
- generate_snapshot builds ObserverSnapshot from ECS state with
  render coordinate conversion
- receive_bridge_inputs/send_bridge_snapshot handle bridge I/O with
  graceful disconnect detection via ServerRunning resource
- main.rs now accepts TCP connections and runs a proper game loop
- PlayerCharacter marker, Player EntityKind, SnapshotBuffer resource

Closes server side of #81, #82, #83.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 21:01:28 +01:00
co-authored by Claude Opus 4.6
parent 1b0e514560
commit 7864bfdf7d
7 changed files with 397 additions and 10 deletions
+94 -2
View File
@@ -4,9 +4,11 @@
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::*;
@@ -55,13 +57,103 @@ impl BridgeResource {
}
}
/// 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,
});
}
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::Transport(ref msg)) if msg.contains("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>,
) {
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);
}
}
}
/// 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) {
// Stub implementation - will be populated in phase 2
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),
send_bridge_snapshot.after(generate_snapshot),
),
);
tracing::debug!("BridgePlugin initialized");
}
}
+8
View File
@@ -2,6 +2,7 @@
// ObserverSnapshot: data crossing the client-server boundary
// PlayerInput: semantic actions from client
use bevy_ecs::prelude::*;
use serde::{Deserialize, Serialize};
/// The ONLY data structure crossing the client-server boundary (D-020)
@@ -32,6 +33,7 @@ pub struct VisibleEntity {
/// Category of visible entity
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EntityKind {
Player,
Npc,
Object,
Terrain,
@@ -63,3 +65,9 @@ pub enum PlayerAction {
Pause,
Unpause,
}
/// Snapshot buffer resource for staging outgoing ObserverSnapshots
#[derive(Resource, Debug, Default)]
pub struct SnapshotBuffer {
pub snapshot: Option<ObserverSnapshot>,
}