With the per-entity searches refuted at word and bit level, the question became which object owns the counter. Sampling +-0x200 around it across a 4->8 transition: the control interval moved 0 of 256 words, and the step moved nine - the counter plus four words holding ASCII '4' -> '8' NUL-padded, and four pointers into 0xbcad2xxx that swap with them. Read live at HUD 008, all four character slots hold '8'. So the neighbourhood is the HUD's rendered text for this counter, which reframes the address: it is the HUD widget's value rather than "the mission's own objective counter" as this file called it. Recorded against that, because it is already measured: there is no separate mission-side copy moving on the same step. ob_hunt scans all of guest memory and requires a match across two transitions, and it left exactly one address. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
243 lines
13 KiB
Markdown
243 lines
13 KiB
Markdown
# `REMAINING OB` — the mission's own objective counter, in RAM
|
||
|
||
**Status:** ✅ `CONFIRMED` for one Stage 02 run: a **big-endian u32** whose value
|
||
is exactly the HUD's `REMAINING OB`, verified across two transitions it was not
|
||
selected by. 🟡 the address itself is from one run — **cross-run stability is
|
||
untested**. ❔ what it counts, and whether objective-marked entities carry a flag.
|
||
|
||
## 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 |
|
||
|
||
**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*.
|