From bd0a149505ffee582574c244aa54ac493a0e06b8 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Sun, 23 Aug 2026 23:21:08 +0000 Subject: [PATCH] docs+tools: the per-entity OB flag is refuted, and the counter decrements when the player kills With the pilot finally shooting, the experiment completed both halves. Sample A at counter 12 over 120 entities gave 2 offsets where exactly 12 entities share a value; the counter then went 12 -> 11 and NEITHER survived. So within +-0x400 of an entity's position triple there is no 4-byte word whose shared-value population tracks REMAINING OB. The limits are recorded as part of the result, because they bound it: the test asks which entities share an EXACT 32-bit value, so a single bit ORed into a word that also carries health or a timer would never show up - a bit-level version of the same differential is the follow-on. Anything outside the window, or on entities that entities2 cannot see (it types by position CHANGING, so stationary objectives are invisible), is untested too, and the populations differed a lot between samples - 120 against 194. Separately: REMAINING OB went 12 -> 11, the first decrement of this whole investigation, while pilot.py logged 411 fire=1 samples and the HUD reached YOU KILLED WARPLANES 0003. Stated carefully - it does NOT show the counter counts kills, since an earlier run had the hostile population fall by a third with no movement; it shows some kills close something the counter tracks. Two robustness fixes: ob_flag retries an empty entity sample (one void run was caused by exactly that), and Pad releases everything on interpreter exit - a file-backed pad PERSISTS after its writer dies, so a tool killed mid-press would leave a button held and the game would walk through menus on its own. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- docs/re/captures/ob-flag-refuted-stage02.json | 11 ++++ docs/re/mission-freeze-and-ob-flag.md | 63 +++++++++++++++++++ tools/re-capture/flight_probe.py | 6 ++ tools/re-capture/ob_flag.py | 15 ++++- 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 docs/re/captures/ob-flag-refuted-stage02.json diff --git a/docs/re/captures/ob-flag-refuted-stage02.json b/docs/re/captures/ob-flag-refuted-stage02.json new file mode 100644 index 00000000..4309a945 --- /dev/null +++ b/docs/re/captures/ob-flag-refuted-stage02.json @@ -0,0 +1,11 @@ +{ + "nA": 12, + "nB": 11, + "hud_a": 12, + "hud_b": null, + "entities_a": 120, + "entities_b": 194, + "class_matches": [], + "candidates_a": 2, + "survivors": [] +} \ No newline at end of file diff --git a/docs/re/mission-freeze-and-ob-flag.md b/docs/re/mission-freeze-and-ob-flag.md index e06bf76f..cda46f5c 100644 --- a/docs/re/mission-freeze-and-ob-flag.md +++ b/docs/re/mission-freeze-and-ob-flag.md @@ -145,3 +145,66 @@ drawn from runs that assumed the pilot was shooting. This run did **not** freeze in ~25 minutes. Of the seven Stage 02 runs that have reached flight, **three froze** — so it is common but not the majority, and a run that survives the first minute or two seems to keep going. + +--- + +## 🔴 2026-08-23 (final) — the per-entity flag hypothesis is REFUTED, on a transition it was not selected by + +With the pilot actually shooting (`pilot-never-fires.md`), the experiment this +file was written for finally completed both halves: + +``` +HUD=12 RAM=12 +[A] counter=12 entities=120 96 turrets, 16 attackers, 7 wingmen, 1 player +[A] classes whose head-count equals the counter: NONE +[A] offsets where exactly 12 entities agree: 2 + pos+0x0238 = 00000006 {12 × e007_ADAN_Turret} + pos+0x0250 = 239d6732 {12 × e010_ADAN_Attacker_S} +counter 12 -> 11 +[B] counter=11 entities=194 +[B] of 2 candidates, 0 still hold exactly 11 +``` +([`captures/ob-flag-refuted-stage02.json`](captures/ob-flag-refuted-stage02.json)) + +**Both candidates die on the verification.** So: within ±0x400 of an entity's +position triple there is **no 4-byte word whose shared-value population tracks +`REMAINING OB`**. The simplest reading of "objectives are marked in the entity +object" is dead. + +### What that does and does not rule out — the limits are the result too + +* 🔴 **Ruled out:** a per-entity word, in that window, holding a common value on + exactly the objective entities. +* ❔ **Not ruled out — a flag packed into a word that also varies.** The test asks + which entities share an *exact* 32-bit value. A single bit ORed into a word + that also carries health, a timer or a state machine would never produce a + shared value, and this method cannot see it. A bit-level version of the same + differential is the obvious follow-on. +* ❔ **Not ruled out — anything outside the window**, or on entities the + enumeration cannot see: `entities2.typed` types entities by their position + *changing*, so a stationary objective is invisible to it. +* ⚠️ **The populations differ a lot between samples** — 120 entities at A, 194 at + B — so a flag on entities that spawned between the two would not have been + tested either. + +## ✅ And the counter DECREMENTS when the player kills + +`REMAINING OB` went **12 → 11** during this run — the first decrement seen in +this whole investigation — while `pilot.py` logged **411** `fire=1` samples and +the HUD's own tally reached **`YOU KILLED WARPLANES 0003`** +([`captures/stage02-you-killed-warplanes-0003.png`](captures/stage02-you-killed-warplanes-0003.png)). + +That is worth stating carefully. It does **not** show that the counter counts +kills — the earlier fifteen-minute run had the hostile population fall by a third +with no movement at all. What it shows is that *some* kills close *something* +the counter tracks, which is exactly the behaviour an objective counter should +have, and it is the first time the player's own guns have been part of the +experiment rather than a bystander. + +## One more robustness fix, from a void run + +`ob_flag.py` now **retries an empty sample**. `entities2.moving()` types entities +by position changing, so a sample that lands on a load or loses the race with +another scanner returns nothing — and a zero-entity sample silently yields zero +candidates and a run that proves nothing. Seen exactly that way once, with the +following sample finding 81 entities moments later. diff --git a/tools/re-capture/flight_probe.py b/tools/re-capture/flight_probe.py index b8413d9c..b4e1eb3f 100755 --- a/tools/re-capture/flight_probe.py +++ b/tools/re-capture/flight_probe.py @@ -11,6 +11,7 @@ then per frame a timestamp, the input vector, and every instance's raw window. Usage: flight_probe.py [seconds] """ +import atexit import os import struct import sys @@ -59,6 +60,11 @@ class Pad: "LT": 0.0, "RT": 0.0} self.buttons = set() self._write() + # A file-backed pad has a failure mode the FIFO did not: the state + # PERSISTS after the writer dies. A tool killed mid-press leaves the + # button held forever, and the game then walks through menus on its own + # with nobody touching it. Release everything on exit. + atexit.register(self.reset) def _write(self): parts = [] diff --git a/tools/re-capture/ob_flag.py b/tools/re-capture/ob_flag.py index 74f5d5a5..fd3d241b 100755 --- a/tools/re-capture/ob_flag.py +++ b/tools/re-capture/ob_flag.py @@ -110,7 +110,20 @@ def main(): "ob_hunt.py before trusting anything below", flush=True) return 2 - nA, entsA, gA = sample(w, defs, out, "A") + # RETRY an empty sample. entities2.moving() types entities by their position + # CHANGING between two reads, so a sample that lands on a load, a stall, or + # simply loses the race with another scanner comes back with nothing — and a + # zero-entity sample silently produces zero candidates and a void run. Seen + # once, with sample B finding 81 entities moments later. + for _ in range(6): + nA, entsA, gA = sample(w, defs, out, "A") + if len(entsA) >= 10: + break + print("[A] too few entities — retrying", flush=True) + time.sleep(5) + if len(entsA) < 10: + print("[A] never got a usable entity sample", flush=True) + return 4 hist = Counter(nm for _, nm in entsA) print(f"[A] class histogram vs counter {nA}:", flush=True) for nm, k in hist.most_common(12):