Skip to main content

box3d_rust/recording/dispatch/
mod.rs

1//! Op-stream dispatcher for recording replay.
2//!
3//! SPDX-FileCopyrightText: 2026 Erin Catto
4//! SPDX-License-Identifier: MIT
5
6use crate::id::{BodyId, JointId, ShapeId};
7use crate::recording::buffer::SnapReader;
8use crate::recording::ops::RecOp;
9use crate::recording::registry::RegistrySlot;
10use crate::recording::session::RecTag;
11use crate::world::World;
12
13use super::player::RecPlayer;
14
15/// Reader state threaded through the replay loop. (b3RecReader)
16pub struct RecReader<'a> {
17    pub data: &'a [u8],
18    pub size: i32,
19    pub cursor: i32,
20    pub ok: bool,
21    pub diverged: bool,
22    pub world: *mut World,
23    pub owner: Option<*mut RecPlayer>,
24    pub slots: Vec<RegistrySlot>,
25    pub tags: Vec<RecTag>,
26    pub pending_query_key: u64,
27    pub pending_body_create: Option<BodyId>,
28    pub pending_body_destroy: Option<BodyId>,
29}
30
31impl<'a> RecReader<'a> {
32    pub fn new(data: &'a [u8]) -> Self {
33        Self {
34            data,
35            size: data.len() as i32,
36            cursor: 0,
37            ok: true,
38            diverged: false,
39            world: std::ptr::null_mut(),
40            owner: None,
41            slots: Vec::new(),
42            tags: Vec::new(),
43            pending_query_key: 0,
44            pending_body_create: None,
45            pending_body_destroy: None,
46        }
47    }
48
49    pub fn snap(&mut self) -> SnapReader<'a> {
50        let mut r = SnapReader::new(self.data);
51        r.set_cursor(self.cursor as usize);
52        r.ok = self.ok;
53        r
54    }
55
56    pub fn sync_from(&mut self, r: &SnapReader<'_>) {
57        self.cursor = r.cursor() as i32;
58        self.ok = r.ok;
59    }
60
61    pub fn make_body_id(&self, recorded: BodyId) -> BodyId {
62        let world = unsafe { &*self.world };
63        BodyId {
64            index1: recorded.index1,
65            world0: world.world_id,
66            generation: recorded.generation,
67        }
68    }
69
70    pub fn make_shape_id(&self, recorded: ShapeId) -> ShapeId {
71        let world = unsafe { &*self.world };
72        ShapeId {
73            index1: recorded.index1,
74            world0: world.world_id,
75            generation: recorded.generation,
76        }
77    }
78
79    pub fn make_joint_id(&self, recorded: JointId) -> JointId {
80        let world = unsafe { &*self.world };
81        JointId {
82            index1: recorded.index1,
83            world0: world.world_id,
84            generation: recorded.generation,
85        }
86    }
87
88    pub fn check_id(
89        ok: &mut bool,
90        kind: &str,
91        got_index: i32,
92        got_gen: u16,
93        rec_index: i32,
94        rec_gen: u16,
95    ) {
96        if got_index != rec_index || got_gen != rec_gen {
97            eprintln!(
98                "b3ReplayFile: {kind} id mismatch (rec index1={rec_index} gen={rec_gen}, got index1={got_index} gen={got_gen})"
99            );
100            *ok = false;
101        }
102    }
103}
104
105mod body_ops;
106mod joint_angular_type_ops;
107mod joint_linear_type_ops;
108mod joint_ops;
109mod query_ops;
110mod shape_ops;
111mod world_ops;
112
113/// Dispatch one framed op. Returns opcode as i32, or -1 when exhausted/broken.
114pub fn dispatch_one(rdr: &mut RecReader<'_>) -> i32 {
115    if rdr.cursor >= rdr.size || !rdr.ok {
116        return -1;
117    }
118    let mut snap = rdr.snap();
119    let opcode = snap.u8();
120    let payload_size = snap.u24();
121    rdr.sync_from(&snap);
122    if !rdr.ok {
123        return -1;
124    }
125    let payload_start = rdr.cursor;
126
127    let Some(op) = RecOp::from_u8(opcode) else {
128        eprintln!("b3ReplayFile: unknown opcode 0x{opcode:02X}, skipping {payload_size} bytes");
129        if payload_size > (rdr.size - payload_start) as u32 {
130            rdr.ok = false;
131        } else {
132            rdr.cursor = payload_start + payload_size as i32;
133        }
134        return opcode as i32;
135    };
136
137    let handled = world_ops::dispatch(op, rdr, payload_start, payload_size)
138        || body_ops::dispatch(op, rdr, payload_start, payload_size)
139        || shape_ops::dispatch(op, rdr, payload_start, payload_size)
140        || joint_ops::dispatch(op, rdr, payload_start, payload_size)
141        || joint_linear_type_ops::dispatch(op, rdr, payload_start, payload_size)
142        || joint_angular_type_ops::dispatch(op, rdr, payload_start, payload_size)
143        || query_ops::dispatch(op, rdr, payload_start, payload_size);
144    debug_assert!(handled, "op {op:?} not claimed by any dispatch family");
145
146    opcode as i32
147}