From 356c4e70f829212be36d18093294001a527cbe81 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Mon, 24 Aug 2026 13:01:19 +0000 Subject: [PATCH] re: motion-independent liveness probe; n looks like craft-per-member 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. --- docs/re/BACKLOG.md | 10 ++++ docs/re/mission-liveness-probe.md | 86 ++++++++++++++++++++++++++++ tools/re-capture/liveness_probe.py | 82 ++++++++++++++++++++++++++ tools/re-capture/liveness_session.sh | 16 ++++++ 4 files changed, 194 insertions(+) create mode 100644 docs/re/mission-liveness-probe.md create mode 100755 tools/re-capture/liveness_probe.py create mode 100755 tools/re-capture/liveness_session.sh diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 13a300b..cc42d00 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -320,6 +320,16 @@ search cannot find a *schedule*. record per member (n=1, NOT promoted; the refutation needs another stage's save, which we do not have — only slot 01 / Stage 02 exists). ❔ Still unmeasured: whether the timetable's `t` is frames or seconds. +* ✅🟡 **Motion-independent liveness probe works; `n` is probably craft-per-member** + (2026-08-24) — [`mission-liveness-probe.md`](mission-liveness-probe.md). + Enumerating by definition pointer instead of by motion removes the ±10 noise + (monotone 298→280). The hunting pilot **does** kill (hull crossing at t=57s). + 🔴 "No births in 164 s" does NOT separate the wave models — everything is + pre-allocated, so an arrival must be a STATE CHANGE, not an allocation. + 🟡 Turret `sum(n)`=216 vs 214 sites found (vs `Count`=21) suggests the mystery + member field `n` is the craft count per member — NOT promoted: the probe counts + definition-pointer *sites*, and the player (1 member → 2 sites) proves some + entities hold several. **Next: collapse sites into entities, then re-test.** * 🟡 **COMPETING MODEL (user, 2026-08-24): waves are event-gated, not clock-driven** — released by kills/objectives rather than at a fixed time, with the route's start time being a post-trigger delay. Fits the evidence better: diff --git a/docs/re/mission-liveness-probe.md b/docs/re/mission-liveness-probe.md new file mode 100644 index 0000000..252181c --- /dev/null +++ b/docs/re/mission-liveness-probe.md @@ -0,0 +1,86 @@ +# A motion-independent live roster — and what `n` probably is + +Status: ✅ the enumeration method; ✅ the hunting pilot gets kills; 🔴 "no +births" cannot distinguish the wave models; 🟡 the member field `n` looks like a +craft count, with a confound that must be removed before believing it. + +## ✅ Enumerate by definition pointer, not by motion + +`entities2.moving()` finds entities *by displacement between two samples*, so +anything stationary in the window is invisible. That is the whole explanation +for the ±10 swing that made the previous run's entity count useless +([mission-wave-arrivals.md](mission-wave-arrivals.md)). + +`tools/re-capture/liveness_probe.py` scans the entity heap +(`0xBD000000–0xBE000000`) for aligned words equal to a known unit-definition VA +instead. Moving or not, an entity is counted. Hull is `f32` at +`position + 0x154` (`HULL_OFF`, already confirmed in `pilot.py`), so a death is +a hull crossing to ≤ 0. + +The difference is immediate — the series is monotone instead of oscillating: + +``` +t= 15s 294 alive born 0 died 0 gone 4 +t= 36s 290 born 0 died 0 gone 4 +t= 57s 289 born 0 died 1 gone 0 <- e010_ADAN_Attacker_S +t= 78s 286 born 0 died 0 gone 4 +t=100s 286 born 0 died 0 gone 0 +t=143s 282 born 0 died 0 gone 4 +t=164s 280 born 0 died 0 gone 2 +``` + +Total decline 298 → 280 = 18, matching the 18 "gone" events exactly. + +## ✅ The hunting pilot kills + +One hull crossing was caught directly — an `e010_ADAN_Attacker_S` at t = 57 s — +plus 18 disappearances. The previous run's worry that `SYLPH_HUNT` shoots but +never destroys anything is settled: it does. + +## 🔴 "No births" does **not** separate the two wave models + +Zero entities appeared in 164 s. That looks like evidence against clock-driven +arrivals at t = 90/120 s — **and it is not**, because the roster finding already +established that all of a mission's entities are allocated at load. If every +participant exists from the first frame, then *neither* model produces a "birth", +and the observation is consistent with both. Recorded so the next run does not +mistake it for a result: **an arrival must be a state change on an existing +entity, not an allocation.** + +## 🟡 `n` looks like the number of craft per roster member + +The member tuple's third field `n` has been ❔ since the roster was decoded. +Comparing the static `sum(n)` per unit type against what is resident: + +| unit | members | `sum(n)` | sites found | +|---|---|---|---| +| `UN_e007_ADAN_Turret` | 21 | **216** | **214** | +| `UN_e105_ADAN_Cruiser` | 7 | 7 | 6 | +| `UN_e106_ADAN_Destroyer` | 19 | 19 | 14 | +| `UN_f105_TCAF_Cruiser` | 11 | 11 | 4 | +| `UN_f106_TCAF_Destroyer` | 14 | 16 | 10 | +| `UN_e010_ADAN_Attacker_S` | 9 | 45 | 32 | +| `UN_f001_TCAF_DeltaSaber_T` | 7 | 7 | **14** | +| `UN_f001_..._Player` | 1 | 1 | **2** | + +The turret row is striking: 216 predicted against 214 found, with the count +already falling before the first sample. `Count` alone predicts 21, which is +off by an order of magnitude. So `n` scaling a member into a flight of craft +fits the dominant unit type well. + +**It is not promoted, because the measurement has an unquantified confound.** +The player is one member and yields **two** sites, and `DeltaSaber_T` yields +exactly double its `sum(n)` — so at least some entity types hold more than one +pointer to their definition. What the probe counts is *definition-pointer sites*, +not entities, and until those are collapsed into distinct entities the turret +match could be a coincidence between a ×1 multiplicity and a ×1 ratio. The +capital-ship rows undershoot instead, which is separately explained by phases 2 +and 3 not having started. + +## Next + +Collapse sites into entities before re-testing `n` — cluster sites by their +implied entity base and take distinct bases. Then the table above becomes a real +test rather than a suggestive one. The same de-duplication is a precondition for +using this probe to watch an arrival, since an arrival is now known to be a +state change rather than an allocation. diff --git a/tools/re-capture/liveness_probe.py b/tools/re-capture/liveness_probe.py new file mode 100755 index 0000000..81c7e21 --- /dev/null +++ b/tools/re-capture/liveness_probe.py @@ -0,0 +1,82 @@ +#!/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()) diff --git a/tools/re-capture/liveness_session.sh b/tools/re-capture/liveness_session.sh new file mode 100755 index 0000000..55ced94 --- /dev/null +++ b/tools/re-capture/liveness_session.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages +SD="$(cd "$(dirname "$0")" && pwd)" +SECS="${1:-180}"; EVERY="${2:-10}"; HUNT="${3:-1}" +CFG=/tmp/nav-live.json +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then + SYLPH_HUNT="$HUNT" SYLPH_KILL_TURRETS=1 nohup python3 "$SD/pilot.py" "$CFG" "$SECS" \ + /tmp/live-pilot.log 2>&1 & + PILOT=$!; echo "--- pilot (SYLPH_HUNT=$HUNT)" +else PILOT=""; echo "--- BIND FAILED, no pilot"; fi +python3 "$SD/liveness_probe.py" "$SECS" "$EVERY"; rc=$? +[ -n "$PILOT" ] && kill "$PILOT" 2>/dev/null +echo "LIVENESS DONE rc=$rc"