mercs2_probe 2.0.0

Mercenaries 2 modernization — headless WAD diagnostic/export tool. Runs the carved-out diagnostics from mercs2_engine::diag as subcommands (no window).
mercs2_probe-2.0.0 is not a library.

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::diag states these subcommands were carved out of the engine's main.rs and are render-agnostic: they read the WAD and print/emit analysis, never opening a window.
  • sm_dump carries 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_diff are written against the engine's draw gate: clause 3 indexes OBJ+0x2a0 by the SEGM record's signed node, and the LOD rung is clamped to [minLOD (M+0x80), maxLOD-1 (M+0x7c)]header_diff exists precisely because minLOD's on-disk source was never located.
  • action_table_probe documents the container layout it verified in ucfx_byteswap::convert + action_table.rs, and targets the base-game ActionTable (type animationtable 0x207359C7, asset 0x6802C321).
  • mesh_probe cross-references docs/ucfx_tag_registry.md §3/§6; indx_dump exists to falsify a claim in accessory_bone_binding_B.md.
  • swim_clip_probe relies on state names being pandemic_hash_m2(name) — verified with m2("Upright") = 0x12C07B18, m2("Fidget") = 0x0C0A7FA6.
  • save-dump prints the .profile header fields this project pinned: character index 0x4D, upgrade index 0x4F, unlocked costumes 0x24A, 0x24B, flags 0x4C, plus the raw header[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
cargo run -p mercs2_probe -- wad-list
cargo run -p mercs2_probe -- comp-probe
cargo run -p mercs2_probe -- comp-dump HibernationControl

# a model's bone tree, with names resolved and a CSV alongside
cargo run -p mercs2_probe -- hier --model 0x39AF17DC --csv hier.csv

# pull one container / one block out to disk
cargo run -p mercs2_probe -- extract --model 0x39AF17DC model.bin
cargo run -p mercs2_probe -- dump-block 667 block667.bin

# hunt a hash / a string / a placement across every block
cargo run -p mercs2_probe -- scan-hash 0xA3CD72A7
cargo run -p mercs2_probe -- find-placement villa

# save files
cargo run -p mercs2_probe -- save-dump "C:/.../save0.profile" cash
cargo run -p mercs2_probe -- overlays "C:/.../save0.profile"

# Scaleform movies out of the WAD
cargo run -p mercs2_probe -- gfx-extract

# explicit WAD
cargo run -p mercs2_probe -- --wad D:/game/data/vz.wad comp-probe

The dev bins take a model name (hashed with pandemic_hash_m2) or a 0xHASH, positionally:

cargo run -p mercs2_probe --bin sm_dump        -- ch_veh_tank_ztz98
cargo run -p mercs2_probe --bin lod_chunks     -- ch_veh_tank_ztz98
cargo run -p mercs2_probe --bin node_witness   -- ch_veh_tank_ztz98
cargo run -p mercs2_probe --bin rigid_probe    -- vz_veh_tank_amx30_elite
cargo run -p mercs2_probe --bin gate_probe     -- oc_veh_helicopter_md500
cargo run -p mercs2_probe --bin charanim                             # defaults to pmc_hum_mattias_v3
cargo run -p mercs2_probe --bin aset_probe     -- 0x9FCAE910 0x89D8DE72
cargo run -p mercs2_probe --bin model_namer    -- --emit fragment.json

mercs2_probe with no subcommand (or -h / --help / help) prints the usage summary and exits 2; a failing diagnostic exits 1.

Notes / gotchas

  • --wad is a main-binary flag only. Every src/bin/* dev bin resolves vz.wad through wad::registry_vz_wad() and panics if the registry key is absent — they have no override.
  • Flag values are consumed positionally: flag_val reads the token after --wad / --model / --index / --clip / --csv, and first_positional skips 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 with pandemic_hash_m2 and accepted only if the hash is actually present in this model's HIER) — unresolved nodes print as ?.
  • model_namer deliberately does not ask "is this candidate's hash anywhere in ASET" — 32-bit pandemic_hash_m2 would 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_probe exists because wad::extract_container resolves 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-placement brute-scans blocks 0..6000 and stops after 96 consecutive decompress failures.
  • tex_dump writes uncompressed BMP on purpose (no extra deps) and lives in a separate binary so it isn't locked by a running game.