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:
80
tools/re-capture/schema_order.py
Normal file
80
tools/re-capture/schema_order.py
Normal 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()
|
||||
Reference in New Issue
Block a user