mercs2_probe 2.1.1

Mercenaries 2 modernization — headless WAD diagnostic/export tool. Runs the carved-out diagnostics from mercs2_engine::diag as subcommands (no window).
//! Acceptance test for `mercs2_engine::model::Model` — the cross-block assembly.
//!
//! For each model: the LOD chain it loaded, and then the FULL three-clause draw gate evaluated at
//! every LOD rung x {intact, wrecked}. A correct assembly means rung 0 (the camera up close) draws
//! the detailed body, not a 371-triangle `_lod_dm` proxy, and that wrecking it swaps the geometry
//! rather than piling wreck on top of hull.
//!
//!   cargo run -p mercs2_probe --bin assembly_check

use mercs2_engine::model::Model;
use mercs2_engine::render_state::RenderState;
use mercs2_engine::wad;
use mercs2_formats::orchestrator as orch;

const MODELS: &[&str] = &[
    "ch_veh_tank_ztz98",
    "oc_veh_helicopter_md500",
    "global_veh_klr650",
    "civ_veh_car_van_crappy",
    "al_veh_boat_destroyer",
    "pmc_hum_mattias_v3",
];

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let names: Vec<String> =
        if args.is_empty() { MODELS.iter().map(|s| s.to_string()).collect() } else { args };
    let mut w = wad::resolve_vz_wad(None).and_then(|p| wad::open(&p).ok()).expect("vz.wad");

    for name in &names {
        let hash = mercs2_formats::hash::pandemic_hash_m2(name.trim_start_matches('_'));
        let m = match Model::load(&mut w, hash) {
            Ok(m) => m,
            Err(e) => {
                println!("{name}: {e}\n");
                continue;
            }
        };
        println!(
            "{name}{} rung(s), {} tri total, {} HIER, {} SEGM, lod_count {}",
            m.rungs.len(),
            m.triangles(),
            m.hier.len(),
            m.segm.len(),
            m.lod_count()
        );
        for r in &m.rungs {
            // Do all rungs live in the SAME space? If a fine rung's bbox doesn't sit on top of the
            // resident one, merging them into one buffer stacks misaligned copies of the object.
            let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
            for v in &r.vertices {
                for i in 0..3 {
                    lo[i] = lo[i].min(v.pos[i]);
                    hi[i] = hi[i].max(v.pos[i]);
                }
            }
            println!(
                "        bbox  min [{:7.2} {:7.2} {:7.2}]  max [{:7.2} {:7.2} {:7.2}]",
                lo[0], lo[1], lo[2], hi[0], hi[1], hi[2]
            );
            println!(
                "     P{:03} block {:5}  {:6} tri  serves rungs {:?}",
                r.level,
                r.block,
                r.triangles(),
                (0..8u8).filter(|b| r.lod_bits() & (1 << b) != 0).collect::<Vec<_>>()
            );
            // The mask histogram AS THE MODEL BOUND IT — if a rung's segments resolved against the
            // wrong SEGM row, the garbage shows up here as masks that don't belong to its band.
            let mut hist: std::collections::BTreeMap<u8, (usize, u32, usize)> = Default::default();
            for d in &r.draws {
                let e = hist.entry(d.lod_mask).or_insert((0, 0, usize::MAX));
                e.0 += 1;
                e.1 += d.index_count / 3;
                e.2 = e.2.min(d.group_index);
            }
            for (mask, (n, tris, _)) in &hist {
                println!("            mask {mask:#04x}  {n:3} draws  {tris:6} tri");
            }
        }

        // Full gate, per LOD rung, at full and zero health.
        print!("     {:>10}", "LOD rung");
        for n in 0..m.lod_count().min(8) {
            print!(" {:>8}", n);
        }
        println!();
        for (label, health) in [("intact", 1.0f32), ("wrecked", 0.0)] {
            let node_enable = match &m.machine {
                Some(sm) => {
                    let chosen = orch::node_states_for_health(sm, health, 0.99);
                    orch::machine_node_enable(sm, &m.hier, &chosen)
                }
                None => Vec::new(),
            };
            print!("     {label:>10}");
            for n in 0..m.lod_count().min(8) {
                let rs = RenderState {
                    lod: n as u8,
                    view_state: 1u8 << (n.min(7)),
                    node_enable: node_enable.clone(),
                };
                let tris: u32 =
                    m.visible_draws(&rs).iter().map(|(_, d)| d.index_count / 3).sum();
                print!(" {tris:>8}");
            }
            println!();
        }
        println!();
    }
}