mercs2_probe 2.2.2

Mercenaries 2 modernization — headless WAD diagnostic/export tool. Runs the carved-out diagnostics from mercs2_engine::diag as subcommands (no window).
//! Ignored probe: confirm the PMC HQ interior hall mesh carries the `hp_playerA_enter` hardpoint in
//! its HIER, and that (actor origin {3750,450,-3840}) + (hardpoint local) reproduces the vanilla
//! `_TeleportHero` target (3794.04, 450.75, -3911.03) — so the interior spawn can be DERIVED from data
//! (actor pos + mesh hardpoint) instead of a baked constant.
//!
//! ```text
//! cargo test -p mercs2_probe --test hardpoint_probe -- --nocapture
//! ```

use mercs2_engine::wad;
use mercs2_formats::hash::pandemic_hash_m2 as m2;

const ACTOR_ORIGIN: [f32; 3] = [3750.0, 450.0, -3840.0];

#[test]
fn interior_hardpoint_derives_the_spawn() {
    let Some(path) = wad::resolve_vz_wad(None) else {
        return eprintln!(
            "SKIPPING: no vz.wad discovered. Run `scripts/find-vz-wad.sh --write` or set MERCS2_GAME_DIR."
        );
    };
    let mut w = wad::open(&path).expect("open vz.wad");

    // The ornate main hall the player teleports onto (the "HqInterior" actor mesh).
    for mesh_name in ["pmcoutpost_interior_hq", "proutpost_interior_job"] {
        let hash = m2(mesh_name);
        let Ok(container) = wad::extract_container(&mut w, hash) else {
            println!("{mesh_name} (0x{hash:08X}): container not found");
            continue;
        };
        let hier = mercs2_formats::orchestrator::parse_hier(&container);
        println!("\n{mesh_name} (0x{hash:08X}): {} HIER nodes", hier.len());

        let hp_enter = m2("hp_playerA_enter");
        if let Some(idx) = hier.iter().position(|n| n.hash == hp_enter) {
            // Compose the parent chain to a ROOT-relative matrix (the local is parent-relative).
            let root = node_root_matrix(&hier, idx);
            let t = [root[12], root[13], root[14]]; // row-major translation
            let world = [ACTOR_ORIGIN[0] + t[0], ACTOR_ORIGIN[1] + t[1], ACTOR_ORIGIN[2] + t[2]];
            println!(
                "  hp_playerA_enter root-relative=({:.2},{:.2},{:.2}) -> actor+hp = ({:.2},{:.2},{:.2})",
                t[0], t[1], t[2], world[0], world[1], world[2]
            );
            println!("    [vanilla _TeleportHero = 3794.04, 450.75, -3911.03]");
            if mesh_name == "pmcoutpost_interior_hq" {
                // The hardpoint IS in the mesh HIER and X derives exactly (3794 vs vanilla 3794.04).
                // The Z/Y deltas (−3820 vs −3911; +0.75) come from the actor's `sAnchorHardpoint` —
                // SpawnActor anchors the actor by a hardpoint, not by origin — so exact vanilla
                // reproduction needs that anchor offset too. This probe confirms the DATA is present.
                assert!((world[0] - 3794.04).abs() < 2.0, "hp_playerA_enter X derives from mesh data");
            }
        } else {
            println!("  hp_playerA_enter NOT in this mesh's HIER");
        }
    }
}

/// Root-relative transform of HIER node `idx` = local · parent.local · … · root.local (row-major).
fn node_root_matrix(hier: &[mercs2_formats::orchestrator::HierNode], idx: usize) -> [f32; 16] {
    let mut m = hier[idx].local;
    let mut p = hier[idx].parent;
    while let Some(pi) = p {
        m = mat4_mul_rowmajor(&m, &hier[pi].local);
        p = hier[pi].parent;
    }
    m
}

/// Row-major 4×4 multiply: (a·b)[r][c] = Σ a[r][k]·b[k][c].
fn mat4_mul_rowmajor(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] {
    let mut o = [0.0f32; 16];
    for r in 0..4 {
        for c in 0..4 {
            let mut s = 0.0;
            for k in 0..4 {
                s += a[r * 4 + k] * b[k * 4 + c];
            }
            o[r * 4 + c] = s;
        }
    }
    o
}