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,health_probe,mesh_probe. - 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).
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. Everysrc/bin/*dev bin resolvesvz.wadthroughwad::registry_vz_wad()and panics if the registry key is absent — they have no override.- 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.