Skip to main content

pylon_replication/
frame.rs

1//! The replication frame: what one subscriber receives per tick.
2//!
3//! Integers are LEB128 varints ([`crate::varint`]); signed values are
4//! zigzag varints. Positions travel quantized: `round(value / precision)`.
5//!
6//! ```text
7//! u8      version (1)
8//! u8      flags: bit 0 FULL: the client clears its table first
9//! f32 LE  precision
10//! varint  despawn count, then ids: the first absolute, each next as the
11//!         difference from the previous (ids ascend)
12//! varint  spawn count, then per entity (ids ascend, delta-coded):
13//!           id, x, y, z (zigzag, absolute quantized), components
14//! varint  update count, then per entity (ids ascend, delta-coded):
15//!           id, u8 mask (1 x, 2 y, 4 z, 8 components),
16//!           the changed axes (zigzag, quantized difference from the value
17//!           last sent), components when bit 8 is set
18//! components: varint count, then per component:
19//!           u8 id, varint (length + 1), bytes; length field 0 = removed
20//! ```
21//!
22//! A spawn carries every present component. An update carries the ones
23//! that changed or were removed since that subscriber was last updated.
24
25use std::collections::BTreeMap;
26
27use crate::varint;
28use crate::{ComponentId, EntityId};
29
30pub const VERSION: u8 = 1;
31pub const FLAG_FULL: u8 = 1;
32
33/// Bits of an update's mask byte.
34pub mod mask {
35    pub const X: u8 = 1;
36    pub const Y: u8 = 2;
37    pub const Z: u8 = 4;
38    pub const COMPONENTS: u8 = 8;
39}
40
41/// `value / precision`, rounded. Non-finite values quantize to 0.
42pub fn quantize(value: f32, precision: f32) -> i64 {
43    if !value.is_finite() {
44        return 0;
45    }
46    let q = (value as f64 / precision as f64).round();
47    q.clamp(i64::MIN as f64, i64::MAX as f64) as i64
48}
49
50pub fn quantize3(pos: [f32; 3], precision: f32) -> [i64; 3] {
51    pos.map(|v| quantize(v, precision))
52}
53
54/// A component change inside an update: new bytes, or removed.
55pub type ComponentChange<'a> = (ComponentId, Option<&'a [u8]>);
56
57/// Encode an update body (everything after the id): the mask, the changed
58/// axes, and the component changes. Returns the body.
59pub fn encode_update_body(delta: [i64; 3], components: &[ComponentChange<'_>]) -> Vec<u8> {
60    let mut body = Vec::with_capacity(8);
61    encode_update_body_into(&mut body, delta, components);
62    body
63}
64
65/// [`encode_update_body`], appended to `body`.
66pub fn encode_update_body_into(
67    body: &mut Vec<u8>,
68    delta: [i64; 3],
69    components: &[ComponentChange<'_>],
70) {
71    let mut m = 0u8;
72    for (i, bit) in [mask::X, mask::Y, mask::Z].into_iter().enumerate() {
73        if delta[i] != 0 {
74            m |= bit;
75        }
76    }
77    if !components.is_empty() {
78        m |= mask::COMPONENTS;
79    }
80    body.push(m);
81    for d in delta {
82        if d != 0 {
83            varint::write_i64(body, d);
84        }
85    }
86    if !components.is_empty() {
87        write_components(body, components.iter().copied());
88    }
89}
90
91fn write_components<'a>(
92    out: &mut Vec<u8>,
93    components: impl ExactSizeIterator<Item = ComponentChange<'a>>,
94) {
95    varint::write_u64(out, components.len() as u64);
96    for (id, bytes) in components {
97        out.push(id);
98        match bytes {
99            Some(b) => {
100                varint::write_u64(out, b.len() as u64 + 1);
101                out.extend_from_slice(b);
102            }
103            None => varint::write_u64(out, 0),
104        }
105    }
106}
107
108/// Builds one frame. Add despawns in any order; add spawns and updates in
109/// ascending id order.
110#[derive(Debug)]
111pub struct FrameBuilder {
112    full: bool,
113    precision: f32,
114    despawns: Vec<EntityId>,
115    spawns: Vec<u8>,
116    spawn_count: u64,
117    last_spawn: Option<EntityId>,
118    updates: Vec<u8>,
119    update_count: u64,
120    last_update: Option<EntityId>,
121}
122
123impl FrameBuilder {
124    pub fn new(full: bool, precision: f32) -> Self {
125        Self {
126            full,
127            precision,
128            despawns: Vec::new(),
129            spawns: Vec::new(),
130            spawn_count: 0,
131            last_spawn: None,
132            updates: Vec::new(),
133            update_count: 0,
134            last_update: None,
135        }
136    }
137
138    pub fn despawn(&mut self, id: EntityId) {
139        self.despawns.push(id);
140    }
141
142    /// Add a spawn. Ids must ascend across calls.
143    pub fn spawn<'a>(
144        &mut self,
145        id: EntityId,
146        q: [i64; 3],
147        components: impl ExactSizeIterator<Item = (ComponentId, &'a [u8])>,
148    ) {
149        write_delta_id(&mut self.spawns, &mut self.last_spawn, id);
150        for v in q {
151            varint::write_i64(&mut self.spawns, v);
152        }
153        write_components(&mut self.spawns, components.map(|(c, b)| (c, Some(b))));
154        self.spawn_count += 1;
155    }
156
157    /// Add an update with a body from [`encode_update_body`]. Ids must
158    /// ascend across calls.
159    pub fn update(&mut self, id: EntityId, body: &[u8]) {
160        write_delta_id(&mut self.updates, &mut self.last_update, id);
161        self.updates.extend_from_slice(body);
162        self.update_count += 1;
163    }
164
165    pub fn is_empty(&self) -> bool {
166        !self.full && self.despawns.is_empty() && self.spawn_count == 0 && self.update_count == 0
167    }
168
169    /// The frame bytes.
170    pub fn finish(mut self) -> Vec<u8> {
171        self.despawns.sort_unstable();
172        self.despawns.dedup();
173        let mut out = Vec::with_capacity(16 + self.spawns.len() + self.updates.len());
174        out.push(VERSION);
175        out.push(if self.full { FLAG_FULL } else { 0 });
176        out.extend_from_slice(&self.precision.to_le_bytes());
177        varint::write_u64(&mut out, self.despawns.len() as u64);
178        let mut last = None;
179        for id in &self.despawns {
180            write_delta_id(&mut out, &mut last, *id);
181        }
182        varint::write_u64(&mut out, self.spawn_count);
183        out.extend_from_slice(&self.spawns);
184        varint::write_u64(&mut out, self.update_count);
185        out.extend_from_slice(&self.updates);
186        out
187    }
188}
189
190fn write_delta_id(out: &mut Vec<u8>, last: &mut Option<EntityId>, id: EntityId) {
191    match *last {
192        Some(prev) => {
193            debug_assert!(id > prev, "ids must ascend");
194            varint::write_u64(out, id.wrapping_sub(prev));
195        }
196        None => varint::write_u64(out, id),
197    }
198    *last = Some(id);
199}
200
201// ---------------------------------------------------------------------------
202// Decoding
203// ---------------------------------------------------------------------------
204
205/// Why a frame did not decode or apply.
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct DecodeError(pub String);
208
209impl std::fmt::Display for DecodeError {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        f.write_str(&self.0)
212    }
213}
214
215impl std::error::Error for DecodeError {}
216
217/// An entity as a client sees it.
218#[derive(Debug, Clone, Default, PartialEq)]
219pub struct ReplicaEntity {
220    /// The quantized position; deltas apply to it exactly.
221    pub q: [i64; 3],
222    pub components: BTreeMap<ComponentId, Vec<u8>>,
223}
224
225impl ReplicaEntity {
226    pub fn pos(&self, precision: f32) -> [f32; 3] {
227        self.q.map(|v| (v as f64 * precision as f64) as f32)
228    }
229}
230
231/// What one frame did.
232#[derive(Debug, Clone, Default, PartialEq, Eq)]
233pub struct FrameSummary {
234    pub full: bool,
235    pub spawned: Vec<EntityId>,
236    pub updated: Vec<EntityId>,
237    pub despawned: Vec<EntityId>,
238}
239
240/// A client's table of the entities it has been told about.
241#[derive(Debug, Clone, Default, PartialEq)]
242pub struct ReplicaTable {
243    pub entities: BTreeMap<EntityId, ReplicaEntity>,
244    pub precision: f32,
245    /// The tick of the stream frame that spawned each entity: a datagram
246    /// update names it (see [`crate::datagram`]). Set only by
247    /// [`ReplicaTable::apply_stream`].
248    pub spawn_ticks: BTreeMap<EntityId, u64>,
249    /// The last datagram applied to each entity (see [`crate::datagram`]).
250    pub datagram_frames: BTreeMap<EntityId, u64>,
251    /// The tick of the last stream frame applied with
252    /// [`ReplicaTable::apply_stream`]. A client on an unreliable transport
253    /// acks each datagram with it, so the server knows which spawns it had.
254    pub stream_tick: u64,
255}
256
257/// Longest id list a frame may declare, so a hostile count cannot make a
258/// decoder allocate without bound.
259const MAX_COUNT: u64 = 1 << 24;
260
261impl ReplicaTable {
262    pub fn new() -> Self {
263        Self::default()
264    }
265
266    pub fn pos(&self, id: EntityId) -> Option<[f32; 3]> {
267        self.entities.get(&id).map(|e| e.pos(self.precision))
268    }
269
270    /// Apply one frame. On an error the table may hold part of the frame;
271    /// the caller should treat the connection as broken.
272    pub fn apply(&mut self, frame: &[u8]) -> Result<FrameSummary, DecodeError> {
273        self.apply_inner(frame, None)
274    }
275
276    /// Apply one frame from the stream of a subscription that also gets
277    /// datagrams, with the tick its header carries. The table records the
278    /// tick each entity spawned at and the tick of the last stream frame,
279    /// which datagrams and their acks name. A full frame starts that record
280    /// over, so frames the server dropped before the client got them, or an
281    /// earlier connection, leave nothing behind.
282    pub fn apply_stream(&mut self, frame: &[u8], tick: u64) -> Result<FrameSummary, DecodeError> {
283        self.apply_inner(frame, Some(tick))
284    }
285
286    fn apply_inner(
287        &mut self,
288        frame: &[u8],
289        tick: Option<u64>,
290    ) -> Result<FrameSummary, DecodeError> {
291        let err = |m: &str| DecodeError(m.to_string());
292        let mut b = frame;
293        let take = |b: &mut &[u8], n: usize| -> Result<Vec<u8>, DecodeError> {
294            if b.len() < n {
295                return Err(DecodeError("frame ends early".into()));
296            }
297            let (head, rest) = b.split_at(n);
298            *b = rest;
299            Ok(head.to_vec())
300        };
301        let head = take(&mut b, 6)?;
302        if head[0] != VERSION {
303            return Err(DecodeError(format!("replication version {}", head[0])));
304        }
305        let full = head[1] & FLAG_FULL != 0;
306        let precision = f32::from_le_bytes(head[2..6].try_into().unwrap());
307        if !(precision.is_finite() && precision > 0.0) {
308            return Err(err("bad precision"));
309        }
310        if full {
311            self.entities.clear();
312            self.datagram_frames.clear();
313            self.spawn_ticks.clear();
314        }
315        self.precision = precision;
316        let mut summary = FrameSummary {
317            full,
318            ..FrameSummary::default()
319        };
320        let count = |b: &mut &[u8]| -> Result<u64, DecodeError> {
321            let n = varint::read_u64(b).ok_or_else(|| err("bad count"))?;
322            if n > MAX_COUNT {
323                return Err(err("count too large"));
324            }
325            Ok(n)
326        };
327        let next_id =
328            |b: &mut &[u8], last: &mut Option<EntityId>| -> Result<EntityId, DecodeError> {
329                let v = varint::read_u64(b).ok_or_else(|| err("bad id"))?;
330                let id = match *last {
331                    Some(prev) => prev
332                        .checked_add(v)
333                        .filter(|_| v > 0)
334                        .ok_or_else(|| err("ids do not ascend"))?,
335                    None => v,
336                };
337                *last = Some(id);
338                Ok(id)
339            };
340
341        let n = count(&mut b)?;
342        let mut last = None;
343        for _ in 0..n {
344            let id = next_id(&mut b, &mut last)?;
345            self.entities.remove(&id);
346            self.datagram_frames.remove(&id);
347            self.spawn_ticks.remove(&id);
348            summary.despawned.push(id);
349        }
350
351        let n = count(&mut b)?;
352        let mut last = None;
353        for _ in 0..n {
354            let id = next_id(&mut b, &mut last)?;
355            let mut q = [0i64; 3];
356            for v in &mut q {
357                *v = varint::read_i64(&mut b).ok_or_else(|| err("bad position"))?;
358            }
359            let mut entity = ReplicaEntity {
360                q,
361                components: BTreeMap::new(),
362            };
363            read_components(&mut b, &mut entity.components)?;
364            self.entities.insert(id, entity);
365            self.datagram_frames.remove(&id);
366            match tick {
367                Some(t) => self.spawn_ticks.insert(id, t),
368                None => self.spawn_ticks.remove(&id),
369            };
370            summary.spawned.push(id);
371        }
372
373        let n = count(&mut b)?;
374        let mut last = None;
375        for _ in 0..n {
376            let id = next_id(&mut b, &mut last)?;
377            let (&m, rest) = b.split_first().ok_or_else(|| err("frame ends early"))?;
378            b = rest;
379            let entity = self
380                .entities
381                .get_mut(&id)
382                .ok_or_else(|| DecodeError(format!("update for unknown entity {id}")))?;
383            for (i, bit) in [mask::X, mask::Y, mask::Z].into_iter().enumerate() {
384                if m & bit != 0 {
385                    let d = varint::read_i64(&mut b).ok_or_else(|| err("bad delta"))?;
386                    entity.q[i] = entity.q[i].wrapping_add(d);
387                }
388            }
389            if m & mask::COMPONENTS != 0 {
390                read_components(&mut b, &mut entity.components)?;
391            }
392            summary.updated.push(id);
393        }
394        if !b.is_empty() {
395            return Err(err("trailing bytes"));
396        }
397        if let Some(t) = tick {
398            self.stream_tick = t;
399        }
400        Ok(summary)
401    }
402}
403
404fn read_components(
405    b: &mut &[u8],
406    into: &mut BTreeMap<ComponentId, Vec<u8>>,
407) -> Result<(), DecodeError> {
408    let err = |m: &str| DecodeError(m.to_string());
409    let n = varint::read_u64(b).ok_or_else(|| err("bad component count"))?;
410    if n > 256 {
411        return Err(err("more than 256 components"));
412    }
413    for _ in 0..n {
414        let (&id, rest) = b.split_first().ok_or_else(|| err("frame ends early"))?;
415        *b = rest;
416        let len = varint::read_u64(b).ok_or_else(|| err("bad component length"))?;
417        if len == 0 {
418            into.remove(&id);
419            continue;
420        }
421        let len = usize::try_from(len - 1).map_err(|_| err("component too large"))?;
422        if b.len() < len {
423            return Err(err("component runs past the end"));
424        }
425        let (value, rest) = b.split_at(len);
426        *b = rest;
427        into.insert(id, value.to_vec());
428    }
429    Ok(())
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn spawn_update_despawn_round_trip() {
438        let p = 0.1;
439        let mut f = FrameBuilder::new(true, p);
440        f.spawn(
441            3,
442            quantize3([1.0, 2.0, 0.0], p),
443            [(1u8, &b"hp"[..])].into_iter(),
444        );
445        f.spawn(10, quantize3([-5.0, 0.0, 0.0], p), std::iter::empty());
446        let mut t = ReplicaTable::new();
447        let s = t.apply(&f.finish()).unwrap();
448        assert!(s.full);
449        assert_eq!(s.spawned, vec![3, 10]);
450        assert_eq!(t.pos(3), Some([1.0, 2.0, 0.0]));
451        assert_eq!(t.entities[&3].components[&1], b"hp");
452
453        let mut f = FrameBuilder::new(false, p);
454        f.update(
455            3,
456            &encode_update_body([5, 0, 0], &[(1, None), (2, Some(b"new"))]),
457        );
458        f.update(10, &encode_update_body([0, -20, 0], &[]));
459        f.despawn(99);
460        let s = t.apply(&f.finish()).unwrap();
461        assert_eq!(s.updated, vec![3, 10]);
462        let pos3 = t.pos(3).unwrap();
463        assert!((pos3[0] - 1.5).abs() < 1e-6);
464        assert!(!t.entities[&3].components.contains_key(&1));
465        assert_eq!(t.entities[&3].components[&2], b"new");
466        assert_eq!(t.pos(10), Some([-5.0, -2.0, 0.0]));
467
468        let mut f = FrameBuilder::new(false, p);
469        f.despawn(10);
470        t.apply(&f.finish()).unwrap();
471        assert!(!t.entities.contains_key(&10));
472    }
473
474    #[test]
475    fn an_unchanged_entity_update_is_two_bytes_plus_the_id() {
476        let body = encode_update_body([3, -2, 0], &[]);
477        assert_eq!(body.len(), 3); // mask + two one-byte deltas
478    }
479
480    #[test]
481    fn hostile_frames_are_refused_without_panicking() {
482        let mut t = ReplicaTable::new();
483        assert!(t.apply(&[]).is_err());
484        assert!(t.apply(&[2, 0, 0, 0, 0x80, 0x3f]).is_err()); // version 2
485        assert!(t.apply(&[1, 0, 0, 0, 0, 0]).is_err()); // precision 0
486        let head = |extra: &[u8]| {
487            let mut v = vec![1, 0];
488            v.extend_from_slice(&1.0f32.to_le_bytes());
489            v.extend_from_slice(extra);
490            v
491        };
492        // A huge declared count.
493        assert!(t.apply(&head(&[0xff, 0xff, 0xff, 0xff, 0x0f])).is_err());
494        // An update for an entity the table never saw.
495        assert!(t.apply(&head(&[0, 0, 1, 7, 0])).is_err());
496        // Ids that do not ascend.
497        assert!(t.apply(&head(&[2, 5, 0, 0, 0])).is_err());
498        // Trailing bytes.
499        assert!(t.apply(&head(&[0, 0, 0, 9])).is_err());
500        // A component that runs past the end.
501        assert!(t.apply(&head(&[0, 1, 1, 0, 0, 0, 1, 4, 50, 1])).is_err());
502    }
503
504    #[test]
505    fn quantize_handles_bad_values() {
506        assert_eq!(quantize(f32::NAN, 0.1), 0);
507        assert_eq!(quantize(f32::INFINITY, 0.1), 0);
508        assert_eq!(quantize(f32::MAX, 1e-30), i64::MAX);
509        assert_eq!(quantize(-0.04, 0.1), 0);
510        assert_eq!(quantize(0.26, 0.1), 3);
511    }
512}