fix(simulation): correct tick synchronization in snapshot generation
Snapshot for tick N should show state at END of tick N. Reorder systems so generate_snapshot runs after validate_movement but before advance_tick. Previously snapshot.tick was the incremented tick, not the tick whose inputs were processed. Also fix main.rs accept error to log address context before exiting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,8 @@ impl Plugin for SimulationPlugin {
|
||||
Update,
|
||||
(
|
||||
input::process_player_input,
|
||||
time::advance_tick.after(input::process_player_input),
|
||||
movement::validate_movement.after(time::advance_tick),
|
||||
movement::validate_movement.after(input::process_player_input),
|
||||
time::advance_tick.after(movement::validate_movement),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
+33
-69
@@ -1,5 +1,5 @@
|
||||
//! E2E integration test: full game loop with input processing and snapshot generation
|
||||
//! Tests the complete pipeline: client sends input → server processes → server sends snapshot
|
||||
//! Tests the complete pipeline: client sends input -> server processes -> server sends snapshot
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use settled_reach_server::bridge::framing::{read_framed, write_framed};
|
||||
@@ -10,31 +10,17 @@ use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition,
|
||||
use settled_reach_server::simulation::SimulationPlugin;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn player_moves_north_through_full_pipeline() {
|
||||
// Pre-bind listener to discover the port
|
||||
// Bind listener first — port guaranteed ready, no sleep needed
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener");
|
||||
let server_addr = listener.local_addr().expect("get local addr");
|
||||
let addr_str = server_addr.to_string();
|
||||
|
||||
// Channel to signal when server is ready to accept
|
||||
let (ready_tx, ready_rx) = mpsc::channel();
|
||||
|
||||
// Spawn server thread
|
||||
let server_handle = thread::spawn(move || {
|
||||
// Drop the pre-bound listener since TcpBridge::accept will bind its own
|
||||
drop(listener);
|
||||
|
||||
// Signal we're about to accept
|
||||
ready_tx.send(()).expect("send ready signal");
|
||||
|
||||
// Accept connection
|
||||
let bridge = TcpBridge::accept(&addr_str).expect("accept connection");
|
||||
eprintln!("Test server accepted connection");
|
||||
let bridge = TcpBridge::accept_on(listener).expect("accept connection");
|
||||
|
||||
// Build app
|
||||
let mut app = App::new();
|
||||
@@ -47,64 +33,42 @@ fn player_moves_north_through_full_pipeline() {
|
||||
|
||||
// Run one tick: receive input, process, validate movement, generate snapshot, send
|
||||
app.update();
|
||||
|
||||
eprintln!("Server completed one update cycle");
|
||||
});
|
||||
|
||||
// Wait for server thread to start
|
||||
ready_rx
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("server did not become ready");
|
||||
// Client: connect and send input (no sleep — listener was pre-bound)
|
||||
let stream = TcpStream::connect(server_addr).expect("client connect");
|
||||
let mut reader = BufReader::new(stream.try_clone().expect("clone for reader"));
|
||||
let mut writer = BufWriter::new(stream);
|
||||
|
||||
// Give server time to bind and call accept()
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
// Send PlayerInput: MoveNorth at tick 0
|
||||
let inputs = vec![PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::MoveNorth,
|
||||
}];
|
||||
let payload = rmp_serde::to_vec_named(&inputs).expect("serialize inputs");
|
||||
write_framed(&mut writer, &payload).expect("send inputs");
|
||||
|
||||
// Client: connect and send input
|
||||
{
|
||||
let stream = TcpStream::connect(server_addr).expect("client connect");
|
||||
let mut reader = BufReader::new(stream.try_clone().expect("clone for reader"));
|
||||
let mut writer = BufWriter::new(stream);
|
||||
// Receive ObserverSnapshot
|
||||
let response = read_framed(&mut reader)
|
||||
.expect("read snapshot")
|
||||
.expect("not EOF");
|
||||
let snapshot: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&response).expect("deserialize snapshot");
|
||||
|
||||
// Send PlayerInput: MoveNorth at tick 0
|
||||
let inputs = vec![PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::MoveNorth,
|
||||
}];
|
||||
let payload = rmp_serde::to_vec_named(&inputs).expect("serialize inputs");
|
||||
write_framed(&mut writer, &payload).expect("send inputs");
|
||||
eprintln!("Client sent MoveNorth input");
|
||||
// Snapshot captures state at end of tick 0 (before advance_tick increments to 1)
|
||||
assert_eq!(snapshot.tick, 0);
|
||||
assert_eq!(snapshot.entities.len(), 1);
|
||||
|
||||
// Receive ObserverSnapshot
|
||||
let response = read_framed(&mut reader)
|
||||
.expect("read snapshot")
|
||||
.expect("not EOF");
|
||||
let snapshot: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&response).expect("deserialize snapshot");
|
||||
let player_entity = &snapshot.entities[0];
|
||||
// Player started at (16, 16, 0), moved north (y-1) to (16, 15, 0)
|
||||
// Render coords: (16.5, 15.5, 0)
|
||||
assert_eq!(player_entity.x, 16.5);
|
||||
assert_eq!(player_entity.y, 15.5);
|
||||
assert_eq!(player_entity.z, 0);
|
||||
assert!(matches!(player_entity.kind, EntityKind::Player));
|
||||
|
||||
eprintln!(
|
||||
"Client received snapshot: tick={}, entities={}",
|
||||
snapshot.tick,
|
||||
snapshot.entities.len()
|
||||
);
|
||||
|
||||
// Verify snapshot
|
||||
assert_eq!(snapshot.tick, 1); // After one tick
|
||||
assert_eq!(snapshot.entities.len(), 1); // One entity (player)
|
||||
|
||||
let player_entity = &snapshot.entities[0];
|
||||
// Player started at (16, 16, 0), moved north (y-1) to (16, 15, 0)
|
||||
// Render coords: (16.5, 15.5, 0)
|
||||
assert_eq!(player_entity.x, 16.5);
|
||||
assert_eq!(player_entity.y, 15.5);
|
||||
assert_eq!(player_entity.z, 0);
|
||||
assert!(matches!(player_entity.kind, EntityKind::Player));
|
||||
|
||||
eprintln!(
|
||||
"Client verified player moved to ({}, {}, {})",
|
||||
player_entity.x, player_entity.y, player_entity.z
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for server thread
|
||||
// Clean up
|
||||
drop(reader);
|
||||
drop(writer);
|
||||
server_handle.join().expect("server thread panicked");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user