use crate::traveler::{PoseSample, PresenceState};
use serde::{Deserialize, Serialize};
pub type PeerId = u32;
pub const MAX_MESH_PEERS: usize = 16;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MeshPose {
pub p: [f32; 3],
pub r: [f32; 4],
pub v: [f32; 3],
pub a: u8,
pub ts: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "t", rename_all = "lowercase")]
pub enum Signal {
Announce { peer: PeerId, world: String },
Welcome { id: PeerId },
Peers { peers: Vec<PeerId> },
Offer {
from: PeerId,
to: PeerId,
sdp: String,
},
Answer {
from: PeerId,
to: PeerId,
sdp: String,
},
Candidate {
from: PeerId,
to: PeerId,
candidate: String,
},
Leave { peer: PeerId },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MeshEvent {
Connected(PeerId),
Disconnected(PeerId),
}
pub trait MeshTransport {
fn broadcast(&mut self, data: &[u8]);
fn poll(&mut self) -> Vec<(PeerId, Vec<u8>)>;
fn events(&mut self) -> Vec<MeshEvent>;
}
pub struct MeshCoordinator {
me: PeerId,
peers: std::collections::BTreeSet<PeerId>,
}
impl MeshCoordinator {
pub fn new(me: PeerId) -> Self {
Self {
me,
peers: std::collections::BTreeSet::new(),
}
}
pub fn id(&self) -> PeerId {
self.me
}
pub fn set_id(&mut self, id: PeerId) {
self.me = id;
}
pub fn peer_count(&self) -> usize {
self.peers.len()
}
pub fn is_full(&self) -> bool {
self.peers.len() >= MAX_MESH_PEERS
}
pub fn send_pose(
&self,
tx: &mut dyn MeshTransport,
pos: [f32; 3],
rot: [f32; 4],
vel: [f32; 3],
anim: u8,
ts: u32,
) {
if self.peers.is_empty() {
return;
}
let pose = MeshPose {
p: pos,
r: rot,
v: vel,
a: anim,
ts,
};
if let Ok(bytes) = serde_json::to_vec(&pose) {
tx.broadcast(&bytes);
}
}
pub fn pump(&mut self, tx: &mut dyn MeshTransport, presence: &mut PresenceState, now_ms: u64) {
for ev in tx.events() {
match ev {
MeshEvent::Connected(p) if p != self.me && !self.is_full() => {
if self.peers.insert(p) {
presence.join(p, format!("peer {p}"), peer_color(p));
}
}
MeshEvent::Connected(_) => {} MeshEvent::Disconnected(p) => {
if self.peers.remove(&p) {
presence.leave(p);
}
}
}
}
for (from, data) in tx.poll() {
if from == self.me || !self.peers.contains(&from) {
continue; }
if let Ok(pose) = serde_json::from_slice::<MeshPose>(&data) {
presence.pose(
from,
PoseSample {
t_ms: now_ms,
pos: pose.p.into(),
rot: glam::Quat::from_xyzw(pose.r[0], pose.r[1], pose.r[2], pose.r[3]),
vel: pose.v.into(),
anim: pose.a,
},
);
}
}
}
}
pub fn should_offer(me: PeerId, other: PeerId) -> bool {
me < other
}
fn peer_color(id: PeerId) -> [f32; 4] {
let h = id.wrapping_mul(2654435761);
[
0.4 + ((h & 0xFF) as f32 / 255.0) * 0.5,
0.4 + (((h >> 8) & 0xFF) as f32 / 255.0) * 0.5,
0.4 + (((h >> 16) & 0xFF) as f32 / 255.0) * 0.5,
1.0,
]
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
struct Loopback {
outbox: Rc<RefCell<Vec<(PeerId, Vec<u8>)>>>, inbox: Rc<RefCell<Vec<(PeerId, Vec<u8>)>>>,
me: PeerId,
pending_events: Vec<MeshEvent>,
}
impl MeshTransport for Loopback {
fn broadcast(&mut self, data: &[u8]) {
self.outbox.borrow_mut().push((self.me, data.to_vec()));
}
fn poll(&mut self) -> Vec<(PeerId, Vec<u8>)> {
std::mem::take(&mut *self.inbox.borrow_mut())
}
fn events(&mut self) -> Vec<MeshEvent> {
std::mem::take(&mut self.pending_events)
}
}
fn linked() -> (Loopback, Loopback) {
let a_inbox = Rc::new(RefCell::new(Vec::new()));
let b_inbox = Rc::new(RefCell::new(Vec::new()));
let a = Loopback {
outbox: b_inbox.clone(),
inbox: a_inbox.clone(),
me: 1,
pending_events: vec![MeshEvent::Connected(2)],
};
let b = Loopback {
outbox: a_inbox,
inbox: b_inbox,
me: 2,
pending_events: vec![MeshEvent::Connected(1)],
};
(a, b)
}
#[test]
fn two_peers_exchange_poses_with_no_server() {
let (mut ta, mut tb) = linked();
let mut a = MeshCoordinator::new(1);
let mut b = MeshCoordinator::new(2);
let mut pa = PresenceState::default();
let mut pb = PresenceState::default();
a.pump(&mut ta, &mut pa, 0);
b.pump(&mut tb, &mut pb, 0);
assert_eq!(a.peer_count(), 1);
assert_eq!(b.peer_count(), 1);
a.send_pose(
&mut ta,
[5.0, 0.0, 3.0],
[0.0, 0.0, 0.0, 1.0],
[1.0, 0.0, 0.0],
2,
100,
);
b.pump(&mut tb, &mut pb, 50);
assert_eq!(pb.count(), 1, "B tracks A after receiving a pose");
}
#[test]
fn a_disconnect_drops_the_peer() {
let (mut ta, _tb) = linked();
let mut a = MeshCoordinator::new(1);
let mut pa = PresenceState::default();
a.pump(&mut ta, &mut pa, 0);
assert_eq!(a.peer_count(), 1);
ta.pending_events = vec![MeshEvent::Disconnected(2)];
a.pump(&mut ta, &mut pa, 10);
assert_eq!(a.peer_count(), 0, "peer removed on disconnect");
}
#[test]
fn mesh_refuses_peers_past_capacity() {
let mut c = MeshCoordinator::new(0);
let mut p = PresenceState::default();
let mut tx = Loopback {
outbox: Rc::new(RefCell::new(Vec::new())),
inbox: Rc::new(RefCell::new(Vec::new())),
me: 0,
pending_events: (1..=(MAX_MESH_PEERS as u32 + 5))
.map(MeshEvent::Connected)
.collect(),
};
c.pump(&mut tx, &mut p, 0);
assert_eq!(c.peer_count(), MAX_MESH_PEERS, "capped at MAX_MESH_PEERS");
}
#[test]
fn signaling_round_trips() {
let s = Signal::Offer {
from: 1,
to: 2,
sdp: "v=0…".into(),
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains(r#""t":"offer""#));
assert_eq!(serde_json::from_str::<Signal>(&json).unwrap(), s);
}
#[test]
fn welcome_assigns_the_peer_id() {
let s: Signal = serde_json::from_str(r#"{"t":"welcome","id":7}"#).unwrap();
assert_eq!(s, Signal::Welcome { id: 7 });
let mut c = MeshCoordinator::new(999);
c.set_id(7);
assert_eq!(c.id(), 7);
}
#[test]
fn exactly_one_side_offers() {
assert!(should_offer(1, 2));
assert!(!should_offer(2, 1));
assert_ne!(should_offer(3, 8), should_offer(8, 3));
}
}