#!/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.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())