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:
123
tools/re-capture/unit_discover.py
Normal file
123
tools/re-capture/unit_discover.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Discover the runtime class that carries the parsed `UN_*` unit definitions.
|
||||
|
||||
No vtable VA is assumed. Procedure:
|
||||
1. locate every disc unit-ID string in the snapshot;
|
||||
2. find every 4-byte-aligned word in RAM that points at (string_va - d) for a
|
||||
few plausible name-record deltas;
|
||||
3. for each such pointer site P and each small back-offset k, read the word at
|
||||
P-k and tally it across *distinct* unit IDs.
|
||||
A word that appears at the same (d, k) for many different units is that class's
|
||||
vtable pointer, and k is the ID-pointer offset inside the object.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
sys.path.insert(0, "/home/fabi/RE - Project Sylpheed/sylpheed-reborn/tools/re-capture")
|
||||
import gmem # noqa: E402
|
||||
|
||||
DELTAS = [0x00, 0x10, 0x08, 0x04, 0x0C, 0x14, 0x18, 0x20]
|
||||
BACK = list(range(0, 0x60, 4))
|
||||
|
||||
|
||||
def load_ids(path):
|
||||
ids = []
|
||||
for line in open(path):
|
||||
if line.startswith("REC "):
|
||||
p = line.split()
|
||||
if p[3] == "Generic" and p[4].startswith("UN_"):
|
||||
ids.append(p[4])
|
||||
return sorted(set(ids))
|
||||
|
||||
|
||||
def scan_strings(f, size, ids):
|
||||
"""{id: [va,...]} — every NUL-terminated occurrence of each id string."""
|
||||
pats = {i: (i.encode() + b"\0") for i in ids}
|
||||
rx = re.compile(b"|".join(re.escape(p) for p in pats.values()))
|
||||
out = defaultdict(list)
|
||||
for a, b in gmem.extents(f.fileno(), size):
|
||||
f.seek(a)
|
||||
blob = f.read(b - a)
|
||||
for m in rx.finditer(blob):
|
||||
off = a + m.start()
|
||||
va = gmem.primary_va(off)
|
||||
if va is not None:
|
||||
out[m.group(0)[:-1].decode()].append(va)
|
||||
return out
|
||||
|
||||
|
||||
def scan_pointers(f, size, targets):
|
||||
"""{target_va: [site_va,...]} for every aligned word equal to a target."""
|
||||
want = {struct.pack(">I", t): t for t in targets}
|
||||
rx = re.compile(b"|".join(re.escape(k) for k in want))
|
||||
out = defaultdict(list)
|
||||
for a, b in gmem.extents(f.fileno(), size):
|
||||
f.seek(a)
|
||||
blob = f.read(b - a)
|
||||
for m in rx.finditer(blob):
|
||||
off = a + m.start()
|
||||
if off % 4:
|
||||
continue
|
||||
va = gmem.primary_va(off)
|
||||
if va is not None:
|
||||
out[want[m.group(0)]].append(va)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ids = load_ids(sys.argv[1])
|
||||
path = gmem.mem_path()
|
||||
size = os.path.getsize(path)
|
||||
with open(path, "rb") as f:
|
||||
strs = scan_strings(f, size, ids)
|
||||
print(f"# {len(ids)} disc unit IDs, {len(strs)} found in RAM "
|
||||
f"({sum(len(v) for v in strs.values())} occurrences)", file=sys.stderr)
|
||||
|
||||
# every (id, string_va - delta) we want pointers to
|
||||
targets, owner = set(), defaultdict(set)
|
||||
for uid, vas in strs.items():
|
||||
for va in vas:
|
||||
for d in DELTAS:
|
||||
t = va - d
|
||||
targets.add(t)
|
||||
owner[t].add((uid, d))
|
||||
ptrs = scan_pointers(f, size, targets)
|
||||
print(f"# {len(ptrs)} of {len(targets)} candidate targets are pointed at",
|
||||
file=sys.stderr)
|
||||
|
||||
# tally (delta, back-offset, word) over distinct unit IDs
|
||||
tally = defaultdict(set)
|
||||
sites = defaultdict(list)
|
||||
for tgt, plist in ptrs.items():
|
||||
for uid, d in owner[tgt]:
|
||||
for p in plist:
|
||||
for k in BACK:
|
||||
base = p - k
|
||||
try:
|
||||
f.seek(gmem.va_to_off(base))
|
||||
except ValueError:
|
||||
continue
|
||||
w = f.read(4)
|
||||
if len(w) < 4:
|
||||
continue
|
||||
(vt,) = struct.unpack(">I", w)
|
||||
if 0x82000000 <= vt < 0x83000000:
|
||||
tally[(d, k, vt)].add(uid)
|
||||
sites[(d, k, vt)].append((base, uid))
|
||||
|
||||
rank = sorted(tally.items(), key=lambda kv: -len(kv[1]))
|
||||
print(f"{'delta':>6} {'idoff':>6} {'word':>10} {'#ids':>5}")
|
||||
for (d, k, vt), s in rank[:25]:
|
||||
print(f"{d:#6x} {k:#6x} {vt:#010x} {len(s):5d} e.g. {sorted(s)[:2]}")
|
||||
if rank:
|
||||
best = rank[0][0]
|
||||
print("\n# object bases for the top candidate (first 20):", file=sys.stderr)
|
||||
for base, uid in sorted(set(sites[best]))[:20]:
|
||||
print(f"# {base:#010x} {uid}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user