1use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::mpsc::{channel, Receiver};
13use std::sync::Arc;
14
15use glam::{Quat, Vec3};
16use crate::traveler::{PoseSample, PresenceState};
17use serde::{Deserialize, Serialize};
18
19#[derive(Serialize)]
21#[serde(tag = "t")]
22enum ClientMsg {
23 #[serde(rename = "join")]
24 Join {
25 passport: String,
26 spawn: Option<String>,
27 },
28 #[serde(rename = "pose")]
29 Pose {
30 ts: u64,
31 p: [f32; 3],
32 r: [f32; 4],
33 v: [f32; 3],
34 a: u8,
35 },
36}
37
38#[derive(Deserialize)]
40#[serde(tag = "t")]
41enum ServerMsg {
42 #[serde(rename = "welcome")]
43 Welcome {
44 #[serde(default)]
45 occupants: Vec<Occupant>,
46 },
47 #[serde(rename = "join")]
49 Join {
50 id: u32,
51 #[serde(default)]
52 sub: Option<String>,
53 #[serde(default)]
54 name: Option<String>,
55 #[serde(default)]
56 avatar: Option<String>,
57 },
58 #[serde(rename = "pose")]
59 Pose {
60 id: u32,
61 p: [f32; 3],
62 #[serde(default = "ident_quat")]
63 r: [f32; 4],
64 #[serde(default)]
65 v: [f32; 3],
66 #[serde(default)]
67 a: u8,
68 },
69 #[serde(rename = "leave")]
70 Leave { id: u32 },
71 #[serde(other)]
73 Other,
74}
75
76#[derive(Deserialize)]
77struct Occupant {
78 id: u32,
79 #[serde(default)]
82 sub: Option<String>,
83 #[serde(default)]
84 name: Option<String>,
85 #[serde(default)]
87 avatar: Option<String>,
88}
89
90fn ident_quat() -> [f32; 4] {
91 [0.0, 0.0, 0.0, 1.0]
92}
93
94#[derive(Debug, Clone)]
98pub struct TravelerHello {
99 pub id: u32,
100 pub sub: Option<String>,
101 pub name: Option<String>,
102 pub avatar: Option<String>,
104}
105
106enum Outgoing {
108 Msg(ClientMsg),
109 Voice(Vec<u8>),
110}
111
112enum Event {
114 Join(TravelerHello),
115 Pose {
116 id: u32,
117 pos: Vec3,
118 rot: Quat,
119 vel: Vec3,
120 anim: u8,
121 },
122 Leave(u32),
123}
124
125pub struct PresenceClient {
127 events: Receiver<Event>,
128 poses: tokio::sync::mpsc::Sender<Outgoing>,
129 voice: Receiver<(u32, Vec<u8>)>,
131 connected: Arc<AtomicBool>,
132}
133
134impl PresenceClient {
135 pub fn connect(
137 relay: String,
138 world_id: String,
139 passport: String,
140 spawn: Option<String>,
141 ) -> Self {
142 Self::connect_any(vec![relay], world_id, passport, spawn)
143 }
144
145 pub fn connect_any(
149 relays: Vec<String>,
150 world_id: String,
151 passport: String,
152 spawn: Option<String>,
153 ) -> Self {
154 let urls: Vec<String> = relays
155 .into_iter()
156 .map(|relay| {
157 if relay.contains("/thread/") {
158 relay
159 } else {
160 format!("{}/thread/{}", relay.trim_end_matches('/'), world_id)
161 }
162 })
163 .collect();
164 let (evt_tx, evt_rx) = channel();
165 let (voice_tx, voice_rx) = channel();
166 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::channel::<Outgoing>(256);
167 let connected = Arc::new(AtomicBool::new(false));
168 let conn2 = connected.clone();
169 std::thread::spawn(move || {
170 let rt = match tokio::runtime::Builder::new_current_thread()
171 .enable_all()
172 .build()
173 {
174 Ok(rt) => rt,
175 Err(e) => {
176 tracing::warn!("presence: runtime init failed: {e}");
177 return;
178 }
179 };
180 rt.block_on(run(urls, passport, spawn, evt_tx, voice_tx, cmd_rx, conn2));
181 });
182 Self {
183 events: evt_rx,
184 poses: cmd_tx,
185 voice: voice_rx,
186 connected,
187 }
188 }
189
190 pub fn is_connected(&self) -> bool {
191 self.connected.load(Ordering::Relaxed)
192 }
193
194 pub fn drain(&self, presence: &mut PresenceState, now_ms: u64) -> Vec<TravelerHello> {
199 let mut hellos = Vec::new();
200 while let Ok(evt) = self.events.try_recv() {
201 match evt {
202 Event::Join(hello) => {
203 let name = hello
204 .name
205 .clone()
206 .or_else(|| hello.sub.clone())
207 .unwrap_or_else(|| format!("traveler {}", hello.id));
208 presence.join(hello.id, name, traveler_color(hello.id));
209 hellos.push(hello);
210 }
211 Event::Leave(id) => presence.leave(id),
212 Event::Pose {
213 id,
214 pos,
215 rot,
216 vel,
217 anim,
218 } => presence.pose(
219 id,
220 PoseSample {
221 t_ms: now_ms,
222 pos,
223 rot,
224 vel,
225 anim,
226 },
227 ),
228 }
229 }
230 hellos
231 }
232
233 pub fn send_pose(&self, pos: Vec3, rot: Quat, vel: Vec3, anim: u8) {
235 let _ = self.poses.try_send(Outgoing::Msg(ClientMsg::Pose {
236 ts: 0,
237 p: pos.to_array(),
238 r: [rot.x, rot.y, rot.z, rot.w],
239 v: vel.to_array(),
240 a: anim,
241 }));
242 }
243
244 pub fn send_voice(&self, frame: Vec<u8>) {
247 let _ = self.poses.try_send(Outgoing::Voice(frame));
248 }
249
250 pub fn drain_voice(&self) -> Vec<(u32, Vec<u8>)> {
252 let mut out = Vec::new();
253 while let Ok(f) = self.voice.try_recv() {
254 out.push(f);
255 }
256 out
257 }
258}
259
260fn traveler_color(id: u32) -> [f32; 4] {
262 let h = id.wrapping_mul(2654435761);
263 [
264 0.4 + ((h & 0xFF) as f32 / 255.0) * 0.5,
265 0.4 + (((h >> 8) & 0xFF) as f32 / 255.0) * 0.5,
266 0.4 + (((h >> 16) & 0xFF) as f32 / 255.0) * 0.5,
267 1.0,
268 ]
269}
270
271async fn run(
274 urls: Vec<String>,
275 passport: String,
276 spawn: Option<String>,
277 events: std::sync::mpsc::Sender<Event>,
278 voice: std::sync::mpsc::Sender<(u32, Vec<u8>)>,
279 mut poses: tokio::sync::mpsc::Receiver<Outgoing>,
280 connected: Arc<AtomicBool>,
281) {
282 use futures_util::{SinkExt, StreamExt};
283 use tokio_tungstenite::tungstenite::Message;
284
285 let mut connection = None;
287 for url in &urls {
288 match tokio_tungstenite::connect_async(url).await {
289 Ok((ws, _)) => {
290 connection = Some((ws, url.clone()));
291 break;
292 }
293 Err(e) => tracing::warn!("presence: relay {url} unreachable: {e}"),
294 }
295 }
296 let (ws, url) = match connection {
297 Some(c) => c,
298 None => {
299 tracing::warn!(
300 "presence: no relay reachable ({} tried) — staying solo",
301 urls.len()
302 );
303 return;
304 }
305 };
306 tracing::info!("presence: connected to {url}");
307 connected.store(true, Ordering::Relaxed);
308 let (mut write, mut read) = ws.split();
309
310 if let Ok(join) = serde_json::to_string(&ClientMsg::Join { passport, spawn }) {
312 let _ = write.send(Message::Text(join)).await;
313 }
314
315 let mut keepalive = tokio::time::interval(std::time::Duration::from_secs(10));
319 keepalive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
320
321 loop {
322 tokio::select! {
323 _ = keepalive.tick() => {
324 if write.send(Message::Ping(Vec::new())).await.is_err() {
325 break;
326 }
327 }
328 incoming = read.next() => match incoming {
329 Some(Ok(Message::Binary(b))) => {
330 if b.len() > 4 {
332 let id = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
333 let _ = voice.send((id, b[4..].to_vec()));
334 }
335 }
336 Some(Ok(msg)) if msg.is_text() => {
337 if let Ok(txt) = msg.to_text() {
338 forward(txt, &events);
339 }
340 }
341 Some(Ok(msg)) if msg.is_close() => break,
342 Some(Err(_)) | None => break,
343 _ => {}
344 },
345 outgoing = poses.recv() => match outgoing {
346 Some(Outgoing::Msg(msg)) => {
347 if let Ok(txt) = serde_json::to_string(&msg) {
348 if write.send(Message::Text(txt)).await.is_err() {
349 break;
350 }
351 }
352 }
353 Some(Outgoing::Voice(frame)) => {
354 if write.send(Message::Binary(frame)).await.is_err() {
355 break;
356 }
357 }
358 None => break,
359 }
360 }
361 }
362 connected.store(false, Ordering::Relaxed);
363 tracing::info!("presence: disconnected from {url}");
364}
365
366fn forward(txt: &str, events: &std::sync::mpsc::Sender<Event>) {
368 let Ok(msg) = serde_json::from_str::<ServerMsg>(txt) else {
369 return;
370 };
371 match msg {
372 ServerMsg::Welcome { occupants } => {
373 tracing::info!(
374 "presence: welcomed — {} traveler(s) already here",
375 occupants.len()
376 );
377 for o in occupants {
378 let _ = events.send(Event::Join(TravelerHello {
379 id: o.id,
380 sub: o.sub,
381 name: o.name,
382 avatar: o.avatar,
383 }));
384 }
385 }
386 ServerMsg::Join {
387 id,
388 sub,
389 name,
390 avatar,
391 } => {
392 tracing::info!(
393 "presence: traveler joined — id {id}{}",
394 name.as_deref()
395 .map(|n| format!(" ({n})"))
396 .unwrap_or_default()
397 );
398 let _ = events.send(Event::Join(TravelerHello {
399 id,
400 sub,
401 name,
402 avatar,
403 }));
404 }
405 ServerMsg::Pose { id, p, r, v, a } => {
406 let _ = events.send(Event::Pose {
407 id,
408 pos: Vec3::from_array(p),
409 rot: Quat::from_xyzw(r[0], r[1], r[2], r[3]),
410 vel: Vec3::from_array(v),
411 anim: a,
412 });
413 }
414 ServerMsg::Leave { id } => {
415 let _ = events.send(Event::Leave(id));
416 }
417 ServerMsg::Other => {}
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn encodes_pose_per_wire_spec() {
427 let json = serde_json::to_string(&ClientMsg::Pose {
428 ts: 12,
429 p: [1.0, 2.0, 3.0],
430 r: [0.0, 0.0, 0.0, 1.0],
431 v: [0.5, 0.0, 0.0],
432 a: 1,
433 })
434 .unwrap();
435 assert!(json.contains(r#""t":"pose""#));
436 assert!(json.contains(r#""p":[1.0,2.0,3.0]"#));
437 }
438
439 #[test]
440 fn decodes_server_messages_and_forwards_events() {
441 let (tx, rx) = channel();
442 forward(r#"{"t":"welcome","occupants":[{"id":7},{"id":9}]}"#, &tx);
443 forward(
444 r#"{"t":"pose","id":7,"p":[3.0,0.0,-1.0],"r":[0,0,0,1],"v":[1,0,0],"a":2}"#,
445 &tx,
446 );
447 forward(r#"{"t":"leave","id":9}"#, &tx);
448 forward(r#"{"t":"voice","id":7}"#, &tx); let mut presence = PresenceState::new();
451 let mut joins = 0;
452 let mut poses = 0;
453 let mut leaves = 0;
454 while let Ok(evt) = rx.try_recv() {
455 match evt {
456 Event::Join(_) => joins += 1,
457 Event::Pose { .. } => poses += 1,
458 Event::Leave(_) => leaves += 1,
459 }
460 }
461 assert_eq!((joins, poses, leaves), (2, 1, 1));
462 let _ = &mut presence; }
464
465 #[test]
466 fn identity_rides_welcome_occupants_and_join_broadcasts() {
467 let (tx, rx) = channel();
468 forward(
470 r#"{"t":"welcome","occupants":[{"id":7,"sub":"did:pixygon:abc","name":"Anders","avatar":"https://api.pixygon.io/v1/passport/avatar/did:pixygon:abc"}]}"#,
471 &tx,
472 );
473 forward(r#"{"t":"join","id":11,"name":"Kaya"}"#, &tx);
474
475 let Ok(Event::Join(a)) = rx.try_recv() else {
476 panic!("expected a join")
477 };
478 assert_eq!(a.id, 7);
479 assert_eq!(a.sub.as_deref(), Some("did:pixygon:abc"));
480 assert_eq!(a.name.as_deref(), Some("Anders"));
481 assert!(a.avatar.as_deref().unwrap().ends_with("did:pixygon:abc"));
482
483 let Ok(Event::Join(b)) = rx.try_recv() else {
484 panic!("expected a join")
485 };
486 assert_eq!(
487 (b.id, b.name.as_deref(), b.sub, b.avatar),
488 (11, Some("Kaya"), None, None)
489 );
490 }
491}