Ran this file's own transition filter on a guarded Stage 02 run (stage asserted), reading the HUD from a crop taken at the same instant as each memory sample: scan at HUD 004 -> 41537 candidates; filter at HUD 008 -> 8; verify across the 008 -> 012 transition, which was NOT selected on -> exactly ONE survivor. That survivor, 0xbdb69668, tracked 4 -> 8 -> 12 against the HUD's 004 -> 008 -> 012. The other seven collapsed into noise at the first unselected transition, which is precisely what that rule exists to catch. The file's "try 0xbdb59668 first, re-scan when it reads 0" rule worked verbatim: it read a hard 0 here, and the re-scan cost about the predicted five minutes. The ...9668 page-offset pattern is REFINED, not reinstated: the three located addresses (0xbdb49668, 0xbdb59668, 0xbdb69668) are three ADJACENT 64 KB pages at one offset, and in this run exactly one of 8192 probed pages held 12 -- the counter -- making it a one-step lookup. But the 2026-08-26 refutation stands as measured (zero ...9668 VAs held the HUD value in that run), so this is a fast heuristic to be HUD-checked, not a law.
359 lines
18 KiB
Markdown
359 lines
18 KiB
Markdown
# `REMAINING OB` — the mission's own objective counter, in RAM
|
||
|
||
**Status:** ✅ `CONFIRMED` as a **big-endian u32** whose value is exactly the
|
||
HUD's `REMAINING OB`. 🟡 the ADDRESS recurs but is not universal — `0xbdb59668`
|
||
carried it in 3 of 5 measured runs, read a hard 0 in 2, and read garbage in a
|
||
6th (2026-08-26). ❔ what it counts, and whether objective-marked entities carry
|
||
a flag.
|
||
|
||
> ## 🟡 A sixth run, and a new failure mode (2026-08-26)
|
||
>
|
||
> ### ❌ First, a correction to this very section
|
||
>
|
||
> As originally written this section claimed to be *testing an untested* 🟡, and
|
||
> concluded "the address is run-dependent — always re-derive". Both halves were
|
||
> wrong, and the evidence was already further down **this same file**:
|
||
>
|
||
> * Cross-run stability was **not** untested. The status line at the top said so,
|
||
> but the body had tested it repeatedly — `0xbdb59668` carries the counter in
|
||
> **3 of the 5** runs measured, and reads a **hard 0** in the other two. I
|
||
> refuted a stale *status line* and presented it as news.
|
||
> * "Always re-derive" is worse advice than the rule this file already gives:
|
||
> **try `0xbdb59668` first, check it against the HUD, and re-scan only when it
|
||
> reads 0.** A scan costs ~5 minutes; the check costs seconds.
|
||
>
|
||
> ### What this run actually adds
|
||
>
|
||
> A **sixth** data point, and a failure mode not previously seen: the address
|
||
> read neither the counter nor 0 but **95748078**, constant over four samples. A fresh Stage 02 run (guarded route, stage
|
||
> asserted via `assert_stage.py UN_f101_TCAF_Acropolis`):
|
||
>
|
||
> | | |
|
||
> |---|---|
|
||
> | HUD | `Remaining OB : 004` ([capture](../captures/remaining-ob-004-crossrun.png)) |
|
||
> | RAM `0xbdb59668` | **95748078**, constant over four samples 12 s apart |
|
||
>
|
||
> Not 4, not near 4, and not moving while the mission ran. So `0xbdb59668` is a
|
||
> property of *that* run's heap, exactly as the corpus's own warning about the
|
||
> heap re-allocating between runs predicts.
|
||
>
|
||
> ❔ **No cheap constant shift either.** Looking for a BE u32 equal to the HUD's
|
||
> `4` near the old address gives **1654** candidates within ±1 MB and **11202**
|
||
> within ±16 MB — far too many to isolate without the transition filter. So there
|
||
> is no "old address + delta" shortcut to be had.
|
||
>
|
||
> **Practical consequence:** treat the address as *derived per run*, never as a
|
||
> constant. The method below (`ob_scan.py`: scan at one value, filter against
|
||
> live memory at a **different** value) is the durable result here — the number
|
||
> `0xbdb59668` is not.
|
||
|
||
## Why it matters
|
||
|
||
[`autopilot-memory-driven.md`](../autopilot-memory-driven.md) ranks this its
|
||
problem #2: a pilot that flies well but ignores the objective cannot finish a
|
||
mission. Its 300 s run took no damage, killed one fighter, and watched
|
||
`REMAINING OB` **rise** from 004 to 011 as waves spawned. Reading the counter is
|
||
what turns "shoot whatever is nearest" into "shoot what closes the mission".
|
||
|
||
## The find
|
||
|
||
Guest VA **`0xbdb59668`**, big-endian u32, in a Stage 02 run on the
|
||
[upstream baseline](../upstream-baseline.md).
|
||
|
||
| time | RAM `0xbdb59668` | HUD |
|
||
|---|---|---|
|
||
| selected on | 18 → 24 | 018 → 024 |
|
||
| t+40 s | 24 | 024 |
|
||
| t+75 s | **23** | **023** |
|
||
| t+110 s | **22** | **022** |
|
||
| t+145 s | 22 | 022 |
|
||
| t+180 s | 22 | 022 |
|
||
|
||
The two middle rows are the ones that matter: the candidate was filtered on the
|
||
18→24 transition, and it then tracked 24→23→22 on its own.
|
||
|
||
## Method, and the two traps in it
|
||
|
||
`tools/re-capture/ob_scan.py`. Scan one snapshot for the current value, then
|
||
filter that candidate set against **live** `/dev/shm/xenia_memory_*` at the next
|
||
distinct value. Only the first pass needs the 4.8 GB copy.
|
||
|
||
**🔴 Filter on a change, not a repeat.** The first attempt scanned at 19, filtered
|
||
at 19 again, then at 18 — and left **zero** survivors. The value moves between
|
||
the memory copy and the screenshot that reads it, so "still 19" is not reliable.
|
||
Scanning 18 and filtering on 24 gave exactly one candidate on the first try.
|
||
|
||
**🔴 Verify across a transition you did not select on.** An earlier differential
|
||
over 19→18 also gave exactly one candidate, `0xbc22e83c` — and it was **wrong**:
|
||
read live it held 26 while the HUD showed 017. It was an unrelated counter that
|
||
happened to step 19→18 in the same window. One matching transition is not
|
||
evidence; the same offset tracking a *later*, unselected change is.
|
||
|
||
## What is not settled
|
||
|
||
* 🟡 **Cross-run stability.** `0xbdb59668` is from a single run. The corpus's
|
||
other runtime finds are re-scanned per run, so the durable result here is the
|
||
**method**, not the number. Testing whether the address repeats costs one boot.
|
||
* ❔ **What it counts.** It rose 18 → 24 while waves spawned and fell as things
|
||
died, so it is objective targets remaining, not kills. Whether every `OB`-badged
|
||
entity is one of them, and whether that badge is a flag in the entity object, is
|
||
the natural follow-on — and it is what the pilot actually needs to *choose*
|
||
targets rather than merely know how many are left.
|
||
* ❔ Whether the same address holds for other stages.
|
||
|
||
## 🔴 2026-08-23 — cross-run stability: **REFUTED**
|
||
|
||
`0xbdb59668` does **not** repeat. A second Stage 02 run on the same baseline
|
||
(rebuilt per [`../dynamic-re-state-restore.md`](../dynamic-re-state-restore.md))
|
||
reads **0** at that address for the whole mission while the HUD counts up:
|
||
|
||
| HUD | RAM `0xbdb59668` |
|
||
|---|---|
|
||
| `004` (t+18 s, flight entry) | 0 |
|
||
| `008` | 0 |
|
||
| `012` (held for ~5 min) | 0 |
|
||
|
||
A **second, independent** run of the same mission repeats it: at `TIME 01:08.96`
|
||
with the HUD on `REMAINING OB 004`, the word is again **0**
|
||
([`../captures/stage02-inflight-run2-ob-address-zero.png`](../captures/stage02-inflight-run2-ob-address-zero.png)).
|
||
|
||
Read three times back-to-back at `004` — all zero — and `±64 bytes` around it are
|
||
zero too. The page is **not** a sparse hole (`SEEK_DATA` at the offset returns the
|
||
offset itself; the next hole is 5 MB later), so this is an allocated,
|
||
zero-filled word, not an unmapped read. The number was a **per-run artefact**, as
|
||
this file already suspected; the method is the durable part, and every future
|
||
session must re-scan.
|
||
|
||
**And re-finding it in the new run failed too — recorded because both failures
|
||
are instructive.** Two candidates were produced and both were killed by the
|
||
"verify across a transition you did not select on" rule:
|
||
|
||
* `0xbc2377dc` — held 12 at selection, then went 12 → 18 while the HUD stayed
|
||
`012`, and was reading **3** two minutes later at HUD `012`.
|
||
* `0xbd295b04` — read 12 once, then 0, 21, 54, 46, 4 in the next 50 s. Noise.
|
||
|
||
🔴 **A correction to this file's own method note.** The trap is *not* that the
|
||
scan is slow: `ob_scan.py scan` over the live `/dev/shm` image takes **0.9 s**
|
||
(9 555 hits at value 12; the SEEK_DATA extent walk skips the sparse 4.6 GB). The
|
||
real trap is that **`REMAINING OB` climbs fast early in Stage 02** — `004` → `008`
|
||
→ `012` inside about four minutes as waves spawn — so a scan is only valid if the
|
||
HUD is confirmed to hold the same value **immediately before and after it**. The
|
||
earlier `4 ∩ 8` intersection here came back with no survivor for exactly that
|
||
reason: the value-8 scan was actually taken at 12.
|
||
|
||
**What blocked finishing it:** with the pilot in `RETIRE` (hull 340/1500) nothing
|
||
was killing objectives, and the counter sat at `012` for five minutes — no
|
||
transition to filter on. A run that intends to find this address needs a pilot
|
||
that is still *engaging*, which is the same constraint
|
||
[`../autopilot-memory-driven.md`](../autopilot-memory-driven.md) records.
|
||
|
||
**Unchanged and still open:** what the counter counts, and whether an `OB`-badged
|
||
entity carries a flag in its entity object. Nothing in this pass touched that.
|
||
|
||
## 🟡 2026-08-23 (later) — a third run, a HUD-clean scan, and one candidate that is very hard to explain away
|
||
|
||
With the launcher repaired the scan could finally be run the way the method
|
||
asks. **HUD confirmed `004` immediately before *and* after the scan** (0.9 s), so
|
||
the 39 596-entry candidate set is valid at that value; then filtered when the
|
||
HUD was confirmed at `012`.
|
||
|
||
| step | HUD | candidates |
|
||
|---|---|---|
|
||
| scan | `004` (confirmed both sides) | 39 596 |
|
||
| filter | `012` (confirmed) | **17** |
|
||
| later sample | — (see caveat) | 7 still 12 |
|
||
|
||
> ## ✅ 2026-08-27 — the counter LOCATED again, at `0xbdb69668`, and the `…9668` pattern is *refined*
|
||
>
|
||
> A sixth located run, by this file's own transition filter, on a guarded Stage 02
|
||
> run (stage asserted). Every step's HUD value was read from a crop taken at the
|
||
> same instant as the memory sample.
|
||
>
|
||
> | step | HUD | candidates |
|
||
> |---|---|---|
|
||
> | scan | `004` | 41 537 |
|
||
> | filter | `008` | **8** |
|
||
> | verify (not selected on) | `008 → 012` | **1** |
|
||
>
|
||
> The single survivor is **`0xbdb69668`**, which tracked `4 → 8 → 12` against the
|
||
> HUD's `004 → 008 → 012` ([capture](../captures/ob-012-located.png)). The other
|
||
> seven collapsed into noise (`1040511`, `3211282537`, …) at the first transition
|
||
> they were not selected on — which is exactly why that rule is in this file.
|
||
>
|
||
> ### ✅ The "try it first, re-scan when it reads 0" rule worked verbatim
|
||
>
|
||
> `0xbdb59668` read a **hard 0** here — the documented failure mode, not garbage —
|
||
> so the re-scan was the right move and cost the five minutes the file predicts.
|
||
>
|
||
> ### 🟡 The `…9668` page offset: a fast HEURISTIC, not a law
|
||
>
|
||
> The three located addresses are **three adjacent 64 KB pages at one offset**:
|
||
>
|
||
> | run | address |
|
||
> |---|---|
|
||
> | run 1 | `0xbdb5``9668` |
|
||
> | run 2 | `0xbdb4``9668` |
|
||
> | this run | `0xbdb6``9668` |
|
||
>
|
||
> And in this run the offset probe is *decisive on its own*: of 8192 pages, exactly
|
||
> **one** VA at offset `0x9668` held `12`, and it was the counter. That is a
|
||
> one-step lookup where the full scan costs five minutes.
|
||
>
|
||
> ⚠️ This **refines rather than reverses** the refutation immediately below. That
|
||
> refutation stands as measured: in that run, zero `…9668` VAs held the HUD value.
|
||
> So the pattern recurs but is not universal — the same shape as the address
|
||
> itself. **Practical order:** try `0xbdb59668`; if it is 0 or garbage, probe the
|
||
> `…9668` pages (seconds); only then run the full scan. Check every candidate
|
||
> against the HUD before believing it.
|
||
|
||
> ## ❌ REFUTED 2026-08-26 — the page-offset `…9668` prediction does not hold
|
||
>
|
||
> The reading below — *"the counter lives at a fixed offset inside an allocation
|
||
> whose base moves by whole 64 KB pages"* — came with its own test: **"a third
|
||
> scan should again land on `…9668`."** That test has now been run, on a fresh
|
||
> guarded Stage 02 run (stage asserted), and it fails.
|
||
>
|
||
> Probing **8192** pages, every VA of the form `0x????9668` across
|
||
> `0xa0000000`–`0xbfffffff`:
|
||
>
|
||
> | HUD | VAs at offset `0x9668` holding that value |
|
||
> |---|---|
|
||
> | `004` | **2** — `0xbc3f9668`, `0xbe3f9668` |
|
||
> | `012` | **0** |
|
||
>
|
||
> Both candidates then failed the file's own transition rule. Watched for 252 s:
|
||
> `0xbc3f9668` held a flat **4** and `0xbe3f9668` flickered 4/0, while the HUD
|
||
> went **004 → 012** ([capture](../captures/remaining-ob-012-crop.png)). A value
|
||
> that does not move when the counter moves is not the counter, and at `012`
|
||
> nothing at that page offset holds `12` at all.
|
||
>
|
||
> Both arms were read at the same instant (cropped HUD digits beside each memory
|
||
> sample), because comparing a stale screenshot against a later memory read is
|
||
> exactly how the earlier `13`-vs-`004` confusion in this session arose.
|
||
>
|
||
> ⚠️ **Scope, stated so this is not over-read:** this refutes the page-offset
|
||
> prediction *for this run*. It does not touch the confirmed finding that
|
||
> `0xbdb59668` carries the counter in some runs — that address is simply not at a
|
||
> reachable `…9668` page here. Nor was the counter located in this run: no
|
||
> transition filter was run, so where it actually lives this time is ❔.
|
||
|
||
**The survivor of interest is `0xbdb49668`** — the run-1 address `0xbdb59668`
|
||
**minus exactly `0x10000`**, at the identical page offset `0x9668`.
|
||
|
||
**That is not a cheap coincidence, and it was checked rather than admired.** Of
|
||
all 39 596 words holding `4` at scan time, exactly **2** sit at page offset
|
||
`0x9668`; one of those two is in the 17, and it is this one. A survivor landing
|
||
there by chance is a ~5 × 10⁻⁵ event. The reading it suggests — the counter lives
|
||
at a fixed offset inside an allocation whose **base moves by whole 64 KB pages**
|
||
between runs — is a testable claim, not a story: a third scan should again land
|
||
on `…9668`.
|
||
|
||
**Why this is 🟡 and not ✅, stated plainly.** The corpus's own rule is that a
|
||
candidate is believable once it tracks a transition it was *not* selected on,
|
||
against the HUD. This one has not done that yet:
|
||
|
||
* The "later sample" that killed 10 of the 17 was taken **during the GAME OVER
|
||
flash** — the escorted ACROPOLIS was lost at ~13 min — so the HUD counter was
|
||
not on screen. The ten that *moved* are genuinely refuted; the seven that did
|
||
not move are merely **unrefuted**, which is a much weaker thing, and the
|
||
counter may simply be frozen once the mission ends.
|
||
* Between the filter and the mission end, `REMAINING OB` sat at `012` for about
|
||
ten minutes: `pilot.py` spent the window in EVADE/RETIRE at hull 822/1500 and
|
||
killed no objectives, so there was no transition to use.
|
||
|
||
**The one experiment that settles it:** a run where the pilot is still engaging,
|
||
scanning and filtering as above, then watching `0xbdb4·9668`-equivalent across a
|
||
HUD-verified change. The blocker is the pilot's survival, not the method — and
|
||
the same run would answer whether the page offset repeats, which is the stronger
|
||
result of the two.
|
||
|
||
## ✅ 2026-08-23 (third pass) — the counter is at `0xbdb59668` again, and the refutation above is *refined*, not reversed
|
||
|
||
Run 4, with the hunt automated end to end
|
||
([`ob_hunt.py`](../../tools/re-capture/ob_hunt.py) + the HUD reader), produced
|
||
**exactly one** surviving address:
|
||
|
||
```
|
||
scanned at 004: 35897 candidates (HUD confirmed 004 on BOTH sides)
|
||
004 -> 008 [SELECTED ON] 7 survive
|
||
008 -> 012 [verification (unselected)] 1 survive
|
||
va 0xbdb59668
|
||
```
|
||
([`captures/ob-hunt-stage02-run4.log`](../captures/ob-hunt-stage02-run4.log),
|
||
[`captures/ob-hunt-stage02-run4.json`](../captures/ob-hunt-stage02-run4.json))
|
||
|
||
That is the corpus's own standard met: one filter to select, a **second on a
|
||
transition it was not selected by** to verify. Then three live paired readings —
|
||
RAM read, screenshot, RAM read again — all `ram 12 -> 12 / hud 012`.
|
||
|
||
And it is **the same number as run 1**.
|
||
|
||
### What this does and does not overturn
|
||
|
||
| claim | status |
|
||
|---|---|
|
||
| the counter sits at `0xbdb59668` in *every* run | 🔴 **still refuted.** Runs 2 and 3 read a hard 0 there, on an allocated page, while the HUD counted 004 → 012. Nothing about run 4 makes those readings wrong. |
|
||
| the run-1 number was a meaningless artefact | 🔴 **withdrawn.** It recurs *exactly*: 2 of the 4 runs measured put the counter at `0xbdb59668`. |
|
||
| the address moves by whole 64 KB pages | 🟡 **consistent, twice.** Run 3's best candidate was `0xbdb49668` — one 64 KB page below — and both known-good addresses share the page offset **`0x9668`**. Run 3's candidate was only ever amber, so this rests on one confirmed address plus one plausible one. |
|
||
|
||
**So the practical rule is: try `0xbdb59668` first and *check it against the
|
||
HUD*, and re-scan when it reads 0.** A scan costs about five minutes now
|
||
(`ob_session.sh`), most of that being the boot.
|
||
|
||
### Why run 3 failed and run 4 did not
|
||
|
||
Nothing about the game changed; the *procedure* did. Run 3 filtered once, on
|
||
`004 → 012`, because the intermediate `008` step went by unnoticed between two
|
||
hand readings — and its 17 survivors then had no second transition to be tested
|
||
against, since `REMAINING OB` held at `012` for ten minutes while the pilot sat
|
||
in EVADE. Run 4 caught **both** early steps, five seconds apart from polling,
|
||
because reading the HUD stopped costing a human round trip. The evidence needed
|
||
was always in the first four minutes of the stage; the earlier runs were simply
|
||
not able to look often enough.
|
||
|
||
**Still open, and untouched:** ❔ what the counter counts, and whether an
|
||
`OB`-badged entity carries a flag in its entity object. That is the part the
|
||
autopilot actually needs, and knowing the address is only its precondition.
|
||
|
||
## ✅ 2026-08-24 — what is AROUND the counter: its own rendered digits
|
||
|
||
With the per-entity searches refuted at both word and bit level
|
||
(`../mission-freeze-and-ob-flag.md`), the next question was what object owns the
|
||
counter. It sits at `0xbdb59668`, inside the entity-heap window, so the answer is
|
||
readable directly: sample ±0x200 around it across one transition and keep the
|
||
words that move **with** it
|
||
([`ob_neighbourhood.py`](../../tools/re-capture/ob_neighbourhood.py),
|
||
[`captures/ob-counter-neighbourhood-stage02.json`](../captures/ob-counter-neighbourhood-stage02.json)).
|
||
|
||
Control first: over an 8-second interval while the counter sat still, **0 of the
|
||
256 words in the window changed**. So the window is quiet, and anything that
|
||
moves on the step is a real neighbour rather than noise.
|
||
|
||
On `4 → 8`, **nine** words moved — the counter and eight others, in two kinds:
|
||
|
||
| offset from the counter | before → after | what it is |
|
||
|---|---|---|
|
||
| `-0x12c`, `-0x11c`, `+0x64`, `+0x74` | `0x34000000 → 0x38000000` | **ASCII `'4'` → `'8'`**, NUL-padded |
|
||
| `-0x10c`, `-0xec`, `+0x84`, `+0xa4` | `0xbcad258c ↔ 0xbcad280c`, `0xbcad2d0c ↔ 0xbcad2f8c` | guest pointers into `0xbcad2xxx` |
|
||
|
||
Read live a moment later with the HUD showing `008`, all four character slots
|
||
hold `'8'`. So the neighbourhood is the **HUD's rendered text for this counter** —
|
||
the digit as a character in four places, each with a pointer that swaps as the
|
||
digit changes (a glyph or sprite record).
|
||
|
||
### 🟡 Which reframes what the address IS
|
||
|
||
This file has called `0xbdb59668` "the mission's own objective counter". The
|
||
evidence now says something narrower: it is **the HUD counter widget's value**,
|
||
sitting beside the characters it renders.
|
||
|
||
🔴 **But there is no separate mission-side copy that moves with it**, and that was
|
||
already measured: `ob_hunt.py` scans *all* of guest memory and requires a
|
||
candidate to match across two transitions, and it left **exactly one** address.
|
||
Whatever the mission script keeps internally either does not change on the same
|
||
step or is not a plain big-endian word.
|
||
|
||
**Next:** follow one of the `0xbcad2xxx` pointers — if they resolve to glyph or
|
||
sprite records, the widget is confirmed and the search for a mission-side count
|
||
moves to the objects those digits are *fed from*.
|