BuildQueue::ProcessTurn 0x00890d50 disassembled to the next function start (Ghidra says 1230 bytes; the body is 1264 and its end lands inside the epilogue -- rule 17 again). Corrects the recorded prototype: the first stack argument is the StrategyServer S frame, not the system, so the build-completed event goes on the sim and the hull's turn stamp is Frame. THE HEADLINE IS A CORRECTION. Lane E2's open census leaf (shpt[0], one destroyer short on both reference pairs) cannot be closed by S11's build-queue sub-pass: measured over the corpus, both reference saves carry three empty BQ frames, every hbq false, and the one TurnCommands_v5 block empty. The order is created inside the turn by the AI -- PvSav shows 11,900 leaving the treasury before spine phase 0. The blocker moves to game/ai. Also: the per-class built counter has EXACTLY ONE writer in the image (image-wide byte scan); ShipRecords sized by enumeration; the ship/fleet birth chain end to end, correcting lane B5 -- FtFlg 0x400 is set by EVERY fleet born through the create path, not by retreat; one home fleet per system cached at ServerSystem+0x238; and a third indirection class vtable_map.py cannot see (the ship-borne wrapper is dispatched from a stack-built pointer table). 21 addresses in ghidra/addresses.d/lane-b6.json, validated to a scratch path against the merged set (1,077 entries, no duplicate names); two entries dropped as duplicates of lane B5's and recorded as agreements instead.
405 lines
24 KiB
Markdown
405 lines
24 KiB
Markdown
# Ship construction — the build queue, and where the missing destroyer actually comes from
|
||
|
||
Lane B6, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs.
|
||
|
||
**Method.** `Game::BuildQueue::ProcessTurn` was disassembled **byte for byte** with
|
||
`objdump -b binary -m i386 -M intel` over the raw image, from 0x00890d50 to the next function start.
|
||
No control-flow claim here comes from the decompiler. The ship/fleet birth chain (§4) was read the same
|
||
way by a delegated sweep; ReVa was used only for cross-reference lists. The corpus measurements in §2
|
||
and §5 come from `verify/save-reader/save_reader.py` over the eleven saves.
|
||
|
||
Addresses: `ghidra/addresses.d/lane-b6.json` (21 entries, validated against the merged set — 1,077
|
||
entries, no duplicate names). Engine side: `sots-engine` `wip/build`, `docs/B6-ship-construction.md`.
|
||
|
||
---
|
||
|
||
## 0. The headline, and it is a correction
|
||
|
||
Lane E2's falsified prediction left one census leaf open — `shpt[0]`, the archived destroyer count,
|
||
one higher than ours on **both** reference pairs — and concluded *"no phase we run builds a ship;
|
||
`S11`'s build-queue sub-pass is the declared input boundary."*
|
||
|
||
The first half is right. **The second half is wrong, and the build queue cannot close that leaf.**
|
||
|
||
> On both reference pairs there is **no build order anywhere in the game** at the moment the save is
|
||
> loaded. `turn1-state.sav` and `turn2-state.sav` each carry exactly three `BQ` frames — one per owned
|
||
> system, players 16, 32 and 576 — and **all three hold zero orders**. Every `hbq` in both files is
|
||
> `false`, so no ship-borne `BQ2` queue exists either. The only `TurnCommands_v5` block belongs to
|
||
> player 16 and is the byte-identical empty one; the other three custom-data blocks are `AIAgent`
|
||
> caches. A faithful build-queue pass has nothing to advance and closes nothing.
|
||
>
|
||
> **`shpt[0]` is blocked on AI order generation (roadmap §4.1, `game/ai`, Rung B), not on S11.**
|
||
|
||
The evidence that the order is created *inside* the turn is in the oracle file itself. On the
|
||
turn1→turn2 pair the owner is player 32, "Fane Lao", an AI, and the pair moves:
|
||
|
||
| leaf | turn1 | turn2 | what it means |
|
||
|---|---|---|---|
|
||
| `Player[32]/NumDes` | 5 | 6 | the AI **designed a hull during the turn** |
|
||
| `/Sim/DesignIDs[]` | 41 | 43 | ids 18 and **1712** appear |
|
||
| `/Sim/NMnx` | 106 | 109 | three ids issued: 1712 (design), 1728 (ship), 1744 (fleet) — counters 107, 108, 109 in that order |
|
||
| `/Sim/ShipIDs[]` | 15 | 16 | id 1728 |
|
||
| `/Sim/FleetIDs[]`, `/Sim/NumFlts` | 6 | 7 | id 1744, `Flt[1744 "Alpha Fleet"]` only-in-oracle |
|
||
| `Player[32]/FNG/FNGNum` | 0 | 1 | one fleet **name** generated |
|
||
| `Player[32]/Maint` | 0 | 500 | the new hull's upkeep |
|
||
| `Player[32]/ShipRecs/srb[0]` | 0 | 1 | the per-class **built** counter |
|
||
| `Player[32]/ShipRecs/srb[3]` | — | new | a per-design record appended |
|
||
| `Player[32]/PvSav` | 50,000 | **38,100** | 11,900 left the treasury **before phase 0** |
|
||
|
||
That last row is the one that settles it. `PvSav` is written by spine phase 0, which snapshots `Sav`
|
||
before anything else in the turn runs. The input file says `Sav = 50,000`; the output file's
|
||
previous-turn snapshot says 38,100. So 11,900 was spent *between the save and the first phase of the
|
||
turn* — the queue-time money deduction (lane O) of a build order that the input file does not
|
||
contain. The AI issues its orders, the queue drains them in S11, and the queue is empty again by the
|
||
time the next save is written.
|
||
|
||
`NMnx` 106 → 109 with the design allocated **first** also says the design did not exist before the
|
||
turn: it is not a pre-existing design being built, it is designed and built in one turn.
|
||
|
||
### 0.1 What this lane delivers instead
|
||
|
||
* the pass read byte for byte, which **independently confirms lane B4's `sim::ProcessBuildQueue`** and
|
||
finds **one divergence** in it (§3.3);
|
||
* the completion bookkeeping — the `ShipRecords` update — which nothing modelled (§3.4), and whose
|
||
per-class counter has **exactly one writer in the whole image**;
|
||
* the ship and fleet birth chain, end to end (§4), including a **correction to lane B5's `0x400`**;
|
||
* an oracle for the whole pass that the campaign already owned and had not noticed (§5): the
|
||
`zuul-turn16 → zuul-turn17` transition completes six orders and partially advances a seventh, and
|
||
the model reproduces it from a single point total per system;
|
||
* a **third indirection class** that `tools/vtable_map.py` cannot see (§6).
|
||
|
||
---
|
||
|
||
## 1. Where construction runs in the phase order
|
||
|
||
```
|
||
StrategyServer::ProcessTurn 0x007dc6c0
|
||
└ phase 11 per-system ServerSystem::ProcessTurn 0x007598e0 (no arguments)
|
||
└ ProcessBuildQueue 0x00752500
|
||
└ BuildQueue::ProcessTurn 0x00890d50 <-- the pass
|
||
├ (per completed order) StarShip creation + attach
|
||
└ (after the loop) the built-ships event poster 0x0088ca00
|
||
```
|
||
|
||
Inside the colony turn the order is: unowned infra decay → infra/pop bonus apply → independent pop
|
||
drift → stability → system bonus accrual → `ProcessPlague` → **`ProcessBuildQueue`** → imperial growth
|
||
→ civilian growth → resources → refuel-in-orbit → the countdown nibbles → `ProcessSlaves` →
|
||
`ProcessRebellion` → addiction. So construction completes **before** the turn's population growth and
|
||
**after** the plague pass, and it is inside the same per-system loop.
|
||
|
||
`ProcessBuildQueue` computes the system's output vector, writes `out[2]` through its out-parameter,
|
||
applies `out[11]`/`out[10]`, and hands `out[8]` — construction points already capped at the queue's
|
||
demand — to the pass. **The points are the pass's only real input, and they are the roadmap's item 1**
|
||
(the per-system output term). Everything below is about what happens once points arrive.
|
||
|
||
The second caller, `0x00789500`, is the construction-ship wrapper: it feeds the *ship's* own queue
|
||
(`ship+0x98`, gated on `hbq`) with the ship's `ConCap` (`ship+0x68`) as the point budget. Same
|
||
function, different queue.
|
||
|
||
---
|
||
|
||
## 2. Corpus coverage, stated before the model (rule 15)
|
||
|
||
Eleven saves. **Six carry no build order at all**; five do.
|
||
|
||
| save | frame | orders | where |
|
||
|---|---:|---:|---|
|
||
| `turn1-state.sav`, `turn2-state.sav`, `turn3-state.sav` | 1, 2, 3 | **0** | three `BQ` frames each, all empty |
|
||
| `human-turn2-orders.sav`, `human-turn3-noderoute.sav` | — | **0** | 20 `BQ` frames, all empty |
|
||
| `zuul-turn15-orders.sav` | 15 | 0 | five build orders sit **pending** in the command block, not yet in a queue |
|
||
| `zuul-turn5-species5.sav` | 5 | 4 | one queue, design 18 ×4 |
|
||
| `zuul-turn16-noderoute.sav` | 16 | 7 | sys 80 (player 32) ×4, sys 384 (player 16) ×3 |
|
||
| `zuul-turn17-orders2.sav`, `zuul-turn17-rollpending.sav` | 17 | 2 | sys 80 ×1, sys 384 ×1 |
|
||
| `zuul-turn23-fleet23.sav` | 23 | 1 | sys 384 ×1 |
|
||
|
||
`SAVE_FORMAT.md` §11's note *"count 0 in turn1..3"* was read campaign-wide as *"the build queue is
|
||
never exercised"*. It is exercised, in five files — just not in either reference pair.
|
||
|
||
**Never exercised anywhere in the corpus**, and therefore hypotheses: a design with a non-zero money
|
||
cost (`design+0xc0`), so the slot-9 money charge and its refusal path have never fired; a ship-borne
|
||
queue with an order in it (every `hbq` in every save is false); an order surviving a turn with
|
||
`conleft <= 0`; and any non-zero value of `srl`/`srk` (losses and kills) anywhere.
|
||
|
||
---
|
||
|
||
## 3. `Game::BuildQueue::ProcessTurn` — 0x00890d50 .. 0x00891240
|
||
|
||
`__thiscall`, `ret 8`. **Ghidra says 1230 bytes; the body is 1264** and Ghidra's end lands inside the
|
||
epilogue, before the security-cookie check (rule 17, sixth lane bitten).
|
||
|
||
### 3.1 The signature is not what `addresses.json` says
|
||
|
||
`addresses.json` carries `int (BuildQueue* this, ServerSystem* sys, int points)`. **The first stack
|
||
argument is not the system.** At the only real call site the pushed value is `[sys+0x10] - 4`
|
||
(0x0075257c `lea edi,[eax-0x4]`, `eax = [esi+0x10]`, `esi` = the `ServerSystem`, `ecx = [esi+0xa4]` =
|
||
the queue). That is the `StrategyServer` **`S`** frame — one word below the `S+4` frame lane B5's
|
||
`StrategyServer_CreateFleet` takes, which is exactly lane T §0's two-bases-four-bytes-apart pattern
|
||
showing up again. Call it `P`.
|
||
|
||
Two things the old prototype hides:
|
||
|
||
* the build-completed event is pushed onto a list at **`P+0x2b0`** (size at `P+0x2b4`), i.e. on the
|
||
**sim**, not on the system;
|
||
* the turn stamp written into the new hull at 0x00890e02 is **`P+0xc`**, which
|
||
`StrategyServer::Write` tags **`Frame`** — the turn number, not the system's anything.
|
||
|
||
### 3.2 The body
|
||
|
||
```
|
||
0x00890d88 if (points <= 0) goto epilogue ; RETURNS THE ARGUMENT, SWEEP AND ALL SKIPPED
|
||
0x00890d9a node = *(this->orders @ +0x10) ; MSVC circular std::list
|
||
0x00890da4 if (node == head) goto removal sweep
|
||
|
||
loop:
|
||
0x00890db5 if (node->conleft(+0x18) > points) ; cannot finish this turn
|
||
0x008910c4 node->conleft -= points; points = 0; BREAK ; <-- the pass STOPS
|
||
0x00890dbe design = node->+0x20
|
||
0x00890dc7 if (design->cost(+0xc0) > 0)
|
||
0x00890ddc if (!this->vft[9](P, (int64)cost)) goto next ; refusal SKIPS, does not stop
|
||
0x00890df0 ship = CreateShip(P, this->owner(+0xc), design) ; §4.1
|
||
0x00890e02 ship->tblt(+0xac) = P->Frame(+0xc)
|
||
0x00890e08 newShips.push_back(ship)
|
||
0x00890e20 this->vft[10](P, ship) ; §4.2 -- fleet routing
|
||
0x00890e22..0x00890e3e five per-ship refresh calls
|
||
0x00890e43 if (design->+0xb8 & 0x04000000) transfer carried population
|
||
0x00890e91 if (design->+0xb8 & 0x00080000) station/hab hook
|
||
0x00890ec1 this->OnShipBuilt(P, ship)
|
||
0x00890ed1 P->OnShipBuilt(this->owner, ship)
|
||
0x00890ef7 owner->ShipRecords.built[design->hullClass(+0x12c)]++ ; §3.4
|
||
0x00890efe..0x00890f61 the per-design record: find, append if absent, ++built
|
||
0x00890f64..0x00891004 build-completed event -> P's event list
|
||
0x00891006 points -= node->conleft; node->conleft = 0
|
||
next:
|
||
0x0089101c node = *node ; if (node != head) goto loop
|
||
|
||
0x0089102b if (!newShips.empty()) this->PostBuiltShipEvents(P, &newShips) ; 0x0088ca00
|
||
0x00891041 REMOVAL SWEEP: collect every node with conleft <= 0, unlink, delete, --count
|
||
0x00891208 return points
|
||
```
|
||
|
||
### 3.3 Independent confirmation of lane B4, and one divergence
|
||
|
||
Lane B4's `sim::ProcessBuildQueue` (`sots-engine` `game/sim/colony.cpp`) was written from a different
|
||
reading. Read against the bytes it is **right on every rule that the corpus can reach**: FIFO order,
|
||
the strict `conleft > points` stop, the money refusal that *continues* rather than stopping, the
|
||
removal sweep being a separate unconditional pass over the whole list rather than a delete-as-you-go,
|
||
and the leftover being the return value. That is an agreement worth recording, not a formality —
|
||
those are exactly the four rules a plausible reimplementation gets wrong.
|
||
|
||
**One divergence.** With `points <= 0` the original's entry test at 0x00890d88 branches to the
|
||
**epilogue**, so the removal sweep does not run either; the engine ran the sweep unconditionally.
|
||
Fixed on `wip/build`. **This is a labelled hypothesis, not a result**: an order can only reach
|
||
`conleft <= 0` inside the pass, and the same pass erases it before returning, so a queue never
|
||
*starts* a turn with one — unless a zero-construction-cost design is ever queued, which no corpus save
|
||
has done. The workload that would decide it: queue a design whose `con` is 0 into a system whose
|
||
construction output is 0 that turn.
|
||
|
||
### 3.4 The completion bookkeeping — `ShipRecords`
|
||
|
||
Two writes per completed hull, and **the first has exactly one writer in the executable**. An
|
||
image-wide byte scan for the indexed-increment form at that displacement (`ff 84 ?? b4 01 00 00`, all
|
||
executable sections, all SIB forms) returns **one site: 0x00890ef7**. So a hull that reaches the wire
|
||
with the per-class counter bumped came through the build queue and through nothing else — including
|
||
the encounter spawner of §4.4, which creates ships and touches no `ShipRecords` at all. That makes
|
||
`srb` a clean discriminator between player-built hulls and spawned ones.
|
||
|
||
Layout, **sized by enumeration rather than by what the code touches** (rule 5). The wire writes
|
||
`srnc` groups of `{srb, srl, srk, sri}` then `srbd` records of `{srd, src, srb, srl, sri}`. Three
|
||
classes × four arrays × 4 bytes from `0x1b4` lands exactly on `0x1e4`, which is the per-design vector
|
||
the very next instruction reads. So:
|
||
|
||
```
|
||
ServerPlayer + 0x1b0 Game::ShipRecords vptr
|
||
+ 0x1b4 int built [3] srb <- 0x00890ef7 increments this
|
||
+ 0x1c0 int lost [3] srl
|
||
+ 0x1cc int killed [3] srk
|
||
+ 0x1d8 int inService[3] sri
|
||
+ 0x1e4 std::vector<{int srd; int src; int srb; int srl; int sri}> stride 0x14
|
||
```
|
||
|
||
The per-design half is a linear scan for a record whose first word equals `*(design+0xa4)`, a
|
||
push_back when there is no hit (writing key, hull class, and three zeroes), then `++record->built`.
|
||
**`srd` is the design's object id** — measured, not inferred: the zuul corpus carries records keyed
|
||
608, 576, 114, 816, 18, 34, 130 against build orders naming exactly those `desID`s. The vector's order
|
||
is first-seen and is load-bearing for the wire.
|
||
|
||
Nothing here writes `srl`, `srk` or `sri`.
|
||
|
||
---
|
||
|
||
## 4. What a new ship and a new fleet look like at birth
|
||
|
||
Instruction-verified unless marked.
|
||
|
||
### 4.1 The ship
|
||
|
||
`0x004f41a0` is a 33-byte thunk: `id = IDMap_AllocateLocalID(this+4); return StarShip_Create(this+4, id, owner, design)`.
|
||
|
||
* **The id comes from lane B5's allocator, confirmed at the instruction level.** `0x0080f710` adds
|
||
0x80 to reach the IDMap sub-object and tail-jumps into `0x008b8b70`, which pushes the network node
|
||
index into `IDMap_AllocateID` `0x008b8ae0`: counter at `table + node*0x14 + 0x10`,
|
||
**pre-incremented** (`inc` at 0x008b8b16, value read after), **id 0 skipped** (0x008b8b20 branches
|
||
on the `inc`'s own flags and increments again), then `(counter << 4) | (node & 0xF)` at
|
||
0x008b8b38–0x008b8b3e, with a round-trip check. B5's model reproduced exactly.
|
||
* `StarShip_Create` `0x008656f0`: `operator new(0xb0)` — **`Game::StarShip` is 176 bytes** — ctor
|
||
`0x00861280`, then `IDMap_Insert` `0x008b9350`, and **that is where the object id lands, in
|
||
`ship+0x04`** (0x008b93b5).
|
||
* Birth values of the save-visible fields: `PlrID(+0x10)` = the queue's owner, `DesID(+0x14)` = the
|
||
design, `Range(+0x20)` / `Health(+0x24..+0x30)` / `ConCap(+0x68)` / `RefCap(+0x6c)` / `RepCap(+0x70)`
|
||
copied from the design's cached stats (`design+0xe8`, `+0xec..`, `+0xd0`, `+0xd8`, `+0xdc`),
|
||
`Plg(+0x48)` and `tsd(+0x60)` at `-1`, everything else zero, and **`FltID(+0x64)` NULL** — the fleet
|
||
link is made later. `hbq(+0x98)` gets a ship-borne `BuildQueue` when `design+0xb8 & 0x400000`;
|
||
`hsp/ppop(+0x9c/+0xa0)` get `Population` objects when `design+0xb8 & 0x4000000`.
|
||
* **`tblt(+0xac)` is born `-1` and the build queue overwrites it with `Frame`.** MEASURED against the
|
||
corpus: the six hulls the `zuul-turn16 → zuul-turn17` pair adds all carry `tblt` equal to the **new**
|
||
turn number, which independently confirms that `BeginProcessTurn`'s `Frame` increment happens
|
||
**before** the spine — a phase reading `Frame` mid-turn sees the turn it is producing, not the one it
|
||
started from.
|
||
|
||
### 4.2 The fleet: join, not create, is the normal case
|
||
|
||
`vft[10]` for `Game::SystemBuildQueue` is `0x00891240` — **which is the "next function start" that
|
||
bounds `ProcessTurn` itself**, and is why the fleet half of construction looked absent from the pass.
|
||
It routes the hull by design role bits (`+0xb8 & 0x80000`, `+0xbc & 4`, `+0xb8 & 0x800000`,
|
||
`+0xb8 & 0x400`, each to its own handler) and then falls through at 0x00891328 to
|
||
|
||
```
|
||
if (ship->FltID == 0) ServerSystem_AttachShipToHomeFleet(sys, ship) ; 0x0074f4d0
|
||
```
|
||
|
||
`ServerSystem_AttachShipToHomeFleet`: **if `sys->+0x238` is non-null the cached home fleet is reused**;
|
||
only when it is null is a fleet created — id from the same allocator (0x0074f500),
|
||
`StrategyServer_CreateFleet` (lane B5, 0x0085b340) at the system's position with a **NULL name
|
||
override**, the result cached into `sys->+0x238`, `FtFlg |= 0x20`, then `StarFleet_AddShip`.
|
||
|
||
So **one home fleet per system**. The reference pair's `NumFlts` 6 → 7 is a *first* build at that
|
||
system; a second turn of building there would add ships to fleet 1744 and create nothing.
|
||
|
||
`ServerSystem+0x238` is a new offset — not in `struct-recovery.md`'s table.
|
||
|
||
### 4.3 The fleet at birth, and a correction to lane B5
|
||
|
||
`StrategyServer_CreateFleet` `0x0085b340`: `operator new(0x120)` — **`Game::StarFleet` is 288 bytes** —
|
||
ctor `0x00706140`, id registered into the same map (so `fleet+0x04` = the id), `+0x10` host,
|
||
**`+0x58` = the owning `ServerPlayer` (`PID`)**, `+0x18..+0x20` and `+0x4c..+0x54` = the position, and
|
||
then
|
||
|
||
```
|
||
0x0085b454 push 1 ; push 0x400 ; call 0x006fe1a0 -> fleet->FtFlg(+0x10c) |= 0x400
|
||
```
|
||
|
||
> **Correction.** Lane B5 recorded the `0x400` on `FtFlg` as something the retreat pipeline sets. It
|
||
> is not retreat-specific: **every fleet born through `0x0085b340` gets it**, at 0x0085b460,
|
||
> unconditionally, before the caller has done anything. The build path then ORs `0x20` on top. The
|
||
> bit means "created at runtime", not "created by a retreat".
|
||
>
|
||
> And it remains a *different word from the design/census `0x400`* — that one is the
|
||
> defence-platform bit in a design's role flags (`game/design/hull.h` already says so), and a **third**
|
||
> `0x400` is a design role bit tested at 0x00891311 inside the routing ladder. Three unrelated
|
||
> `0x400`s on this one code path.
|
||
|
||
The constructor leaves `FtFlg` zero, `FtMS(+0x11c)` at `-1`, and everything else zero. **There is no
|
||
scalar ship count**: the wire's `NShips` is the length of the `vector<StarShip*>` at
|
||
`+0xa4/+0xa8/+0xac`. The 16-bit word at `+0xc0` is *not* a count — `StarFleet_AddShip` does
|
||
`+0xc0 += f(design->hullClass)` at 0x00706348, an accumulated tonnage.
|
||
|
||
`StarFleet_AddShip` `0x007062e0` is the join step: push into the vector, **`ship->FltID = fleet`**
|
||
(0x0070631b), OR the design's two role words into the fleet's aggregates (`+0xb8`, `+0xbc`), add the
|
||
tonnage, and recompute.
|
||
|
||
**The fleet name.** `StrategyServer_CreateFleet` asks the generator only when the name-override
|
||
argument is NULL, which the build path passes. The generator `0x00855f80` reads
|
||
`ServerPlayer+0x298` (`FNG`), divides the counter at `FNG+0x4` by the name-table size to pick a name
|
||
and a wrap count, and **increments `FNG+0x4` at 0x00855fe9** — so `FNGNum` is bumped once per
|
||
*generated* name. That is the `FNGNum` 0 → 1 in the reference pair, and it is why the fleet is called
|
||
"Alpha Fleet".
|
||
|
||
### 4.4 Ships are created outside the build queue too
|
||
|
||
`StarShip_Create` has exactly two roots in the image. The second is
|
||
`TradeManager_SpawnEncounterSquadron` `0x0088f070`: it draws ids from the same allocator, creates a
|
||
fleet through `StrategyServer_CreateFleet`, then loops ship creation + `StarFleet_AddShip`. It is
|
||
reached only from `ServerTradeManagerImpl` vtable slot 17, which picks its target with an RNG draw. It
|
||
does **not** touch `ModCount` and does **not** touch `ShipRecords` — which is what makes the per-class
|
||
`srb` counter a clean discriminator (§3.4). A functional reimplementation needs the ship factory
|
||
reachable from both roots.
|
||
|
||
---
|
||
|
||
## 5. The oracle the campaign already owned
|
||
|
||
`zuul-turn16-noderoute.sav` (frame 16) → `zuul-turn17-rollpending.sav` (frame 17) is a genuine
|
||
consecutive-turn pair of one game. `rollpending` is the clean turn-17 start state; `orders2` is the
|
||
same state with a command block on top.
|
||
|
||
| system | owner | frame 16 queue (`desID`:`conleft`) | frame 17 queue | points that reproduce it |
|
||
|---|---|---|---|---:|
|
||
| 384 | 16 | 608:1753, 576:1860, 576:1860 | 576:**1291** | **4182** |
|
||
| 80 | 32 | 114:959, 114:1889, 114:1889, 114:1889 *(+816:6974 appended during the turn)* | 816:**3818** | **9782** |
|
||
|
||
Six orders complete, one is partially advanced, and **the after-state is reproduced by a single
|
||
non-negative integer point total per system** — searched for exhaustively over 0..200,000, and the
|
||
solution is **unique** in both cases. That is the falsification test for the whole model at once: a
|
||
per-order point budget, a non-FIFO drain, or "skip and continue" instead of "stop at the first order
|
||
that cannot finish" each fail it.
|
||
|
||
The completions agree with the counters the same two files carry:
|
||
|
||
* player 16: class-0 `srb` 2 → 4; design 608's record 2 → 3; a **new** record for design 576 with 1;
|
||
* player 32: class-0 `srb` 53 → 57; design 114's record 18 → 22; design 816 unchanged at 6 because its
|
||
order did not finish;
|
||
* ship count 60 → 66; `NMnx` 159 → 165, one id per hull, **no new fleet** — every hull joined its
|
||
system's cached home fleet, exactly as §4.2 predicts.
|
||
|
||
The one fitted element is order 58's presence in the frame-16 before-state: the AI appended it during
|
||
the turn, and its `con` and `ordID` are taken from the frame-17 file. Everything else is measured on
|
||
both sides. The engine carries this as
|
||
`tests/game_sim/test_construction.cpp::test_corpus_pair`.
|
||
|
||
**What this oracle does not test:** the money charge (no order in the corpus has a money cost), the
|
||
ship and fleet birth of §4 (only that the counters move), and the point *derivation*, which is item 1.
|
||
|
||
---
|
||
|
||
## 6. Indirect edges — a third class `vtable_map.py` cannot see
|
||
|
||
Checked before concluding anything about reachability, as lane V2's result demands.
|
||
|
||
* `BuildQueue::ProcessTurn` 0x00890d50 is **in no vftable**. Its two direct callers (0x0075258a in
|
||
`ProcessBuildQueue`, 0x00789551 in the ship-borne wrapper) are therefore the complete set.
|
||
* The family's vtables: `Game::BuildQueue` 0x00a31328 is **abstract from slot 4 through slot 10** —
|
||
seven `purecall`s. `Game::SystemBuildQueue` 0x00a31358 binds slot 9 → 0x00809910 and slot 10 →
|
||
**0x00891240**; `Game::ShipBuildQueue` 0x00a31388 binds 0x0091e480 and 0x0081c000.
|
||
* **`FUN_00789500`, the ship-borne wrapper, has zero call sites AND is in no vtable.** Its address is
|
||
written into a **stack-built function-pointer table** by the ship-action dispatcher
|
||
(0x007b9c4b `mov eax,0x789500`, stored at 0x007b9c50), alongside five siblings —
|
||
`0x0078c930`, `0x00789590`, `0x00789620`, `0x00792420`, `0x00792450`.
|
||
|
||
That last one is a new finding for the campaign's reachability model. Lane V2 established that "no
|
||
direct caller" is a lower bound because of vtables; **"no direct caller and no vtable slot" is also a
|
||
lower bound**, because of locally-constructed dispatch tables. `tools/vtable_map.py` should grow an
|
||
"address stored into a stack or struct slot" edge class before any further "unreachable" claim is
|
||
made.
|
||
|
||
---
|
||
|
||
## 7. Open, and named
|
||
|
||
1. **What creates the AI's build order, and when in the turn.** The measured constraint is tight: the
|
||
money leaves the treasury *before* spine phase 0, and the design is allocated *before* the ship.
|
||
That points at command application, not at a spine phase. `game/ai` (lane AI2) owns this.
|
||
2. **The four non-default routing handlers** in `SystemBuildQueue_AttachBuiltShip` (`0x0088e7f0`,
|
||
`0x00751480`, `0x00751460`, `0x0074f460`) — predicates and call sites verified, bodies not read.
|
||
Every corpus completion takes the fall-through path.
|
||
3. **The money charge** `vft[9]`. Never fired: no design in the corpus has `design+0xc0 > 0`. The
|
||
refusal-continues rule is read from the bytes and has never been observed.
|
||
4. **The three message variants** the built-ships event poster (`0x0088ca00`) produces, keyed on
|
||
`ship+0x18 & 0x80000` / `& 0x800000` / neither. Structure read, format strings not resolved.
|
||
5. **`FUN_00845430`**, which builds the ship-borne queue at `ship+0x98`. Not opened; no save has ever
|
||
carried a non-empty one.
|
||
6. **The workload that would close `shpt[0]` without the AI**: a save with a build order already in a
|
||
system queue, plus the autosave the game writes from ending that turn. `zuul-turn16` is such a save
|
||
and the campaign does **not** hold its autosave — only the next turn's manual save, which also
|
||
carries the AI's actions. One VM140 session (load `zuul-turn16-noderoute`, End Turn, keep the
|
||
autosave) would turn §5's fitted point totals into a full before/after oracle for the pass, the
|
||
ship birth and the fleet birth at once.
|