E2E game loop: client connects via TCP, sends MoveNorth, receives snapshot with updated position
## Summary
- **Wire format fix**: rmp_serde::to_vec() to to_vec_named() in LocalBridge and all tests — client Protocol.gd expects named maps, not positional arrays
- **TCP transport**: New TcpBridge for Godot client connection (Godot has no Unix socket API). LocalBridge retained for Rust-to-Rust tests
- **Input processing**: process_player_input system drains InputQueue, converts PlayerAction variants to MoveIntent components or pause/unpause toggles
- **Snapshot generation**: generate_snapshot builds ObserverSnapshot from ECS state with render coordinate conversion
- **Bridge I/O**: receive_bridge_inputs / send_bridge_snapshot systems wire the bridge to the ECS pipeline with graceful disconnect detection
- **Game loop**: main.rs now accepts TCP connections, runs a proper tick loop with ServerRunning resource, configurable via CLI arg or SR_ADDR env var
- **New resources/types**: PlayerCharacter marker, Player EntityKind variant, SnapshotBuffer, ServerRunning
Addresses server side of #81, #82, #83.
## Test plan
- make ci-server green — clippy, fmt, build, nextest (53 tests, 0 failures)
- Wire format: all serialization tests use to_vec_named
- TCP bridge: 3 integration tests (snapshot roundtrip, input roundtrip, EOF detection)
- Input processing: 4 unit tests (move intent, pause toggle, no-player safety, future tick ignored)
- E2E game loop: client connects via TCP, sends MoveNorth, receives snapshot with updated position
Client-side Protocol.gd expects rmp_serde::to_vec_named() (maps with
string keys), but LocalBridge was using to_vec() (compact positional
arrays). Fix send_snapshot and update all test serialization calls to
match actual wire format. Also change EOF from Ok(vec![]) to
BridgeError::Transport so bridge systems can detect disconnects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Godot has no Unix socket API, so TCP localhost is required for
client-server IPC. TcpBridge implements SimBridge with the same
framing protocol as LocalBridge. Includes accept/connect methods
and three integration tests over TCP.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Solid implementation with good test coverage (11 new tests including E2E). Two critical issues found.
#
File
Severity
Issue
1
game_loop.rs:54
critical
Tick synchronization: snapshot is generated after advance_tick, so snapshot.tick is the NEW tick (1), but the state reflects processing from tick 0. The snapshot for tick N should show the state at the END of tick N. Fix: move generate_snapshot before advance_tick, or label snapshots with previous_tick.
2
bridge_tcp.rs (all 3 tests)
critical
Race condition: ready signal sent BEFORE accept() completes. Client connects before server is listening. 100ms sleep masks the timing gap. Fix: refactor TcpBridge::accept() to accept an existing TcpListener so the test can bind once, pass the listener in, and the port is guaranteed ready — no sleep needed.
3
bridge/mod.rs:115-119 send_bridge_snapshot
warning
Send errors logged but don't set ServerRunning=false. Server continues for one tick after send failure. Consider matching receive_bridge_inputs behavior.
4
main.rs:23
warning
expect() on accept — panics with no address/error context. Use unwrap_or_else with tracing.
5
bridge/mod.rs:108 receive_bridge_inputs
suggestion
Disconnect detection via msg.contains("disconnected") is string-matching. Consider a BridgeError::Disconnected variant for cleaner matching.
6
bridge/mod.rs generate_snapshot
suggestion
No tracing for snapshot entity count — hard to debug empty/missing entities from logs.
Tyre (Architecture): APPROVE
Production-quality server architecture. Clean separation of concerns, thorough three-layer testing (D-030), explicit system ordering, deterministic input processing (D-010 principle 4). Full D-020, D-010, D-030, D-012 compliance verified. No architectural issues.
#
File
Severity
Issue
1
tcp.rs Mutex expect
suggestion
Acceptable for prototype; consider graceful degradation later.
2
bridge/mod.rs:108
suggestion
String-matching for disconnect; consider explicit error variant.
Verdict: CHANGES REQUESTED
Fix 2 critical issues before merge: (1) tick synchronization semantics in snapshot generation, (2) test race condition — refactor TcpBridge::accept() to take an existing TcpListener instead of binding its own, eliminating the 100ms sleep hack.
## Dual-Agent Review: server -> main
### Hoshe (Code Quality): REQUEST_CHANGES
Solid implementation with good test coverage (11 new tests including E2E). Two critical issues found.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | `game_loop.rs:54` | critical | Tick synchronization: snapshot is generated after `advance_tick`, so `snapshot.tick` is the NEW tick (1), but the state reflects processing from tick 0. The snapshot for tick N should show the state at the END of tick N. Fix: move `generate_snapshot` before `advance_tick`, or label snapshots with `previous_tick`. |
| 2 | `bridge_tcp.rs` (all 3 tests) | critical | Race condition: ready signal sent BEFORE `accept()` completes. Client connects before server is listening. 100ms sleep masks the timing gap. Fix: refactor `TcpBridge::accept()` to accept an existing `TcpListener` so the test can bind once, pass the listener in, and the port is guaranteed ready — no sleep needed. |
| 3 | `bridge/mod.rs:115-119` send_bridge_snapshot | warning | Send errors logged but don't set `ServerRunning=false`. Server continues for one tick after send failure. Consider matching receive_bridge_inputs behavior. |
| 4 | `main.rs:23` | warning | `expect()` on accept — panics with no address/error context. Use `unwrap_or_else` with tracing. |
| 5 | `bridge/mod.rs:108` receive_bridge_inputs | suggestion | Disconnect detection via `msg.contains("disconnected")` is string-matching. Consider a `BridgeError::Disconnected` variant for cleaner matching. |
| 6 | `bridge/mod.rs` generate_snapshot | suggestion | No tracing for snapshot entity count — hard to debug empty/missing entities from logs. |
### Tyre (Architecture): APPROVE
Production-quality server architecture. Clean separation of concerns, thorough three-layer testing (D-030), explicit system ordering, deterministic input processing (D-010 principle 4). Full D-020, D-010, D-030, D-012 compliance verified. No architectural issues.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | `tcp.rs` Mutex expect | suggestion | Acceptable for prototype; consider graceful degradation later. |
| 2 | `bridge/mod.rs:108` | suggestion | String-matching for disconnect; consider explicit error variant. |
### Verdict: CHANGES REQUESTED
Fix 2 critical issues before merge: (1) tick synchronization semantics in snapshot generation, (2) test race condition — refactor `TcpBridge::accept()` to take an existing `TcpListener` instead of binding its own, eliminating the 100ms sleep hack.
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>
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>
Replace expect() with unwrap_or_else that logs the bind address
and error via tracing before exiting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Addresses server side of #81, #82, #83.
Test plan
Dual-Agent Review: server -> main
Hoshe (Code Quality): REQUEST_CHANGES
Solid implementation with good test coverage (11 new tests including E2E). Two critical issues found.
game_loop.rs:54advance_tick, sosnapshot.tickis the NEW tick (1), but the state reflects processing from tick 0. The snapshot for tick N should show the state at the END of tick N. Fix: movegenerate_snapshotbeforeadvance_tick, or label snapshots withprevious_tick.bridge_tcp.rs(all 3 tests)accept()completes. Client connects before server is listening. 100ms sleep masks the timing gap. Fix: refactorTcpBridge::accept()to accept an existingTcpListenerso the test can bind once, pass the listener in, and the port is guaranteed ready — no sleep needed.bridge/mod.rs:115-119send_bridge_snapshotServerRunning=false. Server continues for one tick after send failure. Consider matching receive_bridge_inputs behavior.main.rs:23expect()on accept — panics with no address/error context. Useunwrap_or_elsewith tracing.bridge/mod.rs:108receive_bridge_inputsmsg.contains("disconnected")is string-matching. Consider aBridgeError::Disconnectedvariant for cleaner matching.bridge/mod.rsgenerate_snapshotTyre (Architecture): APPROVE
Production-quality server architecture. Clean separation of concerns, thorough three-layer testing (D-030), explicit system ordering, deterministic input processing (D-010 principle 4). Full D-020, D-010, D-030, D-012 compliance verified. No architectural issues.
tcp.rsMutex expectbridge/mod.rs:108Verdict: CHANGES REQUESTED
Fix 2 critical issues before merge: (1) tick synchronization semantics in snapshot generation, (2) test race condition — refactor
TcpBridge::accept()to take an existingTcpListenerinstead of binding its own, eliminating the 100ms sleep hack.Pull request closed