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:
60
crates/sylpheed-formats/examples/roster_target.rs
Normal file
60
crates/sylpheed-formats/examples/roster_target.rs
Normal 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(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -450,3 +450,33 @@ Two things the run pinned down, both cheap to re-learn the hard way:
|
|||||||
⚠️ One unit's object is **not** byte-identical between the two missions —
|
⚠️ One unit's object is **not** byte-identical between the two missions —
|
||||||
`UN_f201_TCAF_Tanker`. Either a per-mission override or a field the runtime mutates;
|
`UN_f201_TCAF_Tanker`. Either a per-mission override or a field the runtime mutates;
|
||||||
the merged CSV holds the later reading, and separating them needs a third snapshot.
|
the merged CSV holds the later reading, and separating them needs a third snapshot.
|
||||||
|
|
||||||
|
### Targeting the next mission, and the check that these really are definitions (2026-08-13)
|
||||||
|
|
||||||
|
[`examples/roster_target.rs`](../../../crates/sylpheed-formats/examples/roster_target.rs)
|
||||||
|
ranks the stages by how many of their roster units are **not yet harvested**. The
|
||||||
|
rosters are `EnumUnit_S<NN>` tables whose TOC entries store a *path hash*, so the
|
||||||
|
stage label comes from hashing the candidate paths
|
||||||
|
(`hash::TOC_NAME_SCHEMES`) rather than from `UnitRoster::stage`, which can only
|
||||||
|
infer a tag when the roster happens to carry a `UN_S<NN>_…` prop.
|
||||||
|
|
||||||
|
It picked **S09 (10 missing)**; flying it took the file to **36 units / 4 785 rows /
|
||||||
|
3 439 defaulted-on-disc values**, adding `UN_e102_ADAN_Battleship`,
|
||||||
|
`UN_e104_ADAN_Carrier`, `UN_e107_ADAN_AAFrigate`, `UN_e011_ADAN_Attacker_B`,
|
||||||
|
`UN_e008_ADAN_TurretPlus`, `UN_be001_ADAN_TerrafoamingUnit`,
|
||||||
|
`UN_e001_ADAN_Elan_GR{,_Violeta}`, `UN_f102_TCAF_LightCarrier_Inv` and
|
||||||
|
`UN_f106_TCAF_Destroyer_Inv`.
|
||||||
|
|
||||||
|
**The objects are mission-independent — measured, not assumed.** Eleven units appear
|
||||||
|
in more than one snapshot, and four of them are *not* byte-identical across missions.
|
||||||
|
Comparing them **through the layout**: **zero mapped fields differ**. The 12 differing
|
||||||
|
4-byte slots are all **unmapped** — offsets `4/8/16/20` (the object header and name
|
||||||
|
pointer) and `0x250/0x268/0x300–0x308/0x330–0x338` (sub-object pointers) — i.e. guest
|
||||||
|
addresses, not data. So a value harvested in one mission is the unit's definition, not
|
||||||
|
a per-mission tweak, and the earlier `UN_f201_TCAF_Tanker` flag resolves the same way.
|
||||||
|
|
||||||
|
Cross-checks against the disc over the three snapshots: **1 052 agree, 0 disagree.**
|
||||||
|
|
||||||
|
One more angle field turned up the same way as the last: `Through_AngleMaximum`
|
||||||
|
(object `1.0472` = 60° in radians) carries neither an `AV_`/`AA_` token nor `Bank`, so
|
||||||
|
the degrees↔radians rule covers **anything with `Angle` in the name** too.
|
||||||
|
|||||||
@@ -26,11 +26,12 @@ import sys
|
|||||||
sys.path.insert(0, __file__.rsplit("/", 1)[0])
|
sys.path.insert(0, __file__.rsplit("/", 1)[0])
|
||||||
import unit_runtime as ur # object locator + disc token reader
|
import unit_runtime as ur # object locator + disc token reader
|
||||||
|
|
||||||
# Angle fields can carry a prefix (`AB_AA_PitchPlus` = afterburner), so match
|
# Angle fields are DEGREES on disc and RADIANS in the object. The set is wider
|
||||||
# the AV_/AA_ token anywhere, not just at the start — anchoring it left two
|
# than the `AV_`/`AA_` families: a prefix may precede them (`AB_AA_PitchPlus` =
|
||||||
# player-craft fields looking like contradictions when they were 15°/16° in
|
# afterburner) and some carry neither (`Through_AngleMaximum`). Both cases showed
|
||||||
# radians.
|
# up as lone "contradictions" whose object value was exactly the disc value in
|
||||||
ANGLE = re.compile(r"(^|_)(AV|AA)_|Bank|Turn_AngularVelocity")
|
# radians — which is the tell.
|
||||||
|
ANGLE = re.compile(r"(^|_)(AV|AA)_|Bank|Angle|Turn_AngularVelocity")
|
||||||
|
|
||||||
|
|
||||||
def read_layout(path):
|
def read_layout(path):
|
||||||
|
|||||||
Reference in New Issue
Block a user