Files
Sylpheed/tools/re-capture/dedupe_probe.py
Sylpheed RE agent b9cb0135e3 re: sites are entities 1:1 — withdraw "n is craft-per-member"
The suspected confound turned out not to exist. Gaps between consecutive
same-unit definition-pointer sites are all >= 0x1000, with 274 of them exactly
0x1000, so entities are page-spaced and there are no near-adjacent pairs to
merge. Clustering at any threshold below 0x1000 gives ratio 1.00 for every unit
type, and hull is plausible on 298 of 298 clustered bases at delta 0x130. The
player shows two objects because there are two, not because one holds two
pointers.

That removes the excuse the previous iteration had used to keep the reading
alive, and the reading does not survive: sum(n) fits the turret row well (216
against 214, with kills already recorded), but DeltaSaber_T, Player and
Acropolis all come out at exactly twice their sum(n). An undershoot can be
blamed on phases 2-3 not having started; an overshoot cannot. n goes back to 
and the previous 🟡 is withdrawn. All the turret row establishes is that a
roster member expands into many craft, not that n is the factor.

Formation slot count was tested as the alternative and rejected outright: 630
turret slots against 214 live.

Side result worth keeping: a FormationSet record's FrameCount is its slot count,
and the name suffix usually agrees -- Turret07_30 -> 30, ArrowHead03_64 -> 64,
4_Bird -> 4 -- with one exception, AttackerS03_12 having 14 slots, so the suffix
is a label and not a guarantee.

Also recorded: the 298 live entities are not the 116 roster records. Both
structures exist at once, and the rule mapping one onto the other is the real
open question.

Probe caveat noted in the doc: entities2.moving() found no movers this run, so
the delta spectrum was empty and the clustering threshold was a fallback rather
than a measurement. It does not change the conclusion, since every gap exceeds
any threshold below 0x1000.
2026-08-24 13:13:43 +00:00

99 lines
3.9 KiB
Python
Executable File

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