Files
Sylpheed/tools/re-capture/wave2_probe.py
Sylpheed RE agent ff921c097d re: diff inside the 116 entity records — no arrival flag found, two probe defects
Since the record count is flat, an arrival would have to flip a field inside a
record. This diffed all 116 records every 5 s for 170 s of Stage 02 flight.

The prediction under test -- groups of 3, 3, 3, 2, 1 records changing state at
t = 90, 120, 170, 210, 240 s -- did not appear. Changes are spread evenly across
ticks with no cluster at any predicted time and no field that transitions once
for exactly three records. Three explanations survive and this run cannot
separate them: the timetable's t is not seconds (at 30 Hz the whole phase-1
schedule finishes inside 8 s, before the first sample); arrival is not marked in
these records; or the mission was not in phase 1.

Supporting, not conclusive: only 10 of 116 records ever changed a byte, and 106
never changed at all. Live entities would be moving, so that fits the
pre-allocated roster reading -- but Stage 02's roster is turret-heavy and a
turret does not move while alive, so "inert" and "not yet arrived" are not
distinguishable here.

Two defects in my own probe, recorded rather than quietly fixed:

  - label() resolved to '?' for all 116 records, so nothing could be tied back
    to a squadron. That association is what would have made the result
    decisive -- "the three records that changed at t=90 are ADN110, ADN111,
    ADN112" is evidence; "records 18, 32, 99" is not. unit_discover.py already
    solves this and should be reused.
  - RECLEN=0x200 was assumed, not measured. The busiest fields are the last
    eight words of the window, which is what spilling into the next object
    looks like.

Method error kept: the first attempt deferred all analysis to the end and the
turn timeout killed it with 240 s of data in memory and nothing written. The
probe now streams transitions to disk and prints a partial ranking every 60 s.
With a 219 s cold-boot title movie, an end-only report has ~300 s of budget and
one bad estimate loses the whole run.
2026-08-24 12:21:48 +00:00

112 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Diff the 116 spawned-entity records over a mission to find the arrival flag.
wave2 follows mission-wave-arrivals.md: the record COUNT is flat at 116 (one per
UnitGroup roster member), so an arrival must flip a field *inside* a record
rather than create one. Route_S02 predicts phase-1 arrivals at t = 90, 120, 170,
210, 240 in groups of 3, 3, 3, 2, 1 -- in units that are still unmeasured. A
per-record word that transitions at ~90 s supports seconds; everything landing
inside the first ~8 s supports frames at 30 Hz.
"""
import os, sys, time, struct, collections, importlib.util
SD = __file__.rsplit('/', 1)[0]
spec = importlib.util.spec_from_file_location('gmem', SD + '/gmem.py')
gmem = importlib.util.module_from_spec(spec); spec.loader.exec_module(gmem)
VT = struct.pack('>I', 0x820AF030)
RECLEN = 0x200
def find_records(f, fd, size):
offs = []
for start, end in gmem.extents(fd, size):
pos = start
while pos < end:
f.seek(pos); buf = f.read(min(1 << 24, end - pos))
if not buf: break
i = buf.find(VT)
while i != -1:
if (pos + i) % 4 == 0: offs.append(pos + i)
i = buf.find(VT, i + 1)
pos += len(buf)
return offs
def read_at(f, off, n):
f.seek(off); return f.read(n)
def label(f, size, off):
"""unit id string: object+0x04 -> name record, +0x10 -> char*"""
try:
nr = struct.unpack('>I', read_at(f, off + 4, 4))[0]
o = gmem.va_to_off(nr)
if o is None or o + 0x14 > size: return '?'
sp = struct.unpack('>I', read_at(f, o + 0x10, 4))[0]
so = gmem.va_to_off(sp)
if so is None: return '?'
b = read_at(f, so, 48)
return b.split(b'\x00')[0].decode('latin-1') or '?'
except Exception:
return '?'
def main():
secs = int(sys.argv[1]) if len(sys.argv) > 1 else 260
every = int(sys.argv[2]) if len(sys.argv) > 2 else 5
path = gmem.mem_path()
fd = os.open(path, os.O_RDONLY); size = os.fstat(fd).st_size
f = os.fdopen(os.dup(fd), 'rb')
offs = find_records(f, fd, size)
if not offs:
print('NO ENTITY RECORDS -- not in a mission'); return 2
ids = [label(f, size, o) for o in offs]
print('%d records; distinct unit ids: %d' % (len(offs), len(set(ids))))
print(' ', collections.Counter(ids).most_common(8))
prev = [read_at(f, o, RECLEN) for o in offs]
t0 = time.time()
# Stream every transition to disk. The first version of this probe deferred
# ALL analysis to the end and a timeout destroyed the whole run's evidence.
jl = open('/tmp/wave2-trans.tsv', 'w')
jl.write('t\trec\tfield\told\tnew\n')
# transitions[(field_off)] -> list of (t, rec_index, old, new)
trans = collections.defaultdict(list)
while time.time() - t0 < secs:
time.sleep(every)
el = round(time.time() - t0)
nch = 0
for k, o in enumerate(offs):
cur = read_at(f, o, RECLEN)
if cur == prev[k]: continue
for w in range(0, RECLEN, 4):
a = prev[k][w:w+4]; b = cur[w:w+4]
if a != b:
ov, nv = struct.unpack('>I', a)[0], struct.unpack('>I', b)[0]
trans[w].append((el, k, ov, nv))
jl.write('%d\t%d\t0x%03x\t%08x\t%08x\n' % (el, k, w, ov, nv))
nch += 1
prev[k] = cur
print(' t=%4ds changed words this tick: %d' % (el, nch), flush=True)
jl.flush()
if el % 60 < every: # partial ranking, timeout-proof
r = sorted(trans.items(), key=lambda kv: -len({x[1] for x in kv[1]}))
print(' partial: ' + '; '.join(
'+0x%03x:%drec' % (w, len({x[1] for x in ev})) for w, ev in r[:6]),
flush=True)
jl.close()
print('\n--- fields by how many records ever changed them ---')
rank = sorted(trans.items(), key=lambda kv: -len({x[1] for x in kv[1]}))
for w, ev in rank[:18]:
recs = {x[1] for x in ev}
times = collections.Counter(x[0] for x in ev)
print(' +0x%03x %3d records, %3d events; busiest ticks %s'
% (w, len(recs), len(ev), times.most_common(5)))
print('\n--- fields that changed for only a FEW records (arrival-like) ---')
for w, ev in rank:
recs = {x[1] for x in ev}
if not (1 <= len(recs) <= 12): continue
print(' +0x%03x records=%s' % (w, sorted(recs)[:12]))
for t, k, a, b in ev[:10]:
print(' t=%4ds rec %3d (%s) %08x -> %08x' % (t, k, ids[k], a, b))
return 0
if __name__ == '__main__':
sys.exit(main())