SYLPH_KEEPOUT makes the hunt keep-out a knob instead of a hard-coded 600. At 1400 the pilot both kills and survives: hull 1500 and escorted asset 100 % for the whole run, ENGAGE throughout, eight loss events against seven in the run where it died at 83 s. That let the elimination test actually run. An e007 Turret squadron was ground 18 -> 10 -> 8 -> 4 -> 2 -> 0, reaching zero at t=163 s with deployed falling 41 to 40 -- an enemy squadron destroyed outright for the first time in nine runs. No arrival followed. ARRIVALS=0 at every sample including all those after t=163. The hypothesis that a wave is released when a squadron is wiped out rather than merely damaged does not survive its first test. Two further losses occurred at t=176 and t=202, so the mission was demonstrably still live and still processing kills; it simply produced no arrival. This refutes elimination-of-one-squadron as the trigger. It does not refute event-gating generally -- a threshold across several squadrons, an objective completion, or a specific squadron could all still be the gate. The valid window is smaller than the log suggests, about 90 s after the elimination rather than 143 s, because the guest stalled around t=255 s. The pilot's telemetry gives it away: the last 400 log lines contain one distinct speed value against 236 in the first 400, with no timestamp gaps -- the process kept logging while the game stopped advancing. Nothing in the probe output distinguished a stall from a quiet mission, and the same ambiguity affects the trailing flat samples of earlier runs. wave6_probe now locates a counter advancing at frame rate, samples it each tick and prints GUEST STALLED when it fails to advance, so future runs validate themselves. Implemented but not yet exercised in a run.
134 lines
5.7 KiB
Python
Executable File
134 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Per-record arrival/loss watch against the verified 116/300 baseline.
|
|
|
|
Supersedes wave5_probe.py, which reported 42 records in a run nobody could
|
|
reproduce. Changes:
|
|
* refuses to interpret a run whose roster count is not the reproduced
|
|
baseline (mission-per-record-strength.md: discard, do not interpret);
|
|
* keys records by file offset rather than by primary VA;
|
|
* samples faster and for longer, since losses appear at ~1 per 10 s and no
|
|
arrival has been seen in five runs.
|
|
|
|
An arrival is a record going 0 -> n; a loss is n -> n-1.
|
|
"""
|
|
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)
|
|
# A guest that has stalled produces flat samples that read exactly like a quiet
|
|
# mission -- that happened at t~255 in the keep-out run and was only caught by
|
|
# eyeballing the pilot log afterwards. timer_probe found counters advancing at
|
|
# frame rate; sampling one of them makes every run self-validating.
|
|
TICK_LO, TICK_HI = 0xBC000000, 0xBE000000
|
|
DELTA, WIN, LINK = 0x130, 0x400, 0x08
|
|
BASELINE = 116
|
|
|
|
def scan_vt(fd, size, vt):
|
|
out = []
|
|
for a, b in gmem.extents(fd, size):
|
|
pos = a
|
|
while pos < b:
|
|
m = min(1 << 24, b - pos)
|
|
blob = os.pread(fd, m, pos)
|
|
i = blob.find(vt)
|
|
while i != -1:
|
|
if (pos + i) % 4 == 0: out.append(pos + i)
|
|
i = blob.find(vt, i + 1)
|
|
pos += m
|
|
return sorted(out)
|
|
|
|
def find_tick(fd, dt=3.0):
|
|
"""One word that advances steadily: the guest-is-running witness."""
|
|
lo, hi = gmem.va_to_off(TICK_LO), gmem.va_to_off(TICK_LO + 0x400000)
|
|
a = os.pread(fd, hi - lo, lo)
|
|
time.sleep(dt)
|
|
b = os.pread(fd, hi - lo, lo)
|
|
best = None
|
|
for k in range(0, min(len(a), len(b)) - 3, 4):
|
|
va = struct.unpack_from('>I', a, k)[0]
|
|
vb = struct.unpack_from('>I', b, k)[0]
|
|
if va < vb and (vb - va) / dt > 5 and (vb - va) < 1 << 20:
|
|
best = (lo + k, (vb - va) / dt); break
|
|
return best
|
|
|
|
def sample(fd, defs, want):
|
|
lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_HI)
|
|
per = collections.Counter(); tot = 0; pos = lo
|
|
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] not in defs: continue
|
|
tot += 1
|
|
head = os.pread(fd, WIN, pos + k - DELTA)
|
|
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, tot
|
|
|
|
def main():
|
|
secs = int(sys.argv[1]) if len(sys.argv) > 1 else 240
|
|
every = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
|
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)
|
|
print('roster records: %d (baseline %d)' % (len(roster), BASELINE))
|
|
if len(roster) != BASELINE:
|
|
print('DISCARD: roster count is not the reproduced baseline. '
|
|
'Per mission-per-record-strength.md this run is not interpreted.')
|
|
return 3
|
|
f = os.fdopen(os.dup(fd), 'rb')
|
|
label, want = {}, {}
|
|
for o in roster:
|
|
va = gmem.primary_va(o)
|
|
if va is None: continue
|
|
label[o] = wave3.resolve_id(f, w.size, o)[0] or '?'
|
|
want[va + LINK] = o
|
|
tick = find_tick(fd)
|
|
if tick: print('tick witness at %#x, ~%.1f/s' % (tick[0], tick[1]))
|
|
else: print('WARNING: no advancing counter found; stalls cannot be detected')
|
|
last_tick = struct.unpack('>I', os.pread(fd, 4, tick[0]))[0] if tick else 0
|
|
log = open('/tmp/wave6.tsv', 'w'); log.write('t\tkind\toff\tunit\tfrom\tto\n')
|
|
prev = None; t0 = time.time(); arr_n = loss_n = 0
|
|
while time.time() - t0 < secs:
|
|
per, tot = sample(fd, defs, want)
|
|
el = round(time.time() - t0)
|
|
if prev is None:
|
|
print('t=%4ds craft=%d deployed=%d/%d strengths %s'
|
|
% (el, tot, len(per), len(roster),
|
|
sorted(collections.Counter(per.values()).items())), flush=True)
|
|
else:
|
|
arr = [(o, per[o]) for o in per if prev.get(o, 0) == 0 < per[o]]
|
|
los = [(o, prev[o], per.get(o, 0)) for o in prev if per.get(o, 0) < prev[o]]
|
|
arr_n += len(arr); loss_n += len(los)
|
|
stalled = ''
|
|
if tick:
|
|
now = struct.unpack('>I', os.pread(fd, 4, tick[0]))[0]
|
|
if now <= last_tick: stalled = ' *** GUEST STALLED ***'
|
|
last_tick = now
|
|
print('t=%4ds craft=%d deployed=%d ARRIVALS=%d losses=%d (cum %d/%d)%s'
|
|
% (el, tot, len(per), len(arr), len(los), arr_n, loss_n, stalled),
|
|
flush=True)
|
|
for o, c in arr:
|
|
print(' ARRIVAL %-30s 0 -> %d' % (label.get(o, '?'), c), flush=True)
|
|
log.write('%d\tarrival\t%#x\t%s\t0\t%d\n' % (el, o, label.get(o, '?'), c))
|
|
for o, a, b in los:
|
|
print(' loss %-30s %d -> %d' % (label.get(o, '?'), a, b), flush=True)
|
|
log.write('%d\tloss\t%#x\t%s\t%d\t%d\n' % (el, o, label.get(o, '?'), a, b))
|
|
log.flush()
|
|
prev = per
|
|
time.sleep(max(0, every - (time.time() - t0 - el)))
|
|
log.close()
|
|
print('\nTOTAL arrivals=%d losses=%d over %ds' % (arr_n, loss_n, secs))
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|