Files
Sylpheed/docs/re/two-entity-enumerations.md
Sylpheed RE agent d2c81249cd re: WITHDRAW "the absence is structural" -- capital ships do have +0x130 blocks
My enumeration sourced entities from moving(), which requires a position to CHANGE
between two samples.  A capital ship holding station changes by exactly zero, so it
can never appear -- which is also why lo=0 and the whole-map scan did not help, and
why parked ArrowHead wingmen were missing.  The filter was the problem, not the
range, and not the game.

navigator.py already does it right and says so in its header: search for the
DEFINITION POINTER and take position = hit - 0x130, "which finds every entity
whether it is moving or not".  The existing mission_state.py scan on Stage 02
returns 149 entities with hull(+0x154) == definition HP for 129 of them:

  UN_f101_TCAF_Acropolis  (+5000, +0, +0)  hull=25000.0  HP=25000.0  frac=1.000
  UN_e105_ADAN_Cruiser                     hull=30000.0  HP=30000.0
  UN_e106_ADAN_Destroyer                   hull= 9150.0  HP=10000.0  (under fire)

So "an autopilot that must protect the Acropolis cannot locate it through the
+0x130 method at all" is withdrawn outright -- it can, and INDEX.md had already
recorded the Acropolis falling 25000 -> 23038 over 240s.

What survives: the two enumerations are still different structures in different
regions (0/116 vtable instances lie in the +0x130 region).  What does not survive
is any claim that the +0x130 model covers only four types.
2026-08-26 23:20:48 +00:00

6.9 KiB
Raw Blame History

The two enumerations are two different structures — and only one is complete

Measured 2026-08-26, live flight HUD, movers precondition checked (18 002).

The stage changed between runs — earlier cross-run comparisons were invalid

LOAD GAME → slot 01 did not land on the same mission it did earlier today. This run's definition table has 14 entries and no asteroids at all:

UN_bf001_TCAF_SchlosBase   UN_f101_TCAF_Acropolis   UN_f201_TCAF_Tanker
UN_e105_ADAN_Cruiser  UN_e106_ADAN_Destroyer  UN_e108_ADAN_ASFrigate
UN_e201_ADAN_ISCMissile  UN_f003_TCAF_ArrowHead  UN_f105/f106 TCAF ...

That is the Stage 02 escort mission (the ACROPOLIS of autopilot-memory-driven.md), not the S01 training area with UN_S01_Asteroid_cmesh_* that earlier runs loaded. Slot 01 is the auto-save, so the mission it restores moves as the save is written.

This invalidates the earlier "101 vs 42" comparison outright — those two numbers came from different stages, not merely different moments. And it means no runtime roster is reproducible across runs unless the stage is verified first.

Same moment, same stage, both methods

type INST_VTABLE scan moving + +0x130
UN_e007_ADAN_Turret 21 14
UN_e010_ADAN_Attacker_S 9 8
UN_f001_TCAF_DeltaSaber_T 7 7
UN_f001_TCAF_DeltaSaber_T_Player 1 1
UN_e106_ADAN_Destroyer 19 0
UN_f106_TCAF_Destroyer 14 0
UN_f105_TCAF_Cruiser 11 0
UN_e105_ADAN_Cruiser 7 0
UN_e108_ADAN_ASFrigate 9 0
UN_e201_ADAN_ISCMissile 9 0
UN_f003_TCAF_ArrowHead 6 0
UN_f101_TCAF_Acropolis / bf001_SchlosBase / f201_Tanker 1 each 0
total 116 30

CORRECTED 2026-08-26 — "the absence is structural" is WRONG

Everything below about capital ships being absent from the +0x130 method is an artifact of how I sourced the entities, not a property of the game.

I enumerated by taking moving() triples and testing +0x130. moving() requires a position to change between two samples. A capital ship holding station changes by exactly zero — which is why dropping the floor to lo=0 did not help either, and why parked ArrowHead wingmen were missing too. The whole-map scan failed for the same reason: the filter, not the range.

