use std::fs::File;
use std::path::Path;
use std::process::ExitCode;
use mercs2_formats::ffcs::load_ffcs_archive;
use mercs2_formats::hash::pandemic_hash_m2;
use mercs2_formats::luachunk::decode;
use mercs2_formats::script_meta::derive;
use mercs2_formats::scripts_block::{container_chunk, container_deps, container_info};
use mercs2_formats::sges::decompress_block;
use mercs2_formats::types::TYPE_HASH_SCRIPT;
fn report(wad: &Path) -> Result<(usize, usize), String> {
let mut f = File::open(wad).map_err(|e| format!("{}: {e}", wad.display()))?;
let size = f.metadata().map_err(|e| format!("{}: {e}", wad.display()))?.len();
let arch = load_ffcs_archive(&mut f, size).map_err(|e| format!("{}: {e}", wad.display()))?;
let (mut rows, mut bad) = (0usize, 0usize);
for bi in 0..arch.indx.len() {
let block = decompress_block(&mut f, &arch.indx, bi as u16).map_err(|e| format!("{} block {bi}: {e}", wad.display()))?;
let rd = |off: usize| u32::from_le_bytes(block[off..off + 4].try_into().unwrap());
let count = rd(0) as usize;
let mut pos = 4 + count * 16;
for i in 0..count {
let row = 4 + i * 16;
let (name_hash, type_hash, size) = (rd(row), rd(row + 4), rd(row + 12) as usize);
let c = &block[pos..pos + size];
pos += size;
if type_hash != TYPE_HASH_SCRIPT {
continue;
}
rows += 1;
let (status, stored, derived, detail, info_name) = classify(c, name_hash);
if status != "match" {
bad += 1;
}
println!(
"{}\t{bi}\t{i}\t0x{name_hash:08x}\t{info_name}\t{status}\t{stored}\t{derived}\t{detail}",
wad.display()
);
}
}
Ok((rows, bad))
}
fn classify(c: &[u8], name_hash: u32) -> (&'static str, String, String, String, String) {
let info = match container_info(c) {
Ok(i) => i,
Err(e) => return ("underivable", "-".into(), "-".into(), format!("INFO: {e}"), "-".into()),
};
let deps = container_deps(c).unwrap_or_default();
let stored = format!("{}/{}/{}", info.tail_u16, info.tail_u8, deps.len());
let luaq = match container_chunk(c, b"BINN") {
Ok(Some(l)) => l,
Ok(None) => return ("underivable", stored, "-".into(), "no BINN".into(), info.name),
Err(e) => return ("underivable", stored, "-".into(), e, info.name),
};
let name_ok = pandemic_hash_m2(&info.name) == name_hash;
let derive_name = if name_ok {
info.name.clone()
} else {
match decode(luaq).ok().and_then(|ch| ch.main.source) {
Some(s) => String::from_utf8_lossy(&s).into_owned(),
None => return ("name", stored, "-".into(), "INFO names another script and the chunk has no source name".into(), info.name),
}
};
match derive(&derive_name, luaq) {
Err(e) => {
let status = if name_ok { "underivable" } else { "name" };
(status, stored, "-".into(), e, info.name)
}
Ok(m) => {
let derived = format!("{}/{}/{}", m.globals, m.inherits, m.deps.len());
let hashes: Vec<u32> = m.deps.iter().map(|d| d.hash).collect();
if !name_ok {
let detail = format!("INFO name {:?} is not this entry's; chunk source is {derive_name:?}", info.name);
("name", stored, derived, detail, info.name)
} else if m.globals == info.tail_u16 && m.inherits == info.tail_u8 && hashes == deps {
("match", stored, derived, String::new(), info.name)
} else {
("differs", stored, derived, String::new(), info.name)
}
}
}
}
fn main() -> ExitCode {
let wads: Vec<String> = std::env::args().skip(1).collect();
if wads.is_empty() {
eprintln!("usage: script_meta_report <wad> [<wad> ...]");
return ExitCode::from(2);
}
println!("wad\tblock\tentry\tname_hash\tinfo_name\tstatus\tstored\tderived\tdetail");
let (mut rows, mut bad) = (0usize, 0usize);
for w in &wads {
match report(Path::new(w)) {
Ok((r, b)) => {
rows += r;
bad += b;
}
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(2);
}
}
}
eprintln!("{rows} scripts, {bad} not matching");
if bad == 0 { ExitCode::SUCCESS } else { ExitCode::from(1) }
}