366 lines
22 KiB
Markdown
366 lines
22 KiB
Markdown
# The treaty-turn stamp, and where `Player.Status` really comes from
|
||
|
||
Lane T2, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs.
|
||
|
||
**Method.** Every claim about control flow below is read from the instruction stream with
|
||
`objdump -b binary -m i386 -M intel` over the raw image (PE section table mapping, call targets resolved
|
||
against `dumps/functions.json`, callers found with a whole-image byte scan for `E8/E9 rel32`). Nothing here
|
||
comes from the decompiler. Where a claim is an inference from save bytes rather than from instructions it
|
||
says so in the sentence that makes it.
|
||
|
||
This lane was briefed on `OnAllCombatDone_Tail` and went looking for the tail phase with the best
|
||
leaves-closed-per-effort. **The answer was not in the tail.** §5 says why, and lists what the tail's own
|
||
phases are actually blocked on. §1–§3 are what the search found instead.
|
||
|
||
---
|
||
|
||
## 0. The prediction, written before the run
|
||
|
||
Two models, both derived statically, both with a falsification section. Recorded here **before** anything
|
||
was built or measured, per rule 2.
|
||
|
||
### 0.1 H02 `StampTreatyTurns`
|
||
|
||
> Every turn, for every ordered pair of players `(A, B)` with a live treaty, `A`'s `DiplomacyStats` entry
|
||
> for `B` has the matching `last*` field stamped with the **current turn** (`Frame`), the entry being
|
||
> created on demand. Concretely, per player `A` in player-vector order, per player `B` in player-vector
|
||
> order, `B != A`:
|
||
>
|
||
> ```
|
||
> rel = 3 if A.plyrIdx == B.plyrIdx else
|
||
> 3 if (A.AL & (1 << B.plyrIdx)) else
|
||
> 2 if (A.NA & (1 << B.plyrIdx)) else
|
||
> 1 if (A.CF & (1 << B.plyrIdx)) else 0
|
||
> if rel == 0: continue
|
||
> e = the first entry of A.dipstats with e.other == B.PlayerID,
|
||
> else a NEW entry appended at the end with
|
||
> {other = B.PlayerID, lastnap = lastally = lastcf = -1, every other field 0}
|
||
> e.lastally = Frame if rel == 3
|
||
> e.lastnap = Frame if rel == 2
|
||
> e.lastcf = Frame if rel == 1
|
||
> ```
|
||
>
|
||
> **Predicted leaves closed: 26 on the reference pair (turn1 -> turn2), 14 on pair 2 (turn2 -> turn3).
|
||
> Predicted regressed: 0. Predicted RNG words: 0.**
|
||
|
||
The 26 are six players gaining a `dipstats` vector (2 entries for each of the two `Singularity` players,
|
||
4 rows each — the container leaf plus its entries — and 4 entries each for the four species-4 players,
|
||
5 rows each). The 14 on pair 2 are `lastnap`/`lastally` moving 2 -> 3 on the fourteen entries that already
|
||
exist.
|
||
|
||
**How this could be wrong, and the symptom of each way.**
|
||
|
||
1. *The stamp is not `Frame`.* `strategic-turn-internals.md` §5.2 says it is `ModCount`. If §5.2 is right
|
||
and I am wrong, the stamped value is 12 (pair 1) or 24 (pair 2), not 2 or 3 — **14 regressed leaves on
|
||
pair 2 and 26 wrong values on pair 1**, and the two are trivially distinguishable because `ModCount` and
|
||
`Frame` differ by an order of magnitude on both pairs.
|
||
2. *The relation mapping is inverted.* If `1 = ally, 2 = NAP, 3 = ceasefire` (also §5.2) rather than
|
||
`3 = ally, 2 = NAP, 1 = ceasefire`, the value lands in `lastcf` where the oracle has `lastally`:
|
||
**pair 1 closes ~12 of 26 and regresses none** (the created entries are still created, with the right
|
||
`other`, but two of their thirteen counters are wrong), and pair 2 **closes 0 and regresses 14**.
|
||
3. *The shift is by vector position, not `plyrIdx`.* Undetectable in this corpus — every save has
|
||
`plyrIdx == vector position`. The symptom would appear only on a save where a player was removed. Stated
|
||
as a limitation, not tested.
|
||
4. *Entries are ordered by something other than the stamping order.* Then the created vectors have the right
|
||
contents in the wrong order and **pair 1 closes the container leaf and regresses the entry leaves**.
|
||
5. *The phase is somewhere else in the turn and sees a different `Frame`.* If `Frame` were bumped after this
|
||
ran, the stamp would be `Frame - 1` — 1 and 2 instead of 2 and 3 — i.e. **0 closed, 26 wrong on pair 1**.
|
||
6. *Some other writer also touches these fields on a normal turn.* Then the closed count is short of 26/14
|
||
by whatever that writer moves, and the residual is named rather than netted.
|
||
|
||
### 0.2 `Player.Status` — evaluated, not written
|
||
|
||
> `Status` is set to **4** by `FUN_00821a40` (§3), which runs on the **End Turn submission** path, before
|
||
> the turn is processed — not between tail phase 31 and the autosave, which is where `backlog.md` item 6
|
||
> looks for it. Which players get it is a property of who submits an End Turn, and the only two saves in
|
||
> the corpus where the field is non-zero agree with `Species != 4`.
|
||
>
|
||
> **Predicted leaves: 4 on the reference pair, 0 on pair 2 — and NOT written**, because the gate is a
|
||
> two-observation inference and the campaign's own rule 20 says a constant fitted to two observations is not
|
||
> a constant.
|
||
|
||
---
|
||
|
||
## 1. `StampTreatyTurns` — 0x007898c0, and it is not in the tail
|
||
|
||
**One caller, whole-image byte scan for `E8/E9 rel32`:** `ApplyTurnCommands` 0x007b18b0 at **0x007b2461**
|
||
(+0xbb1), which is `strategic-turn-internals.md` §5.2's "right after the alliance diff". So the phase runs
|
||
**before** either turn driver, in the host's command-application step — the same bracket
|
||
`phase_catalog.h` already calls `Driver::Host`.
|
||
|
||
**Rule 17, a fifth time: that call site is 0x1c bytes past Ghidra's reported end of its own caller.**
|
||
Ghidra sizes `ApplyTurnCommands` at 2965 bytes, ending 0x007b2445; the real body runs to `ret 0x10` at
|
||
**0x007b248d**, 0x48 bytes further, and the call to 0x007898c0 sits inside that tail. A sweep clipped at
|
||
`fva + size` would have found **no caller at all** for this function and concluded it was dead. The
|
||
surrounding instructions corroborate the identification independently: the call is the last real statement
|
||
of `ApplyTurnCommands`, its three stack arguments are three locals at `[ebp-0x344]`, `[ebp-0x34c]`,
|
||
`[ebp-0x348]`, and it is immediately followed by `FUN_007acb50(S, <0x00a24f9c>)` — the function
|
||
`strategic-turn-internals.md` §5.2 already names as the one that "dumps the Alliances / Non Aggression /
|
||
Cease fires tables to the log".
|
||
|
||
`void __thiscall (StrategyServer* S, vector<uint>* allianceBroken, vector<uint>* napBroken,
|
||
vector<uint>* cfBroken)`, `ret 0xc`, 700 B. `this` is the **`S` frame** — `[edi+0x54]`/`[edi+0x58]` is the
|
||
`vector<ServerPlayer*> Players` and `[edi+0xc]` is `Frame`, both `S`-frame offsets lane T and lane Z
|
||
established.
|
||
|
||
### 1.1 Pass A, the stamp — 0x00789920..0x007899c4, read as instructions
|
||
|
||
```
|
||
007898fc mov eax,[edi+0x58] ; Players._Mylast
|
||
007899 02 mov ecx,[edi+0x54] ; ... _Myfirst
|
||
sar eax,2 ; nPlayers
|
||
loop i:
|
||
007d9920 mov ebx,[ecx+esi*4] ; A = Players[i]
|
||
loop j:
|
||
007d9930 mov esi,[ecx+edx*4] ; B = Players[j]
|
||
007d9933 cmp ebx,esi
|
||
007d9935 je 0x78999a ; A == B -> skip
|
||
007d9937 push esi ; mov ecx,ebx
|
||
007d993a call 0x80e050 ; rel = A->GetRelation(B)
|
||
007d993f dec eax ; je 0x78995a ; rel == 1 -> slot 8
|
||
007d9942 dec eax ; je 0x789951 ; rel == 2 -> slot 0
|
||
007d9945 dec eax ; jne 0x78999a ; rel != 3 -> skip
|
||
007d9948 mov [ebp-0x5c],4 ; rel == 3 -> slot 4
|
||
007d9961 lea ecx,[ebp-0x58] ; call 0x80e7b0 ; DiplomacyStats ctor on the stack
|
||
007d9969 push esi ; lea eax,[ebp-0x58] ; push eax
|
||
mov ecx,ebx ; call 0x8180e0 ; A->GetDipStat(&local, B)
|
||
007d997c mov eax,[edi+0xc] ; Frame
|
||
007d9986 mov WORD PTR [ebp+ecx*2-0x50],ax ; local.<slot> = (short)Frame
|
||
007d998e push esi ; lea edx,[ebp-0x58] ; push edx
|
||
mov ecx,ebx ; call 0x863950 ; A->SetDipStat(&local, B)
|
||
```
|
||
|
||
The stamped field is at `local + 8 + slot*2`, because the ctor's `this` is `[ebp-0x58]` and the store's base
|
||
is `[ebp-0x50]`. Against `objects/layouts.md`'s `Game::DiplomacyStats` (`+8 lastnap`, `+0x10 lastally`,
|
||
`+0x18 lastcf`), that gives:
|
||
|
||
| `GetRelation` | slot | field |
|
||
|---|---|---|
|
||
| 1 | 8 | `lastcf` |
|
||
| 2 | 0 | `lastnap` |
|
||
| 3 | 4 | `lastally` |
|
||
|
||
### 1.2 `GetRelation` 0x0080e050 — 33 bytes, fully resolved
|
||
|
||
```c
|
||
// 0x0080e050: thiscall, ret 4 -> tail-calls the cdecl 0x006d2050 with
|
||
// (this->PlyrIdx(+0x28), &this->Alliances(+0x168), other->PlyrIdx(+0x28))
|
||
int Relation(int myIdx, PlayerAlliances* a, int otherIdx) { // 0x006d2050, 58 bytes
|
||
if (myIdx == otherIdx) return 3;
|
||
uint32_t bit = 1u << otherIdx; // shl edx,cl -- x86 masks the count to 5 bits
|
||
if (a->AL(+0x04) & bit) return 3;
|
||
if (a->NA(+0x08) & bit) return 2;
|
||
return (a->CF(+0x0c) & bit) ? 1 : 0;
|
||
}
|
||
```
|
||
|
||
**So the relation codes are `3 = ally (and self)`, `2 = NAP`, `1 = ceasefire`, `0 = war`.**
|
||
`strategic-turn-internals.md` §5.2 has 1 and 3 the wrong way round; it labelled itself MEDIUM confidence on
|
||
exactly this mapping, and it was the half that was wrong. The corrected mapping is independently confirmed by
|
||
`combat-done-tail.md` §2A, which reads `Node::ResupplyAlliedFleets` as gated on `GetRelation == 3 (allied/self)`
|
||
and `UpdateDiplomacyStatsFromCombat` as gated on `GetRelation < 1` (war).
|
||
|
||
Two further notes on this function, both places a reimplementation goes quietly wrong:
|
||
|
||
* The shift count is the **`PlyrIdx` field** (`ServerPlayer+0x28`), *not* the player's position in the player
|
||
vector. This is the exact opposite of the alliance/shared-vision mask (`app/alliance.h` §1), which uses the
|
||
vector position and never loads the index field. Both are in the same subsystem and they disagree. No save
|
||
in the corpus separates them — `plyrIdx == vector position` on all eleven — so this is an
|
||
instruction-stream reading with no behavioural evidence behind it.
|
||
* `AL` is consulted **unconditionally**. It is *not* gated on `ALid != -1` the way the shared-vision mask's
|
||
alliance term is.
|
||
|
||
### 1.3 The three helpers
|
||
|
||
**`DiplomacyStats::DiplomacyStats()` 0x0080e7b0** (55 B): vptr `0x00a21430`, everything zeroed, then
|
||
`lastcf(+0x18) = lastnap(+8) = lastally(+0x10) = -1`. So a fresh entry is
|
||
`{other 0, lastnap -1, lastnapbty 0, bknnap 0, btynap 0, lastally -1, lastallybty 0, bknally 0, btyally 0,
|
||
lastcf -1, lastcfbty 0, bkncf 0, btycf 0, deadhome 0}`.
|
||
|
||
**`ServerPlayer::GetDipStat(out, other)` 0x008180e0** (294 B, `ret 8`): if `out == 0` return; re-initialise
|
||
`*out` to those defaults; `out->other = other->+0x4` (the handle id — the wire's `PlayerID`); if `other == 0`
|
||
return; then a **linear, first-match** scan of the `0x24`-stride vector at `ServerPlayer+0x230` for
|
||
`entry.other == GetId(other)` and, on a hit, copy the entry's thirteen `int16` fields into `out`.
|
||
|
||
**`ServerPlayer::SetDipStat(src, other)` 0x00863950** (328 B, `ret 8`): if `src == 0 || other == 0` return;
|
||
the same linear first-match scan; **on a miss, default-construct and `push_back`**, then set
|
||
`back().other = other->+0x4`; finally copy `src`'s thirteen `int16` fields into the entry and re-write
|
||
`other`. So a new entry is appended **at the end**, and the append order is the order in which pairs are
|
||
first stamped — i.e. the `(i, j)` double-loop order, which is player-vector order.
|
||
|
||
The `0x24` stride is read twice, once in each helper, from `0x38e38e39 / sar 3` over
|
||
`[p+0x234] - [p+0x230]`. That is `objects/layouts.md`'s `dipstats` vector at `ServerPlayer+0x230` and
|
||
`sizeof(Game::DiplomacyStats) == 0x24`, agreeing with the serializer's own enumeration.
|
||
|
||
### 1.4 The value is `Frame`, not `ModCount`
|
||
|
||
`mov eax,[edi+0xc]` with `edi = S`, and lane Z's `StrategyServer::Write` tags `S+0x8` as `ModCount` and
|
||
`S+0xc` as `Frame`. The save bytes settle it independently and are worth stating because they are the
|
||
cheaper check: across `turn1/2/3-state.sav` `ModCount` runs 0 -> 12 -> 24 while `Frame` runs 1 -> 2 -> 3, and
|
||
every stamped `last*` in the corpus is 2 or 3. **`strategic-turn-internals.md` §5.2's "set to the current
|
||
`ModCount`" is wrong**; it is the turn.
|
||
|
||
`Frame` is already the *new* turn when this runs: `StrategyServer::BeginProcessTurn` 0x007d990a does
|
||
`inc [esi+0xc]` and the host order is `BeginProcessTurn -> ApplyTurnCommands -> ... -> ProcessTurn`.
|
||
|
||
### 1.5 Pass B is the betrayal counter, and it needs a command stream
|
||
|
||
0x007899ca onward is a second double loop, gated on `GetRelation(A,B) <= 0` (`jg` skips), that indexes the
|
||
three `vector<uint32>` arguments by the inner loop index — with a size check against `nPlayers` that
|
||
substitutes a zero local when the vector is the wrong length. Those three vectors are the alliance-broken /
|
||
NAP-broken / CF-broken masks the alliance diff in `ApplyTurnCommands` builds from the turn's `SNMUpdate`
|
||
commands. **With no commands they are empty, the size check fails, the zero local is used, and pass B is a
|
||
no-op.** It is not modelled: the standalone has no command stream, and a betrayal cannot occur without one.
|
||
|
||
---
|
||
|
||
## 2. What the save bytes say, and they say the same thing
|
||
|
||
`dipstats` across the reference family, every non-zero field shown:
|
||
|
||
| save | `Frame` | `ModCount` | P496 | P512 | P528 / P544 / P560 / P576 |
|
||
|---|---|---|---|---|---|
|
||
| turn1-state | 1 | 0 | — | — | — (all six vectors empty) |
|
||
| turn2-state | 2 | 12 | `{other 512, lastally 2}` | `{other 496, lastally 2}` | three entries each, `lastnap 2`, `other` = the other three in vector order |
|
||
| turn3-state | 3 | 24 | `lastally 3` | `lastally 3` | `lastnap 3` |
|
||
|
||
Every created entry carries `lastnap = -1, lastcf = -1` beside the stamped `lastally`, or `lastally = -1,
|
||
lastcf = -1` beside the stamped `lastnap` — which is the 0x0080e7b0 constructor, byte for byte. The two
|
||
`Singularity` players are allied to each other (relation 3); the four species-4 players are mutually
|
||
non-aggressive (relation 2); the two real empires have no treaty with anyone and their vectors stay empty on
|
||
all three turns. Nothing else in the vector ever moves in this corpus, so `lastnapbty`, `bkn*`, `bty*` and
|
||
`deadhome` are **unexercised** and are hypotheses, per rule 6.
|
||
|
||
---
|
||
|
||
## 3. `Player.Status` — the writer, and a correction to the roadmap
|
||
|
||
`Status` is `ServerPlayer+0x164` (`objects/layouts.json` grades it `off_abs 356`). A whole-image scan for
|
||
`C7 8x 64 01 00 00 imm32` — a `mov dword [reg+0x164], imm` — finds every immediate store to that
|
||
displacement in the image. Three matter:
|
||
|
||
| VA | value | containing function |
|
||
|---|---|---|
|
||
| 0x007dcc8a | **1** | `ProcessTurn` +0x5ca, inside a `0x44`-stride loop (the encounter-member stride), gated on two bytes at `member+0xf9`/`+0xfa` |
|
||
| 0x00821a6b | **4** | `FUN_00821a40`, 60 B |
|
||
| 0x007ddd41 | **0** | `ResumePlaying` 0x007ddc90 +0xb1 |
|
||
|
||
`FUN_00821a40(this, playerId)`, `ret 4`, in full:
|
||
|
||
```
|
||
p = HandleMap::Resolve(this + 0x80, playerId) ; sub ecx,0xffffff80 is add ecx,0x80
|
||
if (!p) { Log(2, <0x00a2fb30>, playerId); return false; }
|
||
p->Status(+0x164) = 4
|
||
return true
|
||
```
|
||
|
||
Its three callers, from the same byte scan: `EndTurn` 0x00783be0 (+0x70), `EndTurnForced` 0x00783d30
|
||
(+0x7b), `OnPlayerEndTurn` 0x007d9af0 (+0x35). In `EndTurn` the id passed is `this->+0x148`, the client's own
|
||
player id, and the call is immediately followed by `[this+0x15c] = 1` and `[this+0x4c8] = 1` and a
|
||
`RaiseEvent(0x21)` — this is the **End Turn submission** path, which runs before the turn is processed.
|
||
|
||
**This corrects `backlog.md` item 6.** The roadmap has `Player.Status` blocked on "the writer between tail
|
||
31 and the autosave — watchpoint". There is no such writer. `Status = 4` is set when a player submits an End
|
||
Turn; `ProcessTurn`'s encounter loop can set 1; `ResumePlaying` sets 0 on load, which is exactly the
|
||
"`Status` resetting 4 -> 0 on load" that `determinism-oracle.md` recorded. Item 6 does not need a VM
|
||
watchpoint; it needs one predicate — *which players submit an End Turn* — and that is a question about the
|
||
client/host controller layer, not about the turn.
|
||
|
||
What the corpus can say about that predicate, and it is not much: of the eleven saves, **only
|
||
`turn2-state.sav` and `turn3-state.sav` carry a non-zero `Status` at all** (every other save was written
|
||
through a load, and `ResumePlaying` had zeroed it). On those two, the four players with `Status = 4` are
|
||
exactly the four with `Species != 4`, and the four with `Status = 0` are exactly the four species-4
|
||
monster/neutral players. That is 8 observations on 2 saves of a 1-bit predicate, on a field that no other
|
||
save exercises. Rule 20 applies. **Evaluated, reported, not written.**
|
||
|
||
The cheap way to settle it is not a watchpoint either: an entry probe on 0x00821a40 recording its `playerId`
|
||
argument over one End Turn names the set directly, and a probe on `OnPlayerEndTurn` separates "the host ends
|
||
the AI's turn" from "the AI player never ends a turn".
|
||
|
||
---
|
||
|
||
## 3A. The measurement
|
||
|
||
`sots-engine` host phase **H02 `StampTreatyTurns`** (`src/app/treaty.{h,cpp}`), run through
|
||
`tools/standalone_report.py`. `closed` and `regressed` are separate columns and are never netted.
|
||
|
||
`main` moved twice while this lane ran (C3's output turn path, then B6's build queue), so the branch was
|
||
merged onto `main` at **`2fd0852`** and everything below re-measured there — both columns on the same tree,
|
||
the same host and the same tool. Rebuilding `main` before measuring it is not a formality: the checked-in
|
||
`build-host` was stale and reported `closed 5` on the reference pair, which would have made this table look
|
||
four times better than it is.
|
||
|
||
| pair | baseline | main `2fd0852` | with H02 | closed | regressed |
|
||
|---|---:|---:|---:|---:|---:|
|
||
| `turn1-state -> turn2-state` (the reference End Turn) | 209 | 157 | **131** | 52 -> **78** | **0** |
|
||
| `turn2-state -> turn3-state` (real End Turn) | 108 | 86 | **72** | 22 -> **36** | **0** |
|
||
| `human-turn2-orders -> human-turn3-noderoute` | 375 | 311 | **299** | 64 -> **76** | **0** |
|
||
| `zuul-turn15-orders -> zuul-turn16-noderoute` | 282 | 264 | **252** | 18 -> **30** | **0** |
|
||
| `zuul-turn16-noderoute -> zuul-turn17-orders2` | 345 | 328 | **316** | 17 -> **29** | **0** |
|
||
|
||
**+26 and +14 on the two reference pairs, exactly the predicted numbers, and +12 on each of three pairs the
|
||
model was never fitted to** — a different game, species 5, turns 2, 15 and 16. Twelve is precisely the number
|
||
of ordered treaty pairs each of those saves holds. Total **76 leaves closed, 0 regressed**, 0 RNG words.
|
||
|
||
The five deltas are **identical against `0f1c007` and against `2fd0852`** — measured on both — so this phase
|
||
is orthogonal to C3's and B6's work and neither of them is closing any of the same leaves.
|
||
|
||
`zuul-turn17-orders2 -> zuul-turn23-fleet23` is a six-turn gap rather than a pair; it reports `closed 0,
|
||
regressed 1` **both before and after**, so that one regression is not this phase's and is untouched by it.
|
||
|
||
The measurement was taken on the WSL host with a WSL-built `sots_turn`, and the `before` column was taken
|
||
with the same tool on the same host from a freshly rebuilt `main`. `verify/results/standalone/report.txt`
|
||
and `status.json` are **deliberately not committed by this lane**: lane C3 published its own numbers into
|
||
those two files minutes before this run, and regenerating them from a branch that does not carry C3's work
|
||
would silently replace a better result with a worse one. They are the integrator's to regenerate after the
|
||
merge.
|
||
|
||
All eleven corpus saves run clean end to end. On ten of them the phase creates **zero** entries and only
|
||
re-stamps, which is the check that matters: the entry set the rule derives is already the entry set the
|
||
game wrote. Only `turn1-state.sav` creates any (14), and that is the save on which no turn has ever run.
|
||
|
||
**What this did not cover, stated as loudly as the divergences.** The cease-fire arm has never executed —
|
||
no save in the corpus contains a cease-fire — so `Relation::CeaseFire` and the `lastcf` store are held by a
|
||
unit test and by the instruction stream, and by nothing else. The betrayal counters (`lastnapbty`, `bkn*`,
|
||
`bty*`) and `deadhome` are likewise untouched by any save; pass B is not modelled at all. Nine of the eleven
|
||
saves have exactly one relation kind (NAP) and eight players, so the corpus tests one shape hard rather than
|
||
many shapes at all. And `plyrIdx == vector position` on every save, so the index-vs-position reading of §1.2
|
||
has no behavioural evidence behind it whatsoever.
|
||
|
||
---
|
||
|
||
## 4. Two names that should be in Ghidra
|
||
|
||
| VA | proposed name | evidence |
|
||
|---|---|---|
|
||
| 0x007898c0 | `StrategyServer::StampTreatyTurns` | §1.1 |
|
||
| 0x006d2050 | `PlayerAlliances::Relation` | §1.2 |
|
||
| 0x0080e050 | `ServerPlayer::GetRelation` | §1.2 |
|
||
| 0x0080e7b0 | `Game::DiplomacyStats::DiplomacyStats` | §1.3 |
|
||
| 0x008180e0 | `ServerPlayer::GetDipStat` | §1.3 |
|
||
| 0x00863950 | `ServerPlayer::SetDipStat` | §1.3 |
|
||
| 0x00821a40 | `StrategyServer::MarkPlayerTurnEnded` | §3 |
|
||
|
||
---
|
||
|
||
## 5. Why the answer was not in the tail
|
||
|
||
The lane was briefed on the post-combat tail and ranked its phases by leaves-closed-per-effort against the
|
||
standalone's current 158-leaf residual on the reference pair. Every tail phase that moves a leaf in this
|
||
corpus is blocked on something another lane owns, or on a blind spot:
|
||
|
||
| tail phase | leaves it would move (pair 1 / pair 2) | why not now |
|
||
|---|---|---|
|
||
| T24 maintenance + research bonus + `ShipRecs` | 9 / 5 | `Maint` moves 0 -> 500 -> 1000 in lockstep with one ship completing per turn, and every `ShipRecs` leaf is a **new** ship's record. Blocked on ship construction, which is lane B6's |
|
||
| T31 `UpdateBankruptcyLimits` | 4 / 4 | already `Blocked` in the phase table, on `ComputeOutput`'s turn path (lane C3) |
|
||
| T36 `FinalizeTurnRecords` | 24 / 8 | already `Blocked`, on the budget and ship construction |
|
||
| T02 first contact, T30 comms mask | 0 / 0 | `HasEnc`/`HasDiscCl` and `ServerPlayer+0x198` move no leaf in this corpus |
|
||
| T05 `UpdateDiplomacyStatsFromCombat` | 0 / 0 | writes `deadhome` and `bty*`; **no battle occurs on either pair** — every encounter result seen has `res->+0x4 != 0` (lane Z). A path no save exercises is a hypothesis |
|
||
| T11 node-line decay | ? | draws RNG; the corpus turn cost is measured at 0 words from this phase, so no line expires |
|
||
| T08 / T20 / T26 script hooks, T23 / T33 trade vtables | 8 / 2 (`SvSctOb`) | the `SVScriptObject` encounter state (`EncObj[*]/Hives`, `did`, `didc`, `ini`, `CDiff`) is the tail's tier-4 blind spot: eleven virtual slots, none identified |
|
||
|
||
The remaining large clusters — `TShn` (10/10) and `rcex` (6/6) — are lane E3's named residual with a
|
||
watchpoint already specified, and `Events` (6/6) needs the whole turn's event inventory, not one phase.
|
||
|
||
So the tail's own ranked list is, right now, correct and unactionable: it is waiting on C3, B6 and a
|
||
vtable sweep. The two items above are what a lane holding no VM and no data-file dependency can close today.
|