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:
2026-07-29 17:27:53 +00:00
parent 6a41525c41
commit 2fa4c33d1a
8 changed files with 2326 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Recover a sub-record's FULL schema field order by merging every unit .tbl.
Each .tbl's string pool lists only the fields that .tbl declares, in schema
order. Merging them is a topological sort over the pairwise "k[i] precedes
k[i+1]" constraints of all 110 tables: any single table is a subsequence of the
schema, so the union order is the schema order — provided the constraints are
acyclic, which is itself the check that the premise holds.
Usage: schema_order.py <raw-token-dump> <SubRecord> [solved.txt]
"""
import sys
from collections import defaultdict
sys.path.insert(0, ".")
from order_check import section, keys_in_order, solved_offsets # noqa: E402
SUBS = {"Generic", "Maneuver", "Shield", "Explosion", "Mass", "Effect", "SE"}
def merged_order(dump, sub):
tbls = [l.split()[1] for l in open(dump) if l.startswith("RAW ")]
succ, indeg, nodes = defaultdict(set), defaultdict(int), []
seqs = []
for t in tbls:
ks = keys_in_order(section(dump, t, sub, SUBS))
if ks:
seqs.append(ks)
for k in ks:
if k not in nodes:
nodes.append(k)
for a, b in zip(ks, ks[1:]):
if b not in succ[a]:
succ[a].add(b)
indeg[b] += 1
# Kahn, breaking ties by first-seen order so the result is deterministic
ready = [n for n in nodes if indeg[n] == 0]
out, cycles = [], False
while ready:
ready.sort(key=nodes.index)
n = ready.pop(0)
out.append(n)
for m in sorted(succ[n], key=nodes.index):
indeg[m] -= 1
if indeg[m] == 0:
ready.append(m)
if len(out) != len(nodes):
cycles = True
out += [n for n in nodes if n not in out]
return out, seqs, cycles
def main():
dump, sub = sys.argv[1], sys.argv[2]
order, seqs, cycles = merged_order(dump, sub)
print(f"# {sub}: {len(order)} fields merged from {len(seqs)} tables"
f"{' !! CYCLE — order is not consistent' if cycles else ''}")
# every table must be a subsequence of the merged order — the real check
pos = {k: i for i, k in enumerate(order)}
bad = [s for s in seqs if [pos[k] for k in s] != sorted(pos[k] for k in s)]
print(f"# tables that are NOT a subsequence of the merged order: {len(bad)}")
if len(sys.argv) > 3:
solved = solved_offsets(sys.argv[3])
anchors = [(i, k, solved[k]) for i, k in enumerate(order) if k in solved]
votes = defaultdict(list)
for i, k, (off, *_) in anchors:
votes[off - 4 * i].append((i, k))
print(f"# {len(anchors)} solved anchors; base votes:")
for b, ks in sorted(votes.items(), key=lambda kv: -len(kv[1])):
idx = sorted(i for i, _ in ks)
print(f"# base {b:#07x}: {len(ks):3d} anchors, indices {idx[0]}..{idx[-1]}")
for i, k in enumerate(order):
s = solved.get(k)
note = f"solver={s[0]:#x} {s[1]} agree={s[2]} dist={s[3]}" if s else ""
print(f" [{i:3d}] {k:<34} {note}")
if __name__ == "__main__":
main()