Skip to main content

arcane_engine/scripting/
replay_ops.rs

1//! Replay ops: serialize/deserialize physics state for snapshot-replay testing.
2//!
3//! Provides `#[op2]` ops that serialize the physics world state to a flat f64 array
4//! and restore it. This enables deterministic replay of physics simulations.
5
6use std::cell::RefCell;
7use std::rc::Rc;
8
9use deno_core::OpState;
10
11use crate::physics::types::*;
12use super::physics_ops::PhysicsState;
13
14/// Serialize the entire physics world state as a flat f64 array.
15///
16/// Layout per body:
17///   [id, body_type, shape_type, shape_p1, shape_p2,
18///    x, y, angle, vx, vy, angular_velocity,
19///    mass, restitution, friction, layer, mask]
20///   = 16 doubles per body
21///
22/// Header: [body_count, gravity_x, gravity_y]
23#[deno_core::op2]
24#[serde]
25fn op_serialize_physics_state(state: &mut OpState) -> Vec<f64> {
26    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
27    let ps = physics.borrow();
28    let world = match ps.0.as_ref() {
29        Some(w) => w,
30        None => return vec![],
31    };
32
33    let mut result: Vec<f64> = Vec::new();
34
35    // Collect active bodies
36    let bodies = world.all_bodies();
37    let gravity = world.gravity();
38
39    // Header
40    result.push(bodies.len() as f64);
41    result.push(gravity.0 as f64);
42    result.push(gravity.1 as f64);
43
44    // Body data
45    for body in &bodies {
46        result.push(body.id as f64);
47        result.push(match body.body_type {
48            BodyType::Static => 0.0,
49            BodyType::Dynamic => 1.0,
50            BodyType::Kinematic => 2.0,
51        });
52        match &body.shape {
53            Shape::Circle { radius } => {
54                result.push(0.0); // shape_type
55                result.push(*radius as f64); // shape_p1
56                result.push(0.0); // shape_p2
57            }
58            Shape::AABB { half_w, half_h } => {
59                result.push(1.0); // shape_type
60                result.push(*half_w as f64); // shape_p1
61                result.push(*half_h as f64); // shape_p2
62            }
63            Shape::Polygon { .. } => {
64                result.push(2.0); // shape_type
65                result.push(0.0);
66                result.push(0.0);
67            }
68        }
69        result.push(body.x as f64);
70        result.push(body.y as f64);
71        result.push(body.angle as f64);
72        result.push(body.vx as f64);
73        result.push(body.vy as f64);
74        result.push(body.angular_velocity as f64);
75        result.push(body.mass as f64);
76        result.push(body.material.restitution as f64);
77        result.push(body.material.friction as f64);
78        result.push(body.layer as f64);
79        result.push(body.mask as f64);
80    }
81
82    result
83}
84
85/// Restore physics world state from a serialized f64 array.
86///
87/// Destroys the existing world and recreates it from the serialized data.
88/// Uses the same layout as op_serialize_physics_state.
89#[deno_core::op2]
90fn op_restore_physics_state(state: &mut OpState, #[serde] data: Vec<f64>) {
91    if data.len() < 3 {
92        return;
93    }
94
95    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
96    let mut ps = physics.borrow_mut();
97
98    let body_count = data[0] as usize;
99    let gravity_x = data[1] as f32;
100    let gravity_y = data[2] as f32;
101
102    // Create a new world
103    let mut world = crate::physics::world::PhysicsWorld::new(gravity_x, gravity_y);
104
105    // Reconstruct bodies
106    let mut offset = 3;
107    for _ in 0..body_count {
108        if offset + 16 > data.len() {
109            break;
110        }
111
112        let _id = data[offset] as u32;
113        let body_type = match data[offset + 1] as u32 {
114            0 => BodyType::Static,
115            1 => BodyType::Dynamic,
116            2 => BodyType::Kinematic,
117            _ => BodyType::Dynamic,
118        };
119        let shape_type = data[offset + 2] as u32;
120        let shape_p1 = data[offset + 3] as f32;
121        let shape_p2 = data[offset + 4] as f32;
122        let x = data[offset + 5] as f32;
123        let y = data[offset + 6] as f32;
124        let _angle = data[offset + 7] as f32;
125        let vx = data[offset + 8] as f32;
126        let vy = data[offset + 9] as f32;
127        let _angular_velocity = data[offset + 10] as f32;
128        let mass = data[offset + 11] as f32;
129        let restitution = data[offset + 12] as f32;
130        let friction = data[offset + 13] as f32;
131        let layer = data[offset + 14] as u16;
132        let mask = data[offset + 15] as u16;
133
134        let shape = match shape_type {
135            0 => Shape::Circle { radius: shape_p1 },
136            1 => Shape::AABB { half_w: shape_p1, half_h: shape_p2 },
137            _ => Shape::AABB { half_w: shape_p1, half_h: shape_p2 },
138        };
139
140        let id = world.add_body(
141            body_type,
142            shape,
143            x, y,
144            mass,
145            Material { restitution, friction },
146            layer, mask,
147        );
148
149        // Restore velocity
150        world.set_velocity(id, vx, vy);
151
152        offset += 16;
153    }
154
155    ps.0 = Some(world);
156}
157
158/// Get the number of active bodies in the physics world.
159#[deno_core::op2(fast)]
160fn op_get_physics_body_count(state: &mut OpState) -> u32 {
161    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
162    let ps = physics.borrow();
163    match ps.0.as_ref() {
164        Some(world) => world.body_count() as u32,
165        None => 0,
166    }
167}
168
169deno_core::extension!(
170    replay_ext,
171    ops = [
172        op_serialize_physics_state,
173        op_restore_physics_state,
174        op_get_physics_body_count,
175    ],
176);