Compare commits

...

2 commits

Author SHA1 Message Date
alex
d6469bd4c5 board: lane W3 - TShn closed 158/158, Status predicate settled, AI4 prediction confirmed with its falsifier 2026-09-08 16:45:56 -04:00
alex
471d6cb973 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.
2026-09-08 16:41:09 -04:00
10 changed files with 1011 additions and 2 deletions

File diff suppressed because one or more lines are too long

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 ## 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 `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` `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 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

@ -1,5 +1,5 @@
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). // GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
// Source: sots-re ghidra/addresses.json @ 3b99e25, generated 2026-09-08 by tools/gen_addresses.py // Source: sots-re ghidra/addresses.json @ 471d6cb, generated 2026-09-08 by tools/gen_addresses.py
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
#pragma once #pragma once
#include <cstdint> #include <cstdint>
@ -1415,6 +1415,64 @@ constexpr uint32_t ClientOrder_SetSystemRates = 0x00363270;
constexpr uint32_t ClientOrder_Colonize = 0x00369640; constexpr uint32_t ClientOrder_Colonize = 0x00369640;
// offset Game::TurnCommands -- the first of the 27 std::list members lane Q enumerated, stride 0x0c, so list N (1-based, as lane Q numbers them) is at +0x70 + (N-1)*0x0c and the last, list 27, is at +0x1a8. Recorded here because the order-method -> list mapping in ai-task-system.md#4 is expressed entirely in these offsets: the adder for list N is the function whose first `this`-relative access is +0x70 + (N-1)*0x0c [verified] // offset Game::TurnCommands -- the first of the 27 std::list members lane Q enumerated, stride 0x0c, so list N (1-based, as lane Q numbers them) is at +0x70 + (N-1)*0x0c and the last, list 27, is at +0x1a8. Recorded here because the order-method -> list mapping in ai-task-system.md#4 is expressed entirely in these offsets: the adder for list N is the function whose first `this`-relative access is +0x70 + (N-1)*0x0c [verified]
constexpr uint32_t TurnCommands_off_ListBase = 0x00000070; constexpr uint32_t TurnCommands_off_ListBase = 0x00000070;
// cdecl void __cdecl Game::StrategyApp::OnClientEvent(int netId, int eventId, void* ev) -- THE AI ENQUEUE SITE (lane AI2 §6.1 open item, closed). Operates on the STATIC StrategyApp at 0x00b29f98 (not a pointer -- `mov ecx,0xb29f98` at 0x007843a2 proves the object itself lives there). Body: (1) `if (*(void**)0x00b29f9c == 0) return` -- app+0x4, the StrategyServer; (2) `if (netId == 0) return`; (3) linear search of the client vector at app+0xc/+0x10 (absolutes 0x00b29fa4/0x00b29fa8) for `client->+0x148 == netId`; not found -> return; (4) `p = client->+0x150`; (5) `if (eventId == 0x26 (SEResumePlaying) && p->+0xf9 != 0 && p->+0xfa == 0)` then `if (!0x00438fe0(&pending, &netId)) 0x0059f1a0(&pending, &netId)` -- a DEDUPLICATED push_back onto the pending-AI vector at app+0x1c (absolute 0x00b29fb4), and RETURN; (6) otherwise `StrategyClient::RaiseEvent 0x00783ee0(client, eventId, ev)` inline. So event 0x26 for an AI player is the ONLY deferred event; everything else is delivered synchronously. Registered as StrategyServer+0x170 by CreateGame at 0x00889177; it has ZERO direct callers and no vtable slot (lane B6's third blind spot) [verified]
constexpr uint32_t StrategyApp_OnClientEvent = 0x00438e10;
// thiscall void __thiscall Game::StrategyServer::ResumePlaying() -- THE STEPPING ORDER. (1) `if (0x0080f4d0(&this->+0x4)) return`; (2) `if (++this->+0x168 == 1 && this->+0x1b4) { obs->vt[4](2,0); obs->vt[7](); }`; (3) 0x007dd230(&this->+0x174, 0) then 0x007dd230(&this->+0x174, playerCount) -- clear+resize; (4) FIRST walk of the player vector at this->+0x54..+0x58, IN INDEX ORDER: `if (!p->Elim /*+0xf8*/) p->Status /*+0x164*/ = 0` -- this is a Player.Status WRITER; (5) SECOND walk, again in index order: `if (p->Status == 0) { ev = {vptr 0x00a23bb8}; cb = this->+0x170; cb ? cb(p->+0x4 /*netId*/, 0x26, &ev) : Log(0x00a23c08); }`. The callback is StrategyApp::OnClientEvent 0x00838e10, so the pending-AI vector is filled in SERVER PLAYER INDEX ORDER -- i.e. save player order -- and RunPendingAITurns then walks it in index order. Both Elim and Status are save-visible ServerPlayer fields [verified]
constexpr uint32_t StrategyServer_ResumePlaying = 0x003ddc90;
// thiscall void __thiscall Game::StrategyServer::SetClientEventCallback(void (__cdecl* cb)(int netId, int eventId, void* ev)) -- RET 4. Two instructions: `this->+0x170 = arg`. The only registration in the image is CreateGame 0x00889177 installing StrategyApp::OnClientEvent 0x00838e10. Every server->client event in the game funnels through this one pointer; SynchronizePlayer 0x007c6220 dispatches through it at 0x007c6384, 0x007c65aa, 0x007c6889, 0x007c6a00, 0x007c6ae0 and more [verified]
constexpr uint32_t StrategyServer_SetClientEventCallback = 0x003861f0;
// data Game::StrategyApp -- the STATIC APP OBJECT ITSELF, not a pointer to one. Proved by `mov ecx,0xb29f98; call <method>` at 0x007842f6/0x007843a2 and by the absolute pair 0x00b29fa4/0x00b29fa8 being read where a method reads this->+0xc/this->+0x10. Layout confirmed this lane: +0x0 flag byte (bit 2 = 'a StrategyServer exists'), +0x4 StrategyServer*, +0xc/+0x10/+0x14 vector<StrategyClient*>, +0x1c/+0x20/+0x24 vector<int> pendingAITurns (absolutes 0x00b29fb4/b8/bc), +0x2c AIProcessMinTime (seconds, float). Lane AI2's scan for 'functions that load 0x00b29f98' missed the enqueue because MSVC folds the object base into the absolute address of the member: the enqueue writes 0x00b29fb4 directly and never materialises 0x00b29f98 [verified]
constexpr uint32_t g_StrategyApp = 0x00729f98;
// cdecl void __cdecl Game::StrategyAIAgent::AcquireFleetsForTask(StrategyAIAgent* agent, IAITask* task, double dA, double dB, void* targetA, void* targetB, vector<Candidate32>* candidates, int pass, int flag, vector<StarFleet*>* out) -- THE HUB EVERY FLEET-SHAPED TASK GOES THROUGH, and where `pass` acquires its meaning. Real span 992 bytes to the next function start (Ghidra's size is short). Three blocks, each a `for (i = 0; i <= pass; ++i) gather(..., i, ...)` loop followed by an 0x00698960 sufficiency test: block A (0x006cef5a, entered when targetB->+0x14 == 0, or == 2 with task->GetTypeId() == 0x1e) gathers via 0x006abf80; block B (0x006cf029) gathers via 0x006b7c90; block C (0x006cf0ee, only if A and B both failed and targetA != 0) gathers via 0x006cb310, taking task->vt[11]() at 0x006cf10e as an argument. ALL THREE order-emitting exits are pass==1 only: 0x006bbd50 (0x006cefcf, 0x006cf180) returns immediately unless pass==1, and 0x006c16c0 (0x006cf198) takes the arm at 0x006c1791 only when pass==1. The result vector at [ebp-0x3c] has exactly two possible writers -- 0x006bbd50 and 0x006c16c0 -- enumerated from every `lea` of that slot in the body, so on pass 0 this function RETURNS AN EMPTY FLEET LIST AND WRITES NO TurnCommands. At 0x006cf1aa it special-cases task->GetTypeId() 0x17 (StockFreighters) and 0x1a (NodeBore), substituting 0.0 for dA [verified]
constexpr uint32_t StrategyAIAgent_AcquireFleetsForTask = 0x002ceef0;
// cdecl void __cdecl Game::StrategyAIAgent::GatherFleetsForTier(StrategyAIAgent* agent, float threshold, void* target, vector<Candidate32>* candidates, int tier, vector<Slot36>* out) -- 112 bytes. Walks the 0x20-stride candidate vector in index order, calling 0x006abb00(agent, threshold, target, &cand[k], tier, out, &out[k]) for each. `tier` is the loop index i of AcquireFleetsForTask's `for (i = 0; i <= pass; ++i)`, so it takes the values 0..pass [verified]
constexpr uint32_t StrategyAIAgent_GatherFleetsForTier = 0x002abf80;
// cdecl void __cdecl Game::StrategyAIAgent::FillCandidateToTierQuota(StrategyAIAgent* agent, float threshold, void* target, Candidate32* cand, int tier, vector<Slot36>* out, Slot36* slot) -- 464 bytes. THE INSTRUCTION THAT DEFINES THE TWO PASSES, at 0x006abb1c..0x006abb4d: `have = 0x00695b90(&slot->+0x10) + 0x00698860(slot) + slot->+0x20; want = (tier == 0) ? cand->+0x10 : (tier == 1) ? cand->+0x14 : 0; if (have >= want) return;` -- a compiler-generated switch on tier with case 0 -> 0x006abb41 (cand->+0x10) and case 1 -> 0x006abb39 (cand->+0x14). So each candidate carries TWO quota fields and `pass` selects which one is in force: pass 0 fills the +0x10 quota, pass 1 re-runs tier 0 and then fills the larger +0x14 quota. Then a per-fleet filter loop rejecting on 0x0069c8c0, IsClaimedByAnotherTask 0x006a8d20 and 0x006ab900 [verified]
constexpr uint32_t StrategyAIAgent_FillCandidateToTierQuota = 0x002abb00;
// cdecl bool __cdecl Game::StrategyAIAgent::IssueRouteForFleets(StrategyAIAgent* agent, int pass, vector<Slot36>* fleets, void* target, vector<StarFleet*>* out) -- 192 bytes. `if (pass != 1) return;` at 0x006bbd78 (the MSVC `sub eax,0 / je / dec / jne` switch shape). Otherwise walks the 0x24-stride fleet vector, calling 0x0057aac0 per element to build a route, then 0x006b76a0(agent, &route, target, out), which is one of the three callers of AI_IssueFleetTask 0x006987e0. This is one of the two pass-1 gates that make pass 0 emit nothing [verified]
constexpr uint32_t StrategyAIAgent_IssueRouteForFleets = 0x002bbd50;
// cdecl void __cdecl Game::StrategyAIAgent::RequestBuildForTask(StrategyAIAgent* agent, IAITask* task, int pass, double, double, void* targetA, void* targetB, void* targetA2, vector<Slot36>* gathered) -- 304 bytes, AcquireFleetsForTask's LAST-RESORT arm: no fleet could be found, so build ships. Two gates in the prologue: (1) 0x006cea7b..0x006ceaa6 `if (agent->+0x10->+0x150->+0x2d8 /*plcy*/ == 0 && task->GetTypeId() != 0x1a /*NodeBore*/) return` -- a SECOND, independent consumer of the save-visible `plcy` field, beyond the two defence creators lane AI2 found; (2) 0x006ceaac `if (pass != 1) return`. Reaches list 3 (build orders) via 0x006ce460 -> 0x006ce360 -> 0x006ce190 -> 0x006bd790 -> 0x006b3bc0 -> 0x00762fd0 [verified]
constexpr uint32_t StrategyAIAgent_RequestBuildForTask = 0x002cea50;
// cdecl void __cdecl Game::StrategyAIAgent::AssignFleetsAndIssueOrders(StrategyAIAgent* agent, IAITask* task, int pass, vector<StarFleet*>* fleets, void* targetA, void* targetB, int flag) -- 3536 bytes, the busiest AI->TurnCommands function. `if (pass != 1) goto 0x006c241f` at 0x006c177e (the same `sub eax,0 / je / dec / jne` shape), so its ENTIRE working body -- including both calls to AI_IssueFleetTask 0x006987e0 at 0x006c1c86 and 0x006c1f78, and the two 0x00699fa0 -> list 8 paths -- runs on pass 1 only. Its only indirect call sites are four import thunks (0x009dd12c/0x009dd150), so its direct-call closure is complete: no vtable edge can escape it [verified]
constexpr uint32_t StrategyAIAgent_AssignFleetsAndIssueOrders = 0x002c16c0;
// thiscall bool __thiscall Game::StrategyAIAgent::IsClaimedByAnotherTask(void* obj) -- RET 4. NAMES IAITask VTABLE SLOT 12 (lane AI2 §10.2). Looks `obj->+4` up in the 8-byte-stride claim registry at agent->+0x2e8..+0x2ec (pairs of {IAITask* owner, int objectId}) and in the 4-byte set at agent->+0x2d8..+0x2dc; if the object is in neither, returns false (free). Otherwise `cur = back(agent->+0x12c /*the task call stack*/)`; with an empty stack or a null top it returns TRUE (claimed). Then at 0x006a8db3: `if (!cur->vt[12]()) return true;` -- and when slot 12 IS set, it returns false (i.e. lets the task take the object) only when the owner exists, `cur->GetTypeId() != owner->GetTypeId()`, and `cur->GetPriority() > owner->GetPriority()`. So SLOT 12 IS A PREEMPTION PERMISSION: 'this task may take an object already claimed by a strictly lower-priority task of a different type'. Default false; five classes set it. Caller 0x006abb00 skips the candidate when this returns true [verified]
constexpr uint32_t StrategyAIAgent_IsClaimedByAnotherTask = 0x002a8d20;
// thiscall int __thiscall Game::StrategyAIAgent::RangePenaltyForTask() -- the ONLY consumer of IAITask vtable slot 13 found in the image, dispatched at 0x00696630 on the `this` receiver. `budget = this ? this->vt[13]() : 15; n = max(1, agent->+0x10->+0x8 - 0x0080da80(player) + 1); if (n < budget) return 0;` else a 7-arm species switch on player->+0x5c through the byte index at 0x006966a8 = [0,0,0,0,2,1,0] and the table at 0x0069669c: species 0,1,2,3,4,6 and out-of-range -> 1000000 (0x000f4240), species 5 (Zuul) -> 0. So slot 13 is a RANGE/HOP BUDGET compared against a count, with a prohibitive penalty past it -- and the Zuul are exempt, a FOURTH independent cross-check on lane AI2's species reading (after Hiver gates, Zuul node-bore and NPC building nothing). The two 'Incoming' defence tasks return INT_MAX from slot 13, so they never take the penalty [verified]
constexpr uint32_t StrategyAIAgent_RangePenaltyForTask = 0x00296620;
// thiscall int __thiscall Game::IAITask::GetPriority_Default() -- 17 bytes: `return AITask_PriorityForType(this->vt[1]() /*GetTypeId*/);`, i.e. the vt[1] dispatch followed by a direct call to the 33-arm table at 0x00691f00. This is what the two tuned GetPriority overrides tail-jump to when their flag bit is CLEAR, so lane AI2's priority table stands with an extra hop in front of it [verified]
constexpr uint32_t AITask_GetPriorityDefaultThunk = 0x00294220;
// data int -- image-initialised value 650 (0x0000028a). AITInvade::GetPriority 0x00683670 is `movzx eax,byte [ecx+4]; not al; test al,1; je +5; jmp 0x00694220; mov eax,ds:0xa1795c; ret` -- so the tunable is returned when BIT 0 OF this->+0x4 IS SET, which is the OPPOSITE of lane AI2's stated `if (!(this->+0x4 & 1))`. It has EXACTLY ONE reference in the whole image (this load) and no writer anywhere: no loader, no CSV path. It is a code constant that happens to live in the writable data section. AITInvade's table priority is 500, so the flag raises it to 650 [verified]
constexpr uint32_t g_AITInvadeUncommittedPriority = 0x0061795c;
// data int -- image-initialised value 750 (0x000002ee), the twin of 0x00a1795c. AITEscortGateInvade::GetPriority 0x006835e0 has the identical shape and the identical inverted polarity: the tunable applies when bit 0 of this->+0x4 IS SET. Exactly one reference in the image, no writer. AITEscortGateInvade's table priority is 400, so the flag raises it to 750 [verified]
constexpr uint32_t g_AITEscortGateInvadeUncommittedPriority = 0x00617960;
// __thiscall void (StrategySim* this, int playerId, float* rate) -- the server-side applier for TurnCommands prologue gate @0x0c. Bumps ModCount as its SECOND instruction, `inc [ecx+4]` @0x00821a84. Called from ApplyTurnCommandBatch+0x616 inside the per-player gate loop that opens at 0x0088fdb0 with esi = block+0x38: the gate tested is `[esi-0x2c]` (= block+0x0c) and the arguments are `[esi-0x34]` (= block+0x04, playerId) and `&[esi-0x30]` (= block+0x08, the rate). THIS NAMES ONE OF LANE W2'S FOUR UNRESOLVED HANDLER EIPs, and it is the one that fires most: because StrategyClient::BuildTurnCommands sets that gate UNCONDITIONALLY, it costs one ModCount bump per SUBMITTED BLOCK regardless of what the player did -- 4 of the 10 command bumps on the reference turn [verified]
constexpr uint32_t TurnCommandApply_ResearchRate = 0x00421a80;
// __thiscall void (StrategySim* this, int playerId, void* element) -- the applier for TurnCommands list 5 (system rates: `{i32 systemId, StarSystem::OutputRates}`). Bumps ModCount @0x0086c3e6. Called from ApplyTurnCommandBatch+0x32d, in the block that walks member +0xa0. Names lane W2's trap EIP 0x0086c3e9. On the reference turn exactly one element was applied, from the one AI player with an empire [verified]
constexpr uint32_t TurnCommandApply_SystemRates = 0x0046c3e0;
// __thiscall void (StrategySim* this, int playerId, void* element) -- the applier for TurnCommands list 3 (build orders: `{i32 ordinal, i32 designId, i32 systemId, i32}`). Bumps ModCount @0x0084946b. Called from ApplyTurnCommandBatch+0x6ef, walking member +0x88. Names lane W2's trap EIP 0x0084946e. This is the site that puts the AI's destroyer into the build queue on the reference pair -- the order lane B6 could not find in the pre-turn save because it does not exist yet when that file is written [verified]
constexpr uint32_t TurnCommandApply_BuildOrder = 0x00449460;
// __thiscall void (StrategySim* this, int playerId, void* element, int flag=1) -- the applier for TurnCommands list 10 (`{i32, i32, counted i32}`). Bumps ModCount @0x0088befe. Called from ApplyTurnCommandBatch+0x8ff with a literal 1 pushed as a fourth argument, walking member +0xdc. Names lane W2's trap EIP 0x0088bf01, which W2 recorded only as 'unresolved (near DestroyFleet)'. WHAT list 10 IS is still open: AI3 has AssignFleetsAndIssueOrders reaching lists 14, 8 and 10 together, the element is an id pair plus an id vector, and the turn that produced it also produced a brand-new fleet -- but that is a shape argument, not a name [verified]
constexpr uint32_t TurnCommandApply_List10 = 0x0048bed0;
// site `inc [ecx+4]` with ecx = the StrategySim -- the ModCount bump for TurnCommands list 14, INLINED into ApplyTurnCommandBatch at +0xabc rather than living in a handler. The containing block starts at 0x00890445 with `add ecx,0x10c` (list 14's member offset) and fetches playerId as `[eax-0x108]`. Lane W2 measured this site firing EXACTLY TWICE on a turn in which exactly ONE fleet was moved (one list-8 bump), which CONFIRMS lane AI2's prediction P1: the AI's fleet-order bridge calls ClientOrder_FleetTask twice, mode 0 then mode 1, and the adder's (fleetId, mode) key makes those two distinct elements, where the UI emits only mode 0 [verified]
constexpr uint32_t TurnCommandApply_List14_Inline = 0x0049046c;
// site `inc [ebx+4]` -- the ModCount bump for TurnCommands prologue gate @0x14 (research target), inlined into ApplyTurnCommandBatch at +0x45a. Guarded by `cmp BYTE [edi-0x24],0` @0x0088fdfa with edi = block+0x38, i.e. the gate at block+0x14; playerId from `[eax-0x34]` and the tech id from `[ecx-0x28]` (= block+0x10). NEVER FIRED in either of lane W2's measured windows, which is consistent with the saves: no player's ResTNm changes on turn 2->3 or 3->4. Lane AI4 predicts it fires exactly THREE times on turn 1->2, once for each of the three AI players, all of which go ResTNm '' -> a named tech [mapped]
constexpr uint32_t TurnCommandApply_ResearchTarget_Inline = 0x0048fe0a;
// site `inc [ebx+4]` -- the ModCount bump for TurnCommands prologue gate @0x20 (research boost), inlined into ApplyTurnCommandBatch at +0xe0c. This is a SEPARATE per-player gate loop that runs AFTER all twenty-seven command lists, opening at 0x008907a6 with esi = block+0x20; the payload `[esi-0x8]`/`[esi-0x4]` is block+0x18 (savings spent) and block+0x1c (a fraction), handed to 0x00820560. So the six prologue gates are applied in THREE separate loops at three different points in the batch, not together [mapped]
constexpr uint32_t TurnCommandApply_ResearchBoost_Inline = 0x004907bc;
// __thiscall void (StrategySim* this, int playerId, void* payload) -- the applier for TurnCommands prologue gate @0x2c (a bool at +0x24 and an int at +0x28). Bumps ModCount @0x00821b93. Called from the LAST loop in ApplyTurnCommandBatch, at +0xe6d, with esi = block+0x24 and the gate tested as `[esi+0x8]`. Never observed set in any save; its AI producer is Process Turn phase 14 (0x006c3510 -> client order method 0x00763910), so an AI turn on which phase 14's own gate opens costs one bump nothing in the corpus has ever paid [mapped]
constexpr uint32_t TurnCommandApply_Group4Gate = 0x00421b90;
// __thiscall void (StrategySim* this, int playerId, void* element) -- the applier for TurnCommands list 1 (`StreamableHelper<ShipDesignDef>` frame plus an int). Bumps ModCount @0x0088291a. Called from ApplyTurnCommandBatch+0x652, walking member +0x70. Lane AI4 predicts this is the site that registers the AI's new hull on the reference pair's turn 1 (`Des[18 "Honor Lance"]`, NumDes 5 -> 6) and that it fires exactly once there; if it does NOT fire, an AI design is not a command at all and is carried some other way -- which is the single most useful falsifier in that prediction [mapped]
constexpr uint32_t TurnCommandApply_NewDesign = 0x00482910;
// site `inc [ebx+4]` -- the ModCount bump for TurnCommands list 7 (colonize: `{i32 shipId, i32}`), inlined into ApplyTurnCommandBatch at +0xc18. The containing block opens at 0x008905b5 with `lea ecx,[eax+0xb8]`, which is why an `add reg,imm` scan for the list member offsets misses it; the per-element mutator is 0x00849280 and does not itself bump. Recorded because the AI's colonize orders are the one command family whose producer is currently mis-attributed: Process Turn phase 32 reaches the colonize order method but runs AFTER cl_EndTurn has latched the client, so its orders are refused (see AIProcessTurn_SubmitLatch) [mapped]
constexpr uint32_t TurnCommandApply_Colonize_Inline = 0x004905c8;
// site `mov BYTE [esi+0x15c],1` inside StrategyClient::EndTurn -- the turn-ended latch. It is set BEFORE the calls to BuildTurnCommands (0x00783cd7) and SendEndTurn (0x00783d0d), and EVERY client order method opens with `if (this->+0x15c) return false`. Since cl_EndTurn is Process Turn phase 28 of 34, PHASES 29-33 ARE DEAD: they run, they call their order methods, and every one is refused -- including phase 32 (0x006af790 -> 0x00769640), which would otherwise issue a colonize order. A reimplementation that ports the phase list literally is one command per AI per turn over on ModCount [verified]
constexpr uint32_t AIProcessTurn_SubmitLatch = 0x00383c59;
// site `cmp DWORD PTR [eax+0x5c],1; jne 0x006c5a28` at the head of Process Turn phase 2 (0x006c57e0), where eax = client->+0x150 (the ClientPlayer) and +0x5c is Species. SPECIES 1 IS HIVER, so the whole of phase 2 -- a cl_Chance draw at 0x006c592a and the client order method 0x00762ca0 at 0x006c59b6, which sets TurnCommands prologue gate @0x3c -- is skipped for every other species. Two consequences: gate @0x3c is AI-only AND Hiver-only, which is why no save has ever carried it (the corpus has no Hiver player); and this is a FIFTH independent cross-check on lane AI2's reading of player+0x5c as Species [verified]
constexpr uint32_t AIProcessTurn_Phase2_HiverGate = 0x002c5813;
// __cdecl The `cl_*` facade entry for the research-target order, reaching client order method 0x00763110 at 0x00578faa. It has EXACTLY ONE caller in the whole image: 0x006caf70, which is AI Process Turn phase 18. So this entry point is AI-only surface, exactly as cl_EndTurn is -- extending lane AI1's P4 list, which was drawn from the order methods rather than the facade. The UI reaches the same order methods directly from 0x005d20a0 / 0x005d2140, so the *method* is shared and the *facade entry* is not [verified]
constexpr uint32_t cl_SetResearchTarget = 0x00178f60;
// __thiscall AI Process Turn phase 18 (called at 0x006cfc82). The only caller of cl_SetResearchTarget 0x00578f60, and it also calls cl_SetResearchRate. This is where an AI player's research target is chosen, and therefore where the ResTNm change lane C3 attributes to 'the AI's own orders, generated during the same turn' originates. Its internal gate -- when the AI does and does not pick a target -- was NOT read; the corpus says it opens on turn 1 for all three AI players and stays shut on turns 2 and 3 [mapped]
constexpr uint32_t AIProcessTurn_Phase18_Research = 0x002caf70;
// thiscall void (CombatResolveContext* this) // THE POST-BATTLE RETREAT PIPELINE. Exactly one caller: CombatResolver_Run 0x007d5af0, unconditionally, at 0x007d5be2. Real body 0x007d5a00..0x007d5abb; the only jcc in it is the operator-new null test whose false arm is a _CxxThrowException. It builds a ~0x2c-byte RetreatContext stack local from the resolver's ctx (rc->+0x00 = ctx->+0x00 = S; rc->+0x04 = ctx->+0x08 = enc; rc->+0x08 = ctx->+0x0c = res; a std::map<int,ServerSystem*> at rc->+0x0c with an operator_new(0x18) head node at rc->+0x10 and _Mysize rc->+0x14; a std::vector<RetreatGroup*> at rc->+0x1c/+0x20/+0x24) and runs SIX unconditional this-calls in a straight line: FUN_0079bb90 (per-player destinations), FUN_0079bcd0 (build groups), FUN_007b0320 (whole vs partial), FUN_00790790 (split partial fleets), FUN_007d5650 (execute; EVENT_FLEET_RETREATED_VIA_TELEPORT), FUN_007a7cd0 (destructor). CORRECTS combat-resolver.md's characterisation of this as 'the per-phase combat pipeline': it is ONE subsystem, retreat, not six combat phases. DRAW-FREE: a 327-function closure (E8 calls plus E9 tail-call thunks) contains zero calls to the four RNG primitives and zero inlined MT tempering immediates [verified] // thiscall void (CombatResolveContext* this) // THE POST-BATTLE RETREAT PIPELINE. Exactly one caller: CombatResolver_Run 0x007d5af0, unconditionally, at 0x007d5be2. Real body 0x007d5a00..0x007d5abb; the only jcc in it is the operator-new null test whose false arm is a _CxxThrowException. It builds a ~0x2c-byte RetreatContext stack local from the resolver's ctx (rc->+0x00 = ctx->+0x00 = S; rc->+0x04 = ctx->+0x08 = enc; rc->+0x08 = ctx->+0x0c = res; a std::map<int,ServerSystem*> at rc->+0x0c with an operator_new(0x18) head node at rc->+0x10 and _Mysize rc->+0x14; a std::vector<RetreatGroup*> at rc->+0x1c/+0x20/+0x24) and runs SIX unconditional this-calls in a straight line: FUN_0079bb90 (per-player destinations), FUN_0079bcd0 (build groups), FUN_007b0320 (whole vs partial), FUN_00790790 (split partial fleets), FUN_007d5650 (execute; EVENT_FLEET_RETREATED_VIA_TELEPORT), FUN_007a7cd0 (destructor). CORRECTS combat-resolver.md's characterisation of this as 'the per-phase combat pipeline': it is ONE subsystem, retreat, not six combat phases. DRAW-FREE: a 327-function closure (E8 calls plus E9 tail-call thunks) contains zero calls to the four RNG primitives and zero inlined MT tempering immediates [verified]
constexpr uint32_t CombatResolve_Retreat = 0x003d5a00; constexpr uint32_t CombatResolve_Retreat = 0x003d5a00;
// thiscall void (RetreatContext* this) // RETREAT PHASE 1. One loop over enc->members (stride 0x44, magic 0x78787879 / sar 5). Per member: FUN_00787210(&enc->+0x1c, enc->+0x0c, member->+0x00 /*ServerPlayer*/, &r1, &r2, &r3), then this->dest[player->PlyrIdx(+0x28)] = the FIRST NON-NULL of (r1, r2, r3) via std::map<int,T*>::operator[] 0x0076bce0. So the per-player retreat destination is: nearest system you own, else nearest system with no hostile presence, else nearest system at all [verified] // thiscall void (RetreatContext* this) // RETREAT PHASE 1. One loop over enc->members (stride 0x44, magic 0x78787879 / sar 5). Per member: FUN_00787210(&enc->+0x1c, enc->+0x0c, member->+0x00 /*ServerPlayer*/, &r1, &r2, &r3), then this->dest[player->PlyrIdx(+0x28)] = the FIRST NON-NULL of (r1, r2, r3) via std::map<int,T*>::operator[] 0x0076bce0. So the per-player retreat destination is: nearest system you own, else nearest system with no hostile presence, else nearest system at all [verified]
@ -1627,6 +1685,8 @@ constexpr uint32_t ServerSystem_OnExploredChanged = 0x003480b0;
constexpr uint32_t StarSystem_PlaceEncounter = 0x003887c0; constexpr uint32_t StarSystem_PlaceEncounter = 0x003887c0;
// fastcall void (StrategyServer* S) // 948 B, tail phase 17. Four passes: (1) walk the per-(system, player) view tree at S+0x228 and drop entries whose player can no longer see the system, then clear the tree and reset S+0x22c = 0; (2) per player x per system, if IsVisibleTo, FUN_0075f550; (3) per player x per system, if IsVisibleTo, ServerSystem_RecordObservation(sys, player, sys->+0x184) -- byte-decoded at 0x007cf7a7..0x007cf7ce, this is the ONLY caller of the NVE writer outside intel sharing; (4) per system x per player, if IsExploredBy and a colony exists, build a PlayerView via 0x00755ab0/0x007561d0 and apply it. Draw-free [mapped] // fastcall void (StrategyServer* S) // 948 B, tail phase 17. Four passes: (1) walk the per-(system, player) view tree at S+0x228 and drop entries whose player can no longer see the system, then clear the tree and reset S+0x22c = 0; (2) per player x per system, if IsVisibleTo, FUN_0075f550; (3) per player x per system, if IsVisibleTo, ServerSystem_RecordObservation(sys, player, sys->+0x184) -- byte-decoded at 0x007cf7a7..0x007cf7ce, this is the ONLY caller of the NVE writer outside intel sharing; (4) per system x per player, if IsExploredBy and a colony exists, build a PlayerView via 0x00755ab0/0x007561d0 and apply it. Draw-free [mapped]
constexpr uint32_t StrategyServer_RebuildPlayerViews = 0x003cf560; constexpr uint32_t StrategyServer_RebuildPlayerViews = 0x003cf560;
// none the three-test gate that guards EVENT_NO_RESEARCH; the post itself is ServerPlayer_ProcessTurn_PostEventNoResearch 0x0089168c. Read byte for byte (ReVa read-memory 0x00891600+160, decoded by hand): cmp [esi+0x294],ebx / jne past the whole block -> test 1, ResT == NULL. Then a 12-byte stack vector is zeroed at [ebp-0x28] and TechTree::CollectResearchedTechs 0x00584e50 is called as ecx = this->TechTree(+0xf4), push 1 (sort), push 0x7fffffff (maxTurn), push [[esi+8]+8] (the SERVER TURN -- the same word the post below hands PostEvent as its turn argument), push &out; then cmp [ebp-0x28],[ebp-0x24] / jne past -> test 2, THE LIST CAME BACK EMPTY, i.e. NO TECH WAS RESEARCHED ON THIS TURN OR LATER. Then ecx = this->TechTree again, call TechTree::FindFirstAvailableTech 0x0057da90, test eax,eax / setne al / cmp al,bl / je past -> test 3, at least one node is in state 2. CORRECTION to the note on ServerPlayer_ProcessTurn_PostEventNoResearch, which reads 0x00584e50 as a ListAvailableTechs and the gate as 'no tech is available': it is TechTree::CollectResearchedTechs (lane T's reading, confirmed here from the call site's argument order) and the gate is about what was RESEARCHED, not about what is available -- test 3 is the availability half. The turn argument is passed as CollectResearchedTechs' minTurn, so the range is [turn, INT_MAX] over node->turnResearched(+0x24) [mapped]
constexpr uint32_t ServerPlayer_ProcessTurn_NoResearchGate = 0x0049162a;
// cdecl void (const char* gamename) /* GameSpy SDK gsAvailable. sprintf("%s.available.gamespy.com"), inet_addr/gethostbyname, UDP socket, sendto port 27900 (htons 0x6cfc) with '\x09\0\0\0\0' + gamename + NUL, len = strlen(gamename)+6. Overridable hostname buffer at 0x00b085b0. Leaves socket = -1 on DNS failure. */ [verified] // cdecl void (const char* gamename) /* GameSpy SDK gsAvailable. sprintf("%s.available.gamespy.com"), inet_addr/gethostbyname, UDP socket, sendto port 27900 (htons 0x6cfc) with '\x09\0\0\0\0' + gamename + NUL, len = strlen(gamename)+6. Overridable hostname buffer at 0x00b085b0. Leaves socket = -1 on DNS failure. */ [verified]
constexpr uint32_t GameSpy_GSIStartAvailableCheck = 0x0000a060; constexpr uint32_t GameSpy_GSIStartAvailableCheck = 0x0000a060;
// cdecl int (void) /* returns 0=waiting 1=available 2=unavailable 3=temporarily-unavailable. Socket==-1 (start failed) => returns 1. Retries once after 2000 ms then returns 1. */ [verified] // cdecl int (void) /* returns 0=waiting 1=available 2=unavailable 3=temporarily-unavailable. Socket==-1 (start failed) => returns 1. Retries once after 2000 ms then returns 1. */ [verified]
@ -1713,6 +1773,40 @@ constexpr uint32_t g_LanScanPortRange = 0x00713c70;
constexpr uint32_t g_GameSpyAvailableHostOverride = 0x007085b0; constexpr uint32_t g_GameSpyAvailableHostOverride = 0x007085b0;
// data const char* /* when non-NULL, replaces "<gamename>.ms<N>.gamespy.com" in SBServerListConnect */ [mapped] // data const char* /* when non-NULL, replaces "<gamename>.ms<N>.gamespy.com" in SBServerListConnect */ [mapped]
constexpr uint32_t g_GameSpyMasterHostOverride = 0x00709440; constexpr uint32_t g_GameSpyMasterHostOverride = 0x00709440;
// thiscall void (ServerSystem* sys) // plain `ret`, REAL END 0x00754b59 -- the body is a loop over group types 0,1,2 whose back edge is at 0x00754b2e (`inc esi; cmp esi,3; jl 0x7543e9`) and lies outside every decompiler `if`, so the function reads as straight-line code if you stop at the first `ret`. Returns immediately when the system has no owner. Per group type: skip when Population::TotalOfType(Pop2, t) <= 0, then skip unless t == 1 -- so only CIVILIANS grow here. Per species: delta = PopGrowthDelta(1, sp); cur = Count(Pop2,1,sp) + Count(pbon2,1,sp); cap = MaxPopGeneric(1, sp, PID, NULL); soft = CivilianSettleLimit(sp); limit = soft < cap ? soft : cap (and soft < cap with cur+delta > soft raises a per-species settle flag); applied = min(delta, limit - cur); haltv[1] with applied > 0 zeroes both. The SYSTEM TOTAL is then clamped into [-50,000,000 (a literal here), POPTYPE[1]+0x08 = 20,000,000] and, when the clamp bit, every entry of the clamp's own sign is rescaled by trunc(applied x (clamped / total)) with NO renormalisation. Write-back is Population::SetCount(Pop2, 1, sp, Count(Pop2,1,sp) + applied) -- note the headroom counted pbon2 and the write does not. THE 20,000,000 CLAMP IS WHAT DECIDES THE VALUE on both reference pairs: the uncapped delta is 7.5x it and the capacity headroom 25x it [verified]
constexpr uint32_t ServerSystem_GrowCivilianPops_G3 = 0x00354220;
// thiscall int64 (ServerSystem* sys, int species) // `ret 4`, ends 0x0074aa2d. Returns 0 without an owner. Otherwise min( MaxPopGeneric(1, sp, PID, &StrategyServer::IdealSuit(server, sp)) , Population::Count(sys->dcs /*+0x104*/, 1, sp) ). The first term is the capacity the colony would have if the planet were perfectly suited; the second is the system's own `dcs` Population, which is on the wire. On Gamma Cephei `dcs` = 1e9 and it is the binding limit, not either capacity [verified]
constexpr uint32_t ServerSystem_CivilianSettleLimit = 0x0034a9a0;
// thiscall int64 (ServerSystem* sys, int groupType, int species, ServerPlayer* p, float* suitOverride) // `ret 0x10`, REAL END 0x0074a6cd -- 0x0074a6d0 is a DIFFERENT function (lane N and E1 both cite 0x0074a6d0 for the capacity-surplus pair; that is the next one along, not this). Returns 0 when species == 4; when the OWNER species' SpeciesDef+0x168[groupType] is not > 0 (which is why Zuul have no civilians); when groupType != 0 and the owner is RebAI; when groupType == 1 and bit 3 of ServerPlayer+0x348[species] is clear; and when groupType == 2 and 0x0082bdf0(p, species) is false. Otherwise ftoi64( (int64)Size x 1e8 x [ hazard x (POPTYPE[t]+0x20 x SpeciesDef(sp)+0x168[t] x (crossSpecies ? SpeciesDef(sp)+0x174[t] : 1)) ] ) via 0x00535eb0, plus the arcology flat bonus 0x0080dd30(p, t), then clamped to the int64 at POPTYPE[t]+0x28 -- see PopTypeTableStaticInit, that clamp is always a no-op -- and finally, for groupType 0 ONLY, scaled by INDSYS_IMPERIAL_POPULATION_MOD when the owner species is 4 [verified]
constexpr uint32_t ServerSystem_MaxPopGeneric_G3 = 0x0034a4a0;
// thiscall float (SpeciesDef* d, int groupType) // `ret 4`, 15 bytes. Returns d->float[0x168 + groupType*4]. The per-species, per-population-group capacity factor -- a DATA FILE value. Its imperial entry for Human is pinned at exactly 1.0 by the corpus (Gamma Cephei's pbon never drains and its Pop never shrinks, which bracket the imperial capacity at Size x 1e8); the civilian entry is bounded below at 0.27 by the observed growth and is otherwise unmeasured [verified]
constexpr uint32_t SpeciesDef_GroupCapacityFactor = 0x0013bb00;
// thiscall float (SpeciesDef* d, int groupType) // `ret 4`, 15 bytes. Returns d->float[0x174 + groupType*4]; applied by MaxPopGeneric only when the system owner's species differs from the population species and is not 4 [verified]
constexpr uint32_t SpeciesDef_GroupCrossSpeciesMod = 0x0013bb20;
// thiscall bool (SpeciesDef* d, int groupType) // `ret 4`. Returns `0.0 < d->float[0x168 + groupType*4]` -- the SAME array SpeciesDef_GroupCapacityFactor reads, so 'this species has this population group' and 'its capacity factor is positive' are one fact. MaxPopGeneric asks it about the system OWNER's species, not the population's [verified]
constexpr uint32_t SpeciesDef_HasPopulationGroup = 0x0013bb40;
// cdecl float (ServerPlayer* p, int species, float suit) // ends 0x00536f8b. Returns 20.0 and logs when p is null; 0.0 when p->RebAI (+0xfc) -- the 'accommodated' case; otherwise min( float32(|float32(StrategyServer::IdealSuit(p->server /*+0x8*/, species) - clamp(suit, 0, 20))|) , p->SuitTol /*+0xb4*/ ). The ideal is the SERVER's per-species baseline (the save's ISsu array), NOT the player's own IdealSuit field [verified]
constexpr uint32_t PopGrowthSuitabilityDistance = 0x00136eb0;
// cdecl int64 (ServerPlayer* p, int groupType, int species, float suit, float factor, int64 pop) // `add esp,0x1c`, 7 dwords of arguments, ends 0x005371ca. Returns 0 unless pop > 0; g = PopGrowthFraction(p, groupType, species, suit, factor); returns 0 when g == 0 (an fucomp against zero, so a NaN g also proceeds); d = ftoi64(g x pop); returns d, or exactly 1 when d == 0 and g is strictly positive, or 0 when d == 0 and g is negative [verified]
constexpr uint32_t PopGrowthDeltaHelper = 0x00137140;
// cdecl void () // the CRT static initialiser for the population-type table at 0x00b104e8, and THE ONLY WRITER of the group-ceiling field. It sets every row's +0x28 to 0xffffffff and +0x2c to 0x7fffffff -- the int64 INT64_MAX -- along with +0x00/+0x04/+0x08/+0x0c to -1/1.0/-1/-1 and the float columns to 1.0. InitPopTypeTable 0x00535ca0 runs later and overwrites +0x00 through +0x20 but NEVER touches +0x24/+0x28/+0x2c. So MaxPopGeneric's group-ceiling clamp is present, always enabled, and always a no-op; a reader who opens only InitPopTypeTable sees zero there and would cap every carrying capacity in the game at nothing [verified]
constexpr uint32_t PopTypeTableStaticInit = 0x005abe20;
// thiscall int (StarShip* sh, bool useAllowance) // `ret 4`, REAL END 0x008151b7 (0x008151ba..0x008151bf is int3). max(0, design->+0xcc - (sh->ConCap /*+0x68*/ + (useAllowance ? design->+0xd0 : 0))). No floating point, no clamp but the floor at zero. Its only caller, RepairShipsInOrbit, always passes 1. UNEXERCISED on the corpus, and that is a measurement rather than an absence: the independent colony's fleet sits over Koa'Vo on both reference pairs and that player's Sav closes exactly with the demand taken as zero [verified]
constexpr uint32_t Ship_RepairCost_G3 = 0x00415180;
// thiscall void (StarShip* sh, int points) // `ret 4`, ends 0x00815221. Does NOTHING unless the ship's cached role word (+0x18, not on the wire) carries bit 0x400000 -- a different bit from the one RepairShipsInOrbit's candidate filter tests, so a ship can be charged points that never reach it. Otherwise sh->ConCap += max(points, 0) and then ConCap = min(max(ConCap, 0), design->+0xcc). THIS IS WHAT NAMES THE TWO FIELDS: ConCap is the construction invested in the hull so far -- not a per-turn capacity, despite the save-format name -- and design+0xcc is its ceiling [verified]
constexpr uint32_t Ship_ApplyRepair_G3 = 0x004151c0;
// cdecl-through-register int (ServerPlayer* owner) /* the system arrives in EBX */ // ends 0x0074615a. CORRECTION to output-turn-path.md, which names this SystemRepairDemandForOwner: it is NOT a repair function. It walks the fleets at the system through the system's own vtable slots 2 and 3, skips a fleet whose owner is not the argument or whose +0x78 byte is clear, and sums 0x0081f8c0 over each ship -- and 0x0081f8c0 gates on the design's CARRIED-POPULATION bit (design->+0xb8 & 0x04000000) and computes GroupIncome over the ship's Population at ship+0x9c. So ComputeOutput's out[6] is the income of population carried in slaver/colony hulls in orbit, which is a slot the engine's BudgetInputs already has, and it feeds no save leaf [verified]
constexpr uint32_t ServerSystem_ShipCarriedPopIncome = 0x003460b0;
// thiscall int (StarShip* sh) // returns 0 unless design->+0xb8 & 0x04000000 (the carried-population bit lane B6 named); otherwise sums GroupIncome over the ship's `hsp` Population at ship+0x9c for group types 0 and 1. Body read to the second GroupIncome call only [unverified]
constexpr uint32_t ShipCarriedPopIncomePerShip = 0x0041f8c0;
// data double 0.009999999776482582 = (double)0.01f -- the savings-interest multiplier ComputeBudget uses at 0x008631ce, then truncated by _ftol2. It is a WIDENED FLOAT, not the exact decimal, and the difference is not cosmetic: a treasury of exactly 50,000 earns 499, not 500. That one money was the whole residual on the human's Sav after civilian growth landed [verified]
constexpr uint32_t g_SavingsInterestRate = 0x005e31c0;
// data double 0.15000000596046448 = (double)0.15f -- the debt-interest multiplier ComputeBudget uses at 0x008631b0. The same constant lane E1 already carries, negated, as the bankruptcy-elimination divisor [verified]
constexpr uint32_t g_DebtInterestRate = 0x005ed188;
// data float 1.5 -- the per-call growth factor PopGrowthDelta 0x00748100 substitutes for 1.0 when the system's GFlags (+0xdc) carries the owner's PlyrIdx bit. It is a BONUS, not a penalty. UNEXERCISED: GFlags is 0 for the growing player on every corpus system [verified]
constexpr uint32_t g_FlaggedSystemGrowthFactor = 0x0061b000;
// data float 0.25 -- POPTYPE[1]+0x04, the civilian growth column, read out of InitPopTypeTable's six-register fxch rotation. Independently re-derived this lane; lane N's whole table is reproduced (imperial +0x04 = 1.0 from 0x009e2ea0's neighbours, civilian +0x10/+0x14 = 0.33f from 0x009f8d48, civilian +0x1c = 0.5, civilian +0x20 = 2.0 from 0x009e5840) [verified]
constexpr uint32_t g_PopTypeCivilianGrowthMod = 0x005e5ac0;
// thiscall int (void* this, int, int) // the ONE callee through which Game::ServerSpyManager vftable 0x00a3073c slot 13 (0x008877b0) reaches the strategic generator. Exactly ONE incoming reference in the image: an unconditional call at 0x00887af2 inside that slot. Real body 0x008408e0..0x00840a59, 370 bytes. Contains three of lane V2's eight draw sites -- Chance 0x00840929, NextInt 0x008409c7, Chance 0x00840a3c -- all loading the generator as [reg+0x16c]. Named for the verified relationship (which slot reaches it, and that it draws), NOT for any semantics: its body was not read. Lane H hooks it with a register-transparent entry counter so that 'the spy path never fired' can be split into 'slot 13 was not entered' and 'slot 13 was entered and gated above this call' [mapped] // thiscall int (void* this, int, int) // the ONE callee through which Game::ServerSpyManager vftable 0x00a3073c slot 13 (0x008877b0) reaches the strategic generator. Exactly ONE incoming reference in the image: an unconditional call at 0x00887af2 inside that slot. Real body 0x008408e0..0x00840a59, 370 bytes. Contains three of lane V2's eight draw sites -- Chance 0x00840929, NextInt 0x008409c7, Chance 0x00840a3c -- all loading the generator as [reg+0x16c]. Named for the verified relationship (which slot reaches it, and that it draws), NOT for any semantics: its body was not read. Lane H hooks it with a register-transparent entry counter so that 'the spy path never fired' can be split into 'slot 13 was not entered' and 'slot 13 was entered and gated above this call' [mapped]
constexpr uint32_t SpyManager_Slot13RngCallee = 0x004408e0; constexpr uint32_t SpyManager_Slot13RngCallee = 0x004408e0;
// thiscall uint (void* fleet, char) // first of the two callees through which Game::ServerTradeManagerImpl vftable 0x00a31b74 slot 13 (0x0088ef80) reaches the strategic generator. Exactly ONE incoming reference: an unconditional call at 0x0088f036. Real body 0x00820ca0..0x00820e53, 436 bytes, containing lane V2's NextFloat site 0x00820e18. Its own body loops the vector at arg+0xa4/+0xa8 testing per-element flags, and its boolean result is what gates the call to TradeManager_Slot13RngCalleeB one instruction later. THE CALLER'S LOOP IS OVER THE FLEETS VECTOR at GetServer()+0x64/+0x68, not over a trade-route list -- which is why 'the tail draws nothing because we have no trade routes' does not explain this slot [mapped] // thiscall uint (void* fleet, char) // first of the two callees through which Game::ServerTradeManagerImpl vftable 0x00a31b74 slot 13 (0x0088ef80) reaches the strategic generator. Exactly ONE incoming reference: an unconditional call at 0x0088f036. Real body 0x00820ca0..0x00820e53, 436 bytes, containing lane V2's NextFloat site 0x00820e18. Its own body loops the vector at arg+0xa4/+0xa8 testing per-element flags, and its boolean result is what gates the call to TradeManager_Slot13RngCalleeB one instruction later. THE CALLER'S LOOP IS OVER THE FLEETS VECTOR at GetServer()+0x64/+0x68, not over a trade-route list -- which is why 'the tail draws nothing because we have no trade routes' does not explain this slot [mapped]
@ -1951,6 +2045,12 @@ constexpr uint32_t FlightPlan_Waypoint_Set = 0x003006e0;
constexpr uint32_t NodeRoute_Construct = 0x002e1b20; constexpr uint32_t NodeRoute_Construct = 0x002e1b20;
// cdecl bool (StarFleet* f) // 90 B. FUN_006fe320(f) returns f->LocID(+0xa0) ONLY when the location's kind tag (+0x14) is 2, a DEEP-SPACE POINT -- never a system. Returns true iff any of the three position components differs by exact IEEE comparison (fucompp, test ah,0x44, jp), no epsilon. OrderFleetMove's opening snap is therefore point-only; combat-retreat-pipeline.md 2.5's 'snaps the fleet's position onto its current system' is corrected here -- a fleet parked at a system is never snapped [verified] // cdecl bool (StarFleet* f) // 90 B. FUN_006fe320(f) returns f->LocID(+0xa0) ONLY when the location's kind tag (+0x14) is 2, a DEEP-SPACE POINT -- never a system. Returns true iff any of the three position components differs by exact IEEE comparison (fucompp, test ah,0x44, jp), no epsilon. OrderFleetMove's opening snap is therefore point-only; combat-retreat-pipeline.md 2.5's 'snaps the fleet's position onto its current system' is corrected here -- a fleet parked at a system is never snapped [verified]
constexpr uint32_t StarFleet_PosDiffersFromPointLocation = 0x0040ec50; constexpr uint32_t StarFleet_PosDiffersFromPointLocation = 0x0040ec50;
// data double 0xBFC3333340000000 = -0.15000000596046448 = (double)(float)-0.15f -- the elimination-limit divisor, loaded by ServerPlayer::UpdateBankruptcyLimits at 0x00818612 with `DD 05` (fld QWORD). It is the negation of g_DebtInterestRate (0x009ed188), and the two are stored separately. Named here because lane N's fix cited the value but not the address [verified]
constexpr uint32_t g_BankruptcyEliminationDivisor = 0x0062ec30;
// data float* -> 0x00b23e28. The config loader's pointer slot for the key BANKRUPTCY_PROTECTION_LIMIT_FACTOR, the same indirection pattern as ADDICTION_INCOME_MOD's (0x00aeca48 -> 0x00aeca44). Read once in the image, by UpdateBankruptcyLimits at 0x0081866e [verified]
constexpr uint32_t PTR_g_BANKRUPTCY_PROTECTION_LIMIT_FACTOR = 0x006edfdc;
// data FLOAT32 -- and the width is the finding. UpdateBankruptcyLimits multiplies it in at 0x00818674 with `D8 09` = FMUL m32fp, i.e. a DWORD, while the elimination divisor two instructions earlier is `DD 05` = FLD m64fp. So whatever decimal the data file carries for this key is narrowed to a 4-byte float before the multiply, and a `double` multiply truncates to a different integer at every boundary -- for a factor of 3.3 that is EVERY max income divisible by ten, starting at maxIncome = 10 (decimal 33.0 -> 33, image 32.99999952 -> 32). The file image of the slot is zero: the loader fills it at run time, so the value itself is a data-file fact, not a binary one. The corpus cannot see the difference -- all seven of its non-zero BnkPr records invert to a max income where the two agree -- so this is an instruction-stream reading, thin-covered by construction (rule 23) [verified]
constexpr uint32_t g_BANKRUPTCY_PROTECTION_LIMIT_FACTOR = 0x00723e28;
// thiscall void __thiscall Game::TurnCommands::Write(Mars::IStream* s) -- the writer for the `Player.<id>.TurnCommands_v5` custom-data block (CDT id -> one CD frame). 764 bytes, no loops of its own. Two halves. (1) A PROLOGUE of six flag-gated groups, each a WriteBool on a member followed, only when that bool is set, by the group's payload; write order is NOT offset order, which is why the offset-sorted layout view cannot be aligned to the wire: bool@0x0c gates f32@0x08 (research rate); bool@0x14 gates i32@0x10 (research target tech); bool@0x20 gates i32@0x18 + f32@0x1c (research boost spend + fraction); bool@0x2c gates bool@0x24 + i32@0x28; bool@0x3c gates f32@0x30,0x34,0x38; bool@0x6c gates a StreamableHelper<Game::CivilianRatios> frame on the member at 0x40. i32@0x04 (player id) is written first and unconditionally. (2) TWENTY-SEVEN std::list<T> members at 0x70..0x1a8, stride 0x0c ({_Myhead, _Mysize, _Alval}), each passed to its own free-function writer as helper(stream, &list). Every one is written unconditionally, so an empty list still costs one zero int: 8 prologue items + 27 zero counts = the 35-item block every no-orders save carries [verified] // thiscall void __thiscall Game::TurnCommands::Write(Mars::IStream* s) -- the writer for the `Player.<id>.TurnCommands_v5` custom-data block (CDT id -> one CD frame). 764 bytes, no loops of its own. Two halves. (1) A PROLOGUE of six flag-gated groups, each a WriteBool on a member followed, only when that bool is set, by the group's payload; write order is NOT offset order, which is why the offset-sorted layout view cannot be aligned to the wire: bool@0x0c gates f32@0x08 (research rate); bool@0x14 gates i32@0x10 (research target tech); bool@0x20 gates i32@0x18 + f32@0x1c (research boost spend + fraction); bool@0x2c gates bool@0x24 + i32@0x28; bool@0x3c gates f32@0x30,0x34,0x38; bool@0x6c gates a StreamableHelper<Game::CivilianRatios> frame on the member at 0x40. i32@0x04 (player id) is written first and unconditionally. (2) TWENTY-SEVEN std::list<T> members at 0x70..0x1a8, stride 0x0c ({_Myhead, _Mysize, _Alval}), each passed to its own free-function writer as helper(stream, &list). Every one is written unconditionally, so an empty list still costs one zero int: 8 prologue items + 27 zero counts = the 35-item block every no-orders save carries [verified]
constexpr uint32_t TurnCommands_Write = 0x00442540; constexpr uint32_t TurnCommands_Write = 0x00442540;
// thiscall void __thiscall Game::TurnCommands::Read(Mars::IStream* s) -- the reader paired with TurnCommands_Write (0x00842540). 1543 bytes; not decompiled by lane Q, listed so the pair is on the record [mapped] // thiscall void __thiscall Game::TurnCommands::Read(Mars::IStream* s) -- the reader paired with TurnCommands_Write (0x00842540). 1543 bytes; not decompiled by lane Q, listed so the pair is on the record [mapped]
@ -2161,6 +2261,98 @@ constexpr uint32_t SVSOCrowDefenders_Write = 0x000f8c90;
constexpr uint32_t SVSOMonitor_Write = 0x000fd810; constexpr uint32_t SVSOMonitor_Write = 0x000fd810;
// thiscall void (Game::SVSODerelict* this, Mars::IStream* s) // NDsn count then a loop of (DsnID, Dwght); NAsg count then a loop of (Eflt, Esys). Two fields per iteration in each, confirmed by the 8-byte element strides [verified] // thiscall void (Game::SVSODerelict* this, Mars::IStream* s) // NDsn count then a loop of (DsnID, Dwght); NAsg count then a loop of (Eflt, Esys). Two fields per iteration in each, confirmed by the 8-byte element strides [verified]
constexpr uint32_t SVSODerelict_Write = 0x000fc2b0; constexpr uint32_t SVSODerelict_Write = 0x000fc2b0;
// site `inc [esi+0x8]` with esi = S -- the FIRST instruction of StrategyServer::ProcessTurn's body bumps ModCount. OBSERVED LIVE by a DR0 4-byte write watchpoint on S+0x8 (lane W2): the trap reports EIP 0x007dc6f3, i.e. the instruction after a 3-byte `inc`, on both measured End Turns. This is the ordering marker for the whole ModCount question -- every command-application bump precedes it and the OnAllCombatDone_Tail bump follows it [verified]
constexpr uint32_t StrategyServer_ProcessTurn_ModCountBump = 0x003dc6f0;
// site ModCount bump at OnAllCombatDone_Tail + 0x2a (trap EIP 0x007d92cd). CORRECTS lane A2's prose, which called it 'OnAllCombatDone_Tail's first instruction': the address A2 predicted is exactly right, the offset is +0x2a and not +0. It is the LAST ModCount write of the turn and it lands AFTER StrategyServer::ProcessTurn has been entered, which refines A2's falsifier (c) -- 'hits after ProcessTurn is entered' is expected for this one site and only this one [verified]
constexpr uint32_t StrategyServer_OnAllCombatDone_Tail_ModCountBump = 0x003d92ca;
// site The turn-number increment: `inc [reg+0xc]` with reg = S, at BeginProcessTurn + 0x2a (trap EIP 0x007d990d). OBSERVED LIVE on DR1 watching S+0xc: EXACTLY ONE write per End Turn, value 3 -> 4 on the second measured turn, against TWELVE writes to S+0x8 in the same window. THIS SETTLES THE NAMING DISPUTE: S+0x8 is a modification counter (lane A2's `StrategySim_off_ModCount`) and S+0xc is the frame/turn number (lane A2's `StrategySim_off_Frame`). addresses.json's `StrategyServer_off_ModCount` (0x8 in the raw frame == S+0xc) carries the name on the wrong word, and lane T's `StrategyServer_off_PhaseCounter` (S+0x8) is ModCount [verified]
constexpr uint32_t StrategyServer_BeginProcessTurn_FrameBump = 0x003d990a;
// site Writes Player.Status(+0x164) = 1 for the LOCAL player only (not for every player), at StrategyServer::ProcessTurn + 0x5ca. Trap EIP 0x007dcc94, so the store is a 10-byte `mov dword ptr [reg+0x164], 1`. Confirms lane T2's static reading of the site AND narrows it: on a two-human-player save only player[0]'s Status moved; player[1]'s did not [verified]
constexpr uint32_t StrategyServer_ProcessTurnTail_PlayerStatusOne = 0x003dcc8a;
// site THE MISSING WRITER OF Player.Status = 4 between tail phase 31 and the post-turn autosave. This address is the instruction AFTER the store (the trap address); the store itself ends here. It lives in StrategyNetworkClient::OnMessage at +0xa15 -- the End-Turn dispatcher -- and it fires AFTER StrategyServer::ProcessTurn has returned and BEFORE the autosave, writing 4 to the local player's Status over the 1 the ProcessTurn tail had just written. CORRECTS lane T2's treaty-turn-stamp.md §3, which read StrategyServer::MarkPlayerTurnEnded 0x00821a40 as 'THE ONLY WRITER OF Player.Status = 4 IN THE IMAGE' and concluded 'there is NO writer between tail phase 31 and the autosave'. There is, and it was watched happening twice on two consecutive End Turns [verified]
constexpr uint32_t StrategyNetworkClient_OnMessage_PlayerStatusFour = 0x00385055;
// site The `p->Status(+0x164) = 4` store inside StrategyServer::MarkPlayerTurnEnded, at +0x35 (trap EIP 0x00821a75, so the store ends here). Called ONCE PER PLAYER at the START of an End Turn, from OnPlayerEndTurn 0x007d9af0 (return address 0x007d9b2a, i.e. the call is at +0x35) -- confirming lane T2's caller list live. It runs BEFORE the pre-turn autosave's successor and before ApplyAllTurnCommands, which is why the `(Autosave EndTurn)` file still carries Status 0 [verified]
constexpr uint32_t StrategyServer_MarkPlayerTurnEnded_StatusStore = 0x00421a75;
// site A ModCount bump observed live but NOT attributable to a named function: the trap EIP is 0x0086c3e9 and the nearest preceding known symbol is ServerTradeManager_ProcessTurn 0x0086b300, +0x10e9 away -- far too far to claim containment. Its return address is 0x0088fce2 (inside StrategySim::ApplyTurnCommandBatch 0x0088f9b0), so it IS one of lane A2's twenty command handlers; only the handler's identity is open. Recorded as an address to disassemble rather than dropped [mapped]
constexpr uint32_t StrategySim_ModCountBump_unresolved_0086c3e6 = 0x0046c3e6;
// site A ModCount bump observed live FOUR times per turn -- the single most frequent command handler on this save. Trap EIP 0x00821a87; nearest known symbol is StrategyServer_MarkPlayerTurnEnded 0x00821a40, but lane T2 measured that function at 60 bytes (ending 0x00821a7c), so this is the NEXT function and MarkPlayerTurnEnded's neighbour, not MarkPlayerTurnEnded. Return address 0x0088ffcb (StrategySim::ApplyTurnCommandBatch) [mapped]
constexpr uint32_t StrategySim_ModCountBump_unresolved_00821a84 = 0x00421a84;
// site A ModCount bump observed live once per turn; trap EIP 0x0084946e, nearest known symbol ServerTradeManagerImpl_vslot11 0x00848570 at +0xefe (not containment). Return address 0x008900a4 (StrategySim::ApplyTurnCommandBatch) [mapped]
constexpr uint32_t StrategySim_ModCountBump_unresolved_0084946b = 0x0044946b;
// site A ModCount bump observed live once per turn; trap EIP 0x0088bf01, nearest known symbol StrategyServer_DestroyFleet 0x0088b980 at +0x581 -- plausibly inside it, but unproven. Return address 0x008902b4 (StrategySim::ApplyTurnCommandBatch), so it is a command handler called from the batch applier rather than an inlined site [mapped]
constexpr uint32_t StrategySim_ModCountBump_unresolved_0088befe = 0x0048befe;
// offset 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 [verified]
constexpr uint32_t ServerSystem_off_AFlags = 0x000000d4;
// offset 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) [verified]
constexpr uint32_t ServerSystem_off_SnLv = 0x00000024;
// offset 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 [verified]
constexpr uint32_t ServerSystem_off_NVO_Myhead = 0x00000274;
// offset 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 [verified]
constexpr uint32_t ServerSystem_off_NVO_Mysize = 0x00000278;
// offset std::map _Myhead for NVE (the sighting record). Write 0x0074a1bb. Confirms lane E3's +0x284/+0x288 [verified]
constexpr uint32_t ServerSystem_off_NVE_Myhead = 0x00000284;
// offset std::map _Mysize for NVE. Write 0x0074a1a1, tag "NVE" [verified]
constexpr uint32_t ServerSystem_off_NVE_Mysize = 0x00000288;
// offset std::map _Myhead for NVs (the per-player PlayerView record). Write 0x0074a27d [verified]
constexpr uint32_t ServerSystem_off_NVs_Myhead = 0x00000294;
// offset std::map _Mysize for NVs. Write 0x0074a263, tag "NVs" [verified]
constexpr uint32_t ServerSystem_off_NVs_Mysize = 0x00000298;
// offset 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 [verified]
constexpr uint32_t ServerSystem_NVO_node_off_TShn = 0x00000012;
// thiscall 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 [verified]
constexpr uint32_t ServerSystem_NVO_operator_index = 0x0075a890;
// thiscall 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." [verified]
constexpr uint32_t ServerSystem_SetVisibleOwner = 0x0075b880;
// offset 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) [verified]
constexpr uint32_t ServerSystem_SetVisibleOwner_TShnStore = 0x0075b961;
// thiscall 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 [verified]
constexpr uint32_t ServerSystem_RecordVisibleOwner = 0x0075bca0;
// thiscall void (ServerSystem* this, ServerPlayer* p) // TShn = Frame, OID = 0 -- 'you now see this system as unowned'. Frame read at 0x0075bc5f [mapped]
constexpr uint32_t ServerSystem_RecordVisibleOwnerNone = 0x0075bc00;
// thiscall 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) [mapped]
constexpr uint32_t ServerSystem_ShareVisibleOwner = 0x0075b9b0;
// thiscall 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 [verified]
constexpr uint32_t ServerSystem_FindVisibleOwner = 0x0074d2e0;
// thiscall int (ServerSystem* this, ServerPlayer* p) // FindVisibleOwner then rec->OID [mapped]
constexpr uint32_t ServerSystem_GetVisibleOwnerId = 0x0074f710;
// thiscall 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 [verified]
constexpr uint32_t ServerSystem_IsKnownTo = 0x00746390;
// thiscall 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 [verified]
constexpr uint32_t ServerSystem_RefreshVisibleOwnerIfKnown = 0x0075bd70;
// fastcall 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 [verified]
constexpr uint32_t SensorPhase_RefreshAllVisibleOwners = 0x00813700;
// thiscall 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 [mapped]
constexpr uint32_t SensorPhase_ComputeContactLevel = 0x00850cf0;
// thiscall void (ServerSystem* this, ServerPlayer* oldOwner, ServerPlayer* newOwner) // Game::ServerSystem vftable 0x00a2044c slot 6; records a visible-owner entry for both parties at 0x0075be77 / 0x0075bf2b [mapped]
constexpr uint32_t ServerSystem_ChangeOwner = 0x0075bde0;
// thiscall 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 [verified]
constexpr uint32_t StrategyServer_OnCommand_SetResearchRate = 0x00821a80;
// thiscall 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` [verified]
constexpr uint32_t StrategyServer_OnCommand_SetSystemRates = 0x0086c3e0;
// thiscall 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` [verified]
constexpr uint32_t StrategyServer_OnCommand_TransferShips = 0x0088bed0;
// thiscall 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` [mapped]
constexpr uint32_t StrategyServer_OnCommand_AddBuildOrder = 0x00849460;
// offset 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" [verified]
constexpr uint32_t StrategyServer_OnCommand_SetResearchProject = 0x0088fe0a;
// thiscall 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 [verified]
constexpr uint32_t StrategyServer_OnCommand_CreateDesign = 0x00882910;
// thiscall 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) [mapped]
constexpr uint32_t BuildQueue_AddOrder = 0x00845260;
// cdecl 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 [mapped]
constexpr uint32_t StrategyServer_CanAddBuildOrder = 0x0083ce50;
// offset 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 [verified]
constexpr uint32_t ServerPlayer_off_ReqCL = 0x000000fd;
// thiscall 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 [verified]
constexpr uint32_t StrategyNetworkServer_AllPlayersTurnEnded = 0x007cfd00;
// thiscall void (StrategyNetworkServer* this) // the ReqCL gate at 0x007bc25c decides which players get an AI client at all; AddAIPlayer 0x007bc333 -> 0x007b8ab0 [verified]
constexpr uint32_t StrategyNetworkServer_SetupAIPlayers = 0x007bc1c0;
// thiscall 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 [verified]
constexpr uint32_t StrategyServer_OnPlayerEndTurn = 0x007d9af0;
// offset 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 [verified]
constexpr uint32_t StrategyServer_ProcessTurn_StatusOneStore = 0x007dcc8a;
// offset 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 [verified]
constexpr uint32_t Game_SNMSetPlayerStatus_vftable = 0x00a2d718;
// thiscall void (StrategyServer* this /*base S*/) // phase 11 of OnAllCombatDone_Tail, called at 0x007d9714 as `mov ecx,esi; call`. 1117 B, three loops. LOOP 1 (0x007ae07a..0x007ae1e2) walks the 0x30-stride Game::NodePath records in the vector at (*(S+0x154))+0x8/+0xc, re-reading _Myfirst/_Mylast every iteration, and per record: (1) NodePath::RemainingLife(r, S->Frame) 0x006e2130, `test eax,eax; jg` -> not expired, NEXT RECORD, NO DRAW; (2) THE DRAW, `mov ecx,[esi+0x16c]; fld [0x009e2ea0] /*0.5f*/; call 0x008e6dd0` = Mars::RNG::Chance(0.5f), exactly one MT word; (3) `test al,al; je` -> roll failed, next record; (4) the 0x20000-fleet scan over S->Fleets (S+0x64/+0x68) calling 0x00703500(fleet,0x20000,0) then 0x0078c360(fleet,npid) and dropping the record when that returns 3; (5) push_back npid into a scratch vector<int>. LOOP 2 collapses each collected line via 0x007a92e0(690 B) then 0x007a4700(2244 B); LOOP 3 posts the decay-stage events through NodePath::DecayStage 0x006e21b0. THE ONLY RNG SITE IN THE WHOLE 1117 BYTES: direct-call sweep to depth 5 over 140 functions from 0x007ae010 against {NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0, Twist 0x00426e00, Seed 0x0049fdf0} yields exactly one hit, 0x007ae010 -> 0x008e6dd0. Neither downstream function draws (138 and 49 functions reached, zero hits) -- caveat: direct calls only, their subtrees contain unresolved indirect sites [verified] // thiscall void (StrategyServer* this /*base S*/) // phase 11 of OnAllCombatDone_Tail, called at 0x007d9714 as `mov ecx,esi; call`. 1117 B, three loops. LOOP 1 (0x007ae07a..0x007ae1e2) walks the 0x30-stride Game::NodePath records in the vector at (*(S+0x154))+0x8/+0xc, re-reading _Myfirst/_Mylast every iteration, and per record: (1) NodePath::RemainingLife(r, S->Frame) 0x006e2130, `test eax,eax; jg` -> not expired, NEXT RECORD, NO DRAW; (2) THE DRAW, `mov ecx,[esi+0x16c]; fld [0x009e2ea0] /*0.5f*/; call 0x008e6dd0` = Mars::RNG::Chance(0.5f), exactly one MT word; (3) `test al,al; je` -> roll failed, next record; (4) the 0x20000-fleet scan over S->Fleets (S+0x64/+0x68) calling 0x00703500(fleet,0x20000,0) then 0x0078c360(fleet,npid) and dropping the record when that returns 3; (5) push_back npid into a scratch vector<int>. LOOP 2 collapses each collected line via 0x007a92e0(690 B) then 0x007a4700(2244 B); LOOP 3 posts the decay-stage events through NodePath::DecayStage 0x006e21b0. THE ONLY RNG SITE IN THE WHOLE 1117 BYTES: direct-call sweep to depth 5 over 140 functions from 0x007ae010 against {NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0, Twist 0x00426e00, Seed 0x0049fdf0} yields exactly one hit, 0x007ae010 -> 0x008e6dd0. Neither downstream function draws (138 and 49 functions reached, zero hits) -- caveat: direct calls only, their subtrees contain unresolved indirect sites [verified]
constexpr uint32_t StrategyServer_NodeLineDecay = 0x003ae010; constexpr uint32_t StrategyServer_NodeLineDecay = 0x003ae010;
// site site, and a CORRECTION to findings/control-flow/combat-done-tail.md §3, which says "the roll is skipped for a line if any fleet with flag 0x20000 is targeting it". IT IS NOT: the fleet scan begins HERE, at 0x007ae0b2, which is 0x1d bytes AFTER the Chance(0.5f) call at 0x007ae0a5 and is reached only when the roll SUCCEEDED (`test al,al; je 0x007ae1e2` at 0x007ae0aa). The scan therefore cannot change the draw count -- it suppresses only the collapse (the 0x007a92e0 / 0x007a4700 pair), never the draw. The straight-line order in loop 1 is: expiry test -> DRAW -> roll gate -> fleet gate -> collect. Lane K's headline claim, one NextFloat per expired node line per turn, survives intact and is now pinned to a concrete expiry formula (NodePath_RemainingLife) [verified] // site site, and a CORRECTION to findings/control-flow/combat-done-tail.md §3, which says "the roll is skipped for a line if any fleet with flag 0x20000 is targeting it". IT IS NOT: the fleet scan begins HERE, at 0x007ae0b2, which is 0x1d bytes AFTER the Chance(0.5f) call at 0x007ae0a5 and is reached only when the roll SUCCEEDED (`test al,al; je 0x007ae1e2` at 0x007ae0aa). The scan therefore cannot change the draw count -- it suppresses only the collapse (the 0x007a92e0 / 0x007a4700 pair), never the draw. The straight-line order in loop 1 is: expiry test -> DRAW -> roll gate -> fleet gate -> collect. Lane K's headline claim, one NextFloat per expired node line per turn, survives intact and is now pinned to a concrete expiry formula (NodePath_RemainingLife) [verified]

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