navigator.py already does it correctly, and says so in its header — it searches for the definition pointer and takes position = hit - 0x130, "which finds every entity whether it is moving or not". Running the existing mission_state.py scan on Stage 02 finds 149 entities, capital ships included:

UN_f101_TCAF_Acropolis    (  +5000,     +0,     +0)  hull= 25000.0  HP= 25000.0  frac=1.000
UN_e105_ADAN_Cruiser      ( -29500,   +500,  +3219)  hull= 30000.0  HP= 30000.0  frac=1.000
UN_e106_ADAN_Destroyer    ( -25500,  -1000,  +7041)  hull=  9150.0  HP= 10000.0  frac=0.915
UN_f106_TCAF_Destroyer    (  +1504,   -498,  -1937)  hull=  9579.2  HP= 10000.0  frac=0.958
UN_f001_..._Player        (  -6676,   +882,  +1004)  hull=  1500.0  HP=  1500.0  frac=1.000

hull(+0x154) == definition HP for 129 of 149, the rest already damaged.

So this specific claim is withdrawn outright: "an autopilot that must protect the Acropolis cannot locate it through the +0x130 method at all." It can. The Acropolis is right there at (+5000, 0, 0) with its hull readable and falling. INDEX.md had already recorded this — ACROPOLIS 25000 → 23038 over 240 s — which makes this the eighth time this session that the corpus held the answer before I measured.

What survives from below: the two enumerations really are different structures in different regions (0/116 vtable instances lie in the +0x130 region), and the vtable scan is still the one that sees stations and scenery. What does not survive is any claim that the +0x130 model is limited to four types.

The absence is structural, not a filter artifact

The obvious explanations were tested and both fail:

  • Speed band. moving() filters displacement to lo=1.0 … hi=4000.0 per 0.5 s, which would exclude a slow capital ship. Dropping the floor to 0 and the ceiling to 1e12 raised the count 30 → 59 and recovered the _Player — but still only 4 types. Capital ships never appear.
  • Address window. moving() defaults to 0xBD0000000xBE000000. Scanning the whole map with no speed floor gives 181 824 movers and still 4 types.

So the ten missing types do not have a definition pointer at position + 0x130 anywhere in memory that a moving-triple scan can reach. The +0x130 block is not the universal entity record — it belongs to a subset.

🟡 The subset is nearly "small craft": every type it does cover is a fighter, attacker or turret. But UN_f003_TCAF_ArrowHead is a TCAF fighter and reads 0, so "small craft" is not the rule. What actually distinguishes them is not established — plausibly an active-flight-model flag, with parked wingmen and capital ships handled by a different update path.

They live in different regions, which is why they are different structures

vtable instances : 0xbc372cc0 .. 0xbc9bc720   (116)
ENT_VA window    : 0xbd000000 .. 0xbe000000
instances inside that window : 0/116

Zero of the 116 vtable objects lie in the window where the +0x130 position blocks are found. This is independent confirmation of the earlier refutation: the vtable object and the position block are separate allocations in separate regions, not two views of one record.

Which list to use

  • Completeness — the INST_VTABLE scan. It sees all 14 types including stations, capital ships and the objective-critical Acropolis.
  • Motion state — the +0x130 blocks. They carry position, speed, orientation (pos0x70, stride 16) and hull (pos+0x154), but only for ~4 types.
  • An autopilot that must protect the Acropolis cannot locate it through the +0x130 method at all. That is a concrete limit on the existing approach.

Also confirmed

The exact 2× duplication reappeared: def-pointer hits come in pairs 0x1000 apart with byte-identical positions (0x011db3bd80 / 0x011db3cd80, both (-1654.7, -3992.7, 458.2), both moving 424.482). It is not page aliasing in gmem.MAP terms — aliased VAs share one file offset, and these differ by 0x1000 in the offset itself, so they are genuinely two copies.

Correction to the previous note: it said "at least one of the counts was un-deduplicated". That is wrong — entities2.py prints its count after ents = list(uniq.values()), so the 101 was already deduplicated. The gap was the stage change, not duplication.