Files
benilla-octo/crates/benilla-formats/examples/wmo_collision_cost.rs
T
SamandClaude Fable 5 fd54ea9197 walkers keep walking, and dungeon entrances stop dropping you
- A creature that was already walking when it came into view stood frozen at
  its spawn point, then teleported — up to a full wander leg, 30 yards in the
  worst measured case — when its next movement packet arrived. The create
  packet carries the live path (where the unit is along it, how much is left)
  and we parsed all of it into the void. The unit now joins its walk exactly
  where the server has it: measured before/after, 13% of movement starts were
  over-a-yard teleports, now effectively zero.
- Walking into Zul'Gurub or Stratholme through the portal dropped you straight
  through the world. The landing check asks "is there a floor under me?" — but
  on the arrival frame the loaded world is still the map you just left, and an
  instance entrance is a copy of its outdoor ground at the same coordinates,
  0.23 yards under your feet. The old floor said yes, then despawned. The hold
  now waits for the destination's own ground to arrive, and a world that never
  arrives keeps the loading screen instead of dropping you into the void.
- Same fix covers a decoder hazard found on the way: a plain two-point path
  announces two points but ships only the destination, and trusting the count
  over-ran the packet — it never fires on our server's pathfinder, but now it
  can't.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 19:38:01 +07:00

138 lines
5.9 KiB
Rust

//! What a WMO's **collision** actually costs, and whether its walking gather has a floor at all:
//! `cargo run -p benilla-formats --example wmo_collision_cost -- <wmo-path-or-substring>...`
//! e.g. `wmo_collision_cost stratholme.wmo zulgurubcity`.
//!
//! The falsifier for a **fall-through** report, which has exactly two shapes and they take opposite
//! fixes:
//!
//! * **No floor to stand on** — the walking gather (`flags & 0x04` DETAIL dropped) came back with no
//! upward-facing faces in a group the camera gather *does* see. Then the collider is present and
//! correct and still cannot hold you: the authored floor is flagged decal-only. A data fact, fixed
//! by widening the gather, and visible here without entering the game.
//! * **A floor that arrives too late** — the faces are there, but the placement's trimesh is big
//! enough that its off-thread build plus the per-frame attach budget runs past the mover's settle
//! timeout, so gravity switches on over geometry that is not solid yet. A *timing* fact; this
//! prints the triangle count that drives it, so the margin can be compared against
//! `SETTLE_TIMEOUT` without guessing which buildings are the heavy ones.
//!
//! Both readings are properties of the shipped file, so read them here rather than inferring them
//! from a fall you may or may not be able to reproduce on your own machine — the timing shape
//! reproduces only on a machine slow enough, which is exactly why it reaches players and not us.
//!
//! Output is Blizzard-derived; pipe it to the scratchpad, never into the repo.
use benilla_formats::{accumulate_wmo_group_camera_collision, accumulate_wmo_group_collision};
/// Faces at least this upward-facing count as a **floor** — the mover's own walkable test
/// (`GROUND_COS`, ~50° from vertical). A group with collidable faces but none of them walkable is
/// a wall/ceiling shell you slide down, not something you can stand on.
const WALKABLE_COS: f32 = 0.64;
/// One gather's shape: how many triangles, and how many of them you could stand on.
struct Gather {
tris: usize,
walkable: usize,
min_z: f32,
max_z: f32,
}
fn gather(positions: &[[f32; 3]], indices: &[u32]) -> Gather {
let (mut walkable, mut min_z, mut max_z) = (0usize, f32::MAX, f32::MIN);
for t in indices.chunks_exact(3) {
let p: [[f32; 3]; 3] = [
positions[t[0] as usize],
positions[t[1] as usize],
positions[t[2] as usize],
];
let u = [p[1][0] - p[0][0], p[1][1] - p[0][1], p[1][2] - p[0][2]];
let v = [p[2][0] - p[0][0], p[2][1] - p[0][1], p[2][2] - p[0][2]];
// Raw WMO space is Z-up, so the walkable axis is the normal's Z.
let n = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len > 0.0 && (n[2] / len).abs() >= WALKABLE_COS {
walkable += 1;
}
for c in p {
min_z = min_z.min(c[2]);
max_z = max_z.max(c[2]);
}
}
Gather {
tris: indices.len() / 3,
walkable,
min_z,
max_z,
}
}
fn main() -> anyhow::Result<()> {
let pats: Vec<String> = std::env::args().skip(1).collect();
if pats.is_empty() {
anyhow::bail!("usage: wmo_collision_cost <wmo-path-or-substring>...");
}
let data = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../WoW/Data");
let mut chain = benilla_formats::open_chain(&data)?;
let names: Vec<String> = chain.list()?.into_iter().map(|e| e.name).collect();
for pat in &pats {
let lower = pat.to_lowercase();
let path = if lower.ends_with(".wmo") && chain.contains(pat) {
pat.clone()
} else {
let Some(found) = names.iter().find(|n| {
let l = n.to_lowercase();
l.ends_with(".wmo")
&& l.contains(&lower)
&& !l.rsplit_once('_').is_some_and(|(_, tail)| {
tail.len() == 7 && tail.starts_with(|c: char| c.is_ascii_digit())
})
}) else {
println!("no root .wmo matching {pat:?}\n");
continue;
};
found.clone()
};
let bytes = chain.read_file(&path.to_ascii_lowercase())?;
let n_groups = benilla_formats::parse_wmo_root(&bytes)?.group_count();
let stem = {
let l = path.to_ascii_lowercase();
l.strip_suffix(".wmo").unwrap_or(&l).to_string()
};
println!("=== {path} — {n_groups} group(s) ===");
let (mut tot_walk, mut tot_cam, mut floorless) = (0usize, 0usize, Vec::new());
for gi in 0..n_groups {
let Ok(gbytes) = chain.read_file(&format!("{stem}_{gi:03}.wmo")) else {
continue;
};
let (mut wp, mut wi) = (Vec::new(), Vec::new());
accumulate_wmo_group_collision(&gbytes, &mut wp, &mut wi);
let (mut cp, mut ci) = (Vec::new(), Vec::new());
accumulate_wmo_group_camera_collision(&gbytes, &mut cp, &mut ci);
let (w, c) = (gather(&wp, &wi), gather(&cp, &ci));
tot_walk += w.tris;
tot_cam += c.tris;
// The defect shape: the camera can stand on this group, the player body cannot.
if w.walkable == 0 && c.walkable > 0 {
floorless.push(gi);
println!(
" group {gi:3}: walk {:6} tri ({:5} walkable) camera {:6} tri ({:5} walkable) \
<-- NO WALKABLE FACE IN THE WALK GATHER (z {:.1}..{:.1})",
w.tris, w.walkable, c.tris, c.walkable, c.min_z, c.max_z
);
}
}
println!(
" TOTAL walk {tot_walk} tri, camera {tot_cam} tri \
({} group(s) with a camera-only floor)\n",
floorless.len()
);
}
Ok(())
}