TShn is refreshed to Frame for every (system, player) that satisfies ServerSystem::IsKnownTo 0x00746390 -- IsVisibleTo OR (2-bit sensor contact at ServerSystem+0x24 AND CCC_AdvSens). Not AFlags alone, which is why lane E3 could not fit it. The writer chain was confirmed live, frame by frame, from a hardware watchpoint on Spica's NVO node; the refresh runs twice per End Turn (driver phase 24 and combat-done phase 25). Also: the trade and spy containers read out of a live game for the first time (both empty, zero traps -- the workload confirmation two lanes lacked); Player.Status's predicate named as ReqCL != 0 && Elim == 0, with Species != 4 shown to be a corpus coincidence; and all ten command ModCount handlers named, two of them only reachable on a turn-1 workload. Corrects objects/layouts.md's ServerSystem PID@0x274 row and closes system-visibility-record.md 7. Runs: build w3tshn-bb81d3d-20260908T2007Z, configs shim.cfg.w3tshn / w3mod / w3control. Rule 19 control passed -- the armed run reproduced the determinism oracle byte for byte.
14 KiB
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) andwatchpoints-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 the0that the previousResumePlayingwrote.
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 at0x007bc25c: a player withReqCL == 0is skipped before the AI list is built, so it never gets a client and can never be the subject ofMarkPlayerTurnEnded.- The game's own definition of "everybody has ended their turn",
StrategyNetworkServer::AllPlayersTurnEnded 0x007cfd00(vftable slot 3), loop0x007cffb7..0x007d0002with the literal4loaded at0x007cffa3:
That is the predicate, written by the game, in one place.for (p : players) { if (!p->ReqCL(+0xfd)) continue; if ( p->Elim (+0xf8)) continue; if ( p->Status(+0x164) != 4) return false; }
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 fcomps 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.savplus 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.
Statuswas 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 theS+0x1e8encounter records that §2's phase-31 loop walks. That is data-dependent and cannot be read.ReqCLwas verified as the gate in two functions; it was not traced through every path that could create a client.