1use super::snapshot::SnapReader;
17use super::write::*;
18use super::{RecHeader, Recording};
19use crate::id::WorldId;
20use crate::math_functions::Aabb;
21use crate::world::World;
22
23pub const OP_DESTROY_WORLD: u8 = 0x01;
27pub const OP_WORLD_ENABLE_SLEEPING: u8 = 0x02;
28pub const OP_WORLD_ENABLE_CONTINUOUS: u8 = 0x03;
29pub const OP_WORLD_SET_RESTITUTION_THRESHOLD: u8 = 0x04;
30pub const OP_WORLD_SET_HIT_EVENT_THRESHOLD: u8 = 0x05;
31pub const OP_WORLD_SET_GRAVITY: u8 = 0x06;
32pub const OP_WORLD_EXPLODE: u8 = 0x07;
33pub const OP_WORLD_SET_CONTACT_TUNING: u8 = 0x08;
34pub const OP_WORLD_SET_CONTACT_RECYCLE_DISTANCE: u8 = 0x09;
35pub const OP_WORLD_SET_MAXIMUM_LINEAR_SPEED: u8 = 0x0A;
36pub const OP_WORLD_ENABLE_WARM_STARTING: u8 = 0x0B;
37pub const OP_WORLD_REBUILD_STATIC_TREE: u8 = 0x0C;
38pub const OP_WORLD_ENABLE_SPECULATIVE: u8 = 0x0D;
39pub const OP_STEP: u8 = 0x80;
40pub const OP_STATE_HASH: u8 = 0xF1;
41pub const OP_RECORDING_BOUNDS: u8 = 0xF2;
42
43pub(crate) fn record_op(world: &mut World, f: impl FnOnce(&mut Recording, WorldId)) {
47 if let Some(mut rec) = world.recording.take() {
48 let world_id = world_id_of(world);
49 f(&mut rec, world_id);
50 world.recording = Some(rec);
51 }
52}
53
54pub(crate) fn write_world_bool(rec: &mut Recording, opcode: u8, world_id: WorldId, flag: bool) {
56 rec.begin_record(opcode);
57 rec_w_worldid(&mut rec.buffer, world_id);
58 rec_w_bool(&mut rec.buffer, flag);
59 rec.end_record();
60}
61
62pub(crate) fn write_world_f32(rec: &mut Recording, opcode: u8, world_id: WorldId, value: f32) {
65 rec.begin_record(opcode);
66 rec_w_worldid(&mut rec.buffer, world_id);
67 rec_w_f32(&mut rec.buffer, value);
68 rec.end_record();
69}
70
71pub(crate) fn write_world_marker(rec: &mut Recording, opcode: u8, world_id: WorldId) {
74 rec.begin_record(opcode);
75 rec_w_worldid(&mut rec.buffer, world_id);
76 rec.end_record();
77}
78
79pub(crate) fn write_world_set_gravity(
80 rec: &mut Recording,
81 world_id: WorldId,
82 gravity: crate::math_functions::Vec2,
83) {
84 rec.begin_record(OP_WORLD_SET_GRAVITY);
85 rec_w_worldid(&mut rec.buffer, world_id);
86 rec_w_vec2(&mut rec.buffer, gravity);
87 rec.end_record();
88}
89
90pub(crate) fn write_world_set_contact_tuning(
91 rec: &mut Recording,
92 world_id: WorldId,
93 hertz: f32,
94 damping_ratio: f32,
95 push_speed: f32,
96) {
97 rec.begin_record(OP_WORLD_SET_CONTACT_TUNING);
98 rec_w_worldid(&mut rec.buffer, world_id);
99 rec_w_f32(&mut rec.buffer, hertz);
100 rec_w_f32(&mut rec.buffer, damping_ratio);
101 rec_w_f32(&mut rec.buffer, push_speed);
102 rec.end_record();
103}
104
105pub(crate) fn write_world_explode(
106 rec: &mut Recording,
107 world_id: WorldId,
108 def: &crate::types::ExplosionDef,
109) {
110 rec.begin_record(OP_WORLD_EXPLODE);
111 rec_w_worldid(&mut rec.buffer, world_id);
112 rec_w_explosiondef(&mut rec.buffer, def);
113 rec.end_record();
114}
115
116#[cfg(feature = "double-precision")]
117pub(crate) fn read_position(r: &mut SnapReader) -> crate::math_functions::Pos {
118 crate::math_functions::Pos {
119 x: r.r_f64(),
120 y: r.r_f64(),
121 }
122}
123
124#[cfg(not(feature = "double-precision"))]
125pub(crate) fn read_position(r: &mut SnapReader) -> crate::math_functions::Pos {
126 crate::math_functions::Pos {
127 x: r.r_f32(),
128 y: r.r_f32(),
129 }
130}
131
132fn world_id_of(world: &World) -> WorldId {
133 WorldId {
134 index1: world.world_id + 1,
135 generation: world.generation,
136 }
137}
138
139pub(crate) fn write_step(rec: &mut Recording, world_id: WorldId, dt: f32, sub_step_count: i32) {
142 rec.begin_record(OP_STEP);
143 rec_w_worldid(&mut rec.buffer, world_id);
144 rec_w_f32(&mut rec.buffer, dt);
145 rec_w_i32(&mut rec.buffer, sub_step_count);
146 rec.end_record();
147}
148
149pub(crate) fn write_state_hash(rec: &mut Recording, world_id: WorldId, hash: u64) {
150 rec.begin_record(OP_STATE_HASH);
151 rec_w_worldid(&mut rec.buffer, world_id);
152 rec_w_u64(&mut rec.buffer, hash);
153 rec.end_record();
154}
155
156pub(crate) fn write_recording_bounds(rec: &mut Recording, bounds: Aabb) {
157 rec.begin_record(OP_RECORDING_BOUNDS);
158 rec_w_aabb(&mut rec.buffer, bounds);
159 rec.end_record();
160}
161
162pub(crate) fn write_destroy_world(rec: &mut Recording, world_id: WorldId) {
163 rec.begin_record(OP_DESTROY_WORLD);
164 rec_w_worldid(&mut rec.buffer, world_id);
165 rec.end_record();
166}
167
168pub(crate) fn start_recording_into_buffer(world: &mut World, mut recording: Recording) {
171 recording.buffer.clear();
173 recording.have_bounds = false;
174
175 let mut blob = Vec::new();
178 super::serialize_world(world, &mut blob);
179
180 let header = RecHeader {
181 magic: super::REC_MAGIC,
182 version_major: super::REC_VERSION_MAJOR,
183 version_minor: super::REC_VERSION_MINOR,
184 length_scale: crate::core::get_length_units_per_meter(),
185 pointer_width: std::mem::size_of::<usize>() as u8,
186 big_endian: 0,
187 validation_enabled: if cfg!(debug_assertions) { 1 } else { 0 },
188 snapshot_size: blob.len() as u64,
189 };
190 header.write(&mut recording.buffer);
191 recording.buffer.extend_from_slice(&blob);
192
193 let (seed, have_bounds) = crate::world::compute_world_bounds(world);
196 if have_bounds {
197 recording.accumulate_bounds(seed);
198 }
199
200 let world_id = world_id_of(world);
203 let hash = super::hash_world_state(world);
204 write_state_hash(&mut recording, world_id, hash);
205
206 world.recording = Some(recording);
207}
208
209pub(crate) fn stop_recording_internal(world: &mut World) -> Option<Recording> {
212 let mut rec = world.recording.take()?;
213
214 let bounds = if rec.have_bounds {
217 rec.accumulated_bounds
218 } else {
219 Aabb::default()
220 };
221 write_recording_bounds(&mut rec, bounds);
222
223 let world_id = world_id_of(world);
226 write_destroy_world(&mut rec, world_id);
227
228 Some(rec)
229}
230
231pub fn world_start_recording(world: &mut World, recording: Recording) -> Option<Recording> {
235 debug_assert!(!world.locked);
237 if world.locked || world.recording.is_some() {
238 return Some(recording);
239 }
240
241 start_recording_into_buffer(world, recording);
242 None
243}
244
245pub fn world_stop_recording(world: &mut World) -> Option<Recording> {
248 debug_assert!(!world.locked);
249 if world.locked {
250 return None;
251 }
252
253 stop_recording_internal(world)
254}
255
256pub(crate) fn record_step_end(world: &mut World) {
260 let Some(mut rec) = world.recording.take() else {
261 return;
262 };
263
264 let world_id = world_id_of(world);
266 let hash = super::hash_world_state(world);
267 write_state_hash(&mut rec, world_id, hash);
268
269 let (bounds, have_bounds) = crate::world::compute_world_bounds(world);
272 if have_bounds {
273 rec.accumulate_bounds(bounds);
274 }
275
276 world.recording = Some(rec);
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Default)]
281pub struct ReplayResult {
282 pub steps: i32,
283 pub hash_checks: i32,
284 pub diverged: bool,
285 pub ok: bool,
286 pub bounds: Aabb,
289 pub have_bounds: bool,
290}
291
292pub fn save_recording_to_file(recording: &Recording, path: &std::path::Path) -> bool {
296 std::fs::write(path, &recording.buffer).is_ok()
297}
298
299pub fn load_recording_from_file(path: &std::path::Path) -> Option<Recording> {
302 let buffer = std::fs::read(path).ok()?;
303 let mut recording = Recording::new(0);
304 recording.buffer = buffer;
305 Some(recording)
306}
307
308pub fn validate_replay(data: &[u8]) -> bool {
314 let result = replay_buffer(data);
315 result.ok && !result.diverged
316}
317
318pub(crate) fn dispatch_world_op(opcode: u8, r: &mut SnapReader, world: &mut World) -> Option<bool> {
322 match opcode {
323 OP_WORLD_ENABLE_SLEEPING => {
324 let _ = r.r_u32();
325 let flag = r.r_bool();
326 crate::world::world_enable_sleeping(world, flag);
327 }
328 OP_WORLD_ENABLE_CONTINUOUS => {
329 let _ = r.r_u32();
330 let flag = r.r_bool();
331 crate::world::world_enable_continuous(world, flag);
332 }
333 OP_WORLD_SET_RESTITUTION_THRESHOLD => {
334 let _ = r.r_u32();
335 let value = r.r_f32();
336 crate::world::world_set_restitution_threshold(world, value);
337 }
338 OP_WORLD_SET_HIT_EVENT_THRESHOLD => {
339 let _ = r.r_u32();
340 let value = r.r_f32();
341 crate::world::world_set_hit_event_threshold(world, value);
342 }
343 OP_WORLD_SET_GRAVITY => {
344 let _ = r.r_u32();
345 let gravity = crate::math_functions::Vec2 {
346 x: r.r_f32(),
347 y: r.r_f32(),
348 };
349 crate::world::world_set_gravity(world, gravity);
350 }
351 OP_WORLD_EXPLODE => {
352 let _ = r.r_u32();
353 let mut def = crate::types::default_explosion_def();
354 def.mask_bits = r.r_u64();
355 def.position = read_position(r);
356 def.radius = r.r_f32();
357 def.falloff = r.r_f32();
358 def.impulse_per_length = r.r_f32();
359 crate::world::world_explode(world, &def);
360 }
361 OP_WORLD_SET_CONTACT_TUNING => {
362 let _ = r.r_u32();
363 let hertz = r.r_f32();
364 let damping_ratio = r.r_f32();
365 let push_speed = r.r_f32();
366 crate::world::world_set_contact_tuning(world, hertz, damping_ratio, push_speed);
367 }
368 OP_WORLD_SET_CONTACT_RECYCLE_DISTANCE => {
369 let _ = r.r_u32();
370 let value = r.r_f32();
371 crate::world::world_set_contact_recycle_distance(world, value);
372 }
373 OP_WORLD_SET_MAXIMUM_LINEAR_SPEED => {
374 let _ = r.r_u32();
375 let value = r.r_f32();
376 crate::world::world_set_maximum_linear_speed(world, value);
377 }
378 OP_WORLD_ENABLE_WARM_STARTING => {
379 let _ = r.r_u32();
380 let flag = r.r_bool();
381 crate::world::world_enable_warm_starting(world, flag);
382 }
383 OP_WORLD_REBUILD_STATIC_TREE => {
384 let _ = r.r_u32();
385 crate::world::world_rebuild_static_tree(world);
386 }
387 OP_WORLD_ENABLE_SPECULATIVE => {
388 let _ = r.r_u32();
389 let flag = r.r_bool();
390 crate::world::world_enable_speculative(world, flag);
391 }
392 _ => return None,
393 }
394 Some(true)
395}
396
397pub fn replay_buffer(data: &[u8]) -> ReplayResult {
399 let mut result = ReplayResult::default();
400
401 let Some(header) = RecHeader::read(data) else {
402 return result;
403 };
404 if header.magic != super::REC_MAGIC
405 || header.version_major != super::REC_VERSION_MAJOR
406 || header.version_minor != super::REC_VERSION_MINOR
407 {
408 return result;
409 }
410
411 let snapshot_start = RecHeader::SIZE;
412 let snapshot_end = snapshot_start + header.snapshot_size as usize;
413 if snapshot_end > data.len() {
414 return result;
415 }
416
417 let Some(mut world) = super::create_world_from_snapshot(&data[snapshot_start..snapshot_end])
418 else {
419 return result;
420 };
421
422 let mut r = SnapReader::new(&data[snapshot_end..]);
423 while r.ok && r.cursor < r.data.len() {
424 let opcode = r.r_u8();
425 let payload_size = r.r_u8() as usize | (r.r_u8() as usize) << 8 | (r.r_u8() as usize) << 16;
427 let payload_start = r.cursor;
428 if !r.ok || payload_start + payload_size > r.data.len() {
429 return result;
430 }
431
432 match opcode {
433 OP_STEP => {
434 let _world_id = r.r_u32();
435 let dt = r.r_f32();
436 let sub_step_count = r.r_i32();
437 crate::world::world_step(&mut world, dt, sub_step_count);
438 result.steps += 1;
439 }
440 OP_STATE_HASH => {
441 let _world_id = r.r_u32();
442 let recorded = r.r_u64();
443 let computed = super::hash_world_state(&world);
444 result.hash_checks += 1;
445 if recorded != computed {
446 result.diverged = true;
449 }
450 }
451 OP_RECORDING_BOUNDS => {
452 result.bounds = Aabb {
453 lower_bound: crate::math_functions::Vec2 {
454 x: r.r_f32(),
455 y: r.r_f32(),
456 },
457 upper_bound: crate::math_functions::Vec2 {
458 x: r.r_f32(),
459 y: r.r_f32(),
460 },
461 };
462 result.have_bounds = true;
463 }
464 OP_DESTROY_WORLD => {
465 result.ok = true;
467 return result;
468 }
469 _ => {
470 let handled = dispatch_world_op(opcode, &mut r, &mut world)
471 .or_else(|| super::ops_body::dispatch_body_op(opcode, &mut r, &mut world, None))
472 .or_else(|| super::ops_shape::dispatch_shape_op(opcode, &mut r, &mut world))
473 .or_else(|| super::ops_joint::dispatch_joint_op(opcode, &mut r, &mut world))
474 .or_else(|| {
475 super::ops_query::dispatch_query_op(opcode, &mut r, &mut world, None)
476 });
477 if let Some(ids_match) = handled {
478 if !ids_match {
479 result.diverged = true;
482 }
483 } else {
484 }
487 }
488 }
489
490 r.cursor = payload_start + payload_size;
491 }
492
493 result.ok = r.ok;
494 result
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use crate::body::create_body;
501 use crate::geometry::{make_box, make_square};
502 use crate::math_functions::to_pos;
503 use crate::math_functions::Vec2;
504 use crate::shape::create_polygon_shape;
505 use crate::types::{default_body_def, default_shape_def, default_world_def, BodyType};
506 use crate::world::world_step;
507
508 #[test]
511 fn record_and_validate_replay() {
512 let world_def = default_world_def();
513 let mut world = World::new(&world_def);
514
515 let bd = default_body_def();
516 let ground = create_body(&mut world, &bd);
517 let sd = default_shape_def();
518 create_polygon_shape(&mut world, ground, &sd, &make_box(20.0, 1.0));
519 for i in 0..10 {
520 let mut bd = default_body_def();
521 bd.type_ = BodyType::Dynamic;
522 bd.position = to_pos(Vec2 {
523 x: -2.0 + 0.45 * i as f32,
524 y: 2.0 + 0.5 * i as f32,
525 });
526 let body = create_body(&mut world, &bd);
527 create_polygon_shape(&mut world, body, &sd, &make_square(0.25));
528 }
529
530 for _ in 0..15 {
532 world_step(&mut world, 1.0 / 60.0, 4);
533 }
534
535 assert!(world_start_recording(&mut world, Recording::new(0)).is_none());
536 assert!(world_start_recording(&mut world, Recording::new(0)).is_some());
538
539 for _ in 0..60 {
540 world_step(&mut world, 1.0 / 60.0, 4);
541 }
542
543 let recording = world_stop_recording(&mut world).expect("active session");
544 assert!(world.recording.is_none());
545 assert!(recording.have_bounds);
546 assert!(recording.buffer.len() > RecHeader::SIZE);
547
548 let result = replay_buffer(&recording.buffer);
549 assert!(result.ok, "stream must parse to the end marker");
550 assert!(!result.diverged, "replay hashes must match");
551 assert_eq!(result.steps, 60);
552 assert_eq!(result.hash_checks, 61);
554 assert!(validate_replay(&recording.buffer));
555
556 let mut corrupt = recording.buffer.clone();
560 let len = corrupt.len();
561 corrupt[len - 30] ^= 0x01;
562 let bad = replay_buffer(&corrupt);
563 assert!(bad.diverged && bad.ok);
564 }
565
566 #[test]
570 fn config_ops_replay() {
571 let mut world_def = default_world_def();
572 world_def.gravity = Vec2 { x: 0.0, y: -10.0 };
573 let mut world = World::new(&world_def);
574
575 let bd = default_body_def();
576 let ground = create_body(&mut world, &bd);
577 let sd = default_shape_def();
578 create_polygon_shape(&mut world, ground, &sd, &make_box(15.0, 1.0));
579 for i in 0..6 {
580 let mut bd = default_body_def();
581 bd.type_ = BodyType::Dynamic;
582 bd.position = to_pos(Vec2 {
583 x: -2.0 + 0.8 * i as f32,
584 y: 2.0,
585 });
586 let body = create_body(&mut world, &bd);
587 create_polygon_shape(&mut world, body, &sd, &make_square(0.3));
588 }
589
590 assert!(world_start_recording(&mut world, Recording::new(0)).is_none());
591
592 for step in 0..90 {
593 if step == 20 {
594 let mut def = crate::types::default_explosion_def();
595 def.position = to_pos(Vec2 { x: 0.0, y: 1.5 });
596 def.radius = 2.0;
597 def.falloff = 2.0;
598 def.impulse_per_length = 4.0;
599 crate::world::world_explode(&mut world, &def);
600 }
601 if step == 40 {
602 crate::world::world_set_gravity(&mut world, Vec2 { x: 0.0, y: 3.0 });
603 crate::world::world_enable_sleeping(&mut world, false);
604 }
605 if step == 60 {
606 crate::world::world_set_gravity(&mut world, Vec2 { x: 0.0, y: -10.0 });
607 crate::world::world_set_contact_tuning(&mut world, 20.0, 5.0, 2.0);
608 }
609 world_step(&mut world, 1.0 / 60.0, 4);
610 }
611
612 let recording = world_stop_recording(&mut world).expect("active session");
613 let result = replay_buffer(&recording.buffer);
614 assert!(result.ok);
615 assert!(!result.diverged, "config ops must re-execute on replay");
616 assert_eq!(result.steps, 90);
617 }
618}