Files
benilla-octo/crates/benilla-formats/examples/m2_shared_section.rs
T
Sam SungandClaude Opus 5.5 8952d590e6 comments cite the reference by address, not by private note (#30)
Comments that pointed into the maintainer's private reverse-engineering notes (a note by name, a
path, a section or finding id, the analysis's own name for a client function, a process word) now
carry the fact itself, cited by the reference client's address, by vmangos file and line, or as
"the reference". Nobody outside could follow those pointers.

Comment text only, plus the test-assert and log strings that carried the same pointers. No claim
changes: a comment the notes have since corrected keeps its wording here, and correcting it is
separate work.

The two reference generators, `scripts/gen-reference-globals.py` and
`scripts/gen-reference-verb-events.py`, named the private notes' layout to find their inputs. They
now take the input files as arguments (`--capture`, `--framexml`, `--names`, `--extents`,
`--disasm`). Run on the same inputs, both write the committed tables row for row; only the two
header lines that named the old environment variable change.

Checked per file:
- the code is identical: each file with its comments stripped compares equal before and after; a
  changed string literal is compared under a mask that proves only literal text moved and every
  literal kept its line count;
- no client address the old text cited was dropped;
- a second reviewer checked every added address against its derivation;
- a detector for the notes' names, paths, ids, function names and process words finds none left;
- `scripts/gates.sh` is green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 12:14:08 +07:00

99 lines
4.4 KiB
Rust

//! **Which M2 batches share their triangles with another batch** — the coplanar-sibling census:
//! `cargo run -p benilla-formats --example m2_shared_section -- [path-substring]`.
//!
//! An M2 skin *section* is a run of triangles; a *batch* is one draw of one section. When two
//! batches name the SAME section they rasterize the SAME triangles — a base layer and a shine /
//! reflect layer authored on top of it (`ARMORREFLECT3`, `BALISTASHINE02`). The reference draws
//! both from one vertex array under depth-write + LEQUAL, so the second always wins the tie
//! exactly (`0x70b3f6`, `0x70c190`).
//!
//! We only match that if both batches take the SAME vertex-transform path. They do not when a
//! consolidator (`static_gx`, `terrain_stream::merge`) admits one and refuses the other: the
//! retained lane bakes world positions on the CPU and the entity lane rotates on the GPU, and the
//! two agree only to the last few ULPs — enough, on coplanar triangles, to turn the depth test
//! into a per-pixel coin flip that re-rolls whenever the camera moves. That is the ballista's
//! flickering bolt heads and shields.
//!
//! So this prints, per model, every multi-batch section and whether its batches DISAGREE on the
//! facts the consolidators exclude on (env-mapped UVs, render flags `0x10` no-depth-write / `0x08`
//! no-depth-test, additive blend 3/4). A `SPLIT` line is a model that can z-fight against itself.
//!
//! Output is Blizzard data — pipe it to the scratchpad, never into the repo.
use std::io::Cursor;
/// The facts every consolidator excludes on — a batch answering `true` can never ride the
/// retained/merged lanes, so it stays on the entity path whatever its siblings do.
fn refusable(model: &benilla_m2::M2Model, batch: &benilla_m2::SkinBatch) -> bool {
let mat = model.materials.get(batch.material_index as usize);
let flags = mat.map_or(0, |m| m.flags.bits());
let blend = mat.map_or(0, |m| m.blend_mode.bits());
model.stage_is_env_mapped(batch, 0)
|| flags & 0x10 != 0
|| flags & 0x08 != 0
|| matches!(blend, 3 | 4)
}
fn main() -> anyhow::Result<()> {
let pat = std::env::args().nth(1).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 names: Vec<String> = chain
.list()?
.into_iter()
.map(|e| e.name)
.filter(|n| {
let l = n.to_lowercase();
l.ends_with(".m2") && l.contains(&pat)
})
.collect();
let (mut models, mut with_shared, mut with_split) = (0usize, 0usize, 0usize);
let (mut batches_total, mut batches_shared, mut batches_split) = (0usize, 0usize, 0usize);
for name in &names {
let Ok(bytes) = chain.read_file(name) else {
continue;
};
let Ok(m2) = benilla_m2::parse_m2(&mut Cursor::new(bytes.as_slice())) else {
continue;
};
let model = m2.model();
let Ok(skin) = model.parse_embedded_skin(&bytes, 0) else {
continue;
};
models += 1;
batches_total += skin.batches().len();
// section index → (batch count, refusable count)
let mut per_section: std::collections::BTreeMap<u16, (usize, usize)> = Default::default();
for b in skin.batches() {
let e = per_section.entry(b.skin_section_index).or_default();
e.0 += 1;
e.1 += usize::from(refusable(model, b));
}
let shared: Vec<_> = per_section.iter().filter(|(_, v)| v.0 > 1).collect();
if shared.is_empty() {
continue;
}
with_shared += 1;
let mut split = false;
for (sec, (n, r)) in &shared {
batches_shared += n;
// Disagreement is the defect: some of this section's batches divert, some cannot.
if *r > 0 && *r < *n {
split = true;
batches_split += n;
println!("SPLIT {name} section {sec}: {n} batches, {r} refusable");
}
}
if split {
with_split += 1;
}
}
println!(
"\n{models} models parsed, {batches_total} batches\n\
{with_shared} models have a multi-batch section ({batches_shared} batches in one)\n\
{with_split} models SPLIT one ({batches_split} batches) — coplanar siblings that land in \
different lanes"
);
Ok(())
}