mod anim_table;
mod bone_census;
mod chunk_compare;
mod dlc_census;
mod pws_survey;
mod tex_compare;
mod tex_rungs;
mod clip_names;
mod clip_pose;
mod dump;
mod dump_hibernation;
mod dump_render;
mod dump_spawnlists;
mod ecs_compare;
mod ecs_survey;
mod ess_live;
mod ess_menu_check;
mod facefx;
mod layer;
mod terrain_cell;
mod gfx_golden;
mod prof_samples;
mod splice_emu;
mod we_template;
use mercs2_engine::diag;
use mercs2_engine::wad;
use mercs2_engine::worldutil::parse_hash;
fn usage() -> ! {
eprintln!(
"usage: mercs2_probe <subcommand> [args] [--wad <path>]\n\
subcommands:\n\
\x20 c3-meta <out.ndjson> placement-hashes <out.json> export-c3-obj <outdir>\n\
\x20 terrainmesh-probe [block] terrain-probe terrain-consumer\n\
\x20 terrain-cell <0xH> [--at <cx> <cz> <x> <z>] (one cell's materials and a patch's draws)\n\
\x20 wad-list wad-meshes [--model H] placement-probe\n\
\x20 world-index stream-probe lod is engine-only\n\
\x20 animdiag/animcheck/skincheck [--model H] [--index I] trackmap [... --clip H]\n\
\x20 entity-find [0xKEY ...] comp-probe comp-dump [Name]\n\
\x20 block-grep <needle> scan-hash <0xH ...> find-ref <0xH ...>\n\
\x20 block-probe <index> placement-names hier --model H [names.txt] [--csv P]\n\
\x20 veg-census <out.json> (foliage instancing: understory coverage + tag-set)\n\
\x20 tree-census <out.json> (foliage instancing: c3-cell canopy trees + instance list)\n\
\x20 bone-census [names.txt] [--csv P] [--wads a.wad,b.wad] (every HIER node in every model)\n\
\x20 gfx-extract <outdir> (every cfx_pack movie of --wad, verbatim)\n\
\x20 gfx-golden <movies_dir> --md <report.md> --json <index.json> (no WAD needed)\n\
\x20 gfx-decode <movies_dir> (decode every movie's definitions; no WAD needed)\n\
\x20 gfx-render <movie.gfx> <frames> <out.png> [--fonts <font.gfx>] [--goto <label>] <images.wad>... (play a movie, write its last frame)\n\
\x20 gfx-shell <out.png> <width>x<height> [wait:<frames>|down:<button>|up:<button>|mouse:<x>,<y>,<buttons>|call:<name>|key:<id>|pad:<vendor>,<product>,<name>|held:<input>,...|get:<path>]... <wad, last mounted first>... (boot the shipped shell headless, movies from the last WAD's Movies folder)\n\
\x20 gfx-actions <movie.gfx> <out.txt> (write a movie's ActionScript as text)\n\
\x20 gfx-tags <movie.gfx> <out.txt> (write a movie's display lists as text: places, removes, scripts, labels per frame)\n\
\x20 bink-audio <movie.bik> <track index> <out.s16> [<other.s16>] (decode a track; compare with another decode)\n\
\x20 bink-video <movie.bik> <frames> <reference.yuv> (decode the first frames; compare with a yuv420p decode)\n\
\x20 splice-emulate <image.bin> <start VA> [--reg name=value]... [--arg value]... [--byte address=value]... [--dword address=value]... [--steps N] (run a SecuROM splice through a decrypted dump with sentinel registers; report what it read, wrote, called and returned)\n\
\x20 bink-frame <movie.bik> <frame> <out.png> (one frame as the game's movie widget draws it, with decode and colour-conversion times)\n\
\x20 gfx-run <movies_dir> <frames> (play every movie; report script failures and gaps)\n\
\x20 ess-live [--addr host:port] [--menu <scripts-rel path>] [--spawn <template>] [--spawn-dist <m>] (live game, no WAD)\n\
\x20 ess-menu-check --ess <Ess.lua> <menu.lua> [<menu.lua> ...] (exact Lua 5.1, engine stubbed, no WAD)\n\
\x20 extract --model H [--type T] <out.bin> dump-block <index> <out.bin> find-placement <substring>\n\
\x20 save-dump <profile> [needle] overlays <profile>\n\
\x20 anim-table <name|0xH> [--handle 0xH] [--list <column>] (an animationtable's rows verbatim)\n\
\x20 clip-names <review.tsv> (name AnimationLookup clips from their rows; a review file, merges nothing)\n\
\x20 facefx <name|0xH> | --all (a FaceFX actor / anim set by name; --all reads every one and prints the census)\n\
\x20 layer <name|0xH> | --all | --frames <capture> | --loaded <capture> | --near <x> <z> <radius> | --catalog <out.tsv> [--also <archives>] [--overlay <wad>] (a placement layer's entities with their models and classes; --all prints the census; --frames tests Transforms against a capture's live positions; --loaded measures which layers a capture holds; --near lists what every layer places around a point; --catalog writes a row per layer, --overlay mounts a level such as the DLC above the base)\n\
\x20 we-template <name|0xKEY>... | --class <Class> | --uses <0xU32> (worldentity templates / records)\n\
\x20 ecs-survey <big-endian archive> <out.tsv> [--raw <dir>] (a console archive's ECS containers as stored; no --wad)\n\
\x20 ecs-compare <archive a> <archive b> <out.tsv> --vz-wad <pc vz.wad> [--diffs <file>] (two archives' ECS containers, converted, record by record; no --wad)\n\
\x20 ecs-names <archive> <out.tsv> --vz-wad <pc vz.wad> (every Name string of an archive's named containers; no --wad)\n\
\x20 chunk-compare <big-endian archive> <out.tsv> --vz-wad <pc vz.wad> --java <java> --unluac <jar> [--only <type>] (every chunk converted against its PC twin, row by row; no --wad)\n\
\x20 model-compare <big-endian archive> <pc vz.wad> <out.tsv> (every model converted against PC over its whole chain; no --wad)\n\
\x20 scrub-compare <big-endian archive> <pc vz.wad> <out.tsv> (every scrub converted against PC; no --wad)\n\
\x20 phy2-diff <big-endian PHY2 body> <PC body or container> (a collider converted, its differences by region; no --wad)\n\
\x20 rung-words <pc wad> (each type's chains against the rule for their table words; no --wad)\n\
\x20 clip-pose --model H --clip H --bones 0xA,0xB.. --ref 0xR [--frames N] (bone positions over a clip)\n\
\x20 dump-read <capture-dir> <0xVA> [len] (retail memory from a v1.0 x32dbg capture, no WAD)\n\
\x20 dump-chain <capture-dir> <0xVA> <0xOFF>... ([[VA]+o0]+o1 ... ; last offset added, not read)\n\
\x20 dump-find <capture-dir> <0xU32> [0xU32...] (every aligned address holding a value)\n\
\x20 dump-render <capture> (camera, wind and tree batches of the last frame)\n\
\x20 foliage (every tree species of the foliage chunk)\n\
\x20 foliage-check <capture> (every tree entity of a capture against the decoded foliage chunk)\n\
\x20 dump-entity <capture> <0xKEY> (every live component record of one key)\n\
\x20 dump-near <capture> <x> <y> <z> <radius> (every SceneObject around a point)\n\
\x20 dump-model <capture> <0xHASH> (a runtime model's LOD range and its render objects)\n\
\x20 dump-find-vec3 <capture> <x> <y> <z> [--tol T] (three consecutive f32 within T)\n\
\x20 dump-containers <capture-dir> (all 334 live component containers)\n\
\x20 dump-pool <capture-dir> <name|0xVA> [--key 0xK] [--bytes N] [--limit N] (one container's live records)\n\
\x20 dump-spawnlists <capture> (the runtime unit lists built from PopulationList)\n\
\x20 dump-hibernation <capture> (the hibernation grid: viewers, rows, awake count)\n\
(vz.wad auto-discovers from MERCS2_GAME_DIR / VZ_WAD, then the registry; override with --wad <path>)"
);
std::process::exit(2);
}
fn flag_val(args: &[String], name: &str) -> Option<String> {
args.iter()
.position(|a| a == name)
.and_then(|i| args.get(i + 1))
.cloned()
.filter(|v| !v.starts_with("--"))
}
fn hex_args(args: &[String]) -> Vec<u32> {
args.iter()
.filter_map(|a| a.strip_prefix("0x").and_then(|h| u32::from_str_radix(h, 16).ok()))
.collect()
}
fn first_positional(args: &[String]) -> Option<String> {
let mut skip_next = false;
for a in args {
if skip_next {
skip_next = false;
continue;
}
if a == "--wad"
|| a == "--wads"
|| a == "--model"
|| a == "--index"
|| a == "--clip"
|| a == "--csv"
|| a == "--skeleton-csv"
|| a == "--md"
|| a == "--json"
|| a == "--type"
{
skip_next = true;
continue;
}
if a.starts_with("--") {
continue;
}
return Some(a.clone());
}
None
}
fn dump_cli(cmd: &str, args: &[String]) -> i32 {
let hex = |s: &String| u32::from_str_radix(s.trim_start_matches("0x"), 16).ok();
let (Some(dir), Some(va)) = (args.first(), args.get(1).and_then(hex)) else {
eprintln!("{cmd}: usage: mercs2_probe {cmd} <capture-dir> <0xVA> {}", if cmd == "dump-read" { "[len]" } else { "<0xOFF>..." });
return 2;
};
let rest = &args[2..];
let run = || -> Result<(), String> {
let cap = dump::Capture::open(std::path::Path::new(dir))?;
let mem = cap.mem()?;
println!("[{cmd}] {} (build {})", cap.path.display(), cap.build_id);
if cmd == "dump-find" {
let mut values = vec![va];
for s in rest {
values.push(hex(s).ok_or_else(|| format!("bad value {s:?}"))?);
}
let hits = mem.find_u32(&values);
for (at, v) in &hits {
println!("0x{at:08X} 0x{v:08X}");
}
println!("{} hit(s)", hits.len());
return Ok(());
}
if cmd == "dump-chain" {
let offs: Vec<u32> = rest.iter().map(|s| hex(s).ok_or_else(|| format!("bad offset {s:?}"))).collect::<Result<_, _>>()?;
let at = mem.chase(va, &offs)?;
println!("0x{at:08X}");
return Ok(());
}
let len = match rest.first() {
None => 0x40,
Some(s) => s.parse::<usize>().map_err(|e| format!("len {s:?}: {e}"))?,
};
print!("{}", dump::hexdump(va, &mem.read(va, len)?));
Ok(())
};
match run() {
Ok(()) => 0,
Err(e) => {
eprintln!("{cmd} failed: {e}");
1
}
}
}
fn dump_vec3_cli(args: &[String]) -> i32 {
let f = |i: usize| args.get(i).and_then(|s| s.parse::<f32>().ok());
let (Some(dir), Some(x), Some(y), Some(z)) = (args.first(), f(1), f(2), f(3)) else {
eprintln!("dump-find-vec3: usage: mercs2_probe dump-find-vec3 <capture> <x> <y> <z> [--tol T]");
return 2;
};
let run = || -> Result<(), String> {
let tol = match flag_val(args, "--tol") {
None => 0.0005,
Some(t) => t.parse::<f32>().map_err(|e| format!("--tol {t:?}: {e}"))?,
};
let cap = dump::Capture::open(std::path::Path::new(dir))?;
println!("[dump-find-vec3] {} (build {})", cap.path.display(), cap.build_id);
let hits = cap.mem()?.find_vec3([x, y, z], tol);
for at in &hits {
println!("0x{at:08X}");
}
println!("{} hit(s)", hits.len());
Ok(())
};
match run() {
Ok(()) => 0,
Err(e) => {
eprintln!("dump-find-vec3 failed: {e}");
1
}
}
}
fn dump_pool_cli(cmd: &str, args: &[String]) -> i32 {
let Some(dir) = args.first() else {
eprintln!("{cmd}: usage: mercs2_probe dump-containers <capture-dir> | dump-pool <capture-dir> <name|0xVA> [--bytes N] [--limit N]");
return 2;
};
let num = |flag: &str| -> Result<Option<usize>, String> {
flag_val(args, flag).map(|v| v.parse::<usize>().map_err(|e| format!("{flag} {v:?}: {e}"))).transpose()
};
let run = || -> Result<(), String> {
let cap = dump::Capture::open(std::path::Path::new(dir))?;
let mem = cap.mem()?;
println!("[{cmd}] {} (build {})", cap.path.display(), cap.build_id);
if cmd == "dump-containers" {
for c in mem.containers()? {
println!(
"0x{:08X} {:?} id {:3} count {:6} stride {:4} per_page {:5} {}",
c.va, c.class, c.type_id, c.count, c.stride, c.per_page, c.name
);
}
return Ok(());
}
let which = args.get(1).ok_or("dump-pool: missing <name|0xVA>")?;
let c = match which.strip_prefix("0x").and_then(|h| u32::from_str_radix(h, 16).ok()) {
Some(va) => mem.containers()?.into_iter().find(|c| c.va == va).ok_or_else(|| format!("no container at {which}"))?,
None => mem.container_named(which)?,
};
let bytes = num("--bytes")?.unwrap_or(c.stride as usize);
let limit = num("--limit")?.unwrap_or(usize::MAX);
let key = match flag_val(args, "--key") {
None => None,
Some(k) => Some(u32::from_str_radix(k.trim_start_matches("0x"), 16).map_err(|e| format!("--key {k:?}: {e}"))?),
};
let all = mem.records(&c)?;
let recs: Vec<_> = all.iter().filter(|r| key.map_or(true, |k| r.key == k)).collect();
println!(
"{} @0x{:08X} {:?} stride {} — {} live record(s){}",
c.name, c.va, c.class, c.stride, all.len(),
key.map_or(String::new(), |k| format!(", {} with key 0x{k:08X}", recs.len()))
);
for r in recs.iter().take(limit) {
println!("-- key 0x{:08X} @0x{:08X}", r.key, r.va);
print!("{}", dump::hexdump(r.va, &mem.read(r.va, bytes)?));
}
Ok(())
};
match run() {
Ok(()) => 0,
Err(e) => {
eprintln!("{cmd} failed: {e}");
1
}
}
}
fn save_dump(profile_path: &str, needle: Option<&str>) -> Result<(), String> {
let bytes = std::fs::read(profile_path).map_err(|e| format!("read {profile_path}: {e}"))?;
let p = mercs2_formats::save::parse(&bytes)?;
println!(
"[save-dump] '{}' | contract {} | playtime {}s | cash {} | fuel {}/{} | character(0x4D) {} | upgrade(0x4F) {} | unlocked_costumes(0x24A) {} | unk(0x24B) {} | flags4C {:#x} | ts {}",
p.save_name(),
p.active_contract(),
p.play_time_seconds,
p.cash,
p.fuel,
p.fuel_capacity,
p.character_index,
p.upgrade_index,
p.unlocked_costumes,
p.unknown_0x24b,
p.flags_0x4c,
p.timestamp
);
let hex: Vec<String> = bytes[0x240..0x260.min(bytes.len())].iter().map(|b| format!("{b:02x}")).collect();
println!("[save-dump] header[0x240..0x260]: {}", hex.join(" "));
let lua = p.decompress_lua()?;
let text = String::from_utf8_lossy(&lua);
match needle {
Some(n) => {
for line in text.lines().filter(|l| l.to_lowercase().contains(n)) {
println!("{line}");
}
}
None => println!("{text}"),
}
Ok(())
}
fn overlays_report(wadpath: &str, profile_path: &str) -> Result<(), String> {
use mercs2_engine::worldutil::{find_terrain_blocks, layer_container, PlacedCatalog, TemplateRecords};
let bytes = std::fs::read(profile_path).map_err(|e| format!("read {profile_path}: {e}"))?;
let profile = mercs2_formats::save::parse(&bytes)?;
let state = profile.save_state()?;
let layers = &state.layers;
println!(
"[overlays] profile '{}' — contract {}, {} active vz_state layers",
profile.save_name(),
profile.active_contract(),
layers.len()
);
let mut w = wad::open(wadpath)?;
let templates = TemplateRecords::load(&mut mercs2_engine::asset::AssetSource::discover(wadpath, &[])?)?;
let mut catalog = PlacedCatalog::default();
let (_low, ls) = find_terrain_blocks(&mut w)?;
let base = catalog.add_block(&ls, &templates)?.len();
let (mut resolved, mut total) = (0usize, 0usize);
let mut unresolved: Vec<&str> = Vec::new();
let mut per: Vec<(String, usize)> = Vec::new();
for l in layers {
match layer_container(&mut w, mercs2_formats::hash::pandemic_hash_m2(l))? {
Some(c) => {
let n = catalog.add_block(&c, &templates).map_err(|e| format!("{l}: {e}"))?.len();
resolved += 1;
total += n;
if n > 0 {
per.push((l.clone(), n));
}
}
None => unresolved.push(l),
}
}
println!("[overlays] resolved {resolved}/{} layers to layer assets", layers.len());
println!(
"[overlays] placed entities: {base} layers_static + {total} layer = {} distinct ({} with a model)",
catalog.len(),
catalog.iter().filter(|o| o.model.is_some()).count()
);
per.sort_by(|a, b| b.1.cmp(&a.1));
println!("[overlays] top overlays by entity count:");
for (n, c) in per.iter().take(10) {
println!(" {c:>5} {n}");
}
if !unresolved.is_empty() {
let sample: Vec<&str> = unresolved.iter().take(6).copied().collect();
println!("[overlays] {} unresolved (sample): {sample:?}", unresolved.len());
}
Ok(())
}
fn hier_report(
wadpath: &str,
model: u32,
names_file: Option<String>,
csv_out: Option<String>,
) -> Result<(), String> {
use mercs2_formats::hash::pandemic_hash_m2;
use mercs2_formats::orchestrator;
use std::collections::{BTreeSet, HashMap};
let mut w = wad::open(wadpath).map_err(|e| format!("open {wadpath}: {e}"))?;
let c = wad::extract_container(&mut w, model).map_err(|e| format!("extract_container: {e}"))?;
let hier = orchestrator::parse_hier(&c);
let swit: BTreeSet<u32> = orchestrator::parse_swit(&c).into_iter().collect();
let dest = orchestrator::classify(&c);
let all_hashes: BTreeSet<u32> = hier.iter().map(|n| n.hash).collect();
let table = mercs2_formats::game_paths::local_config_rainbow_table(std::path::Path::new(env!("CARGO_MANIFEST_DIR")))?;
let mut names = mercs2_engine::worldutil::rainbow_names(&table, &all_hashes)?;
if let Some(p) = names_file {
let text = std::fs::read_to_string(&p).map_err(|e| format!("read {p}: {e}"))?;
for line in text.lines() {
let cand = line.trim();
if cand.len() < 2 {
continue;
}
let h = pandemic_hash_m2(cand);
if all_hashes.contains(&h) {
names.entry(h).or_insert_with(|| cand.to_string());
}
}
}
println!(
"model 0x{model:08X}: {} HIER nodes, {} in SWIT, {} resolved names",
hier.len(),
hier.iter().filter(|n| swit.contains(&n.hash)).count(),
hier.iter().filter(|n| names.contains_key(&n.hash)).count()
);
let mut kids: HashMap<usize, Vec<usize>> = HashMap::new();
let mut roots = Vec::new();
for n in &hier {
match n.parent {
Some(p) => kids.entry(p).or_default().push(n.index),
None => roots.push(n.index),
}
}
#[allow(clippy::too_many_arguments)]
fn walk(
i: usize,
depth: usize,
hier: &[orchestrator::HierNode],
kids: &HashMap<usize, Vec<usize>>,
names: &HashMap<u32, String>,
swit: &BTreeSet<u32>,
dest: &Option<orchestrator::Destruction>,
csv: &mut Option<String>,
) {
let n = &hier[i];
let name = names.get(&n.hash).map(|s| s.as_str()).unwrap_or("?");
let state = dest
.as_ref()
.and_then(|d| d.state_of_node(i))
.map(|s| s.as_str())
.unwrap_or("-");
let t = [n.local[12], n.local[13], n.local[14]];
let dim = [
n.bbox_max[0] - n.bbox_min[0],
n.bbox_max[1] - n.bbox_min[1],
n.bbox_max[2] - n.bbox_min[2],
];
println!(
"{:indent$}[{i:>3}] 0x{:08X} {name:<44} {}{state:<12} t=({:.2},{:.2},{:.2}) bbox {:.1}x{:.1}x{:.1}",
"",
n.hash,
if swit.contains(&n.hash) { "SWIT " } else { "" },
t[0],
t[1],
t[2],
dim[0],
dim[1],
dim[2],
indent = depth * 2
);
if let Some(out) = csv {
let parent = n.parent.map(|p| p.to_string()).unwrap_or_default();
out.push_str(&format!(
"{i},{parent},{depth},0x{:08X},{},{},{state},{:.4},{:.4},{:.4},{:.3},{:.3},{:.3}\n",
n.hash,
if name == "?" { "" } else { name },
u8::from(swit.contains(&n.hash)),
t[0], t[1], t[2],
dim[0], dim[1], dim[2],
));
}
for &k in kids.get(&i).map(|v| v.as_slice()).unwrap_or(&[]) {
walk(k, depth + 1, hier, kids, names, swit, dest, csv);
}
}
let mut csv = csv_out.as_ref().map(|_| {
String::from("index,parent_index,depth,hash,name,in_swit,state,tx,ty,tz,bbox_dx,bbox_dy,bbox_dz\n")
});
for &r in &roots {
walk(r, 0, &hier, &kids, &names, &swit, &dest, &mut csv);
}
if let (Some(path), Some(out)) = (&csv_out, &csv) {
std::fs::write(path, out).map_err(|e| format!("write {path}: {e}"))?;
println!("csv -> {path}");
}
if let Some(d) = &dest {
println!(
"destruction: {} switch groups, {} hulls, INDX {} mesh->node entries",
d.switch_group_count,
d.hull_count,
d.indx.len()
);
}
Ok(())
}
fn main() {
let all: Vec<String> = std::env::args().collect();
if all.len() < 2 {
usage();
}
let cmd = all[1].clone();
let args: Vec<String> = all[2..].to_vec();
if cmd == "ess-live" {
let spawn_dist = match flag_val(&args, "--spawn-dist") {
None => ess_live::DEFAULT_SPAWN_DIST,
Some(d) => d.parse::<f64>().unwrap_or_else(|e| {
eprintln!("ess-live: --spawn-dist {d:?}: {e}");
std::process::exit(2);
}),
};
let opts = ess_live::Options {
addr: flag_val(&args, "--addr").unwrap_or_else(|| mercs2_bridge::DEFAULT_ADDR.to_string()),
menu: flag_val(&args, "--menu"),
spawn: flag_val(&args, "--spawn"),
spawn_dist,
remove_deadline: ess_live::DEFAULT_REMOVE_DEADLINE,
poll_interval: ess_live::DEFAULT_POLL_INTERVAL,
swf_deadline: ess_live::DEFAULT_SWF_DEADLINE,
};
match ess_live::run(&opts) {
Ok(results) => {
for r in &results {
println!("{r}");
}
std::process::exit(ess_live::exit_code(&results));
}
Err(e) => {
eprintln!("ess-live: bridge at {} unreachable: {e}", opts.addr);
std::process::exit(2);
}
}
}
if cmd == "dump-find-vec3" {
std::process::exit(dump_vec3_cli(&args));
}
if cmd == "dump-read" || cmd == "dump-chain" || cmd == "dump-find" {
std::process::exit(dump_cli(&cmd, &args));
}
if cmd == "dump-containers" || cmd == "dump-pool" {
std::process::exit(dump_pool_cli(&cmd, &args));
}
if cmd == "dump-hibernation" {
let Some(capture) = args.first() else {
eprintln!("dump-hibernation: usage: mercs2_probe dump-hibernation <capture>");
std::process::exit(2);
};
if let Err(e) = dump_hibernation::dump_hibernation(capture) {
eprintln!("dump-hibernation failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "dump-model" {
let hash = args.get(1).and_then(|k| u32::from_str_radix(k.trim_start_matches("0x"), 16).ok());
let (Some(capture), Some(hash)) = (args.first(), hash) else {
eprintln!("dump-model: usage: mercs2_probe dump-model <capture> <0xHASH>");
std::process::exit(2);
};
if let Err(e) = dump_render::dump_model(capture, hash) {
eprintln!("dump-model failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "dump-near" {
let n: Vec<f32> = args[1.min(args.len())..].iter().filter_map(|a| a.parse().ok()).collect();
let (Some(capture), [x, y, z, radius]) = (args.first(), n.as_slice()) else {
eprintln!("dump-near: usage: mercs2_probe dump-near <capture> <x> <y> <z> <radius>");
std::process::exit(2);
};
if let Err(e) = dump_render::dump_near(capture, [*x, *y, *z], *radius) {
eprintln!("dump-near failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "dump-entity" {
let key = args.get(1).and_then(|k| u32::from_str_radix(k.trim_start_matches("0x"), 16).ok());
let (Some(capture), Some(key)) = (args.first(), key) else {
eprintln!("dump-entity: usage: mercs2_probe dump-entity <capture> <0xKEY>");
std::process::exit(2);
};
if let Err(e) = dump_render::dump_entity(capture, key) {
eprintln!("dump-entity failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "dump-render" {
let Some(capture) = args.first() else {
eprintln!("dump-render: usage: mercs2_probe dump-render <capture>");
std::process::exit(2);
};
if let Err(e) = dump_render::dump_render(capture) {
eprintln!("dump-render failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "dump-spawnlists" {
let Some(capture) = args.first() else {
eprintln!("dump-spawnlists: usage: mercs2_probe dump-spawnlists <capture>");
std::process::exit(2);
};
if let Err(e) = dump_spawnlists::dump_spawnlists(capture) {
eprintln!("dump-spawnlists failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "ecs-survey" {
let (Some(archive), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("ecs-survey: usage: mercs2_probe ecs-survey <big-endian archive> <out.tsv> [--raw <dir>]");
std::process::exit(2);
};
let raw = flag_val(&args, "--raw");
if let Err(e) = ecs_survey::ecs_survey(
std::path::Path::new(archive),
std::path::Path::new(out),
raw.as_deref().map(std::path::Path::new),
) {
eprintln!("ecs-survey failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "chunk-compare" {
let (Some(archive), Some(out), Some(vz), Some(java), Some(unluac)) =
(args.first(), args.get(1), flag_val(&args, "--vz-wad"), flag_val(&args, "--java"), flag_val(&args, "--unluac"))
else {
eprintln!("chunk-compare: usage: mercs2_probe chunk-compare <big-endian archive> <out.tsv> --vz-wad <pc vz.wad> --java <java> --unluac <jar> [--only <type>]");
std::process::exit(2);
};
let only = flag_val(&args, "--only");
let inputs = chunk_compare::Inputs {
archive: std::path::Path::new(archive),
vz_wad: std::path::Path::new(&vz),
java: std::path::Path::new(&java),
unluac: std::path::Path::new(&unluac),
only: only.as_deref(),
};
if let Err(e) = chunk_compare::chunk_compare(inputs, std::path::Path::new(out)) {
eprintln!("chunk-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "terrain-position-w" {
let (Some(wad), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("terrain-position-w: usage: mercs2_probe terrain-position-w <PC wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::terrain_position_w(std::path::Path::new(wad), std::path::Path::new(out)) {
eprintln!("terrain-position-w failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "terrain-alt-strips" {
let (Some(wad), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("terrain-alt-strips: usage: mercs2_probe terrain-alt-strips <PC wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::terrain_alt_strips(std::path::Path::new(wad), std::path::Path::new(out)) {
eprintln!("terrain-alt-strips failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "terrain-decls" {
let (Some(wad), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("terrain-decls: usage: mercs2_probe terrain-decls <PC wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::terrain_decls(std::path::Path::new(wad), std::path::Path::new(out)) {
eprintln!("terrain-decls failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "terrain-half-tally" {
let (Some(wad), Some(usage), Some(component), Some(out)) = (args.first(), args.get(1).and_then(|u| u.parse::<u8>().ok()), args.get(2).and_then(|c| c.parse::<usize>().ok()), args.get(3)) else {
eprintln!("terrain-half-tally: usage: mercs2_probe terrain-half-tally <PC wad> <decl usage> <component 0-3> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::terrain_half_tally(std::path::Path::new(wad), usage, component, std::path::Path::new(out)) {
eprintln!("terrain-half-tally failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "terrain-vertex-compare" {
let (Some(a), Some(b), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("terrain-vertex-compare: usage: mercs2_probe terrain-vertex-compare <PC wad> <PC wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::terrain_vertex_compare(std::path::Path::new(a), std::path::Path::new(b), std::path::Path::new(out)) {
eprintln!("terrain-vertex-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "terrain-draw-ranges" {
let (Some(wad), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("terrain-draw-ranges: usage: mercs2_probe terrain-draw-ranges <PC wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::terrain_draw_ranges(std::path::Path::new(wad), std::path::Path::new(out)) {
eprintln!("terrain-draw-ranges failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "terrain-compare" {
let (Some(a), Some(b), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("terrain-compare: usage: mercs2_probe terrain-compare <wad a> <wad b> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::terrain_compare(std::path::Path::new(a), std::path::Path::new(b), std::path::Path::new(out)) {
eprintln!("terrain-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "scrub-compare" {
let (Some(archive), Some(pc), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("scrub-compare: usage: mercs2_probe scrub-compare <big-endian archive> <PC vz.wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::scrub_compare(std::path::Path::new(archive), std::path::Path::new(pc), std::path::Path::new(out)) {
eprintln!("scrub-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "rung-words" {
let Some(wad) = args.first() else {
eprintln!("rung-words: usage: mercs2_probe rung-words <PC wad>");
std::process::exit(2);
};
if let Err(e) = dlc_census::rung_words(std::path::Path::new(wad)) {
eprintln!("rung-words failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "model-compare" {
let (Some(archive), Some(pc), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("model-compare: usage: mercs2_probe model-compare <big-endian archive> <PC vz.wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::model_compare(std::path::Path::new(archive), std::path::Path::new(pc), std::path::Path::new(out)) {
eprintln!("model-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "pixel-shaders" {
let (Some(dir), Some(pc)) = (args.first(), args.get(1)) else {
eprintln!("pixel-shaders: usage: mercs2_probe pixel-shaders <dir of model .ucfx> <PC wad>");
std::process::exit(2);
};
if let Err(e) = dlc_census::pixel_shaders(std::path::Path::new(dir), std::path::Path::new(pc)) {
eprintln!("pixel-shaders failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "vs-compare" {
let (Some(archive), Some(pc), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("vs-compare: usage: mercs2_probe vs-compare <big-endian archive> <PC vz.wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::vs_compare(std::path::Path::new(archive), std::path::Path::new(pc), std::path::Path::new(out)) {
eprintln!("vs-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "type-compare" {
let (Some(archive), Some(pc), Some(ty)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("type-compare: usage: mercs2_probe type-compare <big-endian archive> <PC wad> <type name>");
std::process::exit(2);
};
if let Err(e) = dlc_census::type_compare(std::path::Path::new(archive), std::path::Path::new(pc), ty) {
eprintln!("type-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "frame-capture" {
let (Some(file), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("frame-capture: usage: mercs2_probe frame-capture <mercs2_profiler_frame_N.bin> <matrices.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::frame_capture(std::path::Path::new(file), std::path::Path::new(out)) {
eprintln!("frame-capture failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "terrain-bounds" {
let (Some(input), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("terrain-bounds: usage: mercs2_probe terrain-bounds <PC wad | dir holding terrainmesh/ and lowresterrain/> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::terrain_bounds(std::path::Path::new(input), std::path::Path::new(out)) {
eprintln!("terrain-bounds failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "model-bounds" {
let (Some(dir), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("model-bounds: usage: mercs2_probe model-bounds <dir of model .ucfx> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::model_bounds(std::path::Path::new(dir), std::path::Path::new(out)) {
eprintln!("model-bounds failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "ucfx-tree" {
let (Some(file), Some(max)) = (args.first(), args.get(1).map_or(Some(64), |v| v.parse::<usize>().ok())) else {
eprintln!("ucfx-tree: usage: mercs2_probe ucfx-tree <container .ucfx> [bytes shown per row, default 64]");
std::process::exit(2);
};
if let Err(e) = dlc_census::ucfx_tree(std::path::Path::new(file), max) {
eprintln!("ucfx-tree failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "water-at" {
let (Some(file), Some(x), Some(z)) = (args.first(), args.get(1).and_then(|v| v.parse::<f32>().ok()), args.get(2).and_then(|v| v.parse::<f32>().ok())) else {
eprintln!("water-at: usage: mercs2_probe water-at <watermap .ucfx | PC wad> <x> <z>");
std::process::exit(2);
};
if let Err(e) = dlc_census::water_at(std::path::Path::new(file), x, z) {
eprintln!("water-at failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "tiny-slots" {
let Some((out, inputs)) = args.split_last().filter(|(_, i)| !i.is_empty()) else {
eprintln!("tiny-slots: usage: mercs2_probe tiny-slots <PC wad | dir of .ucfx | --be <big-endian archive>>... <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::tiny_slots(inputs, std::path::Path::new(out)) {
eprintln!("tiny-slots failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "phy2-diff" {
let (Some(be), Some(pc)) = (args.first(), args.get(1)) else {
eprintln!("phy2-diff: usage: mercs2_probe phy2-diff <big-endian PHY2 body> <PC body or container>");
std::process::exit(2);
};
if let Err(e) = dlc_census::phy2_diff(std::path::Path::new(be), std::path::Path::new(pc)) {
eprintln!("phy2-diff failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "chunk-rows" {
let (Some(archive), Some(name), Some(dir)) = (args.first(), args.get(1).and_then(|h| parse_hash(h)), args.get(2)) else {
eprintln!("chunk-rows: usage: mercs2_probe chunk-rows <big-endian archive> <0xNAME> <out dir>");
std::process::exit(2);
};
if let Err(e) = dlc_census::chunk_rows(std::path::Path::new(archive), name, std::path::Path::new(dir)) {
eprintln!("chunk-rows failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "decl-table" {
let (Some(ps3), Some(converted), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("decl-table: usage: mercs2_probe decl-table <ps3 archive> <converted wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::decl_table(std::path::Path::new(ps3), std::path::Path::new(converted), std::path::Path::new(out)) {
eprintln!("decl-table failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "tex-info" {
let (Some(archive), Some(pc), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("tex-info: usage: mercs2_probe tex-info <big-endian archive> <pc vz.wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = tex_compare::tex_info(std::path::Path::new(archive), std::path::Path::new(pc), std::path::Path::new(out)) {
eprintln!("tex-info failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "pc-diff" {
let (Some(converted), Some(pc), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("pc-diff: usage: mercs2_probe pc-diff <converted wad> <pc vz.wad> <out.tsv> [--type 0xHASH]");
std::process::exit(2);
};
let only = flag_val(&args, "--type").and_then(|h| parse_hash(&h));
if let Err(e) = dlc_census::pc_diff(std::path::Path::new(converted), std::path::Path::new(pc), only, std::path::Path::new(out)) {
eprintln!("pc-diff failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "clip-compare" {
let (Some(converted), Some(pc), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("clip-compare: usage: mercs2_probe clip-compare <converted wad> <pc vz.wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::clip_compare(std::path::Path::new(converted), std::path::Path::new(pc), std::path::Path::new(out)) {
eprintln!("clip-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "dlc-census" {
let (Some(package), Some(base), Some(pc), Some(out)) = (args.first(), args.get(1), args.get(2), args.get(3)) else {
eprintln!("dlc-census: usage: mercs2_probe dlc-census <big-endian dlc archive> <big-endian retail wad> <pc vz.wad> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = dlc_census::dlc_census(std::path::Path::new(package), std::path::Path::new(base), std::path::Path::new(pc), std::path::Path::new(out)) {
eprintln!("dlc-census failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "pws-pc" {
let (Some(wad), Some(audios), Some(out)) = (args.first(), args.get(1), args.get(2)) else {
eprintln!("pws-pc: usage: mercs2_probe pws-pc <pc wad> <Audios dir> <out.tsv>");
std::process::exit(2);
};
if let Err(e) = pws_survey::pc_streams(std::path::Path::new(wad), std::path::Path::new(audios), std::path::Path::new(out)) {
eprintln!("pws-pc failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "pws-survey" {
let (Some(package), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("pws-survey: usage: mercs2_probe pws-survey <stfs package> <out.tsv> [--extract <dir>]");
std::process::exit(2);
};
let extract = flag_val(&args, "--extract");
if let Err(e) = pws_survey::pws_survey(std::path::Path::new(package), std::path::Path::new(out), extract.as_deref().map(std::path::Path::new)) {
eprintln!("pws-survey failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "stfs-list" {
let Some(package) = args.first() else {
eprintln!("stfs-list: usage: mercs2_probe stfs-list <stfs package>");
std::process::exit(2);
};
let reader = match std::fs::read(package) {
Ok(data) => mercs2_formats::dlc_stfs::StfsReader::new(data),
Err(e) => {
eprintln!("stfs-list: {package}: {e}");
std::process::exit(1);
}
};
for e in reader.file_table.iter().filter(|e| !e.is_dir) {
println!("{}\t{}\t{}", e.path_ind, e.file_size, e.name);
}
return;
}
if cmd == "tex-compare" {
let (Some(archive), Some(out), Some(vz)) = (args.first(), args.get(1), flag_val(&args, "--vz-wad")) else {
eprintln!("tex-compare: usage: mercs2_probe tex-compare <xbox archive> <out.tsv> --vz-wad <pc vz.wad> [--ps3 <ps3 archive>]");
std::process::exit(2);
};
let ps3 = flag_val(&args, "--ps3");
if let Err(e) = tex_compare::tex_compare(std::path::Path::new(archive), std::path::Path::new(&vz), ps3.as_deref().map(std::path::Path::new), std::path::Path::new(out)) {
eprintln!("tex-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "tex-rungs" {
let (Some(archive), Some(out), Some(vz)) = (args.first(), args.get(1), flag_val(&args, "--vz-wad")) else {
eprintln!("tex-rungs: usage: mercs2_probe tex-rungs <big-endian archive> <out.tsv> --vz-wad <pc vz.wad> [--raw <dir> --hash <0xHASH>]");
std::process::exit(2);
};
let dir = flag_val(&args, "--raw");
let hash = flag_val(&args, "--hash").and_then(|h| u32::from_str_radix(h.trim_start_matches("0x"), 16).ok());
let raw = match (&dir, hash) {
(Some(d), Some(hash)) => Some(tex_rungs::Raw { hash, dir: std::path::Path::new(d) }),
(None, None) => None,
_ => {
eprintln!("tex-rungs: --raw and --hash go together, and --hash is hex");
std::process::exit(2);
}
};
if let Err(e) = tex_rungs::tex_rungs(std::path::Path::new(archive), std::path::Path::new(&vz), std::path::Path::new(out), raw.as_ref()) {
eprintln!("tex-rungs failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "ecs-names" {
let (Some(archive), Some(out), Some(vz)) = (args.first(), args.get(1), flag_val(&args, "--vz-wad")) else {
eprintln!("ecs-names: usage: mercs2_probe ecs-names <archive> <out.tsv> --vz-wad <pc vz.wad>");
std::process::exit(2);
};
if let Err(e) = ecs_compare::ecs_names(std::path::Path::new(archive), std::path::Path::new(&vz), std::path::Path::new(out)) {
eprintln!("ecs-names failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "ecs-compare" {
let (Some(a), Some(b), Some(out), Some(vz)) = (args.first(), args.get(1), args.get(2), flag_val(&args, "--vz-wad")) else {
eprintln!("ecs-compare: usage: mercs2_probe ecs-compare <archive a> <archive b> <out.tsv> --vz-wad <pc vz.wad> [--diffs <file>]");
std::process::exit(2);
};
let diffs = flag_val(&args, "--diffs");
if let Err(e) = ecs_compare::ecs_compare(
std::path::Path::new(a),
std::path::Path::new(b),
std::path::Path::new(&vz),
std::path::Path::new(out),
diffs.as_deref().map(std::path::Path::new),
) {
eprintln!("ecs-compare failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "ess-menu-check" {
std::process::exit(ess_menu_check::main(&args));
}
if cmd == "prof-samples" {
std::process::exit(prof_samples::main(&args));
}
if cmd == "gfx-run" {
let (Some(dir), Some(frames)) = (args.first(), args.get(1).and_then(|f| f.parse::<u32>().ok())) else {
eprintln!("gfx-run: usage: mercs2_probe gfx-run <movies_dir> <frames>");
std::process::exit(2);
};
if let Err(e) = gfx_golden::gfx_run(std::path::Path::new(dir), frames) {
eprintln!("gfx-run failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "bink-audio" {
let (Some(movie), Some(track), Some(out)) = (args.first(), args.get(1).and_then(|t| t.parse::<usize>().ok()), args.get(2)) else {
eprintln!("bink-audio: usage: mercs2_probe bink-audio <movie.bik> <track index> <out.s16> [<other.s16>]");
std::process::exit(2);
};
let against = args.get(3).map(std::path::Path::new);
if let Err(e) = gfx_golden::bink_audio(std::path::Path::new(movie), track, std::path::Path::new(out), against) {
eprintln!("bink-audio failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "bink-video" {
let (Some(movie), Some(frames), Some(against)) = (args.first(), args.get(1).and_then(|f| f.parse::<u32>().ok()), args.get(2)) else {
eprintln!("bink-video: usage: mercs2_probe bink-video <movie.bik> <frames> <reference.yuv>");
std::process::exit(2);
};
if let Err(e) = gfx_golden::bink_video(std::path::Path::new(movie), frames, std::path::Path::new(against)) {
eprintln!("bink-video failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "splice-emulate" {
if let Err(e) = splice_emu::run(&args) {
eprintln!("splice-emulate failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "bink-frame" {
let (Some(movie), Some(index), Some(out)) = (args.first(), args.get(1).and_then(|f| f.parse::<u32>().ok()), args.get(2)) else {
eprintln!("bink-frame: usage: mercs2_probe bink-frame <movie.bik> <frame> <out.png>");
std::process::exit(2);
};
if let Err(e) = gfx_golden::bink_frame(std::path::Path::new(movie), index, std::path::Path::new(out)) {
eprintln!("bink-frame failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "gfx-actions" {
let (Some(movie), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("gfx-actions: usage: mercs2_probe gfx-actions <movie.gfx> <out.txt>");
std::process::exit(2);
};
if let Err(e) = gfx_golden::gfx_actions(std::path::Path::new(movie), std::path::Path::new(out)) {
eprintln!("gfx-actions failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "gfx-tags" {
let (Some(movie), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("gfx-tags: usage: mercs2_probe gfx-tags <movie.gfx> <out.txt>");
std::process::exit(2);
};
if let Err(e) = gfx_golden::gfx_tags(std::path::Path::new(movie), std::path::Path::new(out)) {
eprintln!("gfx-tags failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "gfx-shell" {
let screen = args.get(1).and_then(|s| s.split_once('x')).and_then(|(w, h)| Some([w.parse::<u32>().ok()?, h.parse::<u32>().ok()?]));
let (Some(out), Some(screen)) = (args.first(), screen) else {
eprintln!("gfx-shell: usage: mercs2_probe gfx-shell <out.png> <width>x<height> [wait:<frames>|down:<button>|up:<button>|mouse:<x>,<y>,<buttons>|call:<name>|key:<id>|pad:<vendor>,<product>,<name>|held:<input>,...|get:<path>]... <wad, last mounted first>...");
std::process::exit(2);
};
let (wads, steps): (Vec<String>, Vec<String>) = args[2..].iter().cloned().partition(|a| a.ends_with(".wad"));
if let Err(e) = gfx_golden::gfx_shell(std::path::Path::new(out), screen, &steps, &wads) {
eprintln!("gfx-shell failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "gfx-render" {
let usage = "gfx-render: usage: mercs2_probe gfx-render <movie.gfx> <frames> <out.png> [--fonts <font.gfx>] [--goto <label>] <images.wad>...";
let (Some(movie), Some(frames), Some(out)) = (args.first(), args.get(1).and_then(|f| f.parse::<u32>().ok()), args.get(2)) else {
eprintln!("{usage}");
std::process::exit(2);
};
let fonts = flag_val(&args, "--fonts");
let label = flag_val(&args, "--goto");
let wads: Vec<String> = args[3..].iter().filter(|a| a.ends_with(".wad")).cloned().collect();
if let Err(e) = gfx_golden::gfx_render(
std::path::Path::new(movie),
frames,
std::path::Path::new(out),
&wads,
fonts.as_deref().map(std::path::Path::new),
label.as_deref(),
) {
eprintln!("gfx-render failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "gfx-decode" {
let Some(dir) = first_positional(&args) else {
eprintln!("gfx-decode: usage: mercs2_probe gfx-decode <movies_dir>");
std::process::exit(2);
};
if let Err(e) = gfx_golden::gfx_decode(std::path::Path::new(&dir)) {
eprintln!("gfx-decode failed: {e}");
std::process::exit(1);
}
return;
}
if cmd == "gfx-golden" {
let usage = "gfx-golden: usage: mercs2_probe gfx-golden <movies_dir> --md <report.md> --json <index.json>";
let (Some(dir), Some(md), Some(json)) =
(first_positional(&args), flag_val(&args, "--md"), flag_val(&args, "--json"))
else {
eprintln!("{usage}");
std::process::exit(2);
};
if let Err(e) = gfx_golden::gfx_golden(
std::path::Path::new(&dir),
std::path::Path::new(&md),
std::path::Path::new(&json),
) {
eprintln!("gfx-golden failed: {e}");
std::process::exit(1);
}
return;
}
let wadpath = match wad::resolve_vz_wad(flag_val(&args, "--wad").as_deref()) {
Some(p) => p,
None => {
eprintln!(
"no vz.wad found — pass --wad <folder-or-file>, set MERCS2_GAME_DIR or VZ_WAD, or install so that\n \
HKLM\\SOFTWARE\\WOW6432Node\\EA Games\\Mercenaries 2 World in Flames\\Install Dir\n \
resolves to a folder containing data\\vz.wad"
);
std::process::exit(1);
}
};
eprintln!("vz.wad: {wadpath}");
let model = flag_val(&args, "--model");
let index = flag_val(&args, "--index");
let clip = flag_val(&args, "--clip").and_then(|c| parse_hash(&c));
let run = |r: Result<(), String>| {
if let Err(e) = r {
eprintln!("{cmd} failed: {e}");
std::process::exit(1);
}
};
match cmd.as_str() {
"c3-meta" => {
let out = first_positional(&args).unwrap_or_else(|| "c3_meta.ndjson".to_string());
run(diag::c3_meta(&wadpath, &out));
}
"placement-hashes" => {
let out = first_positional(&args).unwrap_or_else(|| "placement_hashes.json".to_string());
run(diag::placement_hashes(&wadpath, &out));
}
"export-c3-obj" => match first_positional(&args) {
Some(out) => run(diag::export_c3_obj(&wadpath, &out)),
None => {
eprintln!("export-c3-obj: usage: mercs2_probe export-c3-obj <outdir>");
std::process::exit(2);
}
},
"terrainmesh-probe" => {
let block = first_positional(&args).and_then(|s| s.parse::<u16>().ok());
run(diag::terrainmesh_probe(&wadpath, block));
}
"terrain-probe" => run(diag::terrain_probe(&wadpath)),
"terrain-consumer" => run(diag::terrain_consumer_scan(&wadpath)),
"wad-list" => run(diag::wad_list(&wadpath)),
"wad-meshes" => run(diag::wad_meshes(&wadpath, model)),
"placement-probe" => run(diag::placement_probe(&wadpath)),
"world-index" => run(diag::world_index_probe(&wadpath)),
"stream-probe" => run(diag::stream_probe(&wadpath)),
"animdiag" => run(diag::animdiag(&wadpath, model, index)),
"animcheck" => run(diag::animcheck(&wadpath, model, index)),
"skincheck" => run(diag::skincheck(&wadpath, model, index)),
"trackmap" => run(diag::trackmap(&wadpath, model, index, clip)),
"entity-find" => run(diag::entity_find(&wadpath, &hex_args(&args))),
"comp-probe" => run(diag::comp_probe(&wadpath)),
"comp-dump" => {
let name = first_positional(&args).unwrap_or_else(|| "HibernationControl".into());
run(diag::comp_dump(&wadpath, &name));
}
"block-grep" => diag::block_grep(&wadpath, &first_positional(&args).unwrap_or_default()),
"scan-hash" => diag::scan_hash(&wadpath, &hex_args(&args)),
"find-ref" => diag::find_ref(&wadpath, &hex_args(&args)),
"block-probe" => match first_positional(&args).and_then(|s| s.parse::<u16>().ok()) {
Some(bi) => diag::block_probe(&wadpath, bi),
None => {
eprintln!("block-probe: usage: mercs2_probe block-probe <block-index>");
std::process::exit(2);
}
},
"placement-names" => diag::placement_names(&wadpath),
"veg-census" => match first_positional(&args) {
Some(out) => run(diag::veg_census(&wadpath, &out)),
None => {
eprintln!("veg-census: usage: mercs2_probe veg-census <veg_tagset.json>");
std::process::exit(2);
}
},
"tree-census" => match first_positional(&args) {
Some(out) => run(diag::tree_census(&wadpath, &out)),
None => {
eprintln!("tree-census: usage: mercs2_probe tree-census <tree_instances.json>");
std::process::exit(2);
}
},
"gfx-extract" => match first_positional(&args) {
Some(out) => run(diag::gfx_extract(&wadpath, &out)),
None => {
eprintln!("gfx-extract: usage: mercs2_probe gfx-extract <outdir> [--wad <path>]");
std::process::exit(2);
}
},
"extract" => {
let mh = model.as_deref().and_then(parse_hash).unwrap_or_else(|| {
eprintln!("extract: usage: mercs2_probe extract --model 0xHASH [--type 0xTYPEHASH] <out.bin>");
std::process::exit(2);
});
let ty = flag_val(&args, "--type").map(|t| {
parse_hash(&t).unwrap_or_else(|| {
eprintln!("extract: --type {t:?} is not a hex hash");
std::process::exit(2);
})
});
let out = first_positional(&args).unwrap_or_else(|| format!("{mh:08X}.bin"));
run((|| -> Result<(), String> {
let mut w = wad::open(&wadpath).map_err(|e| format!("open {wadpath}: {e}"))?;
let c = match ty {
Some(t) => wad::extract_container_typed(&mut w, mh, t).map_err(|e| format!("extract_container_typed: {e}"))?,
None => wad::extract_container(&mut w, mh).map_err(|e| format!("extract_container: {e}"))?,
};
std::fs::write(&out, &c).map_err(|e| format!("write {out}: {e}"))?;
println!("0x{mh:08X}: {} bytes -> {out}", c.len());
Ok(())
})());
}
"dump-block" => {
let bi = first_positional(&args).and_then(|s| s.parse::<u16>().ok()).unwrap_or_else(|| {
eprintln!("dump-block: usage: mercs2_probe dump-block <block-index> <out.bin>");
std::process::exit(2);
});
let out = args.iter().rev().find(|a| a.ends_with(".bin")).cloned().unwrap_or_else(|| format!("block_{bi}.bin"));
run((|| -> Result<(), String> {
let mut w = wad::open(&wadpath).map_err(|e| format!("open {wadpath}: {e}"))?;
let dec = wad::decompress_block_index(&mut w, bi).map_err(|e| format!("block {bi}: {e}"))?;
std::fs::write(&out, &dec).map_err(|e| format!("write {out}: {e}"))?;
println!("block {bi}: {} bytes decompressed -> {out}", dec.len());
Ok(())
})());
}
"ucfx-check" => {
let path = first_positional(&args).unwrap_or_default();
let entry: Option<usize> = args.iter().position(|a| a == "--entry")
.and_then(|i| args.get(i + 1)).and_then(|s| s.parse().ok());
run((|| -> Result<(), String> {
let data = std::fs::read(&path).map_err(|e| format!("read {path}: {e}"))?;
let (parsed, issues) = mercs2_formats::ucfx::walk_decompressed_block(&data, "blk");
println!("[ucfx-check] {} entries; {} UCFX issues", parsed.entry_count, issues.len());
for iss in issues.iter().take(12) {
println!(" {}: {}", iss.context, iss.detail);
}
if issues.len() > 12 {
println!(" ... and {} more", issues.len() - 12);
}
if let Some(ei) = entry {
if let Some(c) = parsed.containers.get(ei) {
if c.len() >= 20 {
let daf = u32::from_le_bytes([c[4], c[5], c[6], c[7]]);
let nd = u32::from_le_bytes([c[16], c[17], c[18], c[19]]);
println!("[ucfx-check] entry[{ei}]: container_len={} field@4(data_area_off?)={} n_desc={}", c.len(), daf, nd);
}
}
}
Ok(())
})());
}
"find-placement" => {
let query = first_positional(&args).unwrap_or_default().to_lowercase();
run((|| -> Result<(), String> {
if query.is_empty() {
return Err("usage: mercs2_probe find-placement <name-substring>".into());
}
let mut w = wad::open(&wadpath).map_err(|e| format!("open {wadpath}: {e}"))?;
let (mut scanned, mut hits, mut miss) = (0usize, 0usize, 0u32);
for bi in 0u16..6000 {
match wad::decompress_block_index(&mut w, bi) {
Ok(dec) => {
miss = 0;
scanned += 1;
if let Ok(ps) = mercs2_formats::placement::load_placements(&dec) {
for p in ps {
if let Some(n) = &p.name {
if n.to_lowercase().contains(&query) {
println!(
"block {bi:>5} key=0x{:08X} pos=[{:9.2},{:9.2},{:9.2}] '{}'",
p.key, p.pos[0], p.pos[1], p.pos[2], n
);
hits += 1;
}
}
}
}
}
Err(_) => {
miss += 1;
if miss > 96 && scanned > 0 {
break;
}
}
}
}
eprintln!("[find-placement] scanned {scanned} blocks, {hits} match '{query}'");
Ok(())
})());
}
"hier" => {
let mh = model.as_deref().and_then(parse_hash).unwrap_or_else(|| {
eprintln!("hier: usage: mercs2_probe hier --model 0xHASH [names.txt]");
std::process::exit(2);
});
run(hier_report(&wadpath, mh, first_positional(&args), flag_val(&args, "--csv")));
}
"bone-census" => {
let wads: Vec<String> = match flag_val(&args, "--wads") {
Some(list) => list.split(',').map(|s| s.trim().to_string()).collect(),
None => vec![wadpath.clone()],
};
run(bone_census::bone_census(
&wads,
flag_val(&args, "--csv"),
first_positional(&args),
flag_val(&args, "--skeleton-csv"),
));
}
"overlays" => {
let prof = first_positional(&args).unwrap_or_else(|| {
eprintln!("overlays: usage: mercs2_probe overlays <profile-path>");
std::process::exit(2);
});
run(overlays_report(&wadpath, &prof));
}
"save-dump" => {
let prof = first_positional(&args).unwrap_or_else(|| {
eprintln!("save-dump: usage: mercs2_probe save-dump <profile-path> [grep-needle]");
std::process::exit(2);
});
let needle = {
let mut seen = false;
let mut n = None;
let mut skip = false;
for a in &args {
if skip { skip = false; continue; }
if a == "--wad" { skip = true; continue; }
if a.starts_with("--") { continue; }
if !seen { seen = true; continue; } n = Some(a.to_lowercase());
break;
}
n
};
run(save_dump(&prof, needle.as_deref()));
}
"anim-table" => {
let name = first_positional(&args).unwrap_or_else(|| {
eprintln!("anim-table: usage: mercs2_probe anim-table <name|0xH> [--handle 0xH] [--list <column>]");
std::process::exit(2);
});
let hash = parse_hash(&name).unwrap_or_else(|| mercs2_formats::hash::pandemic_hash_m2(&name));
let handle = match flag_val(&args, "--handle") {
Some(h) => Some(parse_hash(&h).unwrap_or_else(|| {
eprintln!("anim-table: --handle {h:?} is not a 0xHASH");
std::process::exit(2);
})),
None => None,
};
run(anim_table::anim_table(&wadpath, hash, handle, flag_val(&args, "--list").as_deref()));
}
"facefx" => {
if args.iter().any(|a| a == "--all") {
run(facefx::facefx_all(&wadpath));
} else {
let name = first_positional(&args).unwrap_or_else(|| {
eprintln!("facefx: usage: mercs2_probe facefx <name|0xH> | --all");
std::process::exit(2);
});
let hash = parse_hash(&name).unwrap_or_else(|| mercs2_formats::hash::pandemic_hash_m2(&name));
run(facefx::facefx(&wadpath, hash));
}
}
"layer" => {
if args.iter().any(|a| a == "--catalog") {
let out = flag_val(&args, "--catalog").unwrap_or_else(|| {
eprintln!("layer: usage: mercs2_probe layer --catalog <out.tsv> [--also <archive,archive>] [--overlay <wad>]");
std::process::exit(2);
});
let also: Vec<String> = flag_val(&args, "--also").map_or(Vec::new(), |v| v.split(',').map(str::to_string).collect());
let overlays: Vec<String> = flag_val(&args, "--overlay").into_iter().collect();
run(layer::layer_catalog(&wadpath, std::path::Path::new(&out), &also, &overlays));
} else if args.iter().any(|a| a == "--frames") {
let capture = flag_val(&args, "--frames").unwrap_or_else(|| {
eprintln!("layer: usage: mercs2_probe layer --frames <capture>");
std::process::exit(2);
});
run(layer::layer_frames(&wadpath, std::path::Path::new(&capture)));
} else if args.iter().any(|a| a == "--all") {
run(layer::layer_all(&wadpath));
} else if let Some(capture) = flag_val(&args, "--loaded") {
run(layer::layer_loaded(&wadpath, std::path::Path::new(&capture)));
} else if let Some(at) = args.iter().position(|a| a == "--near") {
let point: Vec<f32> = args[at + 1..].iter().take(3).filter_map(|a| a.parse().ok()).collect();
let [x, z, radius] = point[..] else {
eprintln!("layer: usage: mercs2_probe layer --near <x> <z> <radius>");
std::process::exit(2);
};
run(layer::layer_near(&wadpath, x, z, radius));
} else {
let name = first_positional(&args).unwrap_or_else(|| {
eprintln!("layer: usage: mercs2_probe layer <name|0xH> [--overlay <wad>] | --all | --frames | --near <x> <z> <radius>");
std::process::exit(2);
});
let hash = parse_hash(&name).unwrap_or_else(|| mercs2_formats::hash::pandemic_hash_m2(&name));
let overlays: Vec<String> = flag_val(&args, "--overlay").into_iter().collect();
run(layer::layer(&wadpath, hash, &overlays));
}
}
"foliage" => run(dump_render::foliage_list(&wadpath)),
"foliage-check" => match first_positional(&args) {
Some(capture) => run(dump_render::foliage_check(&wadpath, &capture)),
None => {
eprintln!("foliage-check: usage: mercs2_probe foliage-check <capture>");
std::process::exit(2);
}
},
"terrain-cell" => {
let usage = "terrain-cell: usage: mercs2_probe terrain-cell <0xH> [--at <centre_x> <centre_z> <x> <z>]";
let name = first_positional(&args).unwrap_or_else(|| {
eprintln!("{usage}");
std::process::exit(2);
});
let hash = parse_hash(&name).unwrap_or_else(|| mercs2_formats::hash::pandemic_hash_m2(&name));
let (centre, point) = match args.iter().position(|a| a == "--at") {
Some(at) => {
let p: Vec<f32> = args[at + 1..].iter().take(4).filter_map(|a| a.parse().ok()).collect();
let [cx, cz, x, z] = p[..] else {
eprintln!("{usage}");
std::process::exit(2);
};
(Some([cx, cz]), Some([x, z]))
}
None => (None, None),
};
run(terrain_cell::terrain_cell(&wadpath, hash, centre, point));
}
"clip-names" => {
let out = first_positional(&args).unwrap_or_else(|| {
eprintln!("clip-names: usage: mercs2_probe clip-names <review.tsv>");
std::process::exit(2);
});
run(clip_names::clip_names(&wadpath, std::path::Path::new(&out)));
}
"clip-pose" => {
let usage = "clip-pose: usage: mercs2_probe clip-pose --model 0xH --clip 0xH --bones 0xA,0xB.. --ref 0xR [--frames N]";
let hash = |flag: &str| {
flag_val(&args, flag).and_then(|v| parse_hash(&v)).unwrap_or_else(|| {
eprintln!("{usage}");
std::process::exit(2);
})
};
let (m, c, r) = (hash("--model"), hash("--clip"), hash("--ref"));
let bones: Vec<u32> = flag_val(&args, "--bones")
.unwrap_or_default()
.split(',')
.filter(|s| !s.is_empty())
.map(|s| {
parse_hash(s).unwrap_or_else(|| {
eprintln!("clip-pose: --bones entry {s:?} is not a 0xHASH");
std::process::exit(2);
})
})
.collect();
if bones.is_empty() {
eprintln!("{usage}");
std::process::exit(2);
}
let frames = match flag_val(&args, "--frames") {
Some(f) => f.parse().unwrap_or_else(|_| {
eprintln!("clip-pose: --frames {f:?} is not a count");
std::process::exit(2);
}),
None => 9,
};
run(clip_pose::clip_pose(&wadpath, m, c, &bones, r, frames));
}
"we-template" => {
let q = if args.iter().any(|a| a == "--enums") {
we_template::Query::Enums
} else if let Some(c) = flag_val(&args, "--class") {
we_template::Query::Class(c)
} else if let Some(v) = flag_val(&args, "--uses") {
we_template::Query::Uses(parse_hash(&v).unwrap_or_else(|| {
eprintln!("we-template: --uses {v:?} is not a 0xHASH");
std::process::exit(2);
}))
} else {
let names: Vec<String> = args
.iter()
.enumerate()
.filter(|(i, a)| !a.starts_with("--") && (*i == 0 || !args[i - 1].starts_with("--")))
.map(|(_, a)| a.clone())
.collect();
if names.is_empty() {
eprintln!("we-template: usage: mercs2_probe we-template <name|0xKEY>... | --class <Class> | --uses <0xU32>");
std::process::exit(2);
}
we_template::Query::Templates(names)
};
run(we_template::we_template(&wadpath, q));
}
"-h" | "--help" | "help" => usage(),
other => {
eprintln!("unknown subcommand '{other}'");
usage();
}
}
}