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
121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Read EVERY field of the live unit-definition objects, using the code-derived
|
|
layout instead of solving offsets from the disc.
|
|
|
|
`unit_runtime.py` scores `(field, offset, encoding)` triples against the disc
|
|
records, so it can only place fields the disc actually values — 58 of 153 on a
|
|
single-mission snapshot. The layout in `data/unit_definition_layout.txt` was read
|
|
out of the title's own loader (`sub_82341A20`: the field NAME of every store is a
|
|
string in the image), so it places **all** of them, including the ones no disc
|
|
record ever sets. That is exactly the Route-B target: the defaulted fields.
|
|
|
|
Discipline is unchanged — every field the disc DOES value is cross-checked against
|
|
the object, and a mismatch is reported rather than written. Angle fields are
|
|
degrees on disc and radians in the object (see the layout header), so those are
|
|
compared after conversion.
|
|
|
|
Usage:
|
|
unit_dump_layout.py <layout.txt> <tokens.txt> <snap.bin>... [--csv out.csv]
|
|
"""
|
|
import csv
|
|
import math
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
sys.path.insert(0, __file__.rsplit("/", 1)[0])
|
|
import unit_runtime as ur # object locator + disc token reader
|
|
|
|
# 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):
|
|
"""[(offset, kind, name)] from the '# offset kind field' table."""
|
|
out = []
|
|
for line in open(path):
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
parts = line.split()
|
|
if len(parts) < 3:
|
|
continue
|
|
off, kind, name = parts[0], parts[1].lower(), parts[2]
|
|
try:
|
|
# The layout table writes offsets in DECIMAL (`48 f32 Size_X`), while
|
|
# the solver's CSV prints them in hex — parsing this file as hex puts
|
|
# every field 0x18 bytes late and makes the disc cross-check fail on
|
|
# everything, which is how the mistake announces itself.
|
|
out.append((int(off, 10), kind, name))
|
|
except ValueError:
|
|
continue
|
|
return out
|
|
|
|
|
|
def value_at(raw, off, kind):
|
|
if off + 4 > len(raw):
|
|
return None
|
|
if kind in ("f32", "float"):
|
|
return struct.unpack_from(">f", raw, off)[0]
|
|
if kind in ("word", "u32", "int", "bool"):
|
|
return struct.unpack_from(">I", raw, off)[0]
|
|
return None # str: a guest pointer, not a value
|
|
|
|
|
|
def as_float(s):
|
|
try:
|
|
return float(str(s).rstrip("fF"))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def main():
|
|
layout_path, tokens_path = sys.argv[1], sys.argv[2]
|
|
snaps = [a for a in sys.argv[3:] if not a.startswith("--")]
|
|
out_csv = next((a.split("=", 1)[1] for a in sys.argv if a.startswith("--csv=")), None)
|
|
|
|
layout = read_layout(layout_path)
|
|
disc, _ambiguous = ur.read_tokens(tokens_path)
|
|
objs, conflicts = ur.runtime_objects(snaps)
|
|
print(f"# layout fields: {len(layout)} runtime objects: {len(objs)} "
|
|
f"disc units: {len(disc)} matched: {len(set(objs) & set(disc))}")
|
|
if conflicts:
|
|
print(f"# !! objects differing between snapshots: {conflicts}")
|
|
|
|
rows, agree, disagree = [], 0, 0
|
|
for unit in sorted(objs):
|
|
raw = objs[unit]
|
|
drec = disc.get(unit, {})
|
|
for off, kind, name in layout:
|
|
v = value_at(raw, off, kind)
|
|
if v is None:
|
|
continue
|
|
dv = as_float(drec.get(name)) if name in drec else None
|
|
if dv is not None:
|
|
# The disc values this field: it is a CHECK, not a new value.
|
|
cmpv = math.degrees(v) if ANGLE.search(name) and isinstance(v, float) else v
|
|
ok = abs(cmpv - dv) <= max(1e-3, abs(dv) * 1e-3)
|
|
agree, disagree = (agree + 1, disagree) if ok else (agree, disagree + 1)
|
|
if not ok:
|
|
print(f"# MISMATCH {unit} {name} @{off:#05x}: object {cmpv!r} vs disc {dv!r}")
|
|
rows.append([unit, name, f"{off:#05x}", kind, repr(v), "disc",
|
|
"verified" if ok else "CONTRADICTED"])
|
|
else:
|
|
rows.append([unit, name, f"{off:#05x}", kind, repr(v),
|
|
"defaulted-on-disc", "layout-derived"])
|
|
print(f"# cross-check against the disc: {agree} agree, {disagree} disagree")
|
|
print(f"# rows: {len(rows)} ({sum(1 for r in rows if r[5] == 'defaulted-on-disc')} defaulted)")
|
|
if out_csv:
|
|
w = csv.writer(open(out_csv, "w", newline=""))
|
|
w.writerow(["unit", "field", "offset", "enc", "value", "source", "conf"])
|
|
w.writerows(rows)
|
|
print(f"# wrote {out_csv}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|