Files
Sylpheed/docs/re/mission-freeze-and-ob-flag.md
Sylpheed RE agent fbea9f9148 re: refute the OB entity-flag on the FULL enumeration -- the stationary-entity hole is closed
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.
2026-08-27 00:40:53 +00:00

18 KiB
Raw Blame History

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.py logged 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.py still said flight, 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.py on 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.typed finds 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 ADAN93 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.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).

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 (pairs 0x1000 apart 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 … +0xC00 window — 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 004 with ~118 hostiles live, rose 004 → 012 as waves spawned, held through a third of the hostile population dying, and decremented once when the player killed.