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:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user