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 fn replay_buffer(data: &[u8]) -> ReplayResult {
320 let mut result = ReplayResult::default();
321
322 let Some(header) = RecHeader::read(data) else {
323 return result;
324 };
325 if header.magic != super::REC_MAGIC
326 || header.version_major != super::REC_VERSION_MAJOR
327 || header.version_minor != super::REC_VERSION_MINOR
328 {
329 return result;
330 }
331
332 let snapshot_start = RecHeader::SIZE;
333 let snapshot_end = snapshot_start + header.snapshot_size as usize;
334 if snapshot_end > data.len() {
335 return result;
336 }
337
338 let Some(mut world) = super::create_world_from_snapshot(&data[snapshot_start..snapshot_end])
339 else {
340 return result;
341 };
342
343 let mut r = SnapReader::new(&data[snapshot_end..]);
344 while r.ok && r.cursor < r.data.len() {
345 let opcode = r.r_u8();
346 let payload_size = r.r_u8() as usize | (r.r_u8() as usize) << 8 | (r.r_u8() as usize) << 16;
348 let payload_start = r.cursor;
349 if !r.ok || payload_start + payload_size > r.data.len() {
350 return result;
351 }
352
353 match opcode {
354 OP_STEP => {
355 let _world_id = r.r_u32();
356 let dt = r.r_f32();
357 let sub_step_count = r.r_i32();
358 crate::world::world_step(&mut world, dt, sub_step_count);
359 result.steps += 1;
360 }
361 OP_STATE_HASH => {
362 let _world_id = r.r_u32();
363 let recorded = r.r_u64();
364 let computed = super::hash_world_state(&world);
365 result.hash_checks += 1;
366 if recorded != computed {
367 result.diverged = true;
370 }
371 }
372 OP_WORLD_ENABLE_SLEEPING => {
373 let _ = r.r_u32();
374 let flag = r.r_bool();
375 crate::world::world_enable_sleeping(&mut world, flag);
376 }
377 OP_WORLD_ENABLE_CONTINUOUS => {
378 let _ = r.r_u32();
379 let flag = r.r_bool();
380 crate::world::world_enable_continuous(&mut world, flag);
381 }
382 OP_WORLD_SET_RESTITUTION_THRESHOLD => {
383 let _ = r.r_u32();
384 let value = r.r_f32();
385 crate::world::world_set_restitution_threshold(&mut world, value);
386 }
387 OP_WORLD_SET_HIT_EVENT_THRESHOLD => {
388 let _ = r.r_u32();
389 let value = r.r_f32();
390 crate::world::world_set_hit_event_threshold(&mut world, value);
391 }
392 OP_WORLD_SET_GRAVITY => {
393 let _ = r.r_u32();
394 let gravity = crate::math_functions::Vec2 {
395 x: r.r_f32(),
396 y: r.r_f32(),
397 };
398 crate::world::world_set_gravity(&mut world, gravity);
399 }
400 OP_WORLD_EXPLODE => {
401 let _ = r.r_u32();
402 let mut def = crate::types::default_explosion_def();
403 def.mask_bits = r.r_u64();
404 def.position = read_position(&mut r);
405 def.radius = r.r_f32();
406 def.falloff = r.r_f32();
407 def.impulse_per_length = r.r_f32();
408 crate::world::world_explode(&mut world, &def);
409 }
410 OP_WORLD_SET_CONTACT_TUNING => {
411 let _ = r.r_u32();
412 let hertz = r.r_f32();
413 let damping_ratio = r.r_f32();
414 let push_speed = r.r_f32();
415 crate::world::world_set_contact_tuning(
416 &mut world,
417 hertz,
418 damping_ratio,
419 push_speed,
420 );
421 }
422 OP_WORLD_SET_CONTACT_RECYCLE_DISTANCE => {
423 let _ = r.r_u32();
424 let value = r.r_f32();
425 crate::world::world_set_contact_recycle_distance(&mut world, value);
426 }
427 OP_WORLD_SET_MAXIMUM_LINEAR_SPEED => {
428 let _ = r.r_u32();
429 let value = r.r_f32();
430 crate::world::world_set_maximum_linear_speed(&mut world, value);
431 }
432 OP_WORLD_ENABLE_WARM_STARTING => {
433 let _ = r.r_u32();
434 let flag = r.r_bool();
435 crate::world::world_enable_warm_starting(&mut world, flag);
436 }
437 OP_WORLD_REBUILD_STATIC_TREE => {
438 let _ = r.r_u32();
439 crate::world::world_rebuild_static_tree(&mut world);
440 }
441 OP_WORLD_ENABLE_SPECULATIVE => {
442 let _ = r.r_u32();
443 let flag = r.r_bool();
444 crate::world::world_enable_speculative(&mut world, flag);
445 }
446 OP_RECORDING_BOUNDS => {
447 result.bounds = Aabb {
448 lower_bound: crate::math_functions::Vec2 {
449 x: r.r_f32(),
450 y: r.r_f32(),
451 },
452 upper_bound: crate::math_functions::Vec2 {
453 x: r.r_f32(),
454 y: r.r_f32(),
455 },
456 };
457 result.have_bounds = true;
458 }
459 OP_DESTROY_WORLD => {
460 result.ok = true;
462 return result;
463 }
464 _ => {
465 let handled = super::ops_body::dispatch_body_op(opcode, &mut r, &mut world)
466 .or_else(|| super::ops_shape::dispatch_shape_op(opcode, &mut r, &mut world))
467 .or_else(|| super::ops_joint::dispatch_joint_op(opcode, &mut r, &mut world))
468 .or_else(|| super::ops_query::dispatch_query_op(opcode, &mut r, &mut world));
469 if let Some(ids_match) = handled {
470 if !ids_match {
471 result.diverged = true;
474 }
475 } else {
476 }
479 }
480 }
481
482 r.cursor = payload_start + payload_size;
483 }
484
485 result.ok = r.ok;
486 result
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::body::create_body;
493 use crate::geometry::{make_box, make_square};
494 use crate::math_functions::to_pos;
495 use crate::math_functions::Vec2;
496 use crate::shape::create_polygon_shape;
497 use crate::types::{default_body_def, default_shape_def, default_world_def, BodyType};
498 use crate::world::world_step;
499
500 #[test]
503 fn record_and_validate_replay() {
504 let world_def = default_world_def();
505 let mut world = World::new(&world_def);
506
507 let bd = default_body_def();
508 let ground = create_body(&mut world, &bd);
509 let sd = default_shape_def();
510 create_polygon_shape(&mut world, ground, &sd, &make_box(20.0, 1.0));
511 for i in 0..10 {
512 let mut bd = default_body_def();
513 bd.type_ = BodyType::Dynamic;
514 bd.position = to_pos(Vec2 {
515 x: -2.0 + 0.45 * i as f32,
516 y: 2.0 + 0.5 * i as f32,
517 });
518 let body = create_body(&mut world, &bd);
519 create_polygon_shape(&mut world, body, &sd, &make_square(0.25));
520 }
521
522 for _ in 0..15 {
524 world_step(&mut world, 1.0 / 60.0, 4);
525 }
526
527 assert!(world_start_recording(&mut world, Recording::new(0)).is_none());
528 assert!(world_start_recording(&mut world, Recording::new(0)).is_some());
530
531 for _ in 0..60 {
532 world_step(&mut world, 1.0 / 60.0, 4);
533 }
534
535 let recording = world_stop_recording(&mut world).expect("active session");
536 assert!(world.recording.is_none());
537 assert!(recording.have_bounds);
538 assert!(recording.buffer.len() > RecHeader::SIZE);
539
540 let result = replay_buffer(&recording.buffer);
541 assert!(result.ok, "stream must parse to the end marker");
542 assert!(!result.diverged, "replay hashes must match");
543 assert_eq!(result.steps, 60);
544 assert_eq!(result.hash_checks, 61);
546 assert!(validate_replay(&recording.buffer));
547
548 let mut corrupt = recording.buffer.clone();
552 let len = corrupt.len();
553 corrupt[len - 30] ^= 0x01;
554 let bad = replay_buffer(&corrupt);
555 assert!(bad.diverged && bad.ok);
556 }
557
558 #[test]
562 fn config_ops_replay() {
563 let mut world_def = default_world_def();
564 world_def.gravity = Vec2 { x: 0.0, y: -10.0 };
565 let mut world = World::new(&world_def);
566
567 let bd = default_body_def();
568 let ground = create_body(&mut world, &bd);
569 let sd = default_shape_def();
570 create_polygon_shape(&mut world, ground, &sd, &make_box(15.0, 1.0));
571 for i in 0..6 {
572 let mut bd = default_body_def();
573 bd.type_ = BodyType::Dynamic;
574 bd.position = to_pos(Vec2 {
575 x: -2.0 + 0.8 * i as f32,
576 y: 2.0,
577 });
578 let body = create_body(&mut world, &bd);
579 create_polygon_shape(&mut world, body, &sd, &make_square(0.3));
580 }
581
582 assert!(world_start_recording(&mut world, Recording::new(0)).is_none());
583
584 for step in 0..90 {
585 if step == 20 {
586 let mut def = crate::types::default_explosion_def();
587 def.position = to_pos(Vec2 { x: 0.0, y: 1.5 });
588 def.radius = 2.0;
589 def.falloff = 2.0;
590 def.impulse_per_length = 4.0;
591 crate::world::world_explode(&mut world, &def);
592 }
593 if step == 40 {
594 crate::world::world_set_gravity(&mut world, Vec2 { x: 0.0, y: 3.0 });
595 crate::world::world_enable_sleeping(&mut world, false);
596 }
597 if step == 60 {
598 crate::world::world_set_gravity(&mut world, Vec2 { x: 0.0, y: -10.0 });
599 crate::world::world_set_contact_tuning(&mut world, 20.0, 5.0, 2.0);
600 }
601 world_step(&mut world, 1.0 / 60.0, 4);
602 }
603
604 let recording = world_stop_recording(&mut world).expect("active session");
605 let result = replay_buffer(&recording.buffer);
606 assert!(result.ok);
607 assert!(!result.diverged, "config ops must re-execute on replay");
608 assert_eq!(result.steps, 90);
609 }
610}