Every earlier refutation in this file carried the caveat that entities2.typed types entities by their position CHANGING, so a stationary objective is invisible to it. This session's definition-pointer enumeration does not have that limit, so the sweep was re-run against it (ob_flag_all.py, guarded route, stage asserted, HUD cropped beside each sample): A: HUD 004, 147 entities -> 152 candidates B: HUD 012, 133 entities -> 15 candidates intersection: 1 The lone survivor pos+0x0250 = 239d6732 is the same offset AND identical value this file already characterised as a per-group word. Membership test: all 12 holders are UN_e010_ADAN_Attacker_S, 12 of 16 live attackers. It is a squad parameter, and it survived only because that population equalled the counter at both samples. Also reconfirms "not a class head-count" on 147 entities including capital ships. Trap recorded: the first sweep reported 298 entities and a class with head-count exactly 4 -- a perfect-looking hit that was pure artifact. Deduping by ADDRESS leaves the measured exact 2x duplication (pairs 0x1000 apart, byte-identical positions) intact and doubles every population. Dedup on the position VALUE.
18 KiB
An in-mission freeze, and the first cut at what REMAINING OB counts
2026-08-23. One Stage 02 run, aimed at the ❔ that
structures/mission-objective-counter.md
leaves open — what the counter counts. It produced one clean refutation, one
new confirmation, and one experiment that proves nothing, for a reason worth
having a tool for.
✅ The address recurs a third time
HUD=4 RAM=4 at 0xbdb59668, read before anything else was attempted. That is
the third run putting the counter at that exact address (runs 1, 4, 5 of the
five measured), against runs 2 and 3 where it read a hard 0. The rule in the
structure file — try it, check it against the HUD, re-scan when it reads 0 —
survives another test.
🔴 The counter is not a live class head-count
With the counter at 4, the typed live entity list was:
| n | class |
|---|---|
| 8 | UN_e010_ADAN_Attacker_S |
| 7 | UN_f001_TCAF_DeltaSaber_T |
| 7 | UN_e007_ADAN_Turret |
| 1 | UN_f001_TCAF_DeltaSaber_T_Player |
No class has 4 members, and no sum of two of them does either. This is a
sharper version of the corpus's existing note (012 on the HUD against 118 live
ADAN): it is not merely "not the hostile count", it is not the count of any
class the enumeration can see. So the counter is either a subset marked some
other way — the OB badge — or a number the mission script keeps of its own.
🔴 The flag experiment proves nothing — the guest had frozen
The plan was the same shape that settled the address: find every 4-byte offset around an entity where exactly N entities agree, then require the survivors to agree exactly N′ times after the counter moves. The first half ran — 20 candidate offsets where exactly 4 of the 23 entities share a value — and then the counter sat at 4 for the whole 600 s window and the run reported "counter never moved".
That reading was wrong, and the correction is the useful part. The guest had stopped advancing about ten seconds into flight:
pilot.pylogged 5 910 samples over 734 s and every one after t=10.6 s carried the identical speed, yaw and pitch — 724 s of frozen state.- Two screenshots six seconds apart were byte-identical: max difference 0
over 863 325 pixels (
captures/stage02-inmission-freeze.png). - Everything that is supposed to notice, did not:
screen_id.pystill saidflight, the emulator was alive at 212 % CPU, and the process liveness check every script uses was satisfied.
So "the counter never moved" was a fact about a dead world, not about the game.
🔴 Withdrawn with it: the claim written into ob_session.sh that the counter
"climbs on its own as waves spawn in the first minutes" — in a run that is
advancing it did (004 → 008 → 012 twice), but this run cannot support the general
statement and the two facts had been about to be conflated.
The freeze is intermittent, not universal: the run an hour earlier stepped
the counter twice and had pilot.py in DEFEND at t=629 s with changing state.
frozen.py
Two screenshots, a gap, exact comparison — no tolerance, because a stopped
simulation is identical, not merely similar. Checked in both directions: 0 on
the frozen pair, max_pixel_delta=254 on two frames of a live one. ob_hunt.py
and ob_flag.py now consult it after a minute of no movement and say
GUEST FROZEN instead of quietly waiting out their timeout.
What is still open
- ❔ The flag itself. 20 candidate offsets exist but have never been put to
the second test. Repeat
ob_flag.pyon a run that is actually advancing; it now aborts early instead of wasting the window if it is not. - ❔ Whether the enumeration can even see the objectives.
entities2.typedfinds entities by their position changing, so a stationary objective is invisible to it. Stage 02's objective is "shoot down all invading enemy fighters", which move — but a null result would not rule out a flag on objects this method never enumerates, and that limit is now written into the tool. - ❔ What causes the freeze. Not investigated here at all; this run only establishes that it happens, that it can happen within seconds of the HUD appearing, and how to detect it in one call.
2026-08-23 (later) — the flag experiment finally ran, and still cannot verify
A run that stayed animating for its whole 25 minutes (checked repeatedly with
frozen.py) got the first real sample:
HUD=12 RAM=12 (0xbdb59668 again — 5 of 7 runs now)
[A] counter=12 entities=161
114 UN_e007_ADAN_Turret
32 UN_e010_ADAN_Attacker_S
14 UN_f001_TCAF_DeltaSaber_T
1 UN_f001_TCAF_DeltaSaber_T_Player
[A] classes whose head-count equals the counter: NONE
[A] offsets where exactly 12 entities agree: 11
with a much bigger entity list than the frozen run's 23, so the earlier "🔴 not a class head-count" result is now confirmed on a 161-entity sample too.
The 11 candidates, with who they group — the part worth looking at:
| offset | value | members |
|---|---|---|
+0x00d0 |
3f57f496 |
12 of the 14 friendly Delta Sabers |
+0x017c–+0x0198 (7 words) |
floats | 12 of the 32 e010 attackers |
+0x00b0, +0x015c |
43f9ffff |
10 turrets + 2 attackers |
+0x0238 |
00000004 |
12 turrets |
The seven consecutive floats at +0x017c…+0x0198 shared by exactly twelve
attackers look far more like one shared parameter block — a squad flying the
same AI or weapon configuration — than like a per-entity objective flag, and
+0x0238 = 4 on twelve turrets is hard to reconcile with an objective that
reads "shoot down all invading enemy fighters". None of this is verified, and
the reason is the next paragraph.
🔴 REMAINING OB held at 012 for fifteen minutes of live flight
Not a frozen world this time — the game was animating throughout, and the run ended only because the experiment's window did. So the counter genuinely did not move, and there was no second sample to test the candidates against.
And it held while the hostile population fell by a third. pilot.py's own
rescans went 132 ADAN → 93 ADAN over the window: roughly 39 hostiles died
(to wingmen and to the ACROPOLIS's guns) and REMAINING OB did not change by
one. That is a real negative about what the counter counts — it is not
"hostiles left", on a much stronger sample than the earlier head-count argument.
🔴 A tool defect that bounds every kill-based experiment
pilot.py never fires: fire=1 appears in 0 of its 13 521 logged
samples in this run. It flies, chases and evades, but the trigger is never
pulled. Every "kill something and watch X" experiment in this corpus is
therefore relying on the wingmen, and the pilot's own contribution to a kill
count is zero. Not fixed here — recorded because it silently weakens conclusions
drawn from runs that assumed the pilot was shooting.
Freeze frequency, updated
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)
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.typedtypes 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).
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.
2026-08-24 — the bit-level test exists; three runs, no verification, and one hole closed
ob_bitflag.py is the follow-on the
word-level refutation named: for every 4-byte offset in the window (now
-0x400 … +0xC00) and every one of its 32 bits, count how many entities have it
set, keep the (offset, bit) pairs whose count is exactly the counter, and
require the survivors to match again after the counter moves. Both polarities are
counted, because an objective could equally be marked by a bit that is clear on
it.
It ran three times and verified nothing. What each attempt cost is the useful part:
| run | sample A | outcome |
|---|---|---|
| 1 | counter 4, 29 entities → 187 set-polarity + 33 clear-polarity candidates | "counter never moved" for the whole 700 s window — and the mission had ended in GAME OVER partway through |
| 2 | — | HUD unreadable: still the GAME OVER screen |
| 3 (fresh boot) | — | the counter was at a different address; the guard refused to run. ob_hunt got one filter (008 → 012, 4 survivors including 0xbdb58668) and then the guest froze |
🔴 The hole run 1 exposed: "animating" is not "still in a mission"
frozen.py asks whether the guest is advancing. The GAME OVER screen animates
perfectly happily — mean colour (114, 22, 63), the magenta plate — so every
liveness check passed while the mission was over, and the tool reported "the
counter never moved", which reads as a fact about the game and was a fact about
a finished one.
Closed: frozen.in_flight() classifies the screen with screen_id.py, and
ob_hunt.py / ob_flag.py / ob_bitflag.py now abort with NO LONGER IN FLIGHT instead of waiting out their windows. That is the second time a
confident negative in this investigation turned out to be about a dead world —
the first was the freeze — and the pattern is worth naming: before believing
"X never happened", show that the thing that would produce X was still running.
Attrition is now the dominant cost
Of the Stage 02 runs that have reached flight, roughly half end early — a freeze, or a GAME OVER when the escorted ACROPOLIS or the craft is lost — and the scan needs the run to survive two counter transitions. Nothing here fixes that; it is why three attempts produced one filter between them.
A fourth bit-level attempt (2026-08-24) — the guard earned its keep
ob_bitflag now picks the counter's address itself from the three seen so far,
so the run started immediately at flight entry:
HUD=4 RAM=4 at 0xbdb59668
[A] counter=4 entities=31
[A] (offset,bit) pairs set on exactly 4: 207; clear on exactly 4: 22
GUEST FROZEN — the counter was never going to move
The freeze was caught in about a minute by frozen() instead of consuming the
whole 700 s window — which is the difference the guard makes, and the only thing
this attempt bought. Still no verification: four runs, four windows, zero
second transitions.
🔴 2026-08-24 — the BIT-level flag is refuted too, on two independent transitions
The short-run strategy worked: both counter steps happen in the first minutes of
flight, so ob_bitflag.py got two complete passes out of one run before anything
could freeze or be killed.
| pass | scan | filter | candidates at A | survivors |
|---|---|---|---|---|
| 1 | counter 4, 24 entities | 4 → 8 (HUD confirmed) | 255 set + 455 clear | 29 |
| 2 | counter 8, 100 entities | 8 → 12 | 133 set + 64 clear | 2 |
The intersection of the two survivor sets is EMPTY.
(captures/ob-bitflag-refuted-stage02.json)
Not one (offset, bit) survives two independently selected transitions. So there
is no per-entity bit — in either polarity, anywhere in -0x400 … +0xC00
around the position triple — whose population tracks REMAINING OB. Together
with the word-level refutation this closes the "objectives are marked in the
entity object" family as far as this method can reach.
Why pass 1 looked richer than it was
16 of its 29 survivors are the same word, pos+0x0250, with sixteen
different bits. That is the signature of a shared value, not a flag: if four
entities hold the identical word at +0x250, every set bit of it "survives"
together, and the bit framing is spurious. It is the same offset the word-level
sweep found shared by 12 attackers (pos+0x0250 = 239d6732), so +0x250 is a
per-class or per-group word — a parameter or a pointer — and its bit counts track
the class population rather than the objective count.
⚠️ One reading caveat, kept: pass 2's B-sample HUD read 8 while RAM read
12; the address had been HUD-verified at sample A and the RAM value is what the
filter used, but the confirming screenshot at that instant did not agree.
What is still not ruled out is unchanged from the word-level pass: anything
outside the window, and anything on entities entities2.typed cannot see (it
types by position changing, so stationary objectives are invisible).
🔴 2026-08-27 — the last hole is closed: refuted on the FULL entity enumeration too
Every refutation above carried the same caveat: "anything on entities
entities2.typed cannot see — it types by position changing, so a stationary
objective is invisible to it." That hole is now closed, because this session
established an enumeration that does not have it — search for the definition
pointer and take position = hit - 0x130
(two-entity-enumerations.md), which finds every
entity moving or not, capital ships and the ACROPOLIS included.
Re-run with that enumeration (ob_flag_all.py),
guarded route, stage asserted, HUD cropped beside every sample:
| pass | HUD | entities | candidates |
|---|---|---|---|
| A | 004 |
147 | 152 |
| B | 012 |
133 | 15 |
| intersection | 1 |
The lone survivor is pos+0x0250 = 239d6732 — and it is the same offset and
the identical value this file already characterised as a per-group word. Put to
the membership test rather than admired:
holders of pos+0x250 == 239d6732 : 12
12 UN_e010_ADAN_Attacker_S (of 16 live attackers)
All twelve are attackers. It is a squad parameter or pointer shared by part of
the e010 population, and it survived only because that population happened to
equal the counter at both samples. Not an objective flag.
What this adds
- 🔴 The "objectives are marked in the entity object" family is now refuted on an enumeration that includes stationary entities — the limit every earlier pass had to state is gone, not merely narrowed.
- 🔴 "Not a class head-count" is reconfirmed on 147 entities including capital ships: no class had a population of 4 at A or 12 at B.
- ⚠️ A trap worth having. The first run of this sweep reported 298
entities and a class with head-count exactly 4 (
UN_f105_TCAF_Cruiser) — an apparently perfect hit. It was an artifact: deduplicating by address leaves this session's measured exact 2× duplication (pairs0x1000apart holding byte-identical positions) intact, so every entity is counted twice and every population doubles. Deduplicating by position value gives 147 and the spurious class hit disappears. Any sweep of this shape must dedup on the position triple, not the address.
Still open
- ❔ Anything outside the
-0x400 … +0xC00window — unchanged. - ❔ Sample A's holder membership was not re-checked (the world had moved on), so "4 attackers at A" is inferred from B's membership plus this file's earlier identification of the same word, not measured.
- ❔ What the counter does count. Still unexplained: it read
004with ~118 hostiles live, rose004 → 012as waves spawned, held through a third of the hostile population dying, and decremented once when the player killed.