entities2.moving() finds entities by displacement between two samples, so anything stationary is invisible -- the entire explanation for the +/-10 swing that made the previous run's count useless. liveness_probe.py enumerates by definition pointer over the entity heap instead, moving or not, and reads hull as f32 at position+0x154. The series is monotone rather than oscillating: 298 -> 280 over 164 s, with the decline matching the 18 disappearance events exactly. The hunting pilot does kill: one hull crossing caught directly, an e010_ADAN_Attacker_S at t=57 s. The previous run's worry that SYLPH_HUNT shoots but never destroys anything is settled. Recorded as a non-result so the next run does not misread it: zero births in 164 s does NOT favour either wave model. The roster finding already established that every participant is allocated at mission load, so neither a clock nor an event model would produce an allocation. An arrival must be a state change on an existing entity. The mystery member field n now has a candidate meaning: the number of craft a roster member spawns. Static sum(n) for turrets is 216 against 214 sites found, with the count already falling before the first sample, where Count alone predicts 21 -- off by an order of magnitude. Not promoted, and the reason is a confound in my own measurement rather than the data: the probe counts definition-pointer SITES, not entities. The player is one member and yields two sites, and DeltaSaber_T yields exactly double its sum(n), so some entity types hold several pointers to their definition. Until sites are collapsed into distinct entities the turret match could be a coincidence between a x1 multiplicity and a x1 ratio. The capital-ship rows undershoot for a separate and expected reason: phases 2 and 3 have not started.
83 lines
3.2 KiB
Python
Executable File
83 lines
3.2 KiB
Python
Executable File
#!/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())
|