makepad/libs/sim/tests/mover_golden.rs
Admin 6d708d9391 Clean the public tree for a checkout other people can use.
Drop the archived old/ tree, root AI notes, splashgame.md (Arcade lives
in the private sandbox), widgets-dll leftovers, and LAN/oracle helper
scripts. Move download_*.sh into tools/, and move sim/math out of
libs/game/ so the public repo no longer has a game/ directory.

Also quiet first-party compile noise and skip asset-ui tests that need
sandbox kits or uncommitted fixtures.
2026-08-18 15:23:19 +02:00

362 lines
13 KiB
Rust

//! Golden world-state hashes for the kinematic/mover path — the half of
//! `step_world` that the rigid-dynamics goldens don't reach: terrain cliffs
//! and floors, static/kinematic sweeps, platform carry, rider attachment,
//! projectile lifetimes and auto-facing.
//!
//! This is the regression gate for optimisation work in `step_world`. Any
//! change that alters an observable result moves these hashes; a pure perf
//! change must leave them alone. (The snapshot ordering matters: movers sweep
//! against the kinematic poses from BEFORE this tick's integration, so an
//! "obvious" simplification that reads live entity positions instead of the
//! snapshot will be caught here.)
use makepad_game_sim::*;
use makepad_math::*;
fn fnv(h: u64, v: u32) -> u64 {
(h ^ v as u64).wrapping_mul(0x100000001b3)
}
/// Hashes everything the mover path can influence, including the flags —
/// `on_floor`/`floor_id`/`hit_wall` are contract surface that scripts read.
fn hash_world(w: &GameWorld) -> u64 {
let mut h = 0xcbf29ce484222325;
for e in &w.entities {
for f in [
e.pos.x, e.pos.y, e.pos.z, e.vel.x, e.vel.y, e.vel.z, e.yaw, e.scale.x,
] {
h = fnv(h, f.to_bits());
}
h = fnv(h, e.id as u32);
h = fnv(h, e.on_floor as u32);
h = fnv(h, e.floor_id as u32);
h = fnv(h, e.hit_wall as u32);
h = fnv(h, e.attached_to as u32);
}
h
}
fn ent(id: u64, kind: BodyKind, pos: Vec3f, half: Vec3f) -> Entity {
Entity {
id,
kind,
pos,
half,
collide: true,
gravity_scale: 1.0,
density: 1.0,
friction: 0.6,
scale: vec3f(1.0, 1.0, 1.0),
scale_target: vec3f(1.0, 1.0, 1.0),
speed_mult: 1.0,
..Default::default()
}
}
/// A ridged heightfield: hills tall enough to be cliffs (blocking sideways
/// movement) and slopes shallow enough to walk up, so both terrain branches
/// are exercised rather than just the flat floor.
fn ridged_terrain(cells: usize) -> Terrain {
let cell_size = 1.0;
let origin = -(cells as f32 * cell_size) * 0.5;
let mut heights = Vec::with_capacity(cells * cells);
let mut colors = Vec::with_capacity(cells * cells);
for z in 0..cells {
for x in 0..cells {
let fx = x as f32 * 0.35;
let fz = z as f32 * 0.21;
// Deterministic ridges: no rng, no libm beyond the sim's own.
let hgt = (math::sin(fx) * 1.4 + math::cos(fz) * 0.9 + 1.0).max(0.0);
heights.push(hgt);
colors.push(vec4f(0.35, 0.55, 0.3, 1.0));
}
}
Terrain { cells, cell_size, origin, heights, colors, revision: 1 }
}
/// Terrain + walls + a moving platform + walkers + a rider + projectiles.
fn mover_scene() -> GameWorld {
let mut w = GameWorld::new();
w.reset_content();
w.terrain = Some(ridged_terrain(33));
let mut id = 0u64;
// Static walls in the walkers' path.
for i in 0..6 {
id += 1;
w.push_entity(ent(
id,
BodyKind::Static,
vec3f(-4.0 + i as f32 * 2.5, 1.0, 3.0),
vec3f(0.6, 1.0, 0.4),
));
}
// Kinematic platform: movers standing on it must be carried.
id += 1;
let mut plat = ent(id, BodyKind::Kinematic, vec3f(0.0, 2.0, -6.0), vec3f(3.0, 0.3, 3.0));
plat.vel = vec3f(1.2, 0.0, 0.0);
let platform_id = id;
w.push_entity(plat);
// A rider standing on the platform.
id += 1;
let mut rider = ent(id, BodyKind::Mover, vec3f(0.0, 3.0, -6.0), vec3f(0.35, 0.7, 0.35));
rider.auto_face = true;
rider.turn_rate = 5.0;
w.push_entity(rider);
// Walkers driving into the walls and up the ridges.
for i in 0..5 {
id += 1;
let mut m = ent(
id,
BodyKind::Mover,
vec3f(-6.0 + i as f32 * 1.7, 4.0, 0.0),
vec3f(0.4, 0.8, 0.4),
);
m.vel = vec3f(1.5 + i as f32 * 0.2, 0.0, 1.1);
m.auto_face = true;
m.turn_rate = 6.0;
m.scale_target = vec3f(1.2, 1.2, 1.2);
w.push_entity(m);
}
// A passenger attached to the platform (attach pin path).
id += 1;
let mut seat = ent(id, BodyKind::Mover, vec3f(0.0, 3.0, -6.0), vec3f(0.3, 0.6, 0.3));
seat.attached_to = platform_id;
seat.attach_offset = vec3f(0.5, 0.9, 0.0);
w.push_entity(seat);
// Projectiles: lifetimes expire mid-run, exercising the retain path.
for i in 0..4 {
id += 1;
let mut p = ent(
id,
BodyKind::Mover,
vec3f(-2.0 + i as f32, 5.0, -1.0),
vec3f(0.15, 0.15, 0.15),
);
p.vel = vec3f(2.0, 1.0, -0.5);
p.hits = true;
p.life = 1.0 + i as f32 * 0.4;
w.push_entity(p);
}
w
}
#[test]
fn mover_path_matches_golden() {
let mut w = mover_scene();
for _ in 0..600 {
step_world(&mut w);
let _ = collect_touches(&w);
}
let got = hash_world(&w);
// Recorded on aarch64. Re-baselined twice, deliberately:
// 1. CONTACT_SKIN made resting contact stop a hair short of flush, which
// shifts every clamped position by 1e-3 — a bug fix (flush contact
// let a falling mover be resolved UP onto a crate it had walked into,
// ignoring the 0.55 step-up contract).
// 2. Box step-up: the 0.55 CLIMB contract now covers BOX solids, not
// just terrain terraces and wedges. A mover pressed against a solid
// whose top is within step reach climbs onto it instead of clamping
// forever — the fix for generated interiors reading as fields of
// invisible ankle-high walls (the arena playtest's door-to-den walk
// is the behavioural gate). Scenarios where a mover leaned on a low
// ledge now step onto it, which is the intended movement.
// 3. Authoritative distance, facing, contact, terrain-normal, and water
// math now crosses makepad-game-math instead of platform float helpers.
// Any FURTHER change to this hash needs the same justification.
assert_eq!(
got, 0x1ffa9f88c246b459,
"mover/terrain/attach path changed observable results (got {got:#x})"
);
}
#[test]
fn mover_path_is_run_to_run_deterministic() {
let mut a = mover_scene();
let mut b = mover_scene();
for _ in 0..300 {
step_world(&mut a);
step_world(&mut b);
}
assert_eq!(hash_world(&a), hash_world(&b));
}
/// The snapshot must predate this tick's kinematic integration: a mover riding
/// a platform is carried by the platform's velocity, not teleported by its
/// already-advanced position. Guards the ordering an optimisation could break.
#[test]
fn platform_carry_uses_pre_integration_snapshot() {
let mut w = GameWorld::new();
w.reset_content();
w.push_entity(ent(1, BodyKind::Static, vec3f(0.0, -0.5, 0.0), vec3f(20.0, 0.5, 20.0)));
let mut plat = ent(2, BodyKind::Kinematic, vec3f(0.0, 1.0, 0.0), vec3f(2.0, 0.5, 2.0));
plat.vel = vec3f(2.0, 0.0, 0.0);
w.push_entity(plat);
let rider = ent(3, BodyKind::Mover, vec3f(0.0, 2.0, 0.0), vec3f(0.3, 0.5, 0.3));
w.push_entity(rider);
// Settle the rider onto the platform.
for _ in 0..30 {
step_world(&mut w);
}
let before = w.entity(3).unwrap().pos.x;
let carried_ticks = 10;
for _ in 0..carried_ticks {
step_world(&mut w);
}
let after = w.entity(3).unwrap().pos.x;
let moved = after - before;
// Carried at the platform's 2.0 u/s for `carried_ticks` at 60Hz.
let expected = 2.0 * TICK_DT * carried_ticks as f32;
assert!(
(moved - expected).abs() < 0.02,
"rider should be carried {expected} but moved {moved}"
);
}
/// A character must not walk through a rigid body. Rigids were absent from the
/// mover sweep set when box3d dynamics landed, so the Knight strolled straight
/// through a crate stack — the most obvious "this world isn't real" tell.
/// Their pose is read back from box3d at the end of the previous tick, so at
/// snapshot time a rigid is as settled as a kinematic.
#[test]
fn mover_is_blocked_by_a_rigid_body() {
let mut w = GameWorld::new();
w.reset_content();
w.push_entity(ent(1, BodyKind::Static, vec3f(0.0, -0.5, 0.0), vec3f(20.0, 0.5, 20.0)));
// A crate sitting on the ground, directly in the walker's path. HEAVY on
// purpose since D4/F8: a walker now SHOVES light rigids along (that is a
// feature, tested in mix_core), so this test's "you cannot walk through
// a rigid" contract needs a crate the mass-ratio push cannot move.
let mut crate_e = ent(2, BodyKind::Rigid, vec3f(2.0, 0.5, 0.0), vec3f(0.5, 0.5, 0.5));
crate_e.density = 50.0;
w.push_entity(crate_e);
let mut walker = ent(3, BodyKind::Mover, vec3f(0.0, 0.5, 0.0), vec3f(0.3, 0.5, 0.3));
walker.vel = vec3f(3.0, 0.0, 0.0);
w.push_entity(walker);
for _ in 0..60 {
// Walk is a velocity the script re-asserts each tick.
w.entity_mut(3).unwrap().vel.x = 3.0;
step_world(&mut w);
}
let x = w.entity(3).unwrap().pos.x;
// Unblocked it would reach x = 3.0; it must stop at the crate's near face
// (2.0 - 0.5 crate half - 0.3 walker half = 1.2).
assert!(
x < 1.35,
"walker should stop at the crate's face (~1.2) but reached {x}"
);
}
/// A stock prop is drawn as a mesh, so its collider is a `hidden` box that
/// the renderer skips. Hidden must affect DRAWING ONLY — if it leaked into
/// the sweep set, every scenery prop would go back to being walk-through
/// scenery, which is the bug this flag exists to fix.
#[test]
fn hidden_props_still_block_a_walker() {
let mut w = GameWorld::new();
w.reset_content();
w.push_entity(ent(1, BodyKind::Static, vec3f(0.0, -0.5, 0.0), vec3f(30.0, 0.5, 30.0)));
// A house collider: invisible, but solid.
let mut house = ent(2, BodyKind::Static, vec3f(4.0, 2.0, 0.0), vec3f(2.0, 2.0, 2.0));
house.hidden = true;
w.push_entity(house);
let mut walker = ent(3, BodyKind::Mover, vec3f(0.0, 0.5, 0.0), vec3f(0.3, 0.5, 0.3));
walker.vel = vec3f(4.0, 0.0, 0.0);
w.push_entity(walker);
for _ in 0..90 {
w.entity_mut(3).unwrap().vel.x = 4.0;
step_world(&mut w);
}
let x = w.entity(3).unwrap().pos.x;
// Unobstructed he would cross the whole slab; the house's near face is at
// 4.0 - 2.0 = 2.0, so he stops around 1.7 (minus his own half-width).
assert!(
x < 1.85,
"hidden collider did not block the walker: reached {x}"
);
// And he must not have been shoved onto its roof.
let y = w.entity(3).unwrap().pos.y;
assert!(y < 1.0, "walker climbed the house: y {y}");
}
/// Walk a mover along +x from `from_z` and report where it stopped.
fn walk_to_x(w: &mut GameWorld, id: u64, z: f32, ticks: usize) -> f32 {
if let Some(e) = w.entity_mut(id) {
e.pos = vec3f(-6.0, 0.5, z);
}
for _ in 0..ticks {
w.entity_mut(id).unwrap().vel.x = 4.0;
step_world(w);
}
w.entity(id).unwrap().pos.x
}
/// A prop's collider is several boxes derived from its own primitives, not one
/// AABB. That distinction is the whole point: a house must stop you at its
/// walls and let you through its doorway. One box would make the door solid,
/// which is the difference between a building and a rock.
#[test]
fn a_multi_box_house_blocks_walls_but_not_the_doorway() {
let mut w = GameWorld::new();
w.reset_content();
w.push_entity(ent(1, BodyKind::Static, vec3f(0.0, -0.5, 0.0), vec3f(40.0, 0.5, 40.0)));
// Two wall slabs at x=0 with a 1.6-wide doorway between them at z=0.
for (i, cz) in [-2.0f32, 2.0].iter().enumerate() {
let mut wall = ent(
2 + i as u64,
BodyKind::Static,
vec3f(0.0, 1.5, *cz),
vec3f(0.4, 1.5, 1.2),
);
wall.hidden = true;
w.push_entity(wall);
}
let mut walker = ent(9, BodyKind::Mover, vec3f(-6.0, 0.5, 0.0), vec3f(0.3, 0.5, 0.3));
walker.vel = vec3f(4.0, 0.0, 0.0);
w.push_entity(walker);
// Straight at a wall: stops short of it.
let blocked = walk_to_x(&mut w, 9, -2.0, 240);
assert!(blocked < -0.5, "wall did not block: reached x {blocked}");
// Through the gap between them: passes clean through.
let through = walk_to_x(&mut w, 9, 0.0, 240);
assert!(
through > 2.0,
"doorway was solid — a single AABB, not a multi-box collider (x {through})"
);
}
/// A tree collides at the trunk only. Blocking the canopy would put an
/// invisible wall metres from the trunk and make a wood impassable.
#[test]
fn a_tree_blocks_at_the_trunk_and_not_under_the_canopy() {
let mut w = GameWorld::new();
w.reset_content();
w.push_entity(ent(1, BodyKind::Static, vec3f(0.0, -0.5, 0.0), vec3f(40.0, 0.5, 40.0)));
// Trunk only: narrow and low. The canopy contributes no collider at all.
let mut trunk = ent(2, BodyKind::Static, vec3f(0.0, 1.2, 0.0), vec3f(0.22, 1.2, 0.22));
trunk.hidden = true;
w.push_entity(trunk);
let mut walker = ent(9, BodyKind::Mover, vec3f(-6.0, 0.5, 0.0), vec3f(0.3, 0.5, 0.3));
walker.vel = vec3f(4.0, 0.0, 0.0);
w.push_entity(walker);
// Dead at the trunk: stopped.
let hit = walk_to_x(&mut w, 9, 0.0, 240);
assert!(hit < -0.4, "trunk did not block: x {hit}");
// A stride to the side — under the canopy — is clear.
let clear = walk_to_x(&mut w, 9, 1.6, 240);
assert!(
clear > 2.0,
"canopy blocked a walker who should have passed under it (x {clear})"
);
}