- WMO water stops running the terrain-liquid program: the material key names WHICH renderer a pool takes, and the two WMO arms land — Blackfathom Deeps' interior pools take their authored per-vertex opacity and the material's own body colour (34, 38, 57 there — the dark blue-grey in the reference shot) instead of coming out opaque and daylit on the zone's river swatch - the exterior WMO arm bands at sub-17, not sub-16 — teal, not olive — which is what the Stormwind canals actually read as - a hole corner's authored height no longer counts as a height, and a corpus-wide instrument reports which WMO water arm every pool takes - the ADT/Felwood golden sweep is bit-identical: open-world water is provably untouched Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
161 lines
6.4 KiB
Rust
161 lines
6.4 KiB
Rust
//! **Which WMO water ARM each pool takes**, over the whole shipped corpus:
|
|
//! `cargo run -p benilla-formats --example wmo_liquid_arms`.
|
|
//!
|
|
//! The reference does not have *one* WMO water renderer. `0x6b62e0` dispatches the type nibble to a
|
|
//! category, and category 0 (river/water — nibbles 0/4/8) then splits again on the owning group's
|
|
//! `MOGP.flags & 0x48`:
|
|
//!
|
|
//! * `& 0x48 != 0` → **EXTERIOR**, kernel `0x6b6630`. A 9-float (`0x24`) vertex — position, an up
|
|
//! normal, a colour dword, `u,v` — and the kernel binds the pixel program
|
|
//! `Shaders\Pixel\MapObjExtWater0.bls` at `0x6b6654` (`GxRsSet(0x3f, …)`, gated on `[0xc9607c]`).
|
|
//! * `& 0x48 == 0` → **INTERIOR**, kernel `0x6b6420`. A 6-float (`0x18`) vertex — position, a colour
|
|
//! dword, `u,v` — with **no normal at all**, the colour coming from the map object's own light
|
|
//! records (`CMapObj+0x1d8`).
|
|
//!
|
|
//! The arm split is **category 0 only**. Magma and slime (nibbles 2/3/6/7) go to category 1 and one
|
|
//! shared kernel `0x6b68f0` whatever their group's flags say, so their rows below are context, not
|
|
//! two arms — they are reported because the same building often holds both, and because a reader
|
|
//! comparing group counts would otherwise assume the split is universal.
|
|
//!
|
|
//! Neither water arm is the **ADT** path (`0x6851b0`/`0x685010` → `ocean0_s.bls`), which is the only
|
|
//! one with a depth-ramp texture on stage 0 and the only one whose vertex carries no colour.
|
|
//!
|
|
//! benilla renders all three through one material and one shader — the ADT one. This census says how
|
|
//! much content each arm actually owns, so the divergence can be sized instead of guessed: the two
|
|
//! reported sites (B136 Blackfathom Deeps, and Stormwind's canals) land on opposite arms, and
|
|
//! neither is the path we implement.
|
|
//!
|
|
//! Output is Blizzard data — pipe it to the scratchpad, never into the repo.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use benilla_formats::{wmo_group_header, wmo_group_liquid_mesh, Chain};
|
|
|
|
/// MOGP `flags & 0x48` — the reference's interior test (`0x6b3f90`, and the liquid dispatch's own
|
|
/// `[owner+0x10] & 0x48` at `0x6b62e0`). Zero ⇒ interior.
|
|
const EXTERIOR_MASK: u32 = 0x48;
|
|
|
|
#[derive(Default, Clone, Copy)]
|
|
struct Tally {
|
|
groups: u32,
|
|
wet_tiles: u32,
|
|
}
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
let data = benilla_formats::wow_data()
|
|
.ok_or_else(|| anyhow::anyhow!("no 1.12.1 install found (set $WOW_DATA)"))?;
|
|
let reader = Chain::open(&data)?;
|
|
|
|
// Group files are `<root stem>_NNN.wmo`; the roots are what the chain lists, so walk the listing
|
|
// once and keep the group files directly — a root's group count is not knowable without parsing
|
|
// it, and the names are self-describing.
|
|
let mut by_arm: BTreeMap<(bool, String), Tally> = BTreeMap::new();
|
|
// Per building, so a site can be looked up rather than re-derived.
|
|
let mut buildings: BTreeMap<String, (u32, u32)> = BTreeMap::new();
|
|
|
|
for entry in reader.list()? {
|
|
let name = entry.name;
|
|
let lower = name.to_ascii_lowercase();
|
|
if !lower.ends_with(".wmo") {
|
|
continue;
|
|
}
|
|
// A group file's stem ends `_NNN`; a root's does not.
|
|
let stem = lower.trim_end_matches(".wmo");
|
|
let is_group = stem
|
|
.rsplit('_')
|
|
.next()
|
|
.is_some_and(|t| t.len() == 3 && t.bytes().all(|b| b.is_ascii_digit()));
|
|
if !is_group {
|
|
continue;
|
|
}
|
|
let Ok(bytes) = reader.read(&name) else {
|
|
continue;
|
|
};
|
|
let Some(mesh) = wmo_group_liquid_mesh(&bytes) else {
|
|
continue;
|
|
};
|
|
let Some(header) = wmo_group_header(&bytes) else {
|
|
continue;
|
|
};
|
|
let interior = header.flags & EXTERIOR_MASK == 0;
|
|
let wet = mesh.wet.iter().filter(|w| **w).count() as u32;
|
|
|
|
let t = by_arm
|
|
.entry((interior, format!("{:?}", mesh.kind)))
|
|
.or_default();
|
|
t.groups += 1;
|
|
t.wet_tiles += wet;
|
|
|
|
let root = stem
|
|
.rsplit_once('_')
|
|
.map_or(stem, |(r, _)| r)
|
|
.rsplit(['\\', '/'])
|
|
.next()
|
|
.unwrap_or(stem)
|
|
.to_string();
|
|
if !mesh.kind.is_fullbright() {
|
|
let e = buildings.entry(root).or_default();
|
|
if interior {
|
|
e.0 += 1;
|
|
} else {
|
|
e.1 += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
println!("WMO liquid groups by ARM and kind (interior = MOGP.flags & 0x48 == 0)\n");
|
|
println!(
|
|
"{:<10} {:<8} {:>8} {:>11} kernel",
|
|
"arm", "kind", "groups", "wet tiles"
|
|
);
|
|
let (mut int_g, mut ext_g, mut int_t, mut ext_t) = (0, 0, 0, 0);
|
|
for ((interior, kind), t) in &by_arm {
|
|
// Only category 0 splits on the flag; the fullbright kinds share `0x6b68f0` either way.
|
|
let water = !matches!(kind.as_str(), "Magma" | "Slime");
|
|
println!(
|
|
"{:<10} {:<8} {:>8} {:>11} {}",
|
|
if *interior { "INTERIOR" } else { "EXTERIOR" },
|
|
kind,
|
|
t.groups,
|
|
t.wet_tiles,
|
|
match (water, interior) {
|
|
(true, true) => "0x6b6420 (interior water)",
|
|
(true, false) => "0x6b6630 (exterior water, MapObjExtWater0.bls)",
|
|
(false, _) => "0x6b68f0 (magma/slime — arm-blind)",
|
|
}
|
|
);
|
|
if !water {
|
|
continue; // the totals below are the WATER arms; fullbright takes neither
|
|
}
|
|
if *interior {
|
|
int_g += t.groups;
|
|
int_t += t.wet_tiles;
|
|
} else {
|
|
ext_g += t.groups;
|
|
ext_t += t.wet_tiles;
|
|
}
|
|
}
|
|
println!(
|
|
"\nWATER arms — INTERIOR {int_g} groups / {int_t} wet tiles EXTERIOR {ext_g} groups / {ext_t} wet tiles"
|
|
);
|
|
|
|
// Buildings carrying WATER on both arms: a single-shader renderer cannot be right for the whole
|
|
// building. (Undercity holds both flags too, but its liquid is slime, which takes neither arm —
|
|
// hence the water-only filter.)
|
|
let mixed: Vec<_> = buildings
|
|
.iter()
|
|
.filter(|(_, (i, e))| *i > 0 && *e > 0)
|
|
.collect();
|
|
println!("\nbuildings carrying WATER on BOTH arms: {}", mixed.len());
|
|
for (root, (i, e)) in mixed.iter().take(20) {
|
|
println!(" {root:<40} interior {i:>3} exterior {e:>3}");
|
|
}
|
|
|
|
for site in ["stormwind", "blackfathom_instance"] {
|
|
if let Some((i, e)) = buildings.get(site) {
|
|
println!("\n{site} water groups: interior {i}, exterior {e}");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|