W3: NVO.TShn's writer trapped live, its gate named, and 158/158 on the corpus

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.
This commit is contained in:
alex 2026-09-08 16:41:09 -04:00
parent 4ce7a1e0a7
commit 471d6cb973
8 changed files with 811 additions and 0 deletions

View file

@ -0,0 +1,197 @@
# 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.

View file

@ -0,0 +1,250 @@
# `NVO.TShn` — the visible-owner record: one writer, one gate, and the gate is not `AFlags`
- **Type:** subsystem (live watchpoint + instruction stream + whole-corpus falsification)
- **Status:** **verified** — the writer was trapped on the running game; the gate was byte-decoded;
the resulting model was then tested against every `NVO` record in all 11 saves
- **Confidence:** high for the writer and the gate (both instruction-verified, one of them live).
Medium for *which conjunct* fails at the corpus's single frozen record, because the field it
turns on is not serialised — see §6.
- **Owner / date:** lane W3 · 2026-09-08
- **Instrument:** `src/shim/hooks/watchpoints.{h,cpp}` `watch.mode=tshn`, build
`w3tshn-bb81d3d-20260908T2007Z`, configs `shim.cfg.w3tshn` / `shim.cfg.w3control`
- **Predictions, committed before the build:** `sots-engine/docs/W3-predictions.md`
- **Supersedes:** `system-visibility-record.md` §7 ("`TShn`'s writer remains unnamed"), and one row
of `objects/layouts.md` (§2 below)
---
## 0. The result in one line
> `NVO[p].TShn` is set to the current `Frame` for every `(system, player)` pair that satisfies
> **`ServerSystem::IsKnownTo(sys, p)`**, once in driver phase 24 and again in combat-done phase 25.
> `IsKnownTo` is `IsVisibleTo` **or** a second branch — partial sensor contact plus the
> `CCC_AdvSens` tech — and that second branch is the whole reason lane E3 could not make `AFlags`
> fit.
`IsKnownTo`, whole body, byte-decoded at `0x00746390` (`0x007463ae`, `0x007463c0`, `0x007463cb`,
`0x007463da`) and again inlined at `0x0075bd70` (`0x0075bd88`, `0x0075bd93`, `0x0075bd9e`,
`0x0075bdb2`):
```c
bool ServerSystem::IsKnownTo(ServerPlayer* p) {
if (!p) return false;
if ((this->AFlags(+0xd4) >> p->PlyrIdx(+0x28)) & 1) return true; // == IsVisibleTo
if (p->PlyrIdx >= 15) return false;
if (((this->SnLv(+0x24) >> (2 * p->PlyrIdx)) & 3) == 0) return false; // sensor contact level
return TechTree::HasResearched(p->TechTree(+0xf4), 10000); // CCC_AdvSens
}
```
`tech-effects.md` line 89 already carried the sentence *"a player in 'partial contact' state (2-bit
field, `+0x24`) counts as known/visible when AdvSens is researched"* against exactly these three
addresses. What was missing was never the identification — it was the connection to `NVO`. That is
method rule 18's corollary, for the second time this campaign.
## 1. What the run measured
Workload: `ref-turn2.sav` (Frame 2, 8 players), one End Turn, four hardware data-write watchpoints
armed from the single `ApplyAllTurnCommands` detour.
**Target selection was done by predicate at arm time, not by a hard-coded address**, and the log
prints all 28 systems so the choice is auditable:
```
watch: systems vector @0x0e4f7c9c first=0x1ee82490 last=0x1ee82500 count=28 (expect 28 …)
watch: NVO header probe -- ServerSystem+0x274 validated on 9 systems, +0x26c on 0; using +0x274
watch: sys[11] @0x0e4e51a8 'Spica' AFlags=0x0 NVO=1 NVE=0 root=0x1eea8c40 key=4 ok=1
watch: SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS
watch: slot 0 -> 'Spica' NVO root+0x10 {touched:i16,TShn:i16} key=player 4 = 0x1eea8c50
watch: slot 1 -> 'Spica' NVO._Mysize (=1 at arm) = 0x0e4e5420
```
Prediction P1.0 was that the predicate "AFlags == 0 and NVO non-empty" has exactly one solution on
this save and that solution is Spica. It named Spica.
**Four traps, two store sites, two calls** (Ghidra VAs; runtime base was `0x00e80000`, so
`VA = EIP - 0xa80000`):
| seq | trap EIP | store | value after | reading |
|---|---|---|---|---|
| 0 | `0x0075b95a` | `0x0075b957` `mov WORD PTR [eax],cx` | `0x00020003` | `touched` 2 → **3**, `TShn` still 2 |
| 1 | `0x0075b965` | `0x0075b961` `mov WORD PTR [eax+0x2],dx` | `0x00030003` | **`TShn` 2 → 3** |
| 2 | `0x0075b95a` | same | `0x00030003` | second call, same values |
| 3 | `0x0075b965` | same | `0x00030003` | second call, same values |
Every trap carried `[ebp+4] = 0x0075bd4a` and `[[ebp]+4] = 0x0075bdc3`. Those are the returns of
the `call 0x0075b880` at `0x0075bd45` and the `call 0x0075bca0` at `0x0075bdbe` — so the chain
```
RefreshVisibleOwnerIfKnown 0x0075bd70 → RecordVisibleOwner 0x0075bca0
→ SetVisibleOwner 0x0075b880
→ NVO::operator[] 0x0075a890
→ the TShn store 0x0075b961
```
is confirmed **live, frame by frame**, not inferred. The stack scan also caught `0x0086a806`, a
stale return inside `StrategyServer::UpdateSensors`'s sibling — the sensor phase, which is where
the loop lives.
Against the predictions: P1.1 (`TShn` ends at 3) held; P1.2 (not `ltis`'s writer `FUN_00743ec0`,
not `RecordObservation 0x00756300`) held; **P1.3 held — slot 1, `NVO._Mysize`, took zero traps**,
which is what makes slot 0's four traps mean "this node was written" rather than "this address is
stale". The prediction said "at least one" trap and got four; the refinement is §3.
## 2. Two corrections to the record (rule 11)
**`objects/layouts.md`, `Game::ServerSystem`.** The rows `PID`@0x274 (handle) and `NVO`@0x278 (int)
are wrong as field rows. `+0x274` is `NVO._Myhead` and `+0x278` is `NVO._Mysize`; the `"PID"` tag on
the wire belongs to the map node's **key**, written by `WriteHandleId` at `0x0074a0c3` after
`node+0xc` is mapped through `server->players[idx]` — exactly as `EPid` does for `NVE`. Same for the
`NVE`@0x288 and `NVs`@0x298 rows: those offsets are the `_Mysize` count words.
The MSVC `_Tree` here is 0x10 bytes — allocator `+0x0`, `_Myhead` `+0x4`, `_Mysize` `+0x8`,
comparator `+0xc` — and the three maps sit at `+0x270`, `+0x280`, `+0x290`.
**The `+0x8` that made three lanes disagree.** `ServerSystem::Write` `0x00749630` is entered on the
**IStreamable subobject at `ServerSystem+0x8`** (the RTTI COL offset for vftable `0x00a2043c` is
`+0x8`), so every displacement in its body is 8 less than the object-relative offset. That is the
whole explanation for `+0x26c` vs `+0x274`, and for `AFlags` reading as `[edi+0xcc]` there and
`+0xd4` everywhere else. Lane E3's `+0x284/+0x288` for `NVE` and its `AFlags(+0xd4)` were both
right. The arming code does not depend on the reconciliation being right: it probes both bases and
logs the count each validates on (`+0x274` on 9 systems, `+0x26c` on 0).
**`NVO` node layout** (instruction-verified from `Write`'s walk `0x0074a0b0..0x0074a195`):
`_Left +0x0`, `_Parent +0x4`, `_Right +0x8`, key = **player index** `+0xc`, then
`touched:int16 +0x10`, `TShn:int16 +0x12`, `OID:int32 +0x14`, `isind:bool +0x18`,
`indi` (inline `IndependenceInfo`) `+0x1c`, `_Color +0x8c`, **`_Isnil +0x8d`**. An `NVO` node is
about `0x8e` bytes; `NVE`'s is `0x20`, with `_Isnil` at `+0x19`. They are not the same shape.
## 3. Why four traps and not two: the two-phase refresh
`SensorPhase_RefreshAllVisibleOwners 0x00813700` is called from
`StrategyServer::UpdateSensors 0x0086a8d0` at `0x0086a91d`, and `UpdateSensors` is called from
three places:
| caller | when |
|---|---|
| `StrategyServer::ProcessTurn` `0x007dcb9a` | **driver phase 24** (`turn-driver.md`) |
| `OnAllCombatDone_Tail` `0x007d981b` | **combat-done phase 25** (`combat-done-tail.md`) |
| `LoadGame` `0x007ddba8` | on load |
The watchpoints were armed during the End Turn, after the load, so the load call is outside the
window and **exactly two** of the three should fire. Two did. That is a live confirmation of both
phase attributions, taken for free from a watchpoint that was pointed at something else.
The inner loops are `for p in players: for s in systems:` and the player loop is **unfiltered** —
there is no NPC or species test anywhere on the write path. The only NPC test in the whole family is
in `SVSOIndependentSystems::Update 0x0075cb20`, and it *excludes* NPCs (`p->NPC(+0xfb) != 0 →
skip`). So the corpus split described below is not a special case for monsters; it is the ordinary
gate resolving differently.
## 4. Every writer of `NVO`, and why the list is complete
`NVO`'s `operator[]` is `0x0075a890` and it has **exactly one caller in the image**:
`ServerSystem::SetVisibleOwner 0x0075b880` at `0x0075b94b`. (`NVE`'s `operator[]` `0x00752730` has
three.) Every caller of `FindVisibleOwner 0x0074d2e0` only reads the record. So `SetVisibleOwner` is
the only function that can write a `TShn`, and its three record producers are:
| producer | `TShn` it supplies |
|---|---|
| `RecordVisibleOwner 0x0075bca0` | `(int16)server->Frame(+0x8)` — the per-turn refresh |
| `RecordVisibleOwnerNone 0x0075bc00` | `(int16)Frame`, with `OID = 0` |
| `ShareVisibleOwner 0x0075b9b0` | the *source player's* `TShn`, unrefreshed (alliance intel; no corpus save has an alliance, so this has never run — rule 6) |
plus `ServerSystem::Read 0x0075f0bd` reconstructing from the wire.
`SetVisibleOwner` always stamps `touched` with the current `Frame` and copies `TShn` from the
source, so **`touched` is "refreshed this turn" and `TShn` is "the turn this ownership view was
taken"**. `SynchronizePlayer 0x007c6220` corroborates from the reading side: it decides whether to
ship an `NVO` update to a client by comparing `touched` against the frame (`0x007c761d`) and puts
`(SnLv >> 2*idx) & 3` in the same packet (`0x007c76a4`).
`SetVisibleOwner`'s own gate is a sanity check, not a rule: skip the write when the caller asks to
record player *p* as owner of a system *p* does not own, logging *"VisibleOwner: %s sees their self
as owner of %s, but they are not."*. Its converge point is `0x0075b983`, the inlined
`IndependenceInfo` destructor that both arms run — checked, per method rule 4, before calling it a
branch.
## 5. The model, and its falsification against the whole corpus
Model, as it would be implemented:
```c
// once per (player, system) in the sensor phase, twice per turn
if (sys->IsKnownTo(p)) {
OwnRec* r = sys->NVO[p->PlyrIdx];
r->touched = (int16)server->Frame;
r->TShn = (int16)server->Frame;
r->OID = sys->PID ? sys->PID->id : 0;
r->isind = sys->indi != NULL; r->indi = *sys->indi;
}
```
Test: for every `NVO` record in all 11 saves, predict *frozen* vs *refreshed* from the record's own
player index, the system's `AFlags`, and whether that player has researched `CCC_AdvSens`, then
compare against `TShn == Frame`.
| predicted | observed | n |
|---|---|---|
| refresh — `AFlags` bit set | `TShn == Frame` | 103 |
| refresh — sensor branch (player has AdvSens) | `TShn == Frame` | 54 |
| **frozen** — `AFlags` bit clear, player lacks AdvSens | **`TShn == 22` at `Frame == 23`** | 1 |
**158 records, 0 mismatches.** The single frozen record is Bismol in `zuul-turn23-fleet23.sav` —
lane E3's named counter-example, and the model predicts it rather than excusing it. Script:
`scripts/…` is not needed; the whole test is 40 lines over `verify/save-reader/save_reader.py` and
is reproduced in §8.
**The discriminator, measured.** `CCC_AdvSens` (`TNm == "CCC_AdvSens"`, tech id 10000) in every
save:
| players | `St` | `TAcq` | `TiAcq` |
|---|---|---|---|
| every Species-4 pseudo-player (`NPC=true, RebAI=false`: Alien Menace, Peacekeeper Enforcer, Von Neumann, Independent Colony) | **4** | **1** | 37 or 43 |
| every real empire, in every save, at every turn | 0 | −1 | −1 |
That is why the seven `AFlags == 0` systems whose `TShn` tracks `Frame` — Spica in the reference
game, Altair / Galifrey / Kithrup / Alversi / Dosadi / Hyrakius in the Zuul game — all key their
single `NVO` entry to a Species-4 player, and why Bismol, keyed to real empire index 1, freezes the
moment its `AFlags` bit clears. It is not a species rule. It is that the monsters start the game
with the entire tech tree.
## 6. What is still a hypothesis, and why (rules 6 and 15)
- **Which conjunct fails at Bismol is not decidable from any save.** `SnLv` (`ServerSystem+0x24`) is
**not serialised** — it is recomputed every turn in the sensor phase from live positions and
sensor ranges. Bismol's record is consistent with *either* "player 1 was outside the sensor
envelope" or "player 1 lacks AdvSens". The corpus proves the second is true; it cannot show the
first is false. Both reduce to the same one-line model, so nothing downstream is blocked, but the
claim "the sensor branch is what refreshes Spica" is **[H]**, not [V]: no measurement of `SnLv`
exists. One arming line (`sys+0x24` in slot 1 instead of `_Mysize`) settles it on the next VM run.
- **The 54 "sensor branch" rows are unobserved in one conjunct each.** They are consistent, not
proven. The honest statement of the corpus result is: *no record contradicts the model, and the
one record that contradicted `AFlags` alone is explained.*
- **`ShareVisibleOwner` has never executed** — no corpus save has an alliance.
- **`p->PlyrIdx < 15`** is required for the sensor branch, and `RefreshVisibleOwnerIfKnown` uses
`1 << PlyrIdx` while the sensor phase indexes `SnLv` by *loop position*. Those agree only if the
player vector is ordered by `PlyrIdx`. It is, on every corpus save, but that was not verified in
code.
- **One system watched, not 28.** Spica's writes were trapped; the other 27 systems' `TShn` writes
are corpus evidence, not watchpoint evidence.
- **Per-thread blind spot.** Debug registers are per-thread; the arm was on the turn thread only.
## 7. Files
- Hit log: `verify/results/shim/watchpoints/w3-tshn-containers.txt`
- Arming log (all 28 systems, the base probe, the canary, the container counts):
`verify/results/shim/watchpoints/w3-shim-log-excerpt.txt`
- Addresses: `ghidra/addresses.d/lane-w3.json`
- Predictions: `sots-engine/docs/W3-predictions.md`; instrument notes `sots-engine/docs/W3-watchpoints.md`
## 8. Reproducing the corpus test
```python
# for each save: Frame; per player (PlyrIdx, CCC_AdvSens St/TAcq); per system (Name, AFlags, PID,
# and each NVO entry's PID key + TShn). Map the wire's handle key back to a PlyrIdx using systems
# whose AFlags has exactly one bit, then:
bit = (AFlags >> idx) & 1
pred = 'refresh' if (bit or advsens[idx]) else 'frozen'
assert (pred == 'frozen') == (TShn != Frame)
```

View file

@ -136,6 +136,17 @@ Note the `Frame > 1` guard — turn 1 is special-cased throughout this subsystem
## 7. What this does NOT explain
> **CLOSED by lane W3, 2026-09-08 — see `nvo-tshn-visible-owner.md`.** `NVO.TShn`'s writer is
> `ServerSystem::SetVisibleOwner 0x0075b880` (trapped live on Spica), reached from the sensor phase
> via `RefreshVisibleOwnerIfKnown 0x0075bd70`. The looser gate this section correctly predicted is
> `ServerSystem::IsKnownTo 0x00746390` = `IsVisibleTo` **or** (2-bit sensor contact at
> `ServerSystem+0x24`, which is *not serialised*, **and** the player has researched `CCC_AdvSens`).
> Spica's entry belongs to a Species-4 NPC pseudo-player and every one of those starts the game with
> `CCC_AdvSens`; Bismol's belongs to a real empire and none of those has it in any corpus save.
> The resulting model predicts all 158 `NVO` records across the 11 saves, including Bismol.
> The section below stands as written — it is what made the case checkable.
`NVO.TShn` moves on **more** systems than `NVE.ETS` does. On `turn1 -> turn2`, Spica has
`AFlags == 0`, no `NVE` entry and no `ltis` move — and its `TShn` still goes 1 -> 2. So `NVO`
has a looser gate than `AFlags`, and it is **not** `VFlags`, `EFlags` or `FFlags` either (all

View file

@ -0,0 +1,256 @@
{
"entries": [
{
"name": "ServerSystem_off_AFlags",
"offset": "0xd4",
"convention": "offset",
"status": "verified",
"prototype": "int AFlags -- the per-player 'has something here' mask, bit = PlyrIdx. Confirmed from ServerSystem::Write 0x00749630 at 0x00749aec (`mov ecx,[edi+0xcc]` with the \"AFlags\" tag at 0xa1f3b0; Write runs on the IStreamable subobject at +0x8, so 0xcc + 8 = 0xd4). Lane E3's +0xd4 is right"
},
{
"name": "ServerSystem_off_SnLv",
"offset": "0x24",
"convention": "offset",
"status": "verified",
"prototype": "int SnLv -- 2 bits per player (level = 0 none / 1 partial / 3 full), NOT SERIALISED. Written once per turn in the sensor phase at 0x0086a789 as `s->SnLv = (s->SnLv & ~(3<<2i)) | (level<<2i)`; read by ServerSystem::IsKnownTo 0x00746390 and RefreshVisibleOwnerIfKnown 0x0075bd70, and shipped to clients by SynchronizePlayer at 0x007c76a4. Because it is absent from every save, any NVO/TShn claim that depends on it is a hypothesis on the corpus alone (rule 6)"
},
{
"name": "ServerSystem_off_NVO_Myhead",
"offset": "0x274",
"convention": "offset",
"status": "verified",
"prototype": "std::map<int PlyrIdx, OwnRec>::_Myhead for NVO (the visible-owner record). The map object starts at +0x270 and is 0x10 bytes: allocator +0x0, _Myhead +0x4, _Mysize +0x8, comparator +0xc. From ServerSystem::Write 0x0074a097/0x0074a195 (`[edi+0x26c]`, +8 for the IStreamable subobject) and from the live arming probe, which validated +0x274 on 9 of 28 systems and +0x26c on 0"
},
{
"name": "ServerSystem_off_NVO_Mysize",
"offset": "0x278",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Mysize for NVO. Write 0x0074a07d (`[edi+0x270]`) carries the \"NVO\" tag 0xa1f2c0, so the wire's NVO count is this word. CORRECTS objects/layouts.md, which lists NVO@0x278 as an int field and PID@0x274 as a handle: +0x274 is _Myhead and the \"PID\" tag belongs to the node KEY, not to the object"
},
{
"name": "ServerSystem_off_NVE_Myhead",
"offset": "0x284",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Myhead for NVE (the sighting record). Write 0x0074a1bb. Confirms lane E3's +0x284/+0x288"
},
{
"name": "ServerSystem_off_NVE_Mysize",
"offset": "0x288",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Mysize for NVE. Write 0x0074a1a1, tag \"NVE\""
},
{
"name": "ServerSystem_off_NVs_Myhead",
"offset": "0x294",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Myhead for NVs (the per-player PlayerView record). Write 0x0074a27d"
},
{
"name": "ServerSystem_off_NVs_Mysize",
"offset": "0x298",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Mysize for NVs. Write 0x0074a263, tag \"NVs\""
},
{
"name": "ServerSystem_NVO_node_off_TShn",
"offset": "0x12",
"convention": "offset",
"status": "verified",
"prototype": "int16 TShn, offset within the NVO map NODE (not the value). Node is _Left +0x0, _Parent +0x4, _Right +0x8, key(PlyrIdx) +0xc, value +0x10 = {int16 touched +0x10, int16 TShn +0x12, int32 OID +0x14, bool isind +0x18, IndependenceInfo indi +0x1c}, _Color +0x8c, _Isnil +0x8d -- so an NVO node is ~0x8e bytes, unlike NVE's 0x20. From Write 0x0074a0ca (`lea ecx,[ebx+0x12]`, tag \"TShn\") and confirmed live: a 4-byte watchpoint on value+0x0 trapped both halves separately"
},
{
"name": "ServerSystem_NVO_operator_index",
"offset": "0x0075a890",
"convention": "thiscall",
"status": "verified",
"prototype": "OwnRec* (NVO_map* this /* ServerSystem+0x270 */, const int* plyrIdx) // std::map::operator[]; returns node+0x10. Exactly ONE caller in the image: ServerSystem::SetVisibleOwner 0x0075b94b (NVE's operator[] 0x00752730 has three). That single-caller fact is what makes the writer inventory below complete"
},
{
"name": "ServerSystem_SetVisibleOwner",
"offset": "0x0075b880",
"convention": "thiscall",
"status": "verified",
"prototype": "void (ServerSystem* this, ServerPlayer* p, const OwnRec* src) // ret 8. Stamps value.touched = (int16)this->owner(+0x10)->Frame(+0x8) and copies src's TShn/OID/isind/indi verbatim into NVO[p->PlyrIdx]. THE ONLY WRITER OF NVO.TShn IN THE IMAGE. Local gate (converge point 0x0075b983, the inlined IndependenceInfo dtor, so it is a real branch and not a rule-4 artifact): skip the whole write when src->OID == p->id but this->PID(+0x100)->id != p->id, logging \"VisibleOwner: %s sees their self as owner of %s, but they are not.\""
},
{
"name": "ServerSystem_SetVisibleOwner_TShnStore",
"offset": "0x0075b961",
"convention": "offset",
"status": "verified",
"prototype": "site `mov WORD PTR [eax+0x2],dx` -- the TShn store. Trapped live by a DR watchpoint on Spica's NVO root node: EIP 0x0075b965 (the trap is taken after the store retires), twice per End Turn. The touched-word store is the instruction before it at 0x0075b957 (trap EIP 0x0075b95a)"
},
{
"name": "ServerSystem_RecordVisibleOwner",
"offset": "0x0075bca0",
"convention": "thiscall",
"status": "verified",
"prototype": "void (ServerSystem* this, ServerPlayer* p) // ret 4. Builds an OwnRec with TShn = (int16)server->Frame, OID = this->PID(+0x100)->id, isind/indi from this->indi(+0x1c8), then calls SetVisibleOwner at 0x0075bd45. THIS is where TShn = Frame is decided. Confirmed live: the trapped frame's [ebp+4] was 0x0075bd4a"
},
{
"name": "ServerSystem_RecordVisibleOwnerNone",
"offset": "0x0075bc00",
"convention": "thiscall",
"status": "mapped",
"prototype": "void (ServerSystem* this, ServerPlayer* p) // TShn = Frame, OID = 0 -- 'you now see this system as unowned'. Frame read at 0x0075bc5f"
},
{
"name": "ServerSystem_ShareVisibleOwner",
"offset": "0x0075b9b0",
"convention": "thiscall",
"status": "mapped",
"prototype": "void (ServerSystem* this, ServerPlayer* from, ServerPlayer* to) // the NVO analogue of ShareObservation 0x00754d90: passes the source player's record verbatim, so TShn is NOT refreshed. No corpus save has an alliance, so this path has never executed (rule 6)"
},
{
"name": "ServerSystem_FindVisibleOwner",
"offset": "0x0074d2e0",
"convention": "thiscall",
"status": "verified",
"prototype": "OwnRec* (ServerSystem* this, ServerPlayer* p) // map find on NVO (0x0074d2f2 `lea ecx,[esi+0x270]`, 0x0074d300 `cmp edx,[esi+0x274]`); returns node+0x10 or NULL. Every caller only READS the record"
},
{
"name": "ServerSystem_GetVisibleOwnerId",
"offset": "0x0074f710",
"convention": "thiscall",
"status": "mapped",
"prototype": "int (ServerSystem* this, ServerPlayer* p) // FindVisibleOwner then rec->OID"
},
{
"name": "ServerSystem_IsKnownTo",
"offset": "0x00746390",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (ServerSystem* this, ServerPlayer* p) // THE NVO GATE, and it is looser than IsVisibleTo 0x00743fb0. Whole body: `p && ( (this->AFlags(+0xd4) >> p->PlyrIdx(+0x28)) & 1 || ( p->PlyrIdx < 15 && ((this->SnLv(+0x24) >> 2*p->PlyrIdx) & 3) != 0 && TechTree::HasResearched(p->TechTree(+0xf4), 10000 /* CCC_AdvSens */) ) )`. Byte-decoded at 0x007463ae / 0x007463c0 / 0x007463cb / 0x007463da"
},
{
"name": "ServerSystem_RefreshVisibleOwnerIfKnown",
"offset": "0x0075bd70",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (ServerSystem* this, ServerPlayer* p) // ret 4. IsKnownTo inlined byte-for-byte (0x0075bd88 AFlags, 0x0075bd93 idx<15, 0x0075bd9e SnLv, 0x0075bdb2 HasResearched(0x2710)), then RecordVisibleOwner at 0x0075bdbe. Confirmed live: the trapped frame's [[ebp]+4] was 0x0075bdc3, the return of that call"
},
{
"name": "SensorPhase_RefreshAllVisibleOwners",
"offset": "0x00813700",
"convention": "fastcall",
"status": "verified",
"prototype": "void (StrategyServer_S4* server) // for p in players(+0x50..+0x54) for s in systems(+0x40..+0x44): s->RefreshVisibleOwnerIfKnown(p) at 0x00813737. The player loop is UNFILTERED -- no NPC/species test anywhere on this path. Called from StrategyServer::UpdateSensors 0x0086a8d0 at 0x0086a91d"
},
{
"name": "SensorPhase_ComputeContactLevel",
"offset": "0x00850cf0",
"convention": "thiscall",
"status": "mapped",
"prototype": "int (Sensors* this, ServerPlayer* p, Entity* e) // 3 if IsVisibleTo (AFlags), else 1 if inside p's sensor envelope, else 0 (0x00850d18 / 0x00850e1b). Its result is packed into ServerSystem::SnLv(+0x24) at 0x0086a789"
},
{
"name": "ServerSystem_ChangeOwner",
"offset": "0x0075bde0",
"convention": "thiscall",
"status": "mapped",
"prototype": "void (ServerSystem* this, ServerPlayer* oldOwner, ServerPlayer* newOwner) // Game::ServerSystem vftable 0x00a2044c slot 6; records a visible-owner entry for both parties at 0x0075be77 / 0x0075bf2b"
},
{
"name": "StrategyServer_OnCommand_SetResearchRate",
"offset": "0x00821a80",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (StrategyServer_S4* this, int playerId, const float* rate) // ret 8. Bumps ModCount at 0x00821a84 then sets player->ResRate(+0xbc) = clamp(*rate, 0.0f, 1.0f), logging \"OnCommand: Research rate %f constrained to %f for %s.\" when it clamps. NAMES lane W2's `StrategySim_ModCountBump_unresolved_00821a84`, the handler that fires 4x per End Turn on ref-turn2 -- once per player that has a client"
},
{
"name": "StrategyServer_OnCommand_SetSystemRates",
"offset": "0x0086c3e0",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (StrategyServer_S4* this, int playerId, const SysRatesCmd* cmd) // ret 8. Bumps ModCount at 0x0086c3e6. Strings \"OnCommand: Unable to set system rates for %d(id). System not found.\" (0xa32448) and \"OnCommand: Player setting system rates for system he does not own.\" (0xa32400). NAMES lane W2's `..._unresolved_0086c3e6`"
},
{
"name": "StrategyServer_OnCommand_TransferShips",
"offset": "0x0088bed0",
"convention": "thiscall",
"status": "verified",
"prototype": "int (StrategyServer_S4* this, int playerId, const TransferCmd* cmd, ...) // ret 0xc. Bumps ModCount at 0x0088befe. Strings \"OnCommand: Cannot transfer ships. Player %d(id) not found.\" (0xa33248) and \"TransferShips: Fleet flagged as empty, but still has ships.\". NAMES lane W2's `..._unresolved_0088befe`"
},
{
"name": "StrategyServer_OnCommand_AddBuildOrder",
"offset": "0x00849460",
"convention": "thiscall",
"status": "mapped",
"prototype": "bool (StrategyServer_S4* this, int requesterId, const AddOrderCmd* cmd, int* outReason) // ret 0xc. Bumps ModCount at 0x0084946b. cmd = {int which, id target, id design, int arg}; picks target->+0xa4 (which==0) or +0x98 (which==1) as the BuildQueue, validates with 0x0083ce50, then BuildQueue::AddOrder 0x00845260 push_backs onto the list at queue+0x10. Reached from the Build Screen 0x00656540 via 0x00762fd0, and from the command replay at 0x0089009f. NAMES lane W2's `..._unresolved_0084946b`"
},
{
"name": "StrategyServer_OnCommand_SetResearchProject",
"offset": "0x0088fe0a",
"convention": "offset",
"status": "verified",
"prototype": "site `inc DWORD PTR [ebx+0x4]` -- a SECOND ModCount bump INLINED into ApplyTurnCommandBatch 0x0088f9b0 (at +0x45a), distinct from the one lane W2 found at 0x0089046c. String \"OnCommand: Cannot set research project for player %d(id). Player not found.\" (0xa30ed0). Fires 3x on turn1-state and 0x on turn2-state -- once per AI player that picks a research target on turn 1; the human player set a rate but no project, which is why the reference game's map banner reads \"No Research Project Assigned\""
},
{
"name": "StrategyServer_OnCommand_CreateDesign",
"offset": "0x00882910",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (StrategyServer_S4* this, int playerId, const DesignCmd* cmd) // ret 8. Bumps ModCount at 0x0088291a. Strings \"OnCommand: Cannot create design for player %d(id), player does not exist.\" (0xa32c40) and \"OnCommand: Unable to create design. ID not specified.\" (0xa32c04). Fires 1x on turn1-state, 0x on turn2-state. ANSWERS LANE AI4'S FALSIFIER: command list 1 emits a design COMMAND, not merely a design object that the build order names"
},
{
"name": "BuildQueue_AddOrder",
"offset": "0x00845260",
"convention": "thiscall",
"status": "mapped",
"prototype": "void (BuildQueue* this, int arg, ShipDesign* design) // builds an order record with five fields pre-set to -1 and push_backs it onto the std::list at this+0x10/+0x14 (\"list<T> too long\" guard at 0x008452e4)"
},
{
"name": "StrategyServer_CanAddBuildOrder",
"offset": "0x0083ce50",
"convention": "cdecl",
"status": "mapped",
"prototype": "bool (ServerPlayer* who, BuildQueue* q, ShipDesign* d, int* outReason) // the 'can this be built' validator; writes a numbered reason (0,1,2,8,...) through arg 4 and calls BankruptcyLevel 0x0080db10"
},
{
"name": "ServerPlayer_off_ReqCL",
"offset": "0xfd",
"convention": "offset",
"status": "verified",
"prototype": "bool ReqCL -- 'this player requires a client'. THE PREDICATE BEHIND Player.Status: a player with ReqCL == 0 never reaches SetupAIPlayers' AI list (gate at 0x007bc25c), so never gets a StrategyClient, so is never the subject of MarkPlayerTurnEnded and keeps the 0 that ResumePlaying wrote. The script binding that creates the four '_NPC' pseudo-players clears it explicitly at 0x00797bcb"
},
{
"name": "StrategyNetworkServer_AllPlayersTurnEnded",
"offset": "0x007cfd00",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (StrategyNetworkServer* this) // vftable slot 3. The game's OWN statement of which players must carry Status == 4: `for p in players: if (!p->ReqCL(+0xfd)) continue; if (p->Elim(+0xf8)) continue; if (p->Status(+0x164) != 4) return false;` -- loop 0x007cffb7..0x007d0002 with edi = 4 loaded at 0x007cffa3"
},
{
"name": "StrategyNetworkServer_SetupAIPlayers",
"offset": "0x007bc1c0",
"convention": "thiscall",
"status": "verified",
"prototype": "void (StrategyNetworkServer* this) // the ReqCL gate at 0x007bc25c decides which players get an AI client at all; AddAIPlayer 0x007bc333 -> 0x007b8ab0"
},
{
"name": "StrategyServer_OnPlayerEndTurn",
"offset": "0x007d9af0",
"convention": "thiscall",
"status": "verified",
"prototype": "void (StrategyServer* this, const SNMPlayerEndTurn* msg) // ONE call per network message, not a loop: handle = msg->+0x4 straight off the wire, MarkPlayerTurnEnded(this+4, handle) at 0x007d9b25 with NO gate before the store. Reached from StrategyNetworkClient::OnMessage 0x00784816. The field tests at 0x007d9b5d/0x007d9ba2/0x007d9bb0 all follow the store and only decide whether to raise a 'waiting on X' event"
},
{
"name": "StrategyServer_ProcessTurn_StatusOneStore",
"offset": "0x007dcc8a",
"convention": "offset",
"status": "verified",
"prototype": "site `mov DWORD PTR [eax+0x164],1` -- the phase-31 Status write lane T2 located. Its gate, decoded at 0x007dcc78, is `if (p->IsAI(+0xf9) == 0 || p->+0xfa != 0)`, over the encounter records at S+0x1e8 (0x74-byte outer, 0x44-byte inner with the ServerPlayer* first). It reads NO species field"
},
{
"name": "Game_SNMSetPlayerStatus_vftable",
"offset": "0x00a2d718",
"convention": "offset",
"status": "verified",
"prototype": "vftable for Game::SNMSetPlayerStatus (factory 0x0080f990, sizeof 0xc: vptr / +4 player INDEX / +8 status). Its handler is the OnMessage+0xa15 store lane W2 found: no gate, no field test -- the value comes verbatim off the wire. The sender is StrategyNetworkServer::Update 0x007cda40 at 0x007cdcf0, which computes `2*(flag!=0) + 2`, i.e. 4 when the player has nothing pending and 2 when it does"
}
]
}

View file

@ -0,0 +1,16 @@
watchhit seq=0 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012ec3e9 value=1(0x00000001) tid=948 ebpret=0x0130fce2 ebpret2=0x0120f6e6 scan=0x0130fce2,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=1 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=2(0x00000002) tid=948 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=2 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130fe0d value=3(0x00000003) tid=948 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
watchhit seq=3 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=4(0x00000004) tid=948 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=4 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130fe0d value=5(0x00000005) tid=948 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
watchhit seq=5 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=6(0x00000006) tid=948 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=6 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130fe0d value=7(0x00000007) tid=948 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
watchhit seq=7 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=8(0x00000008) tid=948 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=8 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130291d value=9(0x00000009) tid=948 ebpret=0x01310007 ebpret2=0x0120f6e6 scan=0x01310007,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=9 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012c946e value=10(0x0000000a) tid=948 ebpret=0x013100a4 ebpret2=0x0120f6e6 scan=0x013100a4,0x013dad98,0x0120f6e6,0x01204909
watchhit seq=10 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0125c6f3 value=11(0x0000000b) tid=948 ebpret=0x723b7324 ebpret2=0x723b6ff4 scan=0x0141005c,0x012c946e,0x00000000,0x00000000
watchhit seq=11 mark=1 slot=2 dr6=0xffff0ff4 eip=0x0125cc94 value=1(0x00000001) tid=948 ebpret=0x723b7324 ebpret2=0x723b6ff4 scan=0x0141005c,0x012c946e,0x00000000,0x00000000
watchhit seq=12 mark=1 slot=2 dr6=0xffff0ff4 eip=0x01205055 value=4(0x00000004) tid=948 ebpret=0x0139dfa1 ebpret2=0x013a5651 scan=0x012317aa,0x01178f0f,0x0140c080,0x0124cac1
watchhit seq=13 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012592cd value=12(0x0000000c) tid=948 ebpret=0x72389974 ebpret2=0x72389644 scan=0x0140fca0,0x01026c32,0x00e81000,0x01205055
watchhit seq=14 mark=1 slot=2 dr6=0xffff0ff4 eip=0x0125dd49 value=0(0x00000000) tid=948 ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x00eb4814,0x01214827
watchhit seq=15 mark=1 slot=3 dr6=0xffff0ff8 eip=0x0125dd49 value=0(0x00000000) tid=948 ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x00eb4814,0x01214827

View file

@ -0,0 +1,29 @@
19:16:30:24.311 [tid 948] config: watch=on
20:16:30:24.311 [tid 948] config: watch.players=2
21:16:30:24.311 [tid 948] config: watch.mode=modcount
22:16:30:24.311 [tid 948] config: watch.out=C:\SOTS\shim.watch.txt
89:16:30:24.921 [tid 948] watch: VEH=01b37d58 text=[0x00e81000,0x0145ca00) out=C:\SOTS\shim.watch.txt
90:16:30:24.936 [tid 948] watch: arm hook StrategyServer::ApplyAllTurnCommands rva=0x0038f6a0 va=0120f6a0 create=MH_OK enable=MH_OK
108:16:33:53.478 [tid 948] watch: players vector @0e997424 begin=0e9b2940 end=0e9b2960 count=8
109:16:33:53.478 [tid 948] watch: SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS
110:16:33:53.478 [tid 948] watch: slot 0 -> S+0x8 (A2:ModCount / T:PhaseCounter) = 0x0e9973d8
111:16:33:53.478 [tid 948] watch: slot 1 -> S+0xc (A2:Frame / addresses.json:ModCount) = 0x0e9973dc
112:16:33:53.478 [tid 948] watch: slot 2 -> player[0]+0x164 Status = 0x0e9175f4
113:16:33:53.478 [tid 948] watch: slot 3 -> player[1]+0x164 Status = 0x0e916a3c
114:16:33:53.478 [tid 948] watch: ARMED on tid 948 dr7=0xdddd0055, S=0e9973d0 (ApplyAllTurnCommands this)
115:16:33:54.368 [tid 9192] watchhit seq=0 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012ec3e9 value=1(0x00000001) tid=948 ebpret=0x0130fce2 ebpret2=0x0120f6e6 scan=0x0130fce2,0x013dad98,0x0120f6e6,0x01204909
116:16:33:54.368 [tid 9192] watchhit seq=1 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=2(0x00000002) tid=948 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
117:16:33:54.368 [tid 9192] watchhit seq=2 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130fe0d value=3(0x00000003) tid=948 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
118:16:33:54.368 [tid 9192] watchhit seq=3 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=4(0x00000004) tid=948 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
119:16:33:54.368 [tid 9192] watchhit seq=4 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130fe0d value=5(0x00000005) tid=948 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
120:16:33:54.368 [tid 9192] watchhit seq=5 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=6(0x00000006) tid=948 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
121:16:33:54.368 [tid 9192] watchhit seq=6 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130fe0d value=7(0x00000007) tid=948 ebpret=0x0120f6e6 ebpret2=0x0139dfa1 scan=0x013dad98,0x0120f6e6,0x01204909,0x00000000
122:16:33:54.368 [tid 9192] watchhit seq=7 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012a1a87 value=8(0x00000008) tid=948 ebpret=0x0130ffcb ebpret2=0x0120f6e6 scan=0x0130ffcb,0x013dad98,0x0120f6e6,0x01204909
123:16:33:54.368 [tid 9192] watchhit seq=8 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0130291d value=9(0x00000009) tid=948 ebpret=0x01310007 ebpret2=0x0120f6e6 scan=0x01310007,0x013dad98,0x0120f6e6,0x01204909
124:16:33:54.368 [tid 9192] watchhit seq=9 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012c946e value=10(0x0000000a) tid=948 ebpret=0x013100a4 ebpret2=0x0120f6e6 scan=0x013100a4,0x013dad98,0x0120f6e6,0x01204909
125:16:33:54.368 [tid 9192] watchhit seq=10 mark=1 slot=0 dr6=0xffff0ff1 eip=0x0125c6f3 value=11(0x0000000b) tid=948 ebpret=0x723b7324 ebpret2=0x723b6ff4 scan=0x0141005c,0x012c946e,0x00000000,0x00000000
126:16:33:54.368 [tid 9192] watchhit seq=11 mark=1 slot=2 dr6=0xffff0ff4 eip=0x0125cc94 value=1(0x00000001) tid=948 ebpret=0x723b7324 ebpret2=0x723b6ff4 scan=0x0141005c,0x012c946e,0x00000000,0x00000000
127:16:33:54.368 [tid 9192] watchhit seq=12 mark=1 slot=2 dr6=0xffff0ff4 eip=0x01205055 value=4(0x00000004) tid=948 ebpret=0x0139dfa1 ebpret2=0x013a5651 scan=0x012317aa,0x01178f0f,0x0140c080,0x0124cac1
128:16:33:54.368 [tid 9192] watchhit seq=13 mark=1 slot=0 dr6=0xffff0ff1 eip=0x012592cd value=12(0x0000000c) tid=948 ebpret=0x72389974 ebpret2=0x72389644 scan=0x0140fca0,0x01026c32,0x00e81000,0x01205055
129:16:33:54.368 [tid 9192] watchhit seq=14 mark=1 slot=2 dr6=0xffff0ff4 eip=0x0125dd49 value=0(0x00000000) tid=948 ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x00eb4814,0x01214827
130:16:33:54.368 [tid 9192] watchhit seq=15 mark=1 slot=3 dr6=0xffff0ff8 eip=0x0125dd49 value=0(0x00000000) tid=948 ebpret=0x01204fe4 ebpret2=0x0139dfa1 scan=0x01410138,0x01204fe4,0x00eb4814,0x01214827

View file

@ -0,0 +1,48 @@
19:16:11:26.261 [tid 2420] config: watch=on
20:16:11:26.261 [tid 2420] config: watch.players=2
21:16:11:26.261 [tid 2420] config: watch.mode=tshn
22:16:11:26.261 [tid 2420] config: watch.out=C:\SOTS\shim.watch.txt
89:16:11:26.886 [tid 2420] watch: VEH=00b79158 text=[0x00e81000,0x0145ca00) out=C:\SOTS\shim.watch.txt
90:16:11:26.902 [tid 2420] watch: arm hook StrategyServer::ApplyAllTurnCommands rva=0x0038f6a0 va=0120f6a0 create=MH_OK enable=MH_OK
108:16:15:22.976 [tid 2420] watch: systems vector @0x0e4f7c9c first=0x1ee82490 last=0x1ee82500 end=0x1ee82500 count=28 (expect 28 on the reference game)
109:16:15:22.976 [tid 2420] watch: NVO header probe -- ServerSystem+0x274 validated on 9 systems, +0x26c on 0; using +0x274 (pointer delta 0)
110:16:15:22.976 [tid 2420] watch: sys[0] @0x0e4d2008 'Caladan' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
111:16:15:22.976 [tid 2420] watch: sys[1] @0x0e4d22e8 'Hyperion' AFlags=0x10 NVO=1 NVE=1 root=0x0e4e34f8 key=4 ok=1
112:16:15:22.976 [tid 2420] watch: sys[2] @0x0e4d25c8 'Tully' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
113:16:15:22.976 [tid 2420] watch: sys[3] @0x0e4d28a8 'Foo' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
114:16:15:22.976 [tid 2420] watch: sys[4] @0x0e4d2b88 'Gamma Cephei' AFlags=0x1 NVO=1 NVE=1 root=0x1eea9490 key=0 ok=1
115:16:15:22.976 [tid 2420] watch: sys[5] @0x0e4d2e68 'Biter' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
116:16:15:22.976 [tid 2420] watch: sys[6] @0x0e4d3148 'Flinx' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
117:16:15:22.976 [tid 2420] watch: sys[7] @0x0e4d3428 'Procyon' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
118:16:15:22.976 [tid 2420] watch: sys[8] @0x0e4d3708 'Kor?Voth' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
119:16:15:22.976 [tid 2420] watch: sys[9] @0x0e4e4be8 'Ke'Rassak' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
120:16:15:22.976 [tid 2420] watch: sys[10] @0x0e4e4ec8 'Koa?Tuun' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
121:16:15:22.976 [tid 2420] watch: sys[11] @0x0e4e51a8 'Spica' AFlags=0x0 NVO=1 NVE=0 root=0x1eea8c40 key=4 ok=1
122:16:15:22.976 [tid 2420] watch: sys[12] @0x0e4e5488 'Junction' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
123:16:15:22.976 [tid 2420] watch: sys[13] @0x0e4e5768 'Nemesis' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
124:16:15:22.976 [tid 2420] watch: sys[14] @0x0e4e5a48 'Keu?Loko' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
125:16:15:22.976 [tid 2420] watch: sys[15] @0x0e4e5d28 'Ke'Dolarra' AFlags=0x2 NVO=1 NVE=1 root=0x1eea9360 key=1 ok=1
126:16:15:22.976 [tid 2420] watch: sys[16] @0x0e4e6008 'Koa?Vo' AFlags=0x80 NVO=1 NVE=1 root=0x1eea9100 key=7 ok=1
127:16:15:22.976 [tid 2420] watch: sys[17] @0x0e52f838 'Delphi' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
128:16:15:22.976 [tid 2420] watch: sys[18] @0x0e52ef80 'Kaa?Vaalu' AFlags=0x10 NVO=1 NVE=1 root=0x1eea9230 key=4 ok=1
129:16:15:22.976 [tid 2420] watch: sys[19] @0x0e5300f0 'Mu Bootis' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
130:16:15:22.976 [tid 2420] watch: sys[20] @0x0e52f550 'Kor?Hakaan' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
131:16:15:22.976 [tid 2420] watch: sys[21] @0x0e5306c0 'Serpens' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
132:16:15:22.976 [tid 2420] watch: sys[22] @0x0e52ec98 'Markab' AFlags=0x10 NVO=1 NVE=1 root=0x1eea96f0 key=4 ok=1
133:16:15:22.976 [tid 2420] watch: sys[23] @0x0e52f268 'Pascal' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
134:16:15:22.976 [tid 2420] watch: sys[24] @0x0e52fb20 'Kao?Lolto' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
135:16:15:22.976 [tid 2420] watch: sys[25] @0x0e52fe08 'Kea?Pono' AFlags=0x10 NVO=2 NVE=1 root=0x1eea8780 key=4 ok=1
136:16:15:22.976 [tid 2420] watch: sys[26] @0x0e5303d8 'Gienah' AFlags=0x0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
137:16:15:22.976 [tid 2420] watch: sys[27] @0x0e534ca0 'Ko'Rorkor' AFlags=0x10 NVO=1 NVE=1 root=0x1eea8f38 key=4 ok=1
138:16:15:22.976 [tid 2420] watch: trade routes -- manager=0x0e4cdd38 (S+4+0x154) vector@mgr+0x3c first=0x00000000 last=0x00000000 end=0x00000000 count=0
139:16:15:22.976 [tid 2420] watch: spy programs -- manager=0x0e512f68 (S+4+0x158) vector@mgr+0x10 first=0x00000000 last=0x00000000 end=0x00000000 count=0
140:16:15:22.976 [tid 2420] watch: SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS
141:16:15:22.976 [tid 2420] watch: slot 0 -> 'Spica' NVO root+0x10 {touched:i16,TShn:i16} key=player 4 = 0x1eea8c50
142:16:15:22.976 [tid 2420] watch: slot 1 -> 'Spica' NVO._Mysize (=1 at arm) = 0x0e4e5420
143:16:15:22.976 [tid 2420] watch: slot 2 -> trade routes vector _Mylast (mgr+0x40, count 0 at arm) = 0x0e4cdd78
144:16:15:22.976 [tid 2420] watch: slot 3 -> spy programs vector _Mylast (mgr+0x14, count 0 at arm) = 0x0e512f7c
145:16:15:22.976 [tid 2420] watch: ARMED on tid 2420 dr7=0xdddd0055, S=0e4f7c58 (ApplyAllTurnCommands this)
146:16:15:24.585 [tid 1892] watchhit seq=0 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db95a value=131075(0x00020003) tid=2420 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
147:16:15:24.585 [tid 1892] watchhit seq=1 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db965 value=196611(0x00030003) tid=2420 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
148:16:15:24.585 [tid 1892] watchhit seq=2 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db95a value=196611(0x00030003) tid=2420 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
149:16:15:24.585 [tid 1892] watchhit seq=3 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db965 value=196611(0x00030003) tid=2420 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806

View file

@ -0,0 +1,4 @@
watchhit seq=0 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db95a value=131075(0x00020003) tid=2420 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
watchhit seq=1 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db965 value=196611(0x00030003) tid=2420 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
watchhit seq=2 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db95a value=196611(0x00030003) tid=2420 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
watchhit seq=3 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db965 value=196611(0x00030003) tid=2420 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806