re: solve the runtime Unit (craft/vessel) struct from live guest memory
Craft stats were the last parked Route-B target: the Hangar exposes only a weight class, and in menus the flight-model object is not instantiated at all. It is instantiated in-mission, so this reads it there. The definition class is discovered rather than assumed (unit_discover.py): scanning for pointers to the disc ID strings and tallying the word behind each pointer site picks out vtable 0x820af844 -- one object per unit ID, name-record pointer at +0x04, string at +0x10, exactly the Weapon shape. The neighbouring class 0x820af030 is the spawned entity, not the definition; the two are told apart by one-object-per-ID and by 14/14 objects being byte-identical across two snapshots 12 minutes and one firefight apart. 22 fields bind with zero contradictions on >=3 distinct values. Beyond that, the Maneuver sub-record turns out to be laid out in schema declaration order, 4 bytes per field, base 0x9c with a two-slot gap after AA_Roll_Min -- 29 independently solved anchors fit two exact bases with no conflicts. That rule pins five fields NO unit table ever values (PitchDragFactor, RollDragFactor, DragFactorThreshold, ArterBurner_Acc, DecPitchFactor); PitchDrag == RollDrag == the disc's YawDrag on 18/18 units corroborates the interpolation. Angles are float32 radians at runtime and degrees on disc. Coverage is 18 of 110 units: unlike weapons, unit definitions are instantiated per stage, so it grows by visiting missions. The AI-behaviour tail of Maneuver is explicitly NOT resolved and marked tentative in the CSV. Captured from Xenia Canary (BASIC CONTROLS tutorial + Stage 02 from save 01).
This commit is contained in:
95
tools/re-capture/order_check.py
Normal file
95
tools/re-capture/order_check.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test the hypothesis that a unit sub-record's runtime fields are laid out in
|
||||
*schema declaration order*, one 4-byte slot each.
|
||||
|
||||
The IDXD string pool lists a sub-record's field names in declaration order,
|
||||
including the ones this .tbl leaves at their default (they appear as a bare key
|
||||
with no preceding value). If the runtime struct is `base + 4*index` over that
|
||||
list, then the offsets the solver found independently must all satisfy it.
|
||||
|
||||
That is a much stronger check than any single field's agreement count: one base
|
||||
offset has to explain dozens of independently-derived anchors at once.
|
||||
|
||||
Usage: order_check.py <raw-token-dump> <tbl-hash> <SubRecord> <solved.txt>
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
NUMERIC = re.compile(r"^[-+]?[0-9]*\.?[0-9]+[fF]?$|^0x[0-9a-fA-F]+$")
|
||||
VALUE_WORDS = {"Yes", "No", "YES", "NO", "On", "Off", "ON", "OFF"}
|
||||
|
||||
|
||||
def section(dump, tbl, sub, subs):
|
||||
"""Ordered token list of one sub-record of one .tbl."""
|
||||
toks, on = [], False
|
||||
for line in open(dump):
|
||||
if line.startswith("RAW "):
|
||||
on = line.split()[1] == tbl
|
||||
continue
|
||||
if on and line.startswith("T "):
|
||||
toks.append(line[2:].rstrip("\n"))
|
||||
starts = [i for i, t in enumerate(toks) if t in subs or t.lstrip("(") in subs]
|
||||
for n, i in enumerate(starts):
|
||||
name = toks[i].lstrip("(")
|
||||
if name != sub:
|
||||
continue
|
||||
end = starts[n + 1] if n + 1 < len(starts) else len(toks)
|
||||
return toks[i + 1 : end]
|
||||
return []
|
||||
|
||||
|
||||
def keys_in_order(toks):
|
||||
"""Field names, in declaration order: every token that is not a value.
|
||||
|
||||
In the numeric sub-records every value is a number literal, so a token that
|
||||
is not numeric and not a Yes/No word is a key.
|
||||
"""
|
||||
out = []
|
||||
for t in toks:
|
||||
if NUMERIC.match(t) or t in VALUE_WORDS:
|
||||
continue
|
||||
if t not in out:
|
||||
out.append(t)
|
||||
return out
|
||||
|
||||
|
||||
def solved_offsets(path):
|
||||
out = {}
|
||||
for line in open(path):
|
||||
m = re.match(r"\s*(0x[0-9a-f]+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\d+)\s+(\d+)\s*$", line)
|
||||
if m:
|
||||
off, enc, field, agree, dist, bad = m.groups()
|
||||
out[field] = (int(off, 16), enc, int(agree), int(dist), int(bad))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
dump, tbl, sub, solvedf = sys.argv[1:5]
|
||||
subs = {"Generic", "Maneuver", "Shield", "Explosion", "Mass", "Effect", "SE"}
|
||||
keys = keys_in_order(section(dump, tbl, sub, subs))
|
||||
solved = solved_offsets(solvedf)
|
||||
anchors = [(i, k, solved[k]) for i, k in enumerate(keys) if k in solved]
|
||||
if not anchors:
|
||||
sys.exit(f"no solved field lands in {sub}")
|
||||
|
||||
# base that the most anchors agree on
|
||||
votes = {}
|
||||
for i, k, (off, *_) in anchors:
|
||||
votes.setdefault(off - 4 * i, []).append(k)
|
||||
base, winners = max(votes.items(), key=lambda kv: len(kv[1]))
|
||||
print(f"# {sub}: {len(keys)} declared fields, {len(anchors)} solved anchors")
|
||||
print(f"# best base = {base:#x} -> {len(winners)}/{len(anchors)} anchors fit "
|
||||
f"offset = base + 4*index\n")
|
||||
for i, k in enumerate(keys):
|
||||
off = base + 4 * i
|
||||
if k in solved:
|
||||
so, enc, agree, dist, bad = solved[k]
|
||||
mark = "fits" if so == off else f"CONFLICT solver={so:#x}"
|
||||
print(f" +{off:#05x} [{i:3d}] {k:<34} {enc} agree={agree:<3} "
|
||||
f"dist={dist:<3} {mark}")
|
||||
else:
|
||||
print(f" +{off:#05x} [{i:3d}] {k:<34} — (never valued on disc)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user