# The trade/spy containers measured, the `Player.Status` predicate named, and the last four `ModCount` handlers - **Type:** control-flow (live measurement + instruction stream) - **Status:** **verified**, with one part explicitly labelled as an absence rather than a result - **Confidence:** high. The container counts are a live reading; the three predicates are byte-decoded, not inferred. - **Owner / date:** lane W3 · 2026-09-08 - **Companion to:** `nvo-tshn-visible-owner.md` (same run, same build) and `watchpoints-modcount-status.md` (lane W2, whose §5 and §8 this closes) --- ## 1. The trade-route and spy-program containers — measured, and both empty Two lanes have failed to build a trade/spy workload, and lane W2 left the right instrument for the next attempt: both containers are one add from the arming point. This lane armed them. **At `ApplyAllTurnCommands` entry on `ref-turn2.sav`, live:** ``` watch: trade routes -- manager=0x0e4cdd38 (S+4+0x154) vector@mgr+0x3c first=0x00000000 last=0x00000000 end=0x00000000 count=0 watch: spy programs -- manager=0x0e512f68 (S+4+0x158) vector@mgr+0x10 first=0x00000000 last=0x00000000 end=0x00000000 count=0 ``` Both managers exist and are non-null; both vectors are **entirely unallocated** — `_Myfirst`, `_Mylast` and `_Myend` are all zero, which is a default-constructed `std::vector` that has never held an element, not an emptied one. Watchpoints on both `_Mylast` words took **zero traps** across the whole End Turn. **Why that is worth writing down rather than a shrug.** The reason two lanes could not build this workload was never that the click path is long; it is that nobody could tell whether an attempt had *taken*. `ServerTradeManagerImpl` and `ServerSpyManager` are both reachable from the `S` frame the existing detour already holds, so the confirmation costs two lines of arithmetic and one log line. The next lane to spend twenty turns on trade stations can now check after every turn whether the container grew, instead of playing blind and reading the save afterwards. **What this does not say (rule 20).** It does not say the containers are never filled. It says: on this save, in this turn, they were empty and nothing grew them. That is the *absence of the workload*, measured — which is exactly what a workload confirmation is for, and it is the first time either container has been read out of a live game. **What is still missing for spies specifically.** The plan was that if the spy vector ever grew, the trapped return addresses would name the UI that creates a spy program — the question no lane has answered. It did not grow, so that question is **still open**, and the instrument that would answer it is now written and proven to arm. The next step is not more reading: it is one game in which a spy program is created by any means, with `watch.mode=tshn` armed. This also bears on the RNG ledger. Lane V proved four tail callees can draw RNG and lane H's predecessor showed they are entered every turn and gated inside on empty containers (rule 20's worked example). This run confirms the containers are empty at the level of the vector's own pointers, so the gating is not a mis-read of a partially-initialised object. If either container ever fills, the ledger and the standalone's generator model both need extending — and the trigger for that work is now detectable. ## 2. `Player.Status` — the predicate is `ReqCL`, and `Species != 4` is a corpus coincidence Lane W2 found the missing writer (`OnMessage + 0xa15`). What was open was *which players get which value*. Only two corpus saves carry a non-zero `Status`, and there `Species != 4`, `!(NPC && !RebAI)` and "has a badge string" all fit the same 8 observations — a 1-bit predicate against nine saves that never exercise it (rule 20). The code separates them. **Answer:** at the moment the post-turn autosave is written, > `Status == 4` ⟺ `ReqCL(+0xfd) != 0 && Elim(+0xf8) == 0`. > Every other player still carries the `0` that the previous `ResumePlaying` wrote. **No species field is read on any path that writes or reads `Status`.** Neither is `NPC(+0xfb)` or `RebAI(+0xfc)`. The four writers, decoded: | writer | value | gate | |---|---|---| | `MarkPlayerTurnEnded 0x00821a40`, store `0x00821a6b` | 4 | **none** — one call per `SNMPlayerEndTurn` message, the handle taken straight off the wire by `OnPlayerEndTurn 0x007d9af0` at `0x007d9b25`. The field tests at `0x007d9b5d`/`0x007d9ba2`/`0x007d9bb0` all follow the store and only decide whether to raise a "waiting on X" event | | `ProcessTurn + 0x5ca`, store `0x007dcc8a` | 1 | `if (p->IsAI(+0xf9) == 0 \|\| p->+0xfa != 0)`, decoded at `0x007dcc78`, over the encounter records at `S+0x1e8` (0x74-byte outer records, 0x44-byte inner entries whose first dword is the `ServerPlayer*`) | | `OnMessage + 0xa15`, store ends `0x00785055` | 4 | **none** — the `Game::SNMSetPlayerStatus` handler (vftable `0x00a2d718`, `{vptr, +4 player INDEX, +8 status}`) writes the value verbatim off the wire. The *sender* is `StrategyNetworkServer::Update 0x007cda40`, which computes `2*(flag != 0) + 2` at `0x007cdcf0` — 4 when the player has nothing pending, 2 when it does — after gating on `Status == 1` at `0x007cdc0b` | | `ResumePlaying + 0xb1`, store `0x007ddd41` | 0 | a loop over **all** players, `0x007ddd18..0x007ddd55`, gated only on `Elim(+0xf8) == 0` | So `Status == 4` marks "this player's End Turn arrived", and the population is "players that own a `StrategyClient`". `ReqCL` is the field that decides that, in two places: - `StrategyNetworkServer::SetupAIPlayers 0x007bc1c0`, gate at `0x007bc25c`: a player with `ReqCL == 0` is skipped before the AI list is built, so it never gets a client and can never be the subject of `MarkPlayerTurnEnded`. - **The game's own definition of "everybody has ended their turn"**, `StrategyNetworkServer::AllPlayersTurnEnded 0x007cfd00` (vftable slot 3), loop `0x007cffb7..0x007d0002` with the literal `4` loaded at `0x007cffa3`: ```c for (p : players) { if (!p->ReqCL(+0xfd)) continue; if ( p->Elim (+0xf8)) continue; if ( p->Status(+0x164) != 4) return false; } ``` That is the predicate, written by the game, in one place. **Why the four special players have `ReqCL == 0`:** `FUN_00797b80`, the script binding that creates them, calls the script `CreatePlayer 0x0057b6c0` with the literal name `"_NPC"` (`0x009f94e0`) and `NPC = 1`, then clears `[esi+0xfd]` explicitly at `0x00797bcb`. **Why the corpus cannot see this:** `ReqCL` is serialised (tag `"ReqCL"`), and `ReqCL == (Species != 4)` holds in all 88 player records of all 11 saves. Two independent-looking checks with one hidden shared assumption is one check (rule 8) — here the assumption is that this game's four NPC slots are the only players with `ReqCL == 0`, which is true of the corpus and is not the rule. This also **corrects `treaty-turn-stamp.md` §3 further than lane W2 did**: `MarkPlayerTurnEnded` is not "the only writer of `Player.Status = 4` in the image" by three more sites, all client-side and all acting on `client->+0x150`: `0x007773be` (writes 4 at player/client bind time), `CancelEndTurn + 0x4a` `0x0078573a` and `0x007774ee` (both write 0). ## 3. The four unnamed `ModCount` handler EIPs — all four named Lane W2 measured exactly 12 `ModCount` bumps per End Turn on `ref-turn2` and could attribute eight of the ten command handlers. The four it recorded as `StrategySim_ModCountBump_unresolved_*` are: | trap EIP | function | name | evidence | |---|---|---|---| | `0x00821a87` (**×4/turn**) | `0x00821a80` | **`StrategyServer::OnCommand_SetResearchRate`** | `"OnCommand: Cannot set research rate for player %d(id). Player not found"` (`0xa2fbb0`), `"OnCommand: Research rate %f constrained to %f for %s."` (`0xa2fb78`); body sets `player->ResRate(+0xbc)` = the argument clamped to `[0,1]` by two `fcomp`s against `0x9e1ef0` / `0x9e1e68` | | `0x0086c3e9` | `0x0086c3e0` | **`StrategyServer::OnCommand_SetSystemRates`** | `"OnCommand: Unable to set system rates for %d(id). System not found."` (`0xa32448`), `"OnCommand: Player setting system rates for system he does not own."` (`0xa32400`) | | `0x0088bf01` | `0x0088bed0` | **`StrategyServer::OnCommand_TransferShips`** | `"OnCommand: Cannot transfer ships. Player %d(id) not found."` (`0xa33248`), `"TransferShips: Fleet flagged as empty, but still has ships."` | | `0x0084946e` | `0x00849460` | **`StrategyServer::OnCommand_AddBuildOrder`** | no string of its own; identified from its callers — the Build Screen `0x00656540` → `0x00762fd0` (`"BuildQueue::DoAddOrder(): Requested design %s was not found…"`, `"Build Screen: Cannot build %s."`) and the command replay at `0x0089009f`. Body: pick `target->+0xa4` or `+0x98` by `cmd[0]`, validate with `0x0083ce50`, then `BuildQueue::AddOrder 0x00845260` push_backs onto the list at `queue+0x10` | **Why `SetResearchRate` fires four times.** The reference game has four players with a client (indices 0–3 — the two humans and the two rebel-AI empires) and four `_NPC` pseudo-players without one. Each client sends one research-rate command per turn. That is the same count, and the same population, as §2's `ReqCL` predicate — the two results are the same fact seen from two sides, and neither was derived from the other. **W2's caveat survives and should be repeated: 12 is not a constant.** It is this save's command count. `turn1→turn2→turn3` moves `ModCount` 0 → 12 → 24 in this game; `human-turn2-orders.sav` sits at 25 on frame 2; the Zuul line runs 63 @ f5 → 210 @ f15 → 412 @ f23, i.e. ~15–25 per turn. What generalises is the structure — two driver bumps plus one per applied command — and now, with all ten handlers named, the per-command half of that is enumerable rather than approximate. ### 3.1 A second turn, a different command set, and two more handlers (lane AI4's probe) Lane AI4 committed a prediction for `turn1-state.sav` before this run: again exactly 12 bumps, but out of a *different* command set, whose trap multiset **contains EIPs never seen on turn 2 and omits three that were**. That is a much sharper test than "does the number match 12" — a wrong model fails visibly instead of landing on 12 by coincidence. The same build, one config key changed (`watch.mode=tshn` → `modcount`), one save swapped. **Result: 12 traps on `S+0x8`, values contiguous 1 → 12** (this save starts at `ModCount = 0`): | Ghidra VA | × | handler | on turn 2? | |---|---|---|---| | `0x0086c3e9` | 1 | `OnCommand_SetSystemRates` | yes, ×1 | | `0x00821a87` | **4** | `OnCommand_SetResearchRate` | yes, ×4 | | **`0x0088fe0d`** | **3** | **`OnCommand_SetResearchProject`** — a *second* bump **inlined** into `ApplyTurnCommandBatch` at `+0x45a`, distinct from lane W2's inlined site `0x0089046c`. String `"OnCommand: Cannot set research project for player %d(id). Player not found."` (`0xa30ed0`) | **never seen** | | **`0x0088291d`** | **1** | **`StrategyServer::OnCommand_CreateDesign`** `0x00882910`. Strings `"OnCommand: Cannot create design for player %d(id), player does not exist."` (`0xa32c40`), `"OnCommand: Unable to create design. ID not specified."` (`0xa32c04`) | **never seen** | | `0x0084946e` | 1 | `OnCommand_AddBuildOrder` | yes, ×1 | | `0x007dc6f3` | 1 | `ProcessTurn`, first instruction (driver) | yes | | `0x007d92cd` | 1 | `OnAllCombatDone_Tail + 0x2a` (driver) | yes | **Absent, and predicted absent:** `0x0089046f` (×2 on turn 2, the AI fleet order), `0x008657ad` (fleet route) and `0x0088bf01` (transfer ships). No fleet moved on turn 1. **AI4's named falsifier is answered: `0x0088291a` DOES fire, once.** So command list 1 emits a design **command**, not merely a design object that the build order names — the engine has to implement the command. **A cross-check that neither model was fitted to.** Four rate-gate bumps and **three** research- project bumps. The three are the AI empires; the human player set a rate but picked no project, which is exactly why the reference game's map banner reads *"No Research Project Assigned"* in the turn-2 screenshot. And the four-vs-three split is the same population §2 derives from `ReqCL`: four players have a client and submit a command block, the four Species-4 pseudo-players submit none. Lane AI4 reached that population by counting bumps; §2 reached it by decoding `AllPlayersTurnEnded`. Neither was derived from the other. **Also confirmed independently this lane:** `ref-turn2.sav` on the VM and `verify/results/saves/turn2-state.sav` in the repo are the same 66,739 bytes with sha256 `ab4ac2d7e2977260…`. Lane AI4's correction stands, and it means the `TShn` measurement in `nvo-tshn-visible-owner.md` was taken on the standalone's own reference pair. **Coverage caveat for this second run.** It has **no oracle control of its own** — the autosaves it produced are a turn-1 workload for which no historical hash exists. What it rests on is that the binary is bit-identical to run 1 (`w3tshn-bb81d3d-20260908T2007Z`), whose armed run *did* reproduce the oracle byte for byte, and that the two configs differ only in `watch.mode`. The canary self-test passed in both. **Not determined:** which build queue `cmd[0] == 1` selects. `Game::ServerSystem` has exactly one `BQ` pointer (`+0xa4`), which fits `cmd[0] == 0`, but the handle map's value type is a base subobject whose offset differs per class (the design handle gets a `-0xa0` adjust at `0x008494ca`), so the frame those two offsets are measured in was not pinned down. ## 4. Coverage — what this run did not establish (rule 15) - **One save, one game, one turn.** Everything above is `ref-turn2.sav` plus the instruction stream. - **The containers are empty, so nothing about their behaviour when full was observed.** Not one element of either has ever been seen in this campaign, live or on the wire. - **`Status` was not watched in this run at all** — §2 is entirely instruction-stream work, resting on lane W2's live ordering. The one thing a run could still add is *membership*: whether players 4–7 ever appear in the `S+0x1e8` encounter records that §2's phase-31 loop walks. That is data-dependent and cannot be read. - **`ReqCL` was verified as the gate in two functions**; it was not traced through every path that could create a client.