Add gc-extract probe to dump structures through the real parser

Reading these formats by unpacking raw offsets out of core_index.bin has
cost real time. The index interleaves single-word region offsets with
count/offset pairs, so miscounting one shifts every field after it: that
produced a page of implausible collision headers on all 27 levels, sent
me chasing a tie-breakoff that turned out not to exist, and nearly had me
"fix" a stride bug in code that was already right.

probe dumps a level's core header and all three class tables through
LevelCoreHeader, so a field name printed is the field name the parser
uses. It also reports how many entries carry geometry inside core_data,
which is what pins the class table strides rather than guessing at them:
shrub reads 7/7 sane at 0x30 against 4/7 read at 0x20.

Reading a structure by hand in a shell still works; this just stops it
being the default, and stops the offset arithmetic being re-derived every
time a new region is investigated.
This commit is contained in:
2026-10-11 02:36:37 +02:00
parent c4ba84232a
commit f6bbbf8d5f
4 changed files with 270 additions and 0 deletions
+74
View File
@@ -26,6 +26,7 @@ fn main() -> ExitCode {
// byte range of an already-unpacked file with the WAD decoder.
Some("wad") => run_wad(&args[1..]),
Some("dump-texture") => run_dump_texture(&args[1..]),
Some("probe") => run_probe(&args[1..]),
Some("--help") | Some("-h") | Some("help") | None => {
usage();
ExitCode::SUCCESS
@@ -66,6 +67,12 @@ fn usage() {
verify <out-dir> <table.tsv>
Check <out-dir> against a previously written table.
probe <unpacked-dir> <level> [max-rows]
Dump a level's core header and class tables through the same parser the
rest of the project uses. Reach for this before reading core_index.bin
by hand: the index interleaves single-word region offsets with
count/offset pairs, and miscounting one shifts every field after it.
dump-texture <unpacked-dir> <level> <index> [mip] <out.ppm>
Decode one tfrag texture to a binary PPM, so a decode can be eyeballed
against the disc rather than trusted. Textures are 8-bit indexed; a wrong
@@ -314,6 +321,73 @@ fn run_dump_texture(args: &[String]) -> ExitCode {
ExitCode::SUCCESS
}
fn run_probe(args: &[String]) -> ExitCode {
if args.len() < 2 {
eprintln!("usage: gc-extract probe <unpacked-dir> <level> [max-rows]");
return ExitCode::FAILURE;
}
let dir = Path::new(&args[0]);
let level: u32 = match args[1].parse() {
Ok(v) => v,
Err(e) => {
eprintln!("bad level: {e}");
return ExitCode::FAILURE;
}
};
let max_rows: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(8);
let p = match gc_extract::probe_level(dir, level) {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e:#}");
return ExitCode::FAILURE;
}
};
println!(
"level{level}: core_data {} bytes, core_index {} bytes",
p.core_len, p.index_len
);
println!("core header");
println!("{}", "-".repeat(64));
for (k, v) in &p.header {
println!(" {k:<22} {v:>14}");
}
for t in &p.classes {
let sane = t.entries.iter().filter(|e| e.in_core(p.core_len)).count();
println!();
println!(
"{} classes: {} entries at stride 0x{:x}, {sane} with geometry in core_data",
t.name,
t.entries.len(),
t.stride
);
println!("{}", "-".repeat(64));
println!(
" {:>4} {:>12} {:>8} {:>10} {:>10}",
"idx", "geom_offset", "o_class", "unknown_8", "unknown_c"
);
for e in t.entries.iter().take(max_rows) {
println!(
" {:>4} {:>12} {:>8} {:>10} {:>10}{}",
e.index,
e.geometry_offset,
e.o_class,
e.unknown_8,
e.unknown_c,
if e.in_core(p.core_len) {
""
} else {
" <- outside core_data"
}
);
}
if t.entries.len() > max_rows {
println!(" ... {} more", t.entries.len() - max_rows);
}
}
ExitCode::SUCCESS
}
fn run_wad(args: &[String]) -> ExitCode {
if args.len() < 4 {
eprintln!("usage: gc-extract wad <file> <offset> <size> <out>");