#!/usr/bin/env python3 """Locate the loaded stage tables in guest RAM and watch for a phase counter. The stage record splits a mission into Phase_1..3 and Route_S.tbl tags every arrival path with a phase (docs/re/structures/stage-mission-tables.md). Nothing static says what *advances* a phase, so this looks for the runtime side: find the table strings in RAM, then diff the words around them over time. """ import subprocess, sys, time, re, collections SD = __file__.rsplit('/', 1)[0] def gmem(*args): r = subprocess.run([sys.executable, SD + '/gmem.py'] + list(args), capture_output=True, text=True, timeout=300) return r.stdout def find(pat): # gmem prints " va " per hit. Match the va column # only -- a bare 0x[0-9a-f]{8} also catches the offset, which is not an # address and silently doubles the anchor list with junk. out = gmem('find', pat) return [int(m, 16) for m in re.findall(r'va 0x([0-9a-f]{8})', out)] def words(va, n): out = gmem('words', hex(va), str(n)) return [int(m, 16) for m in re.findall(r'\b([0-9a-f]{8})\b', out)] NEEDLES = ['Phase_1', 'Phase_2', 'Route_ADN101_p1F', 'SUBOBJ_010', 'AI_ADAN_CraftSquadron_Veteran', 'UnitGroup_S02.tbl'] def main(): secs = int(sys.argv[1]) if len(sys.argv) > 1 else 180 hits = {} for n in NEEDLES: v = find(n) hits[n] = v print('%-32s %d hit(s) %s' % (n, len(v), [hex(x) for x in v[:4]])) anchors = [] for n, v in hits.items(): for va in v[:2]: anchors.append((n, va)) if not anchors: print('NO TABLE STRINGS IN RAM -- the stage data is not resident, or the ' 'run never reached flight'); return 2 base = {} for n, va in anchors: lo = (va - 0x400) & ~3 base[(n, va)] = words(lo, 512) print('\nbaseline captured for %d anchors; watching %ds' % (len(anchors), secs)) t0 = time.time() changed = collections.Counter() while time.time() - t0 < secs: time.sleep(20) for n, va in anchors: lo = (va - 0x400) & ~3 now = words(lo, 512) b = base[(n, va)] for i, (x, y) in enumerate(zip(b, now)): if x != y: changed[(n, lo + i * 4, x, y)] += 1 base[(n, va)] = now print(' t=%4ds distinct changing words so far: %d' % (time.time() - t0, len(changed))) print('\n--- words that changed near a stage-table string ---') for (n, va, x, y), c in changed.most_common(40): print(' %-32s va=0x%08x %08x -> %08x (%d times)' % (n, va, x, y, c)) if not changed: print(' none -- the loaded tables sit in read-only memory, so the ' 'runtime phase state is NOT adjacent to them') return 0 if __name__ == '__main__': sys.exit(main())