re: per-record strength measured, but the run does not reproduce

The link from the previous iteration gives a per-record live strength, and the
measurement is internally sound: 11 records at 2, 2 at 8 and 11 at 12 sums to
exactly the 170 craft counted, with 24 of 42 records deployed.

It does not reproduce. The previous run saw 116 roster records and 300 craft
with strengths 2/4/8/18; this one sees 42 and 170 with 2/8/12 -- same disc, same
save slot, same launch script, same stage.

Two explanations were checked and both fail. The save has not drifted:
game01/savedata is unmodified since 2026-08-23 and only the profile .gpd files
were written today. The guest was not frozen: the pilot's telemetry over the
same 190 s shows speed varying across dozens of values and a live engagement
with an e007 Turret at 259-680 m.

So the discrepancy is real and unexplained, and until it is understood
per-record counts cannot be used as a time series. Recorded as the blocker
rather than worked around. The same within-run-only discipline already noted for
the global craft count now applies one level down.

Also recorded: zero arrivals and zero losses across 190 s. That is weak evidence
against clock-driven arrivals at t = 90/120/170 seconds, since a 0 -> n
transition should have appeared and none did. It is no test of the event-gated
model at all, because nothing was killed -- fc=0, no record lost strength, and
the player's hull never moved off 1500. The pilot closes to 259 m and misses, so
the gap is accuracy rather than engagement.
This commit is contained in:
Sylpheed RE agent
2026-08-24 14:04:24 +00:00
parent 46e511e791
commit f137d3eae3
4 changed files with 205 additions and 2 deletions

104
tools/re-capture/wave5_probe.py Executable file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Per-record craft counts over a mission: catch arrivals and kills by squadron.
The link is a pointer at roster_base + 0x08, verified 300/300 by independent
unit-type agreement (roster-to-craft-link.md). So a roster member's live
strength is simply how many craft point at it:
0 -> n the member deployed (an ARRIVAL)
n -> n-1 one of its craft died (a KILL)
Both are attributable to a UnitGroup member, which is what ties an observation
back to Route_S<NN>.tbl. This watches those counts instead of any global total.
"""
import os, sys, time, struct, collections, importlib.util
SD = __file__.rsplit('/', 1)[0]
sys.path.insert(0, SD)
import gmem, gworld, entities2
_w3 = importlib.util.spec_from_file_location('w3', SD + '/wave3_probe.py')
wave3 = importlib.util.module_from_spec(_w3); _w3.loader.exec_module(wave3)
ROSTER_VT = struct.pack('>I', 0x820AF030)
DELTA, WIN, LINK = 0x130, 0x400, 0x08
def scan_vt(fd, size, vt):
out = []
for a, b in gmem.extents(fd, size):
pos = a
while pos < b:
n = min(1 << 24, b - pos)
blob = os.pread(fd, n, pos)
i = blob.find(vt)
while i != -1:
if (pos + i) % 4 == 0: out.append(pos + i)
i = blob.find(vt, i + 1)
pos += n
return sorted(out)
def counts(fd, defs, want):
"""craft per roster VA, this instant"""
lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_HI)
per = collections.Counter(); pos = lo; n = 0
while pos < hi:
m = min(1 << 24, hi - pos)
blob = os.pread(fd, m, pos)
for k in range(0, len(blob) - 3, 4):
if blob[k:k+4] in defs:
base = pos + k - DELTA
n += 1
head = os.pread(fd, WIN, base)
for j in range(0, len(head) - 3, 4):
(p,) = struct.unpack_from('>I', head, j)
if p in want:
per[want[p]] += 1
break
pos += m
return per, n
def main():
secs = int(sys.argv[1]) if len(sys.argv) > 1 else 200
every = int(sys.argv[2]) if len(sys.argv) > 2 else 15
w = gworld.World(); fd = w.fd
defs = entities2.definitions(w)
if not defs: print('NOT IN A MISSION'); return 2
roster = scan_vt(fd, w.size, ROSTER_VT)
f = os.fdopen(os.dup(fd), 'rb')
label, want = {}, {}
for o in roster:
va = gmem.primary_va(o)
if va is None: continue
label[va] = wave3.resolve_id(f, w.size, o)[0] or '?'
want[va + LINK] = va
print('roster records: %d' % len(label))
log = open('/tmp/wave5-per-record.tsv', 'w')
log.write('t\tva\tunit\tfrom\tto\n')
prev, total0 = None, None
t0 = time.time()
while time.time() - t0 < secs:
per, n = counts(fd, defs, want)
el = round(time.time() - t0)
if prev is None:
print('t=%4ds craft=%d deployed records=%d/%d strength histogram %s'
% (el, n, len(per), len(label),
sorted(collections.Counter(per.values()).items())))
else:
arr = [(va, per[va]) for va in per if prev.get(va, 0) == 0 and per[va] > 0]
kil = [(va, prev[va], per[va]) for va in prev
if per.get(va, 0) < prev[va]]
print('t=%4ds craft=%d deployed=%d ARRIVALS=%d losses=%d'
% (el, n, len(per), len(arr), len(kil)), flush=True)
for va, c in arr:
print(' ARRIVAL %#010x %-30s 0 -> %d' % (va, label.get(va, '?'), c))
log.write('%d\t%#x\t%s\t0\t%d\n' % (el, va, label.get(va, '?'), c))
for va, a, b in kil:
print(' loss %#010x %-30s %d -> %d' % (va, label.get(va, '?'), a, b))
log.write('%d\t%#x\t%s\t%d\t%d\n' % (el, va, label.get(va, '?'), a, b))
log.flush()
prev = per
time.sleep(max(0, every - (time.time() - t0 - el)))
log.close()
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages
SD="$(cd "$(dirname "$0")" && pwd)"
SECS="${1:-180}"; EVERY="${2:-10}"; HUNT="${3:-1}"
CFG=/tmp/nav-live.json
"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; }
if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then
SYLPH_HUNT="$HUNT" SYLPH_KILL_TURRETS=1 nohup python3 "$SD/pilot.py" "$CFG" "$SECS" \
</dev/null >/tmp/live-pilot.log 2>&1 &
PILOT=$!; echo "--- pilot (SYLPH_HUNT=$HUNT)"
else PILOT=""; echo "--- BIND FAILED, no pilot"; fi
python3 "$SD/wave5_probe.py" ; rc=$?
[ -n "$PILOT" ] && kill "$PILOT" 2>/dev/null
echo "LIVENESS DONE rc=$rc"