Implements the StartupMessage protocol: client generates world_seed in SessionManager.new_game(), sends it after handshake, server uses it to seed SimRng and sample EntanglementConfig. EntanglementConfig samples flat ∈ [25,35]%, intrigue ∈ [15,25]%, mundane as remainder (D-029). Same seed produces identical config (D-010 determinism). Different seeds produce distinct configs in ≥90% of pairs. Protocol flow: HandshakeMessage (server→client) → StartupMessage with world_seed (client→server) → SimRng initialization → tick loop. 10 Rust tests (determinism, variation, bounds, sum invariant). 9 GDScript test stubs + 2 encode tests for client-side pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,21 @@ impl LocalBridge {
|
||||
}
|
||||
|
||||
impl SimBridge for LocalBridge {
|
||||
fn receive_startup(&self) -> Result<super::StartupMessage, BridgeError> {
|
||||
let mut reader = self
|
||||
.reader
|
||||
.lock()
|
||||
.map_err(|e| BridgeError::MutexPoisoned(format!("reader: {}", e)))?;
|
||||
match read_framed(reader.get_mut())? {
|
||||
Some(payload) => {
|
||||
let msg: super::StartupMessage = rmp_serde::from_slice(&payload)?;
|
||||
tracing::info!("received startup message: world_seed={}", msg.world_seed);
|
||||
Ok(msg)
|
||||
}
|
||||
None => Err(BridgeError::Disconnected),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_handshake(&self) -> Result<(), BridgeError> {
|
||||
use super::types::{HandshakeMessage, PROTOCOL_VERSION};
|
||||
let msg = HandshakeMessage {
|
||||
|
||||
@@ -40,6 +40,11 @@ pub trait SimBridge: Send + Sync {
|
||||
/// any ObserverSnapshot is sent.
|
||||
fn send_handshake(&self) -> Result<(), BridgeError>;
|
||||
|
||||
/// Receive the client's startup message containing the world seed (#175).
|
||||
/// Called exactly once, after send_handshake(), before entering the tick loop.
|
||||
/// Blocks until the client sends the message.
|
||||
fn receive_startup(&self) -> Result<StartupMessage, BridgeError>;
|
||||
|
||||
/// Send an observer snapshot to the client
|
||||
fn send_snapshot(&self, snapshot: &ObserverSnapshot) -> Result<(), BridgeError>;
|
||||
|
||||
@@ -64,6 +69,10 @@ impl BridgeResource {
|
||||
self.inner.send_handshake()
|
||||
}
|
||||
|
||||
pub fn receive_startup(&self) -> Result<StartupMessage, BridgeError> {
|
||||
self.inner.receive_startup()
|
||||
}
|
||||
|
||||
pub fn send_snapshot(&self, snapshot: &ObserverSnapshot) -> Result<(), BridgeError> {
|
||||
self.inner.send_snapshot(snapshot)
|
||||
}
|
||||
|
||||
@@ -129,6 +129,28 @@ impl TcpBridge {
|
||||
}
|
||||
|
||||
impl SimBridge for TcpBridge {
|
||||
fn receive_startup(&self) -> Result<super::StartupMessage, BridgeError> {
|
||||
let mut reader = self
|
||||
.reader
|
||||
.lock()
|
||||
.map_err(|e| BridgeError::MutexPoisoned(format!("reader: {}", e)))?;
|
||||
// Toggle to blocking for reliable startup message read.
|
||||
// The client sends StartupMessage immediately after handshake validation,
|
||||
// so this read should complete quickly.
|
||||
reader.get_mut().set_nonblocking(false).map_err(BridgeError::Io)?;
|
||||
let result = read_framed(reader.get_mut());
|
||||
// Restore non-blocking for the tick loop
|
||||
reader.get_mut().set_nonblocking(true).map_err(BridgeError::Io)?;
|
||||
match result? {
|
||||
Some(payload) => {
|
||||
let msg: super::StartupMessage = rmp_serde::from_slice(&payload)?;
|
||||
tracing::info!("received startup message: world_seed={}", msg.world_seed);
|
||||
Ok(msg)
|
||||
}
|
||||
None => Err(BridgeError::Disconnected),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_handshake(&self) -> Result<(), BridgeError> {
|
||||
use super::types::{HandshakeMessage, PROTOCOL_VERSION};
|
||||
let msg = HandshakeMessage {
|
||||
|
||||
@@ -29,6 +29,25 @@ pub struct HandshakeMessage {
|
||||
pub protocol_version: u8,
|
||||
}
|
||||
|
||||
/// Startup message sent by the client after receiving HandshakeMessage (#175).
|
||||
/// Contains the world seed for deterministic simulation (D-010, D-029).
|
||||
///
|
||||
/// Protocol flow:
|
||||
/// 1. Server sends HandshakeMessage (server → client)
|
||||
/// 2. Client validates protocol_version
|
||||
/// 3. Client sends StartupMessage (client → server)
|
||||
/// 4. Server reads world_seed, initializes SimRng
|
||||
/// 5. Normal tick loop begins
|
||||
///
|
||||
/// Wire format: MessagePack, same 4-byte length-prefixed framing.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct StartupMessage {
|
||||
/// World seed for SimRng initialization.
|
||||
/// Generated by SessionManager.new_game() on the client.
|
||||
/// Same seed → same EntanglementConfig → same NPC population (D-029).
|
||||
pub world_seed: u64,
|
||||
}
|
||||
|
||||
/// The ONLY data structure crossing the client-server boundary (D-020)
|
||||
/// Contains all information visible to the observer at a given tick.
|
||||
///
|
||||
@@ -814,6 +833,31 @@ mod tests {
|
||||
assert_ne!(decoded.protocol_version, wrong_version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_message_roundtrip() {
|
||||
let msg = StartupMessage { world_seed: 0xDEADBEEF };
|
||||
let bytes = rmp_serde::to_vec_named(&msg).expect("serialize");
|
||||
let decoded: StartupMessage = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded, msg);
|
||||
assert_eq!(decoded.world_seed, 0xDEADBEEF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_message_zero_seed() {
|
||||
let msg = StartupMessage { world_seed: 0 };
|
||||
let bytes = rmp_serde::to_vec_named(&msg).expect("serialize");
|
||||
let decoded: StartupMessage = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded.world_seed, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_message_max_seed() {
|
||||
let msg = StartupMessage { world_seed: u64::MAX };
|
||||
let bytes = rmp_serde::to_vec_named(&msg).expect("serialize");
|
||||
let decoded: StartupMessage = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded.world_seed, u64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_is_distinct_from_snapshot() {
|
||||
// HandshakeMessage and ObserverSnapshot are different types on the wire.
|
||||
|
||||
Reference in New Issue
Block a user