use std::collections::HashMap;
use glam::{Mat4, Quat, Vec3};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Instance {
pub model: Mat4,
pub color: [f32; 4],
}
const INTERP_DELAY_MS: u64 = 100;
const MAX_EXTRAP_MS: u64 = 250;
const MAX_BUFFER: usize = 16;
#[derive(Debug, Clone, Copy)]
pub struct PoseSample {
pub t_ms: u64,
pub pos: Vec3,
pub rot: Quat,
pub vel: Vec3,
pub anim: u8,
}
pub struct TravelerPose {
pub id: u32,
pub position: Vec3,
pub model: Mat4,
pub color: [f32; 4],
pub body: u32,
pub speed: f32,
}
pub struct Traveler {
pub id: u32,
pub name: String,
pub color: [f32; 4],
pub avatar: Option<infinite_avatar::AvatarSpec>,
buffer: Vec<PoseSample>,
}
impl Traveler {
fn new(id: u32, name: String, color: [f32; 4]) -> Self {
Self {
id,
name,
color,
avatar: None,
buffer: Vec::new(),
}
}
fn push(&mut self, sample: PoseSample) {
if self.buffer.last().is_some_and(|l| sample.t_ms < l.t_ms) {
return;
}
self.buffer.push(sample);
if self.buffer.len() > MAX_BUFFER {
self.buffer.remove(0);
}
}
fn sample_at(&self, render_t: u64) -> Option<(Vec3, Quat)> {
let first = self.buffer.first()?;
let last = self.buffer.last()?;
if render_t <= first.t_ms {
return Some((first.pos, first.rot));
}
if render_t >= last.t_ms {
let gap = (render_t - last.t_ms).min(MAX_EXTRAP_MS);
return Some((last.pos + last.vel * (gap as f32 / 1000.0), last.rot));
}
for pair in self.buffer.windows(2) {
let (a, b) = (&pair[0], &pair[1]);
if a.t_ms <= render_t && render_t < b.t_ms {
let span = (b.t_ms - a.t_ms).max(1) as f32;
let f = (render_t - a.t_ms) as f32 / span;
return Some((a.pos.lerp(b.pos, f), a.rot.slerp(b.rot, f)));
}
}
Some((last.pos, last.rot))
}
}
#[derive(Default)]
pub struct PresenceState {
travelers: HashMap<u32, Traveler>,
}
impl PresenceState {
pub fn new() -> Self {
Self::default()
}
pub fn count(&self) -> usize {
self.travelers.len()
}
pub fn is_empty(&self) -> bool {
self.travelers.is_empty()
}
pub fn join(&mut self, id: u32, name: impl Into<String>, color: [f32; 4]) {
self.travelers
.entry(id)
.or_insert_with(|| Traveler::new(id, name.into(), color));
}
pub fn leave(&mut self, id: u32) {
self.travelers.remove(&id);
}
pub fn restyle(&mut self, id: u32, name: Option<String>, color: Option<[f32; 4]>) {
if let Some(t) = self.travelers.get_mut(&id) {
if let Some(n) = name {
t.name = n;
}
if let Some(c) = color {
t.color = c;
}
}
}
pub fn set_avatar(&mut self, id: u32, avatar: Option<infinite_avatar::AvatarSpec>) {
if let Some(t) = self.travelers.get_mut(&id) {
t.avatar = avatar;
}
}
pub fn pose(&mut self, id: u32, sample: PoseSample) {
self.travelers
.entry(id)
.or_insert_with(|| Traveler::new(id, format!("traveler {id}"), default_color(id)))
.push(sample);
}
pub fn entities(&self, now_ms: u64, foot_lift: f32) -> Vec<Instance> {
self.poses(now_ms, foot_lift)
.into_iter()
.map(|p| Instance { model: p.model, color: p.color })
.collect()
}
pub fn poses(&self, now_ms: u64, foot_lift: f32) -> Vec<TravelerPose> {
let render_t = now_ms.saturating_sub(INTERP_DELAY_MS);
self.travelers
.values()
.filter_map(|t| {
let (pos, rot) = t.sample_at(render_t)?;
let model =
Mat4::from_rotation_translation(rot, pos + Vec3::new(0.0, foot_lift, 0.0));
let body = t
.avatar
.as_ref()
.map(|s| s.get(infinite_avatar::AvatarSlot::Body))
.unwrap_or(0);
let speed = t
.buffer
.last()
.map(|s| Vec3::new(s.vel.x, 0.0, s.vel.z).length())
.unwrap_or(0.0);
Some(TravelerPose {
id: t.id,
position: pos,
model,
color: t.color,
body,
speed,
})
})
.collect()
}
}
fn default_color(id: u32) -> [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::*;
fn s(t_ms: u64, pos: Vec3, vel: Vec3) -> PoseSample {
PoseSample {
t_ms,
pos,
rot: Quat::IDENTITY,
vel,
anim: 0,
}
}
#[test]
fn interpolates_between_poses() {
let mut t = Traveler::new(1, "x".into(), [1.0; 4]);
t.push(s(0, Vec3::ZERO, Vec3::ZERO));
t.push(s(100, Vec3::new(10.0, 0.0, 0.0), Vec3::ZERO));
let (p, _) = t.sample_at(50).unwrap();
assert!((p.x - 5.0).abs() < 1e-4, "midpoint lerp, got {}", p.x);
assert!((t.sample_at(0).unwrap().0.x - 0.0).abs() < 1e-4);
assert!((t.sample_at(100).unwrap().0.x - 10.0).abs() < 1e-4);
}
#[test]
fn extrapolates_along_velocity_then_freezes() {
let mut t = Traveler::new(1, "x".into(), [1.0; 4]);
t.push(s(0, Vec3::ZERO, Vec3::ZERO));
t.push(s(100, Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0)));
assert!((t.sample_at(150).unwrap().0.x - 0.5).abs() < 1e-4);
assert!((t.sample_at(5000).unwrap().0.x - 2.5).abs() < 1e-4);
}
#[test]
fn restyle_upgrades_the_placeholder_look() {
let mut p = PresenceState::new();
p.pose(5, s(0, Vec3::ZERO, Vec3::ZERO)); p.restyle(5, Some("Anders".into()), Some([0.1, 0.2, 0.3, 1.0]));
let t = p.travelers.get(&5).unwrap();
assert_eq!(t.name, "Anders");
assert_eq!(t.color, [0.1, 0.2, 0.3, 1.0]);
p.restyle(99, Some("ghost".into()), None);
p.restyle(5, None, Some([1.0; 4]));
assert_eq!(p.travelers.get(&5).unwrap().name, "Anders");
assert_eq!(p.travelers.get(&5).unwrap().color, [1.0; 4]);
}
#[test]
fn produces_one_entity_per_traveler() {
let mut p = PresenceState::new();
p.pose(1, s(1000, Vec3::new(3.0, 0.0, 0.0), Vec3::ZERO));
p.pose(2, s(1000, Vec3::new(-3.0, 0.0, 0.0), Vec3::ZERO));
assert_eq!(p.count(), 2);
let ents = p.entities(1100, 0.9);
assert_eq!(ents.len(), 2);
p.leave(1);
assert_eq!(p.entities(1100, 0.9).len(), 1);
}
}