Files
Sylpheed/tools/re-capture/phase_probe.py
Sylpheed RE agent a0a0214b24 re: probe the runtime phase state — tables are resident, phase counter is not there
Static reading had gone as far as it could: the stage record splits a mission
into Phase_1..3 and every arrival route is phase-tagged, but nothing in the data
says what ends a phase. So this took it to the oracle -- one Stage 02 flight,
160 s under the survival pilot.

Confirmed, and this is the useful half: every string the static decode predicts
is present in live guest memory -- Phase_1, Phase_2, Route_ADN101_p1F,
SUBOBJ_010, AI_ADAN_CraftSquadron_Veteran, UnitGroup_S02.tbl. The game loads
exactly the tables the stage record names, under exactly the names we resolved,
and they can be located in RAM by content. That is the first dynamic
confirmation of the whole static table layer.

Refuted: the phase state is not adjacent to those strings. The probe reported
862 changed words around the anchors, which looks like a signal until you read
the values -- each word takes its predecessor's previous value and every value
points into the same region. It is one block shifted down four bytes, a single
memmove in a pointer list, occurring once between t=66s and t=89s. Diffing
around a string anchor was the cheap thing to try and it did not work.

Also recorded: a defect in my own probe. It scraped hit addresses with
0x([0-9a-f]{8}), but gmem.py find prints both the backing-file offset and the
guest VA, so half the anchors were file offsets read as addresses. Fixed to
match the va column only. It did not change the conclusion -- the anchor that
produced the shift was a real VA -- but a negative result from one of those
junk anchors would have been worthless.

Not settled: what advances a phase. Next handles are watching Route_ADN101_p1F
fire against entity positions, or working back from the SUBOBJ_*_Mes_L1 HUD
strings; the phase state is more likely near the known mutable REMAINING OB
counter than near the tables.
2026-08-24 11:31:20 +00:00

75 lines
2.8 KiB
Python
Executable File

#!/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<NN>.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 "<file offset> va <guest 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())