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 0000000..4309a94 --- /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/captures/stage02-you-killed-warplanes-0003.png b/docs/re/captures/stage02-you-killed-warplanes-0003.png new file mode 100644 index 0000000..a87bb6d Binary files /dev/null and b/docs/re/captures/stage02-you-killed-warplanes-0003.png differ diff --git a/docs/re/mission-freeze-and-ob-flag.md b/docs/re/mission-freeze-and-ob-flag.md index e06bf76..cda46f5 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 b8413d9..b4e1eb3 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 74f5d5a..fd3d241 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):