use super::input::MAX_AHEAD;
use super::{NetSession, PeerId, HOST};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::collections::BTreeMap;
use std::io;
const MAGIC: &[u8; 4] = b"\0cmd";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TickCommands<C> {
pub tick: u64,
pub hash: u64,
pub commands: Vec<C>,
}
pub fn send<C: Serialize>(
session: &NetSession,
tick: u64,
hash: u64,
commands: &[C],
) -> io::Result<()> {
let bytes = [
MAGIC.as_slice(),
&tick.to_le_bytes(),
&hash.to_le_bytes(),
&serde_json::to_vec(commands)?,
]
.concat();
session.broadcast(&bytes)
}
#[derive(Clone, Debug)]
pub struct CommandStream<C> {
waiting: BTreeMap<u64, (u64, Vec<C>)>,
next: u64,
}
impl<C: DeserializeOwned> CommandStream<C> {
pub fn new(tick: u64) -> Self {
Self {
waiting: BTreeMap::new(),
next: tick,
}
}
pub fn accept(
&mut self,
from: PeerId,
bytes: &[u8],
) -> Option<Result<(), String>> {
let body = bytes.strip_prefix(MAGIC)?;
if from != HOST {
return Some(Err(format!("peer {from} sent commands")));
}
let Some((head, json)) = body.split_at_checked(16) else {
return Some(Err("short commands".into()));
};
let read = |b: &[u8]| u64::from_le_bytes(b.try_into().unwrap());
let (tick, hash) = (read(&head[..8]), read(&head[8..]));
if tick < self.next || self.waiting.contains_key(&tick) {
return Some(Err(format!("commands for tick {tick} came twice")));
}
if tick - self.next > MAX_AHEAD {
return Some(Err(format!(
"commands for tick {tick} are too early"
)));
}
Some(
serde_json::from_slice(json)
.map(|commands| {
self.waiting.insert(tick, (hash, commands));
})
.map_err(|error| format!("bad commands: {error}")),
)
}
pub fn ready(&mut self) -> Option<TickCommands<C>> {
let (hash, commands) = self.waiting.remove(&self.next)?;
self.next += 1;
Some(TickCommands {
tick: self.next - 1,
hash,
commands,
})
}
pub fn tick(&self) -> u64 {
self.next
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::net::replicate::{Replication, Simulated, TRANSFORM};
use crate::net::NetEvent;
use crate::runtime::{SceneComponentRegistry, SceneId};
use crate::Transform;
use bevy_ecs::prelude::*;
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct Push(f32);
fn step(world: &mut World, commands: &[Push]) {
for Push(dx) in commands {
for mut transform in world.query::<&mut Transform>().iter_mut(world)
{
transform.position[0] += dx;
}
}
}
fn state(world: &mut World) -> u64 {
let mut query = world.query::<&Transform>();
query
.iter(world)
.map(|t| u64::from(t.position[0].to_bits()))
.sum()
}
#[test]
fn clients_run_the_hosts_commands_instead_of_receiving_bodies() {
let (host, clients) = NetSession::loopback(1);
host.poll();
let id = SceneId::new();
let mut worlds = [World::new(), World::new()];
for world in &mut worlds {
world.insert_resource(SceneComponentRegistry::default());
world.spawn((
crate::net::replicate::Replicated,
Simulated,
id,
Transform::default(),
));
}
let mut replication = Replication::new();
replication.component(TRANSFORM);
let first = replication.delta(&mut worlds[0]).unwrap();
let ticks = [vec![Push(1.0)], vec![], vec![Push(2.5), Push(-0.5)]];
for (tick, commands) in (5..).zip(&ticks) {
send(&host, tick, state(&mut worlds[0]), commands).unwrap();
step(&mut worlds[0], commands);
}
assert!(replication.delta(&mut worlds[0]).unwrap().is_none());
let mut stream = CommandStream::<Push>::new(5);
assert_eq!(stream.ready(), None);
for event in clients[0].poll() {
let NetEvent::Message { from, bytes } = event else {
panic!("expected a message")
};
assert_eq!(stream.accept(from, &bytes), Some(Ok(())));
}
while let Some(tick) = stream.ready() {
assert_eq!(tick.hash, state(&mut worlds[1]));
step(&mut worlds[1], &tick.commands);
}
assert_eq!(stream.tick(), 8);
assert_eq!(state(&mut worlds[0]), state(&mut worlds[1]));
assert!(first.is_some_and(
|bytes| !String::from_utf8_lossy(&bytes).contains(TRANSFORM)
));
let message = |tick: u64| {
[MAGIC.as_slice(), &tick.to_le_bytes(), &[0; 8], b"[]"].concat()
};
assert!(matches!(stream.accept(HOST, &message(7)), Some(Err(_))));
assert!(matches!(
stream.accept(HOST, &message(8 + MAX_AHEAD + 1)),
Some(Err(_))
));
assert!(matches!(stream.accept(1, &message(8)), Some(Err(_))));
assert!(matches!(stream.accept(HOST, b"\0cmd\x01"), Some(Err(_))));
assert_eq!(stream.accept(HOST, b"game bytes"), None);
assert_eq!(stream.accept(HOST, &message(8)), Some(Ok(())));
assert_eq!(stream.ready().map(|tick| tick.tick), Some(8));
}
}