#!/usr/bin/env python3 """Collapse definition-pointer SITES into distinct entities, then re-test `n`. liveness_probe.py counts every aligned word equal to a unit-definition VA. Some entity types hold more than one such pointer -- the player is one roster member and yields two sites -- so its counts are sites, not entities, and the `sum(n)` == entities test could not be believed (mission-liveness-probe.md). This measures the multiplicity instead of assuming it: 1. the delta spectrum -- how many distinct (entity -> def pointer) offsets exist; 2. the gap distribution between consecutive sites of the SAME unit type, which shows whether sites cluster in pairs; 3. a clustering threshold derived from that gap distribution, not guessed; 4. hull sanity: a real entity's f32 at +0x154 should be a plausible HP. """ import os, sys, collections, struct sys.path.insert(0, __file__.rsplit('/', 1)[0]) import gmem, gworld, entities2 HULL_OFF = 0x154 def sites(fd, defs): lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_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, nm)) pos += n return out def f32(fd, off): try: return struct.unpack('>f', os.pread(fd, 4, off))[0] except Exception: return None def main(): w = gworld.World(); fd = w.fd defs = entities2.definitions(w) print('definitions: %d' % len(defs)) if not defs: print('NOT IN A MISSION'); return 2 S = sites(fd, defs) print('sites: %d' % len(S)) print('by unit:', collections.Counter(n for _, n in S).most_common(8)) # (1) delta spectrum, measured on movers movers = entities2.moving(fd, w.size) votes = entities2.find_delta(fd, defs, movers) print('\n--- delta spectrum (entity -> def pointer offsets) ---') for d, c in votes.most_common(8): print(' %+#08x seen %d' % (d, c)) # (2) gaps between consecutive sites of the same unit bygroup = collections.defaultdict(list) for off, nm in S: bygroup[nm].append(off) gaps = collections.Counter() for nm, offs in bygroup.items(): offs.sort() for i in range(1, len(offs)): gaps[offs[i] - offs[i-1]] += 1 print('\n--- gaps between same-unit sites (smallest 10) ---') for g, c in sorted(gaps.items())[:10]: print(' %#08x (%6d) x%d' % (g, g, c)) # (3) cluster with a threshold taken from the spectrum, not guessed span = max(votes.most_common(4), key=lambda kv: kv[0])[0] - min( d for d, _ in votes.most_common(4)) if len(votes) > 1 else 0 thresh = max(0x40, abs(span) + 4) print('\nclustering threshold from delta spread: %#x' % thresh) ents = collections.Counter() for nm, offs in bygroup.items(): offs.sort(); last = None for o in offs: if last is None or o - last > thresh: ents[nm] += 1 last = o print('\n--- sites vs clustered entities ---') sc = collections.Counter(n for _, n in S) print('%-34s %7s %9s %6s' % ('unit', 'sites', 'entities', 'ratio')) for nm, c in sc.most_common(): e = ents[nm] print('%-34s %7d %9d %6.2f' % (nm, c, e, c / e if e else 0)) print('\ntotal sites=%d entities=%d' % (len(S), sum(ents.values()))) # (4) hull sanity on clustered bases for the top delta d0 = votes.most_common(1)[0][0] if votes else 0x130 ok = bad = 0 for nm, offs in bygroup.items(): offs.sort(); last = None for o in offs: if last is None or o - last > thresh: h = f32(fd, o - d0 + HULL_OFF) if h is not None and 0 < h <= 200000: ok += 1 else: bad += 1 last = o print('hull plausible on %d clustered bases, implausible on %d (delta %#x)' % (ok, bad, d0)) return 0 if __name__ == '__main__': sys.exit(main())