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.
362 lines
13 KiB
Rust
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})"
|
|
);
|
|
}
|