Files
benilla-octo/crates/benilla-assets/examples/face_facing_at.rs
T
SamandClaude Fable 5 6b980b9efb the player build: one binary that carries its content
- a player build ships beside the dev one — dev chords and probe surfaces go dark, and a gate enforces what a player can still reach, by medium not by module
- the binary carries its own shaders and its shipped UI, finds the WoW folder by itself, and keeps its local state in one benilla-config folder
- the glue screens get a capture shutter, and the engine stops depending on the protocol crate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:18:02 +07:00

187 lines
7.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Which way do the collidable faces under a world pin actually FACE — and would the reference
//! block on them?
//! `cargo run -p benilla-assets --example face_facing_at -- <map> <x> <y> <z> [wmo-substring]`
//! e.g. `face_facing_at Kalimdor -8137.9 -4897.2 2.0 caverns`.
//!
//! The reference's movement collision is **one-sided** (decision 0967, wow-re
//! `collision/scratch/wmo-movement-group-gate.md`): `0x671cc0` emits each candidate face's plane at
//! the **unflipped file winding**, and `0x632700` then processes it only if `n·dir <= -1e-5`. A
//! player falling straight down has `dir` = −up, so **a face blocks the fall iff its authored normal
//! points up**. Ours is a parry trimesh — two-sided — so it blocks either way.
//!
//! This tool turns that difference into a number at a specific `.go xyz` pin: every walking-collidable
//! WMO face whose footprint contains the pin, with its world height, its authored normal's up
//! component, and the reference's verdict. Where a face reads `PASSES`, the reference falls through
//! it and we stand on it.
//!
//! It goes through the **same transform the app bakes colliders with** — `wow_to_bevy` per vertex
//! then the placement `Transform`, exactly as `terrain_stream::collider::placement_collider_data`
//! does — so the geometry here is the geometry the player collides with, not a re-derivation. Both
//! maps are proper rotations, so a face's winding (and the sign of `n·dir`) survives them.
//!
//! Output is Blizzard data — pipe it to the scratchpad, never into the repo.
use benilla_assets::coords::{placement_rotation, wow_to_bevy};
use bevy::prelude::*;
/// The reference's facing gate: `[0x80c5c4] = 0xb727c5ac`. A face is processed iff `n·dir <= EPS`.
const FACING_EPS: f32 = f32::from_bits(0xb727_c5ac);
fn main() -> anyhow::Result<()> {
let mut args = std::env::args().skip(1);
let usage = || anyhow::anyhow!("usage: face_facing_at <map> <x> <y> <z> [wmo-substring]");
let map = args.next().ok_or_else(usage)?;
let x: f32 = args.next().ok_or_else(usage)?.parse()?;
let y: f32 = args.next().ok_or_else(usage)?.parse()?;
let z: f32 = args.next().ok_or_else(usage)?.parse()?;
let filter = args.next().unwrap_or_default().to_lowercase();
let data = benilla_formats::wow_data().expect("no WoW install found (set $WOW_DATA)");
let mut chain = benilla_formats::open_chain(&data)?;
let tiles = benilla_formats::MapTiles::load(&mut chain, &map)?;
let pin = wow_to_bevy([x, y, z]);
// Falling: WoW −Z, which `wow_to_bevy` carries to Bevy −Y.
let dir = Vec3::NEG_Y;
println!("{map}: pin ({x}, {y}, {z}) — faces under a straight-down approach\n");
// Every placement in the 3x3 tile neighbourhood, deduped by uniqueId (a building spans tiles).
let mut seen = std::collections::HashSet::new();
let mut placements: Vec<(String, Transform, u32)> = Vec::new();
for (tx, ty) in tiles.existing_in_radius(x, y, 1) {
let tile = benilla_formats::load_tile_mesh(&mut chain, &map, tx, ty)?;
for w in &tile.wmos {
if !filter.is_empty() && !w.model.to_lowercase().contains(&filter) {
continue;
}
if seen.insert(w.unique_id) {
placements.push((
w.model.clone(),
Transform {
translation: wow_to_bevy(w.position),
rotation: placement_rotation(w.rotation),
scale: Vec3::ONE,
},
w.unique_id,
));
}
}
}
let mut hits: Vec<Hit> = Vec::new();
for (model, transform, uid) in &placements {
let stem = model.to_ascii_lowercase().replace('\\', "/");
let stem = stem.strip_suffix(".wmo").unwrap_or(&stem).to_string();
// Walk group files by index — the same `<stem>_NNN.wmo` naming the loader uses. A gap is not
// expected, but tolerate a few misses rather than trusting a count we did not read.
let (mut gi, mut misses) = (0u32, 0u32);
while misses < 4 {
let Ok(gbytes) = chain.read_file(&format!("{stem}_{gi:03}.wmo")) else {
misses += 1;
gi += 1;
continue;
};
misses = 0;
// The WALKING gather — the player-body audience, skip DETAIL (`0x04`). Identical call to
// the one the WMO asset loader makes.
let (mut pos, mut idx) = (Vec::new(), Vec::new());
benilla_formats::accumulate_wmo_group_collision(&gbytes, &mut pos, &mut idx);
let verts: Vec<Vec3> = pos
.iter()
.map(|p| transform.transform_point(wow_to_bevy(*p)))
.collect();
for t in idx.chunks_exact(3) {
let (Some(&a), Some(&b), Some(&c)) = (
verts.get(t[0] as usize),
verts.get(t[1] as usize),
verts.get(t[2] as usize),
) else {
continue;
};
let Some(height) = plane_height_under(pin, [a, b, c]) else {
continue;
};
let n = (b - a).cross(c - a);
if n.length_squared() < 1e-18 {
continue;
}
hits.push(Hit {
height,
dot: n.normalize().dot(dir),
group: gi,
uid: *uid,
model: model.clone(),
});
}
gi += 1;
}
}
if hits.is_empty() {
println!("no walking-collidable WMO face has this pin in its footprint");
return Ok(());
}
hits.sort_by(|p, q| {
(p.height - pin.y)
.abs()
.total_cmp(&(q.height - pin.y).abs())
});
println!(
"{:>9} {:>9} {:>9} {:>8} {:>4} wmo",
"worldZ", "Δ pin", "n·dir", "ref", "grp"
);
let (mut blocks, mut passes) = (0usize, 0usize);
for h in &hits {
// The pin is Bevy-space; its Y is the WoW Z the tester read off the panel.
let reference_blocks = h.dot <= FACING_EPS;
if reference_blocks {
blocks += 1;
} else {
passes += 1;
}
println!(
"{:>9.2} {:>9.2} {:>9.4} {:>8} {:>4} #{} {}",
h.height,
h.height - pin.y,
h.dot,
if reference_blocks { "BLOCKS" } else { "PASSES" },
h.group,
h.uid,
h.model,
);
}
println!(
"\n{} face(s) under the pin: reference blocks on {blocks}, falls through {passes}. \
We block on all {} (parry trimesh is two-sided).",
hits.len(),
hits.len()
);
Ok(())
}
struct Hit {
/// World height (Bevy +Y = WoW +Z) where the pin's vertical line meets this face's plane.
height: f32,
/// The authored winding normal dotted with the fall direction — the reference's gate input.
dot: f32,
group: u32,
uid: u32,
model: String,
}
/// Where the vertical line through `pin` meets the triangle's plane — `None` unless the pin's
/// horizontal footprint is inside the triangle (the XZ projection in Bevy space, since +Y is up).
fn plane_height_under(pin: Vec3, tri: [Vec3; 3]) -> Option<f32> {
let p = Vec2::new(pin.x, pin.z);
let [a, b, c] = tri.map(|v| Vec2::new(v.x, v.z));
let area = (b - a).perp_dot(c - a);
if area.abs() < 1e-9 {
return None; // edge-on to the vertical: no footprint to stand in
}
let (u, v) = (
(b - p).perp_dot(c - p) / area,
(c - p).perp_dot(a - p) / area,
);
let w = 1.0 - u - v;
(u >= 0.0 && v >= 0.0 && w >= 0.0).then(|| u * tri[0].y + v * tri[1].y + w * tri[2].y)
}