use crate::body::{body_is_valid, create_body};
use crate::collision::{Circle, Segment};
use crate::debug_draw::{DebugDraw, HexColor};
use crate::geometry::make_box;
use crate::math_functions::{aabb_extents, to_pos, Aabb, Pos, Vec2, WorldTransform};
use crate::recording::{
hash_world_state_deep, world_start_recording, world_stop_recording, RecPlayer, Recording,
};
use crate::shape::{create_circle_shape, create_polygon_shape, create_segment_shape};
use crate::types::{
default_body_def, default_query_filter, default_shape_def, default_world_def, BodyType,
};
use crate::world::{
world_cast_ray, world_get_counters, world_is_valid, world_overlap_aabb, world_step, World,
};
#[derive(Default)]
struct CountDraw {
lines: usize,
points: usize,
polygons: usize,
capsules: usize,
}
impl DebugDraw for CountDraw {
fn draw_polygon(&mut self, _t: WorldTransform, _v: &[Vec2], _c: HexColor) {
self.polygons += 1;
}
fn draw_line(&mut self, _p1: Pos, _p2: Pos, _c: HexColor) {
self.lines += 1;
}
fn draw_point(&mut self, _p: Pos, _s: f32, _c: HexColor) {
self.points += 1;
}
fn draw_solid_capsule(&mut self, _p1: Pos, _p2: Pos, _r: f32, _c: HexColor) {
self.capsules += 1;
}
}
fn replay_deep_hash(player: &RecPlayer) -> u64 {
hash_world_state_deep(player.world())
}
fn record_bracketed_scene() -> Recording {
let mut world_def = default_world_def();
world_def.gravity = Vec2 { x: 0.0, y: -10.0 };
let mut world = World::new(&world_def);
assert!(world_start_recording(&mut world, Recording::new(0)).is_none());
let mut ground_def = default_body_def();
ground_def.position = to_pos(Vec2 { x: 0.0, y: -10.0 });
let ground = create_body(&mut world, &ground_def);
let sd = default_shape_def();
create_circle_shape(
&mut world,
ground,
&sd,
&Circle {
center: Vec2 { x: 0.0, y: 0.0 },
radius: 10.0,
},
);
create_segment_shape(
&mut world,
ground,
&sd,
&Segment {
point1: Vec2 { x: -20.0, y: 0.0 },
point2: Vec2 { x: 20.0, y: 0.0 },
},
);
let mut bd = default_body_def();
bd.type_ = BodyType::Dynamic;
bd.position = to_pos(Vec2 { x: 0.0, y: 4.0 });
let body = create_body(&mut world, &bd);
let mut ball_def = default_shape_def();
ball_def.density = 1.0;
create_circle_shape(
&mut world,
body,
&ball_def,
&Circle {
center: Vec2 { x: 0.0, y: 0.0 },
radius: 0.5,
},
);
let filter = default_query_filter();
for i in 0..60 {
world_step(&mut world, 1.0 / 60.0, 4);
if i % 2 == 0 {
world_overlap_aabb(
&mut world,
to_pos(Vec2 { x: 0.0, y: 2.0 }),
Aabb {
lower_bound: Vec2 { x: -3.0, y: -3.0 },
upper_bound: Vec2 { x: 3.0, y: 3.0 },
},
filter,
|_| true,
);
world_cast_ray(
&mut world,
to_pos(Vec2 { x: -20.0, y: 2.0 }),
Vec2 { x: 40.0, y: 0.0 },
filter,
|_, _, _, fraction| fraction,
);
}
}
world_stop_recording(&mut world).expect("active session")
}
#[test]
fn recording_player_test() {
let rec = record_bracketed_scene();
let player = RecPlayer::create(&rec.buffer);
let mut player = player.expect("player opens the recording");
let rec_bounds = player.info().bounds;
let rec_extents = aabb_extents(rec_bounds);
assert!(rec_extents.x > 0.0 && rec_extents.y > 0.0);
assert!(rec_bounds.lower_bound.x <= -20.0 && rec_bounds.upper_bound.x >= 20.0);
assert!(rec_bounds.lower_bound.y <= -20.0);
assert_eq!(player.info().frame_count, 60);
assert_eq!(player.info().time_step, 1.0 / 60.0);
assert_eq!(player.info().sub_step_count, 4);
let mut draw = CountDraw::default();
let mut frames = 0;
let mut drew_any = 0usize;
while player.step_frame() {
if frames % 2 == 0 {
player.draw_frame_queries(&mut draw, -1);
drew_any += player.frame_query_count() as usize;
}
frames += 1;
}
assert_eq!(frames, 60);
assert_eq!(player.frame(), 60);
assert!(player.is_at_end());
assert!(!player.has_diverged());
assert!(player.is_ok());
assert!(drew_any > 0);
assert!(draw.lines > 0, "ray casts draw lines");
assert!(draw.polygons > 0, "overlap AABBs draw boxes");
assert!(world_is_valid(player.world()));
player.restart();
assert_eq!(player.frame(), 0);
assert!(!player.is_at_end());
let mut frames2 = 0;
while player.step_frame() {
frames2 += 1;
}
assert_eq!(frames2, 60);
assert!(!player.has_diverged());
}
#[test]
fn recording_outliner_test() {
let mut world_def = default_world_def();
world_def.gravity = Vec2 { x: 0.0, y: -10.0 };
let mut world = World::new(&world_def);
let ground_def = default_body_def();
let ground = create_body(&mut world, &ground_def);
let gsd = default_shape_def();
create_circle_shape(
&mut world,
ground,
&gsd,
&Circle {
center: Vec2 { x: 0.0, y: 0.0 },
radius: 10.0,
},
);
let dynamic_count = 3;
for i in 0..dynamic_count {
let mut bd = default_body_def();
bd.type_ = BodyType::Dynamic;
bd.position = to_pos(Vec2 {
x: i as f32,
y: 4.0,
});
let body = create_body(&mut world, &bd);
let sd = default_shape_def();
create_circle_shape(
&mut world,
body,
&sd,
&Circle {
center: Vec2 { x: 0.0, y: 0.0 },
radius: 0.5,
},
);
}
let expected_bodies = 1 + dynamic_count;
world_step(&mut world, 1.0 / 60.0, 4);
assert!(world_start_recording(&mut world, Recording::new(0)).is_none());
for _ in 0..10 {
world_step(&mut world, 1.0 / 60.0, 4);
}
let rec = world_stop_recording(&mut world).expect("active session");
drop(world);
assert!(!rec.buffer.is_empty());
let mut player = RecPlayer::create(&rec.buffer).expect("player opens");
let seed_count = player.body_count();
assert_eq!(seed_count, expected_bodies);
assert_eq!(seed_count, world_get_counters(player.world()).body_count);
for ord in 0..seed_count {
assert!(body_is_valid(player.world(), player.body_id(ord)));
}
while player.step_frame() {}
player.restart();
assert_eq!(player.body_count(), seed_count);
}
fn check_keyframe_seek(rec_data: &[u8], budget_bytes: usize, min_interval: i32) {
let mut reference = RecPlayer::create(rec_data).expect("reference player opens");
let frame_count = reference.info().frame_count;
assert!(frame_count > 0);
let mut ref_hash = vec![replay_deep_hash(&reference)];
for _ in 1..=frame_count {
assert!(reference.step_frame());
ref_hash.push(replay_deep_hash(&reference));
}
assert!(!reference.has_diverged());
drop(reference);
let mut player = RecPlayer::create(rec_data).expect("player opens");
if budget_bytes > 0 {
player.set_keyframe_policy(budget_bytes, min_interval);
}
while player.step_frame() {}
assert_eq!(player.frame(), frame_count);
let targets = [
frame_count,
1,
frame_count - 1,
290,
17,
271,
256,
128,
33,
200,
5,
300,
100,
frame_count,
];
for t in targets {
let t = t.min(frame_count);
player.seek_frame(t);
assert_eq!(player.frame(), t);
assert!(!player.has_diverged(), "diverged seeking to frame {t}");
let got = replay_deep_hash(&player);
assert_eq!(
got, ref_hash[t as usize],
"keyframe seek mismatch at frame {t}"
);
}
}
#[test]
fn recording_keyframe_test() {
let mut world_def = default_world_def();
world_def.gravity = Vec2 { x: 0.0, y: -10.0 };
let mut world = World::new(&world_def);
let ground_def = default_body_def();
let ground = create_body(&mut world, &ground_def);
let gsd = default_shape_def();
create_polygon_shape(&mut world, ground, &gsd, &make_box(20.0, 1.0));
for i in 0..8 {
let mut bd = default_body_def();
bd.type_ = BodyType::Dynamic;
bd.position = to_pos(Vec2 {
x: 0.05 * i as f32,
y: 2.0 + 1.1 * i as f32,
});
let id = create_body(&mut world, &bd);
let mut sd = default_shape_def();
sd.density = 1.0;
create_polygon_shape(&mut world, id, &sd, &make_box(0.5, 0.5));
}
assert!(world_start_recording(&mut world, Recording::new(0)).is_none());
for _ in 0..320 {
world_step(&mut world, 1.0 / 60.0, 4);
}
let rec = world_stop_recording(&mut world).expect("active session");
drop(world);
assert!(!rec.buffer.is_empty());
let probe = RecPlayer::create(&rec.buffer).expect("probe opens");
let snap_size = crate::recording::world_snapshot(probe.world()).len();
drop(probe);
assert!(snap_size > 0);
let tight_budget = 6 * snap_size;
check_keyframe_seek(&rec.buffer, 0, 0);
check_keyframe_seek(&rec.buffer, tight_budget, 8);
}
fn build_scrub_pyramid(world: &mut World, base_count: i32) {
let mut bd = default_body_def();
bd.position = to_pos(Vec2 { x: 0.0, y: -1.0 });
let ground = create_body(world, &bd);
let gsd = default_shape_def();
create_polygon_shape(world, ground, &gsd, &make_box(40.0, 1.0));
let h = 0.5f32;
let pitch = 2.0 * h + 0.05;
let box_poly = make_box(h, h);
let mut sd = default_shape_def();
sd.density = 1.0;
for row in 0..base_count {
let count = base_count - row;
let y = h + row as f32 * pitch;
let x_start = -0.5 * (count - 1) as f32 * pitch;
for col in 0..count {
let mut body = default_body_def();
body.type_ = BodyType::Dynamic;
body.position = to_pos(Vec2 {
x: x_start + col as f32 * pitch,
y,
});
let id = create_body(world, &body);
create_polygon_shape(world, id, &sd, &box_poly);
}
}
}
fn issue_pile_queries(world: &mut World) {
let filter = default_query_filter();
let aabb = Aabb {
lower_bound: Vec2 { x: -12.0, y: -2.0 },
upper_bound: Vec2 { x: 12.0, y: 22.0 },
};
world_overlap_aabb(world, to_pos(Vec2 { x: 0.0, y: 0.0 }), aabb, filter, |_| {
true
});
world_cast_ray(
world,
to_pos(Vec2 { x: -12.0, y: 10.0 }),
Vec2 { x: 24.0, y: 0.0 },
filter,
|_, _, _, _| 1.0,
);
world_cast_ray(
world,
to_pos(Vec2 { x: 0.0, y: 22.0 }),
Vec2 { x: 0.0, y: -24.0 },
filter,
|_, _, _, _| 1.0,
);
}
fn record_pyramid_scene(step_count: i32, with_queries: bool) -> Recording {
let world_def = default_world_def();
let mut world = World::new(&world_def);
build_scrub_pyramid(&mut world, 6);
assert!(world_start_recording(&mut world, Recording::new(0)).is_none());
for _ in 0..step_count {
world_step(&mut world, 1.0 / 60.0, 4);
if with_queries {
issue_pile_queries(&mut world);
}
}
world_stop_recording(&mut world).expect("active session")
}
fn check_scrub_all_frames(rec_data: &[u8], budget_bytes: usize, min_interval: i32) {
let mut reference = RecPlayer::create(rec_data).expect("reference opens");
let frame_count = reference.info().frame_count;
assert!(frame_count > 0);
let mut ref_hash = vec![replay_deep_hash(&reference)];
for _ in 1..=frame_count {
assert!(reference.step_frame());
ref_hash.push(replay_deep_hash(&reference));
}
assert!(!reference.has_diverged());
drop(reference);
let mut player = RecPlayer::create(rec_data).expect("player opens");
if budget_bytes > 0 {
player.set_keyframe_policy(budget_bytes, min_interval);
}
while player.step_frame() {}
for t in (0..=frame_count).rev() {
player.seek_frame(t);
assert_eq!(player.frame(), t);
let got = replay_deep_hash(&player);
let diverged = player.has_diverged();
assert!(
got == ref_hash[t as usize] && !diverged,
"scrub mismatch at frame {t} (budget {budget_bytes}): {} divergence",
if got == ref_hash[t as usize] {
"query-order"
} else {
"state"
}
);
}
}
fn scrub_recording(rec: &Recording) {
assert!(!rec.buffer.is_empty());
let probe = RecPlayer::create(&rec.buffer).expect("probe opens");
let snap_size = crate::recording::world_snapshot(probe.world()).len();
drop(probe);
let tight_budget = 4 * snap_size;
check_scrub_all_frames(&rec.buffer, tight_budget, 8);
check_scrub_all_frames(&rec.buffer, 0, 0);
}
#[test]
fn recording_scrub_test() {
let rec = record_pyramid_scene(80, false);
scrub_recording(&rec);
}
#[test]
fn recording_query_scrub_test() {
let rec = record_pyramid_scene(80, true);
scrub_recording(&rec);
}