#!/usr/bin/env python3 """A STABLE live-entity roster with hull, so kills and arrivals are observable. Why not reuse the pilot's scan: entities2.moving() finds entities *by motion* between two samples, so anything stationary in the window is invisible. That is why the pilot's count swings +/-10 between ticks and why no wave conclusion could be drawn from it (mission-wave-arrivals.md). This enumerates by definition pointer instead: every aligned word in the entity heap that equals a known unit-definition VA marks an entity, whether it is moving or not. Hull is f32 at position + 0x154 (HULL_OFF, already confirmed in pilot.py), so a death is a hull crossing to <= 0 and an arrival is a position that was not there before. """ import os, sys, time, struct, collections sys.path.insert(0, __file__.rsplit('/', 1)[0]) import gmem, gworld, entities2 ENT_LO, ENT_HI = entities2.ENT_VA_LO, entities2.ENT_VA_HI HULL_OFF = 0x154 def enumerate_entities(fd, defs, delta): """All entities in the heap region, moving or not.""" lo, hi = gmem.va_to_off(ENT_LO), gmem.va_to_off(ENT_HI) out = [] pos = lo while pos < hi: n = min(1 << 24, hi - pos) blob = os.pread(fd, n, pos) for k in range(0, len(blob) - 3, 4): nm = defs.get(blob[k:k+4]) if nm: out.append((pos + k - delta, nm)) pos += n return out def hull(fd, off): try: return struct.unpack('>f', os.pread(fd, 4, off + HULL_OFF))[0] except Exception: return None def main(): secs = int(sys.argv[1]) if len(sys.argv) > 1 else 180 every = int(sys.argv[2]) if len(sys.argv) > 2 else 10 delta = int(sys.argv[3], 0) if len(sys.argv) > 3 else 0x130 w = gworld.World(); fd = w.fd defs = entities2.definitions(w) print('unit definitions: %d' % len(defs)) if not defs: print('NO DEFINITIONS -- not in a mission'); return 2 ents = enumerate_entities(fd, defs, delta) print('entities enumerated (motion-independent): %d' % len(ents)) print('composition:', collections.Counter(n for _, n in ents).most_common(8)) log = open('/tmp/liveness.tsv', 'w'); log.write('t\talive\tborn\tdied\n') prev = {off: hull(fd, off) for off, _ in ents} names = dict(ents) t0 = time.time() while time.time() - t0 < secs: time.sleep(every) el = round(time.time() - t0) cur_ents = enumerate_entities(fd, defs, delta) cur = {off: hull(fd, off) for off, _ in cur_ents} for off, n in cur_ents: names.setdefault(off, n) born = [o for o in cur if o not in prev] gone = [o for o in prev if o not in cur] died = [o for o in cur if prev.get(o) is not None and cur[o] is not None and prev[o] > 0 >= cur[o]] alive = sum(1 for o, h in cur.items() if h is not None and h > 0) print(' t=%4ds entities=%3d alive=%3d born=%2d died=%2d gone=%2d %s' % (el, len(cur), alive, len(born), len(died), len(gone), [names[o].replace('UN_', '') for o in (born + died)[:4]]), flush=True) log.write('%d\t%d\t%d\t%d\n' % (el, alive, len(born), len(died))); log.flush() prev = cur log.close() return 0 if __name__ == '__main__': sys.exit(main())