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

@@ -26,11 +26,12 @@ import sys
sys.path.insert(0, __file__.rsplit("/", 1)[0])
import unit_runtime as ur # object locator + disc token reader
# Angle fields can carry a prefix (`AB_AA_PitchPlus` = afterburner), so match
# the AV_/AA_ token anywhere, not just at the start — anchoring it left two
# player-craft fields looking like contradictions when they were 15°/16° in
# radians.
ANGLE = re.compile(r"(^|_)(AV|AA)_|Bank|Turn_AngularVelocity")
# Angle fields are DEGREES on disc and RADIANS in the object. The set is wider
# than the `AV_`/`AA_` families: a prefix may precede them (`AB_AA_PitchPlus` =
# afterburner) and some carry neither (`Through_AngleMaximum`). Both cases showed
# up as lone "contradictions" whose object value was exactly the disc value in
# radians — which is the tell.
ANGLE = re.compile(r"(^|_)(AV|AA)_|Bank|Angle|Turn_AngularVelocity")
def read_layout(path):