re(units): target missions by roster, harvest S09 — 36 units / 3 439 defaulted values

examples/roster_target.rs ranks stages by how many roster units are still
unharvested. The EnumUnit_S<NN> tables are found by hashing candidate TOC paths
(hash::TOC_NAME_SCHEMES) — UnitRoster::stage can only infer a tag when the roster
carries a UN_S<NN>_ prop, which most do not.

It picked S09 (10 missing). Flying it: 26 -> 36 units, 3 345 -> 4 785 rows,
2 351 -> 3 439 defaulted-on-disc values. New: e102_Battleship, e104_Carrier,
e107_AAFrigate, e011_Attacker_B, e008_TurretPlus, be001_TerrafoamingUnit,
e001_Elan_GR{,_Violeta}, f102_LightCarrier_Inv, f106_Destroyer_Inv.

Also settled: the definition objects are mission-independent. Eleven units appear in
more than one snapshot and four are not byte-identical, but compared through the
layout ZERO mapped fields differ — the 12 differing slots are all unmapped (offsets
4/8/16/20 and 0x250/0x268/0x300-0x308/0x330-0x338: object header and sub-object
pointers). So a harvested value is the definition, not a per-mission tweak, and the
earlier UN_f201_TCAF_Tanker flag resolves the same way. Cross-checks over three
snapshots: 1 052 agree, 0 disagree.

Third angle field found the same way (Through_AngleMaximum = 60 degrees in radians),
so the degrees<->radians rule covers any name containing "Angle".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
This commit is contained in:
2026-08-13 13:48:12 +00:00
parent 14d5ed54d1
commit bf6825e278
4 changed files with 1536 additions and 5 deletions

View File

@@ -0,0 +1,60 @@
//! Which mission should the next unit harvest fly?
//!
//! Unit definitions are instantiated per stage, so coverage grows by visiting
//! missions — but only if the mission fields units we have not read yet. This
//! ranks every stage by how many of its roster units are missing from the
//! harvested CSV.
//!
//! Usage: roster_target <disc-root> <unit-runtime-fields.csv>
use sylpheed_formats::hash::name_hash;
use sylpheed_formats::idxd::IdxdObject;
use sylpheed_formats::pak::PakArchive;
use std::collections::{BTreeMap, BTreeSet};
fn main() {
let a: Vec<String> = std::env::args().collect();
let pak = PakArchive::open(format!("{}/dat/GP_MAIN_GAME_E.pak", a[1])).expect("pak");
let have: BTreeSet<String> = std::fs::read_to_string(&a[2])
.expect("csv")
.lines()
.skip(1)
.filter_map(|l| l.split(',').next().map(str::to_string))
.collect();
// Label each roster by STAGE: the TOC stores a hash of the original path, and
// the table names are known (`EnumUnit_SNN.tbl`), so hashing the candidates
// maps entries back to stages. Most rosters carry no `UN_S<NN>_` prop id, which
// is all `UnitRoster::stage` can infer from.
let mut by_hash: BTreeMap<u32, String> = BTreeMap::new();
// The TOC hashes a PATH, and these tables live under a directory — try the
// known schemes (see hash::TOC_NAME_SCHEMES) rather than the bare name.
for i in 1..=29 {
let stage = format!("S{i:02}");
for pre in ["", "unit\\", "battle\\", "stage\\", "enemy\\"] {
for suf in [".tbl", ""] {
by_hash.insert(name_hash(&format!("{pre}EnumUnit_{stage}{suf}")), stage.clone());
}
}
}
let mut rows: Vec<(usize, String, Vec<String>)> = Vec::new();
for e in pak.entries() {
let Some(stage) = by_hash.get(&e.name_hash).cloned() else { continue };
let Ok(b) = pak.read(e) else { continue };
let Ok(o) = IdxdObject::parse(&b) else { continue };
let mut units: Vec<String> = Vec::new();
for id in o.tokens().iter().filter(|s| s.starts_with("UN_")) {
let prop = id.contains("Asteroid") || id.contains("cmesh") || id.contains("_Box");
if !prop && !units.iter().any(|u| u == id) {
units.push(id.clone());
}
}
let missing: Vec<String> = units.iter().filter(|u| !have.contains(*u)).cloned().collect();
rows.push((missing.len(), stage, missing));
}
rows.sort_by_key(|(n, _, _)| std::cmp::Reverse(*n));
println!("units already harvested: {}\n", have.len());
println!("stage missing roster units not yet read");
for (n, stage, missing) in rows.iter().take(12) {
let s: Vec<&str> = missing.iter().take(6).map(|s| s.as_str()).collect();
println!("{stage:<8} {n:>5} {}", s.join(", "));
}
}