mercs2_probe
Headless WAD diagnostic and export tool for the Mercenaries 2 modernization — it runs the
diagnostics carved out of mercs2_engine::diag as subcommands, and never opens a window.
What it is
A binary-only crate. It ships two kinds of executable:
1. mercs2_probe itself (src/main.rs) — one subcommand per diagnostic. Most delegate straight
to a mercs2_engine::diag::* entry point; a handful are implemented in main.rs itself. The full
set, exactly as matched in main():
| subcommand | what it does |
|---|---|
c3-meta <out.ndjson> |
diag::c3_meta |
placement-hashes <out.json> |
diag::placement_hashes |
export-c3-obj [outdir] |
diag::export_c3_obj |
terrainmesh-probe [block] |
diag::terrainmesh_probe |
terrain-probe / terrain-consumer |
diag::terrain_probe / diag::terrain_consumer_scan |
wad-list |
diag::wad_list |
wad-meshes [--model H] |
diag::wad_meshes |
placement-probe / placement-names |
diag::placement_probe / diag::placement_names |
world-index / stream-probe |
diag::world_index_probe / diag::stream_probe |
animdiag / animcheck / skincheck [--model H] [--index I] |
the animation diagnostics |
trackmap [--model H] [--index I] [--clip H] |
diag::trackmap |
entity-find [0xKEY ...] |
diag::entity_find |
comp-probe / comp-dump [Name] |
reflection-component census / dump (comp-dump defaults to HibernationControl) |
block-grep <needle> |
diag::block_grep |
scan-hash <0xH ...> / find-ref <0xH ...> |
diag::scan_hash / diag::find_ref |
block-probe <index> |
diag::block_probe |
gfx-extract [outdir] |
diag::gfx_extract — dumps the Scaleform movies (default output/gfx_movies) |
extract --model 0xHASH [out.bin] |
wad::extract_container → file |
dump-block <index> [out.bin] |
wad::decompress_block_index → file |
find-placement <name-substring> |
scans blocks 0..6000, prints every placement whose name matches |
hier --model 0xHASH [names.txt] [--csv path] |
HIER bone tree with name resolution (see below) |
overlays <profile-path> |
resolves a save's active vz_state layers and folds them through the real streaming catalog |
save-dump <profile-path> [grep-needle] |
parsed .profile header + the decompressed SaveSingleton Lua payload |
2. ~34 single-purpose dev bins under src/bin/ — each is a standalone investigation, run with
cargo run -p mercs2_probe --bin <name>. They cluster into:
-
Model assembly / LOD:
lod_chunks,lodchain_probe,model_blocks,segm_join,assembly_probe,assembly_check,overlap_probe,stray_probe,viewstate_probe,header_diff,rigid_probe. -
Node binding (which HIER node places which mesh):
indx_dump,segfix_probe,segm_probe2,node_witness,place_probe,hier_dump,hier_chain. -
Destruction / draw gate:
sm_dump,gate_probe,govern_probe,seedcmp,mesh_probe.health_probewas removed (2026-07-26). It printed drawn/total draw groups per health band, but assembled geometry fromextract_container+build_indexed_allon the resident block alone — which for a vehicle is the far-LOD proxy (game_world.rs: "renders 371-triangle tanks"). Its counts described the coarse stand-in, not what the engine draws, and read as a destruction bug:ch_veh_apc_wz551showed 3/13 groups pristine and 0/13 destroyed. Measured through the full LOD chain (Model::load, asgovern_probedoes) the same model gives 39/78 → 49/78 → 9/78, which is the expected shape.seedcmpreplaces it and additionally compares bothNodeSeedvariants. If a per-health probe is wanted again, bring it back under a name that says what it measures — and load the LOD chain. -
Animation:
action_table_probe,charanim,clipbind,swim_clip_probe. -
Textures / materials / FX / lights:
tex_dump,texcmp,mtrl_probe,fx_probe,light_probe. -
Archive / naming:
aset_probe(every ASET row a name-hash owns + a multi-block striping census),model_namer(derives a model's name from the textures it uses, and can emit a rainbow- table name fragment). -
Water:
watermap_probe— resolves thewatrsingleton throughAssetSource::extract_singletonand reports grid, world extent, wet/dry census, waterline range and the Layer 0 × Layer 1 cross-tab. The cross-tab is the regression guard on the 44-byte header: a wet cell carrying the dry sentinel (or vice versa) means the height field has slipped against the mask, which on retail is silent and costs 4,681 coastline cells. It must read0on both misalignment rows.
Where it comes from
vz.wad is the oracle. Every probe reads the retail archive and reports what is actually on disk —
nothing here reimplements exe code, it interrogates the data the exe consumes, via
mercs2_engine::{wad, mesh, worldutil, diag} and mercs2_formats::{orchestrator, placement, save, hash}.
Provenance the source itself records:
mercs2_engine::diagstates these subcommands were carved out of the engine'smain.rsand are render-agnostic: they read the WAD and print/emit analysis, never opening a window.sm_dumpcarries the cracked destruction state vocabulary (InitState 0x0ACE072A,PristineState 0xACB51200,DamagedState 0x1D5575A1,InitDestroyedState 0x5D308F4F,DestroyedState 0x7687DF41,StartDestroyedState 0x92791EBB,GoneState 0xCA261E5B,DamageMsg 0xC6507EE1,DestroyMsg 0x1ED7AD78,DestroyMsg2 0x3D0D4C99).gate_probe/viewstate_probe/header_diffare written against the engine's draw gate: clause 3 indexesOBJ+0x2a0by the SEGM record's signednode, and the LOD rung is clamped to[minLOD (M+0x80), maxLOD-1 (M+0x7c)]—header_diffexists precisely becauseminLOD's on-disk source was never located.action_table_probedocuments the container layout it verified inucfx_byteswap::convert+action_table.rs, and targets the base-game ActionTable (typeanimationtable 0x207359C7, asset0x6802C321).mesh_probecross-referencesdocs/ucfx_tag_registry.md§3/§6;indx_dumpexists to falsify a claim inaccessory_bone_binding_B.md.swim_clip_proberelies on state names beingpandemic_hash_m2(name)— verified withm2("Upright") = 0x12C07B18,m2("Fidget") = 0x0C0A7FA6.save-dumpprints the.profileheader fields this project pinned: character index0x4D, upgrade index0x4F, unlocked costumes0x24A,0x24B, flags0x4C, plus the rawheader[0x240..0x260]window and the zlib-decompressed Lua payload.
Usage
vz.wad auto-discovers from the EA Games registry key; override it with --wad <path>:
HKLM\SOFTWARE\WOW6432Node\EA Games\Mercenaries 2 World in Flames\Install Dir
# archive census
# a model's bone tree, with names resolved and a CSV alongside
# pull one container / one block out to disk
# hunt a hash / a string / a placement across every block
# save files
# Scaleform movies out of the WAD
# explicit WAD
The dev bins take a model name (hashed with pandemic_hash_m2) or a 0xHASH, positionally:
mercs2_probe with no subcommand (or -h / --help / help) prints the usage summary and exits 2;
a failing diagnostic exits 1.
Notes / gotchas
--wadis a main-binary flag only, but everysrc/bin/*dev bin now resolvesvz.wadthroughwad::resolve_vz_wad(None), soVZ_WADoverrides them all.VZ_WADtakes the install root, itsdatafolder, or thevz.wadfile itself. The dev bins previously went straight to the EA Games registry key and panicked without it, which made them unrunnable off Windows and after a copied-off install. The main binary's--wadaccepts the same folder-or-file forms.- Flag values are consumed positionally:
flag_valreads the token after--wad/--model/--index/--clip/--csv, andfirst_positionalskips exactly those five flags and their values. A flag value that itself starts with--is rejected. hier's name resolution is rainbow-table first, then an optional plain-text candidate file (one string per line, hashed withpandemic_hash_m2and accepted only if the hash is actually present in this model's HIER) — unresolved nodes print as?.model_namerdeliberately does not ask "is this candidate's hash anywhere in ASET" — 32-bitpandemic_hash_m2would fabricate names. It only asks whether a candidate generated from this model's own materials hashes to this model's hash (~50-1000 candidates against one target).aset_probeexists becausewad::extract_containerresolves one ASET row (primary, else any) and slices one span out of one block — if an asset's rows span several blocks, that call returns a fragment, not the asset.find-placementbrute-scans blocks0..6000and stops after 96 consecutive decompress failures.tex_dumpwrites uncompressed BMP on purpose (no extra deps) and lives in a separate binary so it isn't locked by a running game.