use serde::{Deserialize, Serialize};
use crate::world::{Entity, World};
const MAX_INBOX_SIZE: usize = 4096;
const MAX_OUTBOX_SIZE: usize = 4096;
const MAX_DEDUP_ENTRIES: usize = 16_384;
const MAX_NODE_ID_LEN: usize = 256;
const MAX_ENTITY_LIST_SIZE: usize = 10_000;
const MAX_INPUT_PAYLOAD_LEN: usize = 1_048_576;
pub type NodeId = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NetRole {
Server,
#[default]
Client,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetOwner(pub NodeId);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Replicated;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Reliability {
Unreliable,
Reliable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NetMessage {
StateSnapshot(StateSnapshot),
StateDelta(StateDelta),
InputReplication(InputMessage),
PlayerJoin {
node_id: NodeId,
},
PlayerLeave {
node_id: NodeId,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StateSnapshot {
pub tick: u64,
pub entities: Vec<EntityState>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EntityState {
pub entity_id: u64,
pub position: [f32; 3],
pub owner: Option<NodeId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StateDelta {
pub base_tick: u64,
pub tick: u64,
pub changes: Vec<EntityState>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputMessage {
pub node_id: NodeId,
pub tick: u64,
pub payload: String,
}
fn validate_message(msg: &NetMessage) -> Result<(), &'static str> {
match msg {
NetMessage::StateSnapshot(snap) => {
if snap.entities.len() > MAX_ENTITY_LIST_SIZE {
return Err("snapshot entity count exceeds MAX_ENTITY_LIST_SIZE");
}
for state in &snap.entities {
if let Some(ref owner) = state.owner
&& owner.len() > MAX_NODE_ID_LEN
{
return Err("entity owner NodeId exceeds MAX_NODE_ID_LEN");
}
}
}
NetMessage::StateDelta(delta) => {
if delta.changes.len() > MAX_ENTITY_LIST_SIZE {
return Err("delta change count exceeds MAX_ENTITY_LIST_SIZE");
}
for state in &delta.changes {
if let Some(ref owner) = state.owner
&& owner.len() > MAX_NODE_ID_LEN
{
return Err("entity owner NodeId exceeds MAX_NODE_ID_LEN");
}
}
}
NetMessage::InputReplication(input) => {
if input.node_id.len() > MAX_NODE_ID_LEN {
return Err("input node_id exceeds MAX_NODE_ID_LEN");
}
if input.payload.len() > MAX_INPUT_PAYLOAD_LEN {
return Err("input payload exceeds MAX_INPUT_PAYLOAD_LEN");
}
}
NetMessage::PlayerJoin { node_id } | NetMessage::PlayerLeave { node_id } => {
if node_id.len() > MAX_NODE_ID_LEN {
return Err("join/leave node_id exceeds MAX_NODE_ID_LEN");
}
}
}
Ok(())
}
pub struct NetState {
pub node_id: NodeId,
pub role: NetRole,
relay: majra::relay::Relay,
peers: Vec<NodeId>,
outbox: Vec<NetMessage>,
inbox: Vec<NetMessage>,
pub tick: u64,
}
impl NetState {
pub fn new(node_id: impl Into<String>, role: NetRole) -> Self {
let node_id = node_id.into();
let relay = majra::relay::Relay::new(&node_id);
Self {
node_id,
role,
relay,
peers: Vec::new(),
outbox: Vec::new(),
inbox: Vec::new(),
tick: 0,
}
}
pub fn server(node_id: impl Into<String>) -> Self {
Self::new(node_id, NetRole::Server)
}
pub fn client(node_id: impl Into<String>) -> Self {
Self::new(node_id, NetRole::Client)
}
pub fn add_peer(&mut self, peer_id: impl Into<String>) {
let id = peer_id.into();
if !self.peers.contains(&id) {
self.peers.push(id);
}
}
pub fn remove_peer(&mut self, peer_id: &str) {
self.peers.retain(|p| p != peer_id);
}
#[must_use]
#[inline]
pub fn peer_count(&self) -> usize {
self.peers.len()
}
#[must_use]
pub fn peers(&self) -> &[NodeId] {
&self.peers
}
pub fn send(&mut self, msg: NetMessage) {
if self.outbox.len() >= MAX_OUTBOX_SIZE {
tracing::warn!(
queue_len = self.outbox.len(),
"outbox full — dropping outbound message"
);
return;
}
self.outbox.push(msg);
}
pub fn drain_outbox(&mut self) -> Vec<NetMessage> {
std::mem::take(&mut self.outbox)
}
pub fn receive(&mut self, msg: NetMessage) {
if let Err(reason) = validate_message(&msg) {
tracing::warn!(reason, "dropping inbound message — validation failed");
return;
}
if self.inbox.len() >= MAX_INBOX_SIZE {
tracing::warn!(
queue_len = self.inbox.len(),
"inbox full — dropping inbound message"
);
return;
}
self.inbox.push(msg);
}
pub fn drain_inbox(&mut self) -> Vec<NetMessage> {
std::mem::take(&mut self.inbox)
}
pub fn advance_tick(&mut self) {
self.tick += 1;
}
pub fn relay(&self) -> &majra::relay::Relay {
&self.relay
}
pub fn broadcast_via_relay(&self, topic: &str, payload: serde_json::Value) -> u64 {
self.relay.broadcast(topic, payload)
}
#[must_use]
#[inline]
pub fn is_server(&self) -> bool {
self.role == NetRole::Server
}
#[must_use]
#[inline]
pub fn is_client(&self) -> bool {
self.role == NetRole::Client
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaggedMessage {
pub message: NetMessage,
pub reliability: Reliability,
pub sequence: u64,
}
pub struct ReliableChannel {
next_sequence: u64,
pending_ack: std::collections::HashMap<u64, TaggedMessage>,
received: std::collections::HashSet<u64>,
}
impl Default for ReliableChannel {
fn default() -> Self {
Self::new()
}
}
impl ReliableChannel {
pub fn new() -> Self {
Self {
next_sequence: 1,
pending_ack: std::collections::HashMap::new(),
received: std::collections::HashSet::new(),
}
}
pub fn send(&mut self, message: NetMessage, reliability: Reliability) -> TaggedMessage {
let seq = self.next_sequence;
self.next_sequence += 1;
let tagged = TaggedMessage {
message,
reliability,
sequence: seq,
};
if reliability == Reliability::Reliable {
self.pending_ack.insert(seq, tagged.clone());
}
tagged
}
pub fn ack(&mut self, sequence: u64) {
self.pending_ack.remove(&sequence);
}
pub fn pending_retransmit(&self) -> Vec<&TaggedMessage> {
self.pending_ack.values().collect()
}
pub fn receive(&mut self, sequence: u64) -> bool {
let is_new = self.received.insert(sequence);
if self.received.len() > MAX_DEDUP_ENTRIES {
let min_seq = sequence.saturating_sub(MAX_DEDUP_ENTRIES as u64 / 2);
self.trim_received(min_seq);
tracing::warn!(
retained = self.received.len(),
min_sequence = min_seq,
"dedup set exceeded MAX_DEDUP_ENTRIES — trimmed"
);
}
is_new
}
#[must_use]
#[inline]
pub fn pending_count(&self) -> usize {
self.pending_ack.len()
}
pub fn trim_received(&mut self, min_sequence: u64) {
self.received.retain(|&seq| seq >= min_sequence);
}
}
pub struct InterestArea {
pub center: [f32; 3],
pub radius: f32,
}
impl InterestArea {
pub fn new(center: [f32; 3], radius: f32) -> Self {
Self { center, radius }
}
#[must_use]
#[inline]
pub fn contains(&self, position: [f32; 3]) -> bool {
let dx = position[0] - self.center[0];
let dy = position[1] - self.center[1];
let dz = position[2] - self.center[2];
(dx * dx + dy * dy + dz * dz) <= self.radius * self.radius
}
}
pub fn build_snapshot_filtered(
world: &World,
tick: u64,
entities: &[Entity],
interest: &InterestArea,
) -> StateSnapshot {
use crate::scene::Position;
let mut states = Vec::new();
for &entity in entities {
if !world.is_alive(entity) {
continue;
}
let position = world
.get_component::<Position>(entity)
.map(|p| [p.0.x, p.0.y, p.0.z])
.unwrap_or([0.0, 0.0, 0.0]);
if !interest.contains(position) {
continue;
}
let owner = world.get_component::<NetOwner>(entity).map(|o| o.0.clone());
states.push(EntityState {
entity_id: entity.id(),
position,
owner,
});
}
StateSnapshot {
tick,
entities: states,
}
}
pub fn build_snapshot(world: &World, tick: u64, entities: &[Entity]) -> StateSnapshot {
use crate::scene::Position;
let mut states = Vec::with_capacity(entities.len());
for &entity in entities {
if !world.is_alive(entity) {
continue;
}
let position = world
.get_component::<Position>(entity)
.map(|p| [p.0.x, p.0.y, p.0.z])
.unwrap_or([0.0, 0.0, 0.0]);
let owner = world.get_component::<NetOwner>(entity).map(|o| o.0.clone());
states.push(EntityState {
entity_id: entity.id(),
position,
owner,
});
}
StateSnapshot {
tick,
entities: states,
}
}
pub fn build_delta(old: &StateSnapshot, new: &StateSnapshot) -> StateDelta {
let mut changes = Vec::new();
if old.entities.len() < 256 {
for new_state in &new.entities {
let changed = old
.entities
.iter()
.find(|o| o.entity_id == new_state.entity_id)
.is_none_or(|old_state| old_state.position != new_state.position);
if changed {
changes.push(new_state.clone());
}
}
} else {
use std::collections::HashMap;
let old_map: HashMap<u64, &EntityState> =
old.entities.iter().map(|e| (e.entity_id, e)).collect();
for new_state in &new.entities {
let changed = old_map
.get(&new_state.entity_id)
.is_none_or(|old_state| old_state.position != new_state.position);
if changed {
changes.push(new_state.clone());
}
}
}
StateDelta {
base_tick: old.tick,
tick: new.tick,
changes,
}
}
pub fn apply_snapshot(world: &mut World, snapshot: &StateSnapshot) {
use crate::scene::Position;
for state in &snapshot.entities {
let entity = Entity::from_id(state.entity_id);
if let Some(pos) = world.get_component_mut::<Position>(entity) {
pos.0 = hisab::Vec3::new(state.position[0], state.position[1], state.position[2]);
}
}
}
#[derive(Debug, Clone)]
pub struct NetInterpolation {
pub previous: [f32; 3],
pub target: [f32; 3],
pub alpha: f32,
}
impl Default for NetInterpolation {
fn default() -> Self {
Self {
previous: [0.0; 3],
target: [0.0; 3],
alpha: 1.0,
}
}
}
impl NetInterpolation {
pub fn set_target(&mut self, target: [f32; 3]) {
self.previous = self.current();
self.target = target;
self.alpha = 0.0;
}
pub fn advance(&mut self, dt: f32) {
self.alpha = (self.alpha + dt).min(1.0);
}
#[must_use]
#[inline]
pub fn current(&self) -> [f32; 3] {
[
self.previous[0] + (self.target[0] - self.previous[0]) * self.alpha,
self.previous[1] + (self.target[1] - self.previous[1]) * self.alpha,
self.previous[2] + (self.target[2] - self.previous[2]) * self.alpha,
]
}
#[must_use]
#[inline]
pub fn is_complete(&self) -> bool {
self.alpha >= 1.0
}
}
pub fn apply_snapshot_interpolated(world: &mut World, snapshot: &StateSnapshot) {
for state in &snapshot.entities {
let entity = Entity::from_id(state.entity_id);
if let Some(interp) = world.get_component_mut::<NetInterpolation>(entity) {
interp.set_target(state.position);
}
}
}
pub fn step_interpolation(world: &mut World, dt: f32) {
use crate::scene::Position;
let entities: Vec<Entity> = world
.query::<NetInterpolation>()
.iter()
.map(|(e, _)| *e)
.collect();
for entity in entities {
if let Some(interp) = world.get_component_mut::<NetInterpolation>(entity) {
interp.advance(dt);
let pos = interp.current();
if let Some(position) = world.get_component_mut::<Position>(entity) {
position.0 = hisab::Vec3::new(pos[0], pos[1], pos[2]);
}
}
}
}
#[derive(Debug, Clone)]
pub struct PredictionBuffer {
history: std::collections::VecDeque<([f32; 3], u64)>,
max_size: usize,
}
impl PredictionBuffer {
pub fn new(max_size: usize) -> Self {
Self {
history: std::collections::VecDeque::with_capacity(max_size),
max_size,
}
}
pub fn record(&mut self, position: [f32; 3], tick: u64) {
if self.history.len() >= self.max_size {
self.history.pop_front();
}
self.history.push_back((position, tick));
}
#[must_use]
pub fn at_tick(&self, tick: u64) -> Option<[f32; 3]> {
self.history
.iter()
.find(|(_, t)| *t == tick)
.map(|(pos, _)| *pos)
}
pub fn check_prediction(&self, server_pos: [f32; 3], tick: u64, threshold: f32) -> Option<f32> {
let predicted = self.at_tick(tick)?;
let dx = server_pos[0] - predicted[0];
let dy = server_pos[1] - predicted[1];
let dz = server_pos[2] - predicted[2];
let error = (dx * dx + dy * dy + dz * dz).sqrt();
if error > threshold { Some(error) } else { None }
}
pub fn discard_before(&mut self, tick: u64) {
self.history.retain(|(_, t)| *t >= tick);
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.history.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.history.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct ClockSync {
pub offset_ms: f64,
pub rtt_ms: f64,
pub samples: u32,
}
impl Default for ClockSync {
fn default() -> Self {
Self {
offset_ms: 0.0,
rtt_ms: 0.0,
samples: 0,
}
}
}
impl ClockSync {
pub fn new() -> Self {
Self::default()
}
pub fn record_sample(&mut self, local_send: f64, server_time: f64, local_recv: f64) {
let rtt = local_recv - local_send;
let estimated_server_now = server_time + rtt / 2.0;
let offset = estimated_server_now - local_recv;
let alpha = if self.samples == 0 { 1.0 } else { 0.1 };
self.offset_ms = self.offset_ms * (1.0 - alpha) + offset * alpha;
self.rtt_ms = self.rtt_ms * (1.0 - alpha) + rtt * alpha;
self.samples += 1;
}
#[must_use]
#[inline]
pub fn to_server_time(&self, local_ms: f64) -> f64 {
local_ms + self.offset_ms
}
#[must_use]
#[inline]
pub fn to_local_time(&self, server_ms: f64) -> f64 {
server_ms - self.offset_ms
}
}
pub fn serialize_component<T: serde::Serialize + 'static + Send + Sync>(
world: &World,
entity: Entity,
) -> Option<String> {
let component = world.get_component::<T>(entity)?;
serde_json::to_string(component).ok()
}
pub fn apply_replicated_component<T: serde::de::DeserializeOwned + 'static + Send + Sync>(
world: &mut World,
entity: Entity,
json: &str,
) -> bool {
if let Ok(component) = serde_json::from_str::<T>(json) {
world.insert_component(entity, component).is_ok()
} else {
false
}
}
pub fn apply_delta(world: &mut World, delta: &StateDelta) {
use crate::scene::Position;
for state in &delta.changes {
let entity = Entity::from_id(state.entity_id);
if let Some(pos) = world.get_component_mut::<Position>(entity) {
pos.0 = hisab::Vec3::new(state.position[0], state.position[1], state.position[2]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scene::Position;
#[test]
fn net_state_server() {
let state = NetState::server("server-1");
assert!(state.is_server());
assert!(!state.is_client());
assert_eq!(state.node_id, "server-1");
assert_eq!(state.peer_count(), 0);
}
#[test]
fn net_state_client() {
let state = NetState::client("player-42");
assert!(state.is_client());
assert_eq!(state.node_id, "player-42");
}
#[test]
fn net_state_peers() {
let mut state = NetState::server("server");
state.add_peer("player-1");
state.add_peer("player-2");
assert_eq!(state.peer_count(), 2);
state.add_peer("player-1");
assert_eq!(state.peer_count(), 2);
state.remove_peer("player-1");
assert_eq!(state.peer_count(), 1);
assert_eq!(state.peers()[0], "player-2");
}
#[test]
fn net_state_messaging() {
let mut state = NetState::server("server");
state.send(NetMessage::PlayerJoin {
node_id: "p1".into(),
});
state.send(NetMessage::PlayerJoin {
node_id: "p2".into(),
});
let outbox = state.drain_outbox();
assert_eq!(outbox.len(), 2);
assert!(state.drain_outbox().is_empty());
}
#[test]
fn net_state_inbox() {
let mut state = NetState::client("player");
state.receive(NetMessage::PlayerJoin {
node_id: "other".into(),
});
let inbox = state.drain_inbox();
assert_eq!(inbox.len(), 1);
}
#[test]
fn net_state_tick() {
let mut state = NetState::server("server");
assert_eq!(state.tick, 0);
state.advance_tick();
state.advance_tick();
assert_eq!(state.tick, 2);
}
#[test]
fn build_snapshot_basic() {
let mut world = World::new();
let e1 = world.spawn();
let e2 = world.spawn();
world
.insert_component(e1, Position(hisab::Vec3::new(1.0, 2.0, 3.0)))
.unwrap();
world
.insert_component(e2, Position(hisab::Vec3::new(4.0, 5.0, 6.0)))
.unwrap();
world
.insert_component(e1, NetOwner("player-1".into()))
.unwrap();
let snapshot = build_snapshot(&world, 10, &[e1, e2]);
assert_eq!(snapshot.tick, 10);
assert_eq!(snapshot.entities.len(), 2);
assert_eq!(snapshot.entities[0].position, [1.0, 2.0, 3.0]);
assert_eq!(snapshot.entities[0].owner, Some("player-1".into()));
assert!(snapshot.entities[1].owner.is_none());
}
#[test]
fn build_delta_detects_changes() {
let old = StateSnapshot {
tick: 1,
entities: vec![
EntityState {
entity_id: 0,
position: [0.0, 0.0, 0.0],
owner: None,
},
EntityState {
entity_id: 1,
position: [10.0, 0.0, 0.0],
owner: None,
},
],
};
let new = StateSnapshot {
tick: 2,
entities: vec![
EntityState {
entity_id: 0,
position: [0.0, 0.0, 0.0], owner: None,
},
EntityState {
entity_id: 1,
position: [15.0, 0.0, 0.0], owner: None,
},
],
};
let delta = build_delta(&old, &new);
assert_eq!(delta.base_tick, 1);
assert_eq!(delta.tick, 2);
assert_eq!(delta.changes.len(), 1); assert_eq!(delta.changes[0].entity_id, 1);
}
#[test]
fn build_delta_new_entity() {
let old = StateSnapshot {
tick: 1,
entities: vec![],
};
let new = StateSnapshot {
tick: 2,
entities: vec![EntityState {
entity_id: 42,
position: [1.0, 2.0, 3.0],
owner: None,
}],
};
let delta = build_delta(&old, &new);
assert_eq!(delta.changes.len(), 1);
}
#[test]
fn apply_snapshot_updates_positions() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, Position(hisab::Vec3::ZERO))
.unwrap();
let snapshot = StateSnapshot {
tick: 5,
entities: vec![EntityState {
entity_id: e.id(),
position: [10.0, 20.0, 30.0],
owner: None,
}],
};
apply_snapshot(&mut world, &snapshot);
let pos = world.get_component::<Position>(e).unwrap();
assert_eq!(pos.0.x, 10.0);
assert_eq!(pos.0.y, 20.0);
assert_eq!(pos.0.z, 30.0);
}
#[test]
fn apply_delta_updates_only_changed() {
let mut world = World::new();
let e1 = world.spawn();
let e2 = world.spawn();
world
.insert_component(e1, Position(hisab::Vec3::new(1.0, 0.0, 0.0)))
.unwrap();
world
.insert_component(e2, Position(hisab::Vec3::new(2.0, 0.0, 0.0)))
.unwrap();
let delta = StateDelta {
base_tick: 1,
tick: 2,
changes: vec![EntityState {
entity_id: e2.id(),
position: [99.0, 0.0, 0.0],
owner: None,
}],
};
apply_delta(&mut world, &delta);
assert_eq!(world.get_component::<Position>(e1).unwrap().0.x, 1.0);
assert_eq!(world.get_component::<Position>(e2).unwrap().0.x, 99.0);
}
#[test]
fn snapshot_serde_roundtrip() {
let snapshot = StateSnapshot {
tick: 42,
entities: vec![EntityState {
entity_id: 7,
position: [1.0, 2.0, 3.0],
owner: Some("player".into()),
}],
};
let json = serde_json::to_string(&snapshot).unwrap();
let decoded: StateSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.tick, 42);
assert_eq!(decoded.entities[0].position, [1.0, 2.0, 3.0]);
}
#[test]
fn net_message_serde() {
let msg = NetMessage::InputReplication(InputMessage {
node_id: "p1".into(),
tick: 100,
payload: r#"{"keys": ["W", "Space"]}"#.into(),
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: NetMessage = serde_json::from_str(&json).unwrap();
match decoded {
NetMessage::InputReplication(input) => {
assert_eq!(input.node_id, "p1");
assert_eq!(input.tick, 100);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn net_owner_component() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, NetOwner("server".into()))
.unwrap();
assert_eq!(world.get_component::<NetOwner>(e).unwrap().0, "server");
}
#[test]
fn replicated_component() {
let mut world = World::new();
let e = world.spawn();
world.insert_component(e, Replicated).unwrap();
assert!(world.has_component::<Replicated>(e));
}
#[test]
fn relay_accessible() {
let state = NetState::server("test");
let relay = state.relay();
assert_eq!(relay.node_id(), "test");
}
#[test]
fn net_role_default() {
assert_eq!(NetRole::default(), NetRole::Client);
}
#[test]
fn empty_delta_no_changes() {
let snap = StateSnapshot {
tick: 1,
entities: vec![EntityState {
entity_id: 0,
position: [1.0, 2.0, 3.0],
owner: None,
}],
};
let delta = build_delta(&snap, &snap);
assert!(delta.changes.is_empty());
}
#[test]
fn delta_with_removed_entity() {
let old = StateSnapshot {
tick: 1,
entities: vec![
EntityState {
entity_id: 0,
position: [0.0, 0.0, 0.0],
owner: None,
},
EntityState {
entity_id: 1,
position: [1.0, 0.0, 0.0],
owner: None,
},
],
};
let new = StateSnapshot {
tick: 2,
entities: vec![
EntityState {
entity_id: 0,
position: [0.0, 0.0, 0.0],
owner: None,
},
],
};
let delta = build_delta(&old, &new);
assert!(delta.changes.is_empty());
}
#[test]
fn apply_snapshot_missing_entity_no_panic() {
let mut world = World::new();
let snapshot = StateSnapshot {
tick: 1,
entities: vec![EntityState {
entity_id: 99999,
position: [1.0, 2.0, 3.0],
owner: None,
}],
};
apply_snapshot(&mut world, &snapshot); }
#[test]
fn large_snapshot_1000() {
let mut world = World::new();
let mut entities = Vec::new();
for i in 0..1000 {
let e = world.spawn();
world
.insert_component(e, Position(hisab::Vec3::new(i as f32, 0.0, 0.0)))
.unwrap();
entities.push(e);
}
let snapshot = build_snapshot(&world, 42, &entities);
assert_eq!(snapshot.entities.len(), 1000);
assert_eq!(snapshot.tick, 42);
}
#[test]
fn net_state_as_world_resource() {
let mut world = World::new();
world.insert_resource(NetState::server("game-server"));
let state = world.get_resource::<NetState>().unwrap();
assert_eq!(state.node_id, "game-server");
assert!(state.is_server());
let state = world.get_resource_mut::<NetState>().unwrap();
state.add_peer("player-1");
assert_eq!(state.peer_count(), 1);
}
#[test]
fn entity_state_partial_eq() {
let a = EntityState {
entity_id: 1,
position: [1.0, 2.0, 3.0],
owner: None,
};
let b = EntityState {
entity_id: 1,
position: [1.0, 2.0, 3.0],
owner: None,
};
let c = EntityState {
entity_id: 1,
position: [4.0, 5.0, 6.0],
owner: None,
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn input_message_serde() {
let msg = InputMessage {
node_id: "p1".into(),
tick: 42,
payload: r#"{"w":true}"#.into(),
};
let json = serde_json::to_string(&msg).unwrap();
let decoded: InputMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.node_id, "p1");
assert_eq!(decoded.tick, 42);
}
#[test]
fn delta_serde_roundtrip() {
let delta = StateDelta {
base_tick: 1,
tick: 2,
changes: vec![EntityState {
entity_id: 5,
position: [10.0, 20.0, 30.0],
owner: Some("server".into()),
}],
};
let json = serde_json::to_string(&delta).unwrap();
let decoded: StateDelta = serde_json::from_str(&json).unwrap();
assert_eq!(delta, decoded);
}
#[test]
fn broadcast_via_relay() {
let state = NetState::server("test");
let seq = state.broadcast_via_relay("game.state", serde_json::json!({"tick": 1}));
assert!(seq > 0);
}
#[test]
fn full_snapshot_delta_apply_cycle() {
let mut world = World::new();
let e1 = world.spawn();
let e2 = world.spawn();
world
.insert_component(e1, Position(hisab::Vec3::new(0.0, 0.0, 0.0)))
.unwrap();
world
.insert_component(e2, Position(hisab::Vec3::new(10.0, 0.0, 0.0)))
.unwrap();
let snap1 = build_snapshot(&world, 1, &[e1, e2]);
world.get_component_mut::<Position>(e1).unwrap().0.x = 5.0;
let snap2 = build_snapshot(&world, 2, &[e1, e2]);
let delta = build_delta(&snap1, &snap2);
assert_eq!(delta.changes.len(), 1);
assert_eq!(delta.changes[0].entity_id, e1.id());
let mut world2 = World::new();
let e1b = world2.spawn();
let e2b = world2.spawn();
world2
.insert_component(e1b, Position(hisab::Vec3::ZERO))
.unwrap();
world2
.insert_component(e2b, Position(hisab::Vec3::new(10.0, 0.0, 0.0)))
.unwrap();
apply_delta(&mut world2, &delta);
assert_eq!(world2.get_component::<Position>(e1b).unwrap().0.x, 5.0);
assert_eq!(world2.get_component::<Position>(e2b).unwrap().0.x, 10.0);
}
#[test]
fn build_delta_large_hashmap_path() {
let old = StateSnapshot {
tick: 1,
entities: (0..300)
.map(|i| EntityState {
entity_id: i,
position: [i as f32, 0.0, 0.0],
owner: None,
})
.collect(),
};
let mut new_entities: Vec<EntityState> = (0..300)
.map(|i| EntityState {
entity_id: i,
position: [i as f32, 0.0, 0.0],
owner: None,
})
.collect();
for i in 0..10 {
new_entities[i * 30].position[0] += 100.0;
}
let new = StateSnapshot {
tick: 2,
entities: new_entities,
};
let delta = build_delta(&old, &new);
assert_eq!(delta.changes.len(), 10);
}
#[test]
fn interpolation_basic() {
let mut interp = NetInterpolation::default();
interp.set_target([10.0, 0.0, 0.0]);
assert_eq!(interp.current(), [0.0, 0.0, 0.0]); interp.advance(0.5);
assert_eq!(interp.current(), [5.0, 0.0, 0.0]); interp.advance(0.5);
assert_eq!(interp.current(), [10.0, 0.0, 0.0]); assert!(interp.is_complete());
}
#[test]
fn interpolation_clamps() {
let mut interp = NetInterpolation::default();
interp.set_target([10.0, 0.0, 0.0]);
interp.advance(2.0); assert_eq!(interp.alpha, 1.0);
assert_eq!(interp.current(), [10.0, 0.0, 0.0]);
}
#[test]
fn interpolation_retarget() {
let mut interp = NetInterpolation::default();
interp.set_target([10.0, 0.0, 0.0]);
interp.advance(0.5);
interp.set_target([20.0, 0.0, 0.0]);
assert_eq!(interp.alpha, 0.0);
assert_eq!(interp.current(), [5.0, 0.0, 0.0]); }
#[test]
fn prediction_buffer_record_and_lookup() {
let mut buf = PredictionBuffer::new(100);
buf.record([1.0, 0.0, 0.0], 1);
buf.record([2.0, 0.0, 0.0], 2);
buf.record([3.0, 0.0, 0.0], 3);
assert_eq!(buf.at_tick(2), Some([2.0, 0.0, 0.0]));
assert_eq!(buf.at_tick(99), None);
assert_eq!(buf.len(), 3);
}
#[test]
fn prediction_buffer_overflow() {
let mut buf = PredictionBuffer::new(3);
buf.record([1.0, 0.0, 0.0], 1);
buf.record([2.0, 0.0, 0.0], 2);
buf.record([3.0, 0.0, 0.0], 3);
buf.record([4.0, 0.0, 0.0], 4);
assert_eq!(buf.len(), 3);
assert_eq!(buf.at_tick(1), None); assert_eq!(buf.at_tick(4), Some([4.0, 0.0, 0.0]));
}
#[test]
fn prediction_check_within_threshold() {
let mut buf = PredictionBuffer::new(10);
buf.record([10.0, 0.0, 0.0], 5);
assert!(buf.check_prediction([10.01, 0.0, 0.0], 5, 0.1).is_none());
let error = buf.check_prediction([15.0, 0.0, 0.0], 5, 0.1);
assert!(error.is_some());
assert!(error.unwrap() > 4.0);
}
#[test]
fn prediction_discard_old() {
let mut buf = PredictionBuffer::new(10);
buf.record([1.0, 0.0, 0.0], 1);
buf.record([2.0, 0.0, 0.0], 2);
buf.record([3.0, 0.0, 0.0], 3);
buf.discard_before(2);
assert_eq!(buf.len(), 2);
assert_eq!(buf.at_tick(1), None);
}
#[test]
fn serialize_replicate_component() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, Position(hisab::Vec3::new(1.0, 2.0, 3.0)))
.unwrap();
world
.insert_component(e, NetOwner("player-1".into()))
.unwrap();
let json = serialize_component::<NetOwner>(&world, e).unwrap();
assert!(json.contains("player-1"));
let mut world2 = World::new();
let e2 = world2.spawn();
assert!(apply_replicated_component::<NetOwner>(
&mut world2,
e2,
&json
));
assert_eq!(world2.get_component::<NetOwner>(e2).unwrap().0, "player-1");
}
#[test]
fn step_interpolation_updates_position() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, Position(hisab::Vec3::ZERO))
.unwrap();
world
.insert_component(e, NetInterpolation::default())
.unwrap();
{
let interp = world.get_component_mut::<NetInterpolation>(e).unwrap();
interp.set_target([10.0, 0.0, 0.0]);
}
step_interpolation(&mut world, 0.5);
let pos = world.get_component::<Position>(e).unwrap();
assert!((pos.0.x - 5.0).abs() < 0.01);
step_interpolation(&mut world, 0.5);
let pos = world.get_component::<Position>(e).unwrap();
assert!((pos.0.x - 10.0).abs() < 0.01);
}
#[test]
fn prediction_buffer_as_component() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, PredictionBuffer::new(64))
.unwrap();
let buf = world.get_component_mut::<PredictionBuffer>(e).unwrap();
buf.record([1.0, 0.0, 0.0], 1);
assert_eq!(buf.len(), 1);
}
#[test]
fn reliable_channel_send_unreliable() {
let mut ch = ReliableChannel::new();
let msg = ch.send(
NetMessage::PlayerJoin {
node_id: "p1".into(),
},
Reliability::Unreliable,
);
assert_eq!(msg.reliability, Reliability::Unreliable);
assert_eq!(ch.pending_count(), 0); }
#[test]
fn reliable_channel_send_reliable() {
let mut ch = ReliableChannel::new();
let msg = ch.send(
NetMessage::PlayerJoin {
node_id: "p1".into(),
},
Reliability::Reliable,
);
assert_eq!(msg.reliability, Reliability::Reliable);
assert_eq!(ch.pending_count(), 1); }
#[test]
fn reliable_channel_ack() {
let mut ch = ReliableChannel::new();
let msg = ch.send(
NetMessage::PlayerLeave {
node_id: "p1".into(),
},
Reliability::Reliable,
);
assert_eq!(ch.pending_count(), 1);
ch.ack(msg.sequence);
assert_eq!(ch.pending_count(), 0);
}
#[test]
fn reliable_channel_dedup() {
let mut ch = ReliableChannel::new();
assert!(ch.receive(1)); assert!(!ch.receive(1)); assert!(ch.receive(2)); }
#[test]
fn reliable_channel_retransmit() {
let mut ch = ReliableChannel::new();
ch.send(
NetMessage::PlayerJoin {
node_id: "a".into(),
},
Reliability::Reliable,
);
ch.send(
NetMessage::PlayerJoin {
node_id: "b".into(),
},
Reliability::Reliable,
);
let pending = ch.pending_retransmit();
assert_eq!(pending.len(), 2);
}
#[test]
fn interest_area_contains() {
let area = InterestArea::new([0.0, 0.0, 0.0], 10.0);
assert!(area.contains([5.0, 0.0, 0.0]));
assert!(area.contains([0.0, 0.0, 0.0]));
assert!(!area.contains([15.0, 0.0, 0.0]));
}
#[test]
fn interest_area_boundary() {
let area = InterestArea::new([0.0, 0.0, 0.0], 10.0);
assert!(area.contains([10.0, 0.0, 0.0]));
assert!(!area.contains([10.1, 0.0, 0.0]));
}
#[test]
fn build_snapshot_filtered_test() {
let mut world = World::new();
let near = world.spawn();
let far = world.spawn();
world
.insert_component(near, Position(hisab::Vec3::new(5.0, 0.0, 0.0)))
.unwrap();
world
.insert_component(far, Position(hisab::Vec3::new(100.0, 0.0, 0.0)))
.unwrap();
let interest = InterestArea::new([0.0, 0.0, 0.0], 20.0);
let snapshot = build_snapshot_filtered(&world, 1, &[near, far], &interest);
assert_eq!(snapshot.entities.len(), 1); assert_eq!(snapshot.entities[0].entity_id, near.id());
}
#[test]
fn build_snapshot_filtered_all_in_range() {
let mut world = World::new();
let e1 = world.spawn();
let e2 = world.spawn();
world
.insert_component(e1, Position(hisab::Vec3::new(1.0, 0.0, 0.0)))
.unwrap();
world
.insert_component(e2, Position(hisab::Vec3::new(2.0, 0.0, 0.0)))
.unwrap();
let interest = InterestArea::new([0.0, 0.0, 0.0], 100.0);
let snapshot = build_snapshot_filtered(&world, 1, &[e1, e2], &interest);
assert_eq!(snapshot.entities.len(), 2);
}
#[test]
fn reliability_serde() {
let r = Reliability::Reliable;
let json = serde_json::to_string(&r).unwrap();
let decoded: Reliability = serde_json::from_str(&json).unwrap();
assert_eq!(r, decoded);
}
#[test]
fn tagged_message_serde() {
let msg = TaggedMessage {
message: NetMessage::PlayerJoin {
node_id: "p1".into(),
},
reliability: Reliability::Reliable,
sequence: 42,
};
let json = serde_json::to_string(&msg).unwrap();
let decoded: TaggedMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.sequence, 42);
}
}