feat(simulation): demux inbound bridge stream — receive() + atlas routing (#969, D-225)

Replaces the fixed-type SimBridge::receive_inputs() with a tagged
receive() -> Option<Inbound>, where Inbound is Inputs(Vec<PlayerInput>) or
AtlasRequest(AtlasLayerRequest). A shared decode_inbound() demuxes a frame by
shape (msgpack array = inputs, map = atlas request) — additive, no wire change
to existing input/snapshot frames. Adds send_atlas_response() to the trait
(both TcpBridge + LocalBridge impls). receive_bridge_inputs routes inputs to
the InputQueue as before; atlas requests to a new AtlasRequestBuffer (drained
by the serve system next). Integration tests (bridge_tcp/bridge_ipc) updated to
the tagged receive(); a demux unit test covers all three branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 18:00:30 +02:00
co-authored by Claude Opus 4.7
parent edbc0f4ff1
commit dfdc578429
5 changed files with 138 additions and 62 deletions
+5 -2
View File
@@ -3,7 +3,7 @@
use settled_reach_server::bridge::framing::{read_framed, write_framed};
use settled_reach_server::bridge::local::LocalBridge;
use settled_reach_server::bridge::types::*;
use settled_reach_server::bridge::SimBridge;
use settled_reach_server::bridge::{Inbound, SimBridge};
use settled_reach_server::simulation::time::{DayPhase, TickRate};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
@@ -116,7 +116,10 @@ fn input_roundtrip_over_unix_socket() {
let server_handle = thread::spawn(move || {
let bridge = LocalBridge::accept(&server_path).expect("failed to accept");
let inputs = bridge.receive_inputs().expect("failed to receive inputs");
let inputs = match bridge.receive().expect("failed to receive") {
Some(Inbound::Inputs(inputs)) => inputs,
other => panic!("expected inputs, got {other:?}"),
};
assert_eq!(inputs.len(), 2);
assert_eq!(inputs[0].tick, 10);
+7 -7
View File
@@ -3,7 +3,7 @@
use settled_reach_server::bridge::framing::{read_framed, write_framed};
use settled_reach_server::bridge::tcp::TcpBridge;
use settled_reach_server::bridge::types::*;
use settled_reach_server::bridge::SimBridge;
use settled_reach_server::bridge::{Inbound, SimBridge};
use settled_reach_server::simulation::time::{DayPhase, TickRate};
use std::net::{TcpListener, TcpStream};
use std::thread;
@@ -101,8 +101,8 @@ fn input_roundtrip_over_tcp() {
// Non-blocking socket: retry until data arrives or timeout.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let inputs = loop {
match bridge.receive_inputs() {
Ok(inputs) if !inputs.is_empty() => break inputs,
match bridge.receive() {
Ok(Some(Inbound::Inputs(inputs))) if !inputs.is_empty() => break inputs,
Ok(_) => {
assert!(
std::time::Instant::now() < deadline,
@@ -168,16 +168,16 @@ fn tcp_bridge_eof_returns_error() {
// Non-blocking socket: retry until we get Disconnected or timeout.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
match bridge.receive_inputs() {
Ok(inputs) if inputs.is_empty() => {
// WouldBlock — client hasn't disconnected yet, retry
match bridge.receive() {
Ok(None) => {
// No data yet — client hasn't disconnected, retry
assert!(
std::time::Instant::now() < deadline,
"timed out waiting for EOF"
);
thread::sleep(std::time::Duration::from_millis(1));
}
Ok(inputs) => panic!("expected Disconnected error, got {} inputs", inputs.len()),
Ok(Some(_)) => panic!("expected Disconnected error, got a message"),
Err(settled_reach_server::bridge::BridgeError::Disconnected) => break,
Err(e) => panic!("expected Disconnected error, got: {}", e),
}