lane CZ: predictions for the z word, committed before the run

Decodes the route-creation gate first (rule 28 practice 2): TradeSector.tssec
gates which player may source a trade route from a sector, and sector 768's
tssec is 0 for every player on az-turn23-tarka-comraid.sav -- so the briefed
Target 1 cannot be built there at all. The three sector masks are decoded and
reproduce all five sectors x seven players from the save; tscr turns out to be
the CnRad roster, which closes lane AZ's open item.

The workload switches to the alternative: player 0's own Tarka raiders parked
on the node of a sector that sources an AI route, which satisfies G_B3 by war
instead of by ownership.
This commit is contained in:
alex 2026-09-09 09:19:51 -04:00
parent 4b3cc82efb
commit fb5a4148b3
3 changed files with 307 additions and 1 deletions

View file

@ -0,0 +1,256 @@
# `z` — the raid interception draw, measured
- **Type:** control-flow (static decode of the route-creation gate + live measurement of the draw)
- **Owner / date:** lane CZ · 2026-09-09 · guest **VM141** (`sots-re-win10-b`, 192.168.10.143)
- **Status:** *predictions committed; results below the line*
- **Decides:** the `z` term of lane AR's `k + s + z` tail model
(`raid-target-pick-verdict.md` §4.2), never observed through `Slot13RngCalleeB` by any lane;
and lane AZ's committed `k = 2` prediction on `az-turn23-tarka-comraid.sav`
(`raid-intercept-species-word.md` §3.3)
- **Instrument:** `draw_sites` at **`probes=8`** (`shim.cfg.hp8`, unmodified), shim build
`cz-e7e2bd6-dirty-20260909T1312Z`, built in `/srv/re-lab/build/sots-engine-cz` after an
`rm -rf` of the whole tree (rule 24), from lane worktree `~/sots-engine-cz` (branch `wip/cz`,
rule 21). **Never `probes=11`.**
---
## 1. Predictions, committed before the guest was touched (rule 2)
*Everything in §1 was written and committed before the game was launched, before any order was
given and before any counter was read. It is not edited afterwards — only annotated with verdicts
below the line.*
### 1.0 The brief's Target 1 is impossible on this save, and the conjunct that kills it is decoded
My brief names the cheap workload as:
> Give player 0 a trade route sourced at `Ku'Paaka` (system index 17) or `Ka'Palum` (index 26) —
> both player-0 colonies and both members of sector 768, where the AI's `Rho Fleet` is already
> parked. **No fleet movement is required.**
**That state cannot be reached on `az-turn23-tarka-comraid.sav`, and not for want of patience.**
Rule 28 practice 2 says to decode the gate before building the state it names, so I read the route
rebuilder from the instruction stream on the host before launching anything.
`ServerTradeManagerImpl::vslot9` `0x00868060` (already named by lane V2) rebuilds the route vector
every turn. Per source endpoint it calls `0x008648e0`, and that function's **first** act after
initialising its scratch list is:
```
008648e0 TradeManager::BuildRoutesForSource(this, out, from, candidates)
0086494c call 0x8209e0 ; GetNumRoutesSupportedFrom(from, owner, true)
00864954 test eax,eax
00864956 jle 0x864b01 ; <<< <= 0 : NOTHING IS APPENDED, ever
...
00864991 call 0x83a690 ; per-candidate pair predicate
008649a4 call 0x8194a0 ; route already exists?
008649b7 ... operator_new(0x38), vtable 0x00a2d848 -> push_back a TradeRoute
```
and `TradeManager::GetNumRoutesSupportedFrom` `0x008209e0` returns **0** unless
```
008209e0 if (owner == 0) return 0
00820a20 sector = ((TradeSector**)this->+0x0c)[from->+0x4] ; the per-system containing-sector
if (sector == 0) return 0 ; table lane AR found
00820a2b if (owner->Species != 4) {
00820a3b if (!(sector->+0x88 & (1 << owner->+0x28))) return 0 ; <<< TSSEC
}
00820a58 if (from->+0x8 == 0) { ; a SYSTEM source
if (sys->PID != owner) return 0
return min(FUN_00819d20(sys), FUN_00819dd0(sys, ...))
}
... a STATION source: the global at 0x00af0c48
```
`TradeSector+0x88` is the serialised **`tssec`** (`objects/layouts.md`
`Game::ServerTradeSector`). So **a player can only source a trade route from a sector whose
`tssec` bit it holds** — and on `az-turn23-tarka-comraid.sav`, `tssec` reads
| `TradeID` | `tssec` | `tsct` | `tscr` |
|---|---|---|---|
| 736 | **0** | 0 | 127 |
| 752 | **2** | 2 | 127 |
| **768** | **0** | 0 | 127 |
| 784 | **0** | 0 | 127 |
| 800 | **2** | 2 | 127 |
**Sector 768's `tssec` is 0 — for the AI as well as for me.** Player 0's bit is set in no sector
at all. So the route the brief asks me to create cannot be created, and no number of turns fixes
it by itself.
### 1.1 The three sector masks decoded, and verified against the save
Because a conjunct that blocks a briefed workload has to be more than an assertion, I decoded the
writer and checked it against every sector of every player in the save.
**`TradeSector::RecomputeStatusMasks` `0x0088df90` writes all three masks and their `p`-prefixed
previous copies** (`0x0088e031`–`0x0088e058` saves `tssec`/`tsct`/`tscr` into
`ptssec`/`ptsct`/`ptscr`):
| mask | offset | written at | value |
|---|---|---|---|
| `tssec` | `+0x88` | `0x0088e0af` | bit `p` set iff `TradeSector_GetPlayerStatus(this, players[p]) == 0` |
| `tscr` | `+0x90` | `0x0088e733` | bit `p` set iff `players[p]->+0x100 != 0` — the **`CnRad`** bool |
| `tsct` | `+0x8c` | `0x0088e73c` | bit `p` set iff `players[p]->+0xff (CnTrd) != 0` **and** `tssec` bit `p` **and** a local mask OR'd from the triples `FUN_00864670` returns for this sector |
**`tscr` is a player-tech roster, not a sector property.** That closes lane AZ's open item, which
recorded the writer of `TradeSector+0x90` as unfound and refused to guess (`§3.2`,
`raid-intercept-species-word.md`). This save has **seven** players and **every one of them carries
`CnRad = True`**, so the mask is `0b1111111 = 127`. Lane AC's lineage has **eight** players with
exactly one `CnRad` false — the AI at index 1 — so the mask is `0b11111101 = 253`, and 252 is the
same roster one turn before the human's own Commerce Raiding completed. Three values, one rule, and
the "NPC slots default set" reading in `gate-indexed-rng-audit.md` §3.3 and `raid-gate-multiplicity.md`
§4 is retired: the NPC slots are set because **the NPC factions have the tech**, not because they
are NPCs.
**`TradeSector::GetPlayerStatus` `0x00819f00`** — twelve exits, decoded to the next function start:
```
if (p == 0) return 1
if (!p->+0xff) /* CnTrd */ return 5
members = this->+0x78..+0x7c
if (members.empty()) return 4
unexplored = friendly = hostile = 0
for (sys : members) {
if (sys->Dstyd || (sys->vnh && sys->vnex3)) continue ; 0x007469e0
if (sys->vnh) continue
if (!((sys->EFlags >> p->+0x28) & 1)) unexplored++ ; 0x00743880
owner = sys->PID
if (owner) {
if (sys->indi != 0 || !AtWar(p, owner)) friendly++ ; 0x00743fa0 / 0x0080ecb0
else hostile++
}
}
if (unexplored > 0) return 4
if (friendly <= 0) return 2
return (hostile <= friendly) ? 0 : 3
```
`sys->+0xd0` is the serialised **`EFlags`**, `+0x100` is **`PID`**, `+0x1c8` is **`indi`**, and
`FUN_0080ecb0` is `a == b || PlayerAlliances_Relation(a, b) >= 1` — i.e. **not at war** — which
reuses lane T2's relation decode from the other side.
**Checked against the save, all five sectors and all seven players, and it reproduces exactly:**
| sector | predicted `tssec` | observed | player 0 status | AI (32) status |
|---|---|---|---|---|
| 736 | 0 | **0** | 4 (3 unexplored; friendly 3, hostile 0) | 4 (5 unexplored; friendly 0) |
| 752 | 2 | **2** | 4 (5 unexplored; **friendly 0, hostile 5**) | **0** (friendly 5, hostile 0) |
| 768 | 0 | **0** | 4 (5 unexplored; **friendly 3, hostile 9**) | 4 (2 unexplored) |
| 784 | 0 | **0** | 4 (3 unexplored; friendly 0, hostile 2) | 4 (1 unexplored) |
| 800 | 2 | **2** | 4 (2 unexplored; friendly 0, hostile 1) | **0** (friendly 1, hostile 0) |
Five sectors, seven players, thirty-five predicates, no disagreement.
**And it says exactly how dead Target 1 is.** In sector 768 player 0 holds **3** systems against the
AI's **9**. Even if I explored all five systems I have not surveyed there, `hostile (9) > friendly
(3)` returns **3**, not 0. The same arithmetic kills 752 (0 vs 5), 784 (0 vs 2) and 800 (0 vs 1).
**The only sector player 0 could ever qualify in is 736** — friendly 3, hostile 0, and only three
unexplored systems, all of them unowned and empty — and no AI fleet is parked at 736's node
(`tsnumflt = 0`). So the briefed "no fleet movement is required" route does not exist here, and the
cheapest state that reaches `z` is the one the brief lists as its **alternative**.
### 1.2 The workload I will actually build, named from the conjunct that survives
`G_B3` needs *a route owned by someone the raider is at war with*. Target 1 tried to satisfy it by
changing the **route**; I will satisfy it by changing the **raider**.
> **Park player 0's own Tarka raiders on the node of a sector that sources an AI route.**
Read from the save rather than inherited (the routes are `trfr = 4 → 9`, `trfr = 27 → 15`,
`trfr = 27 → 1`, all `tro = 32`, and `index = id/16 − 3` maps 4 to `Ke'Sothram` in sector **752** and
27 to `Keo'Va` in sector **800**):
| conjunct | how it is satisfied | evidence |
|---|---|---|
| G0–G2 fleet at a trade-sector node | order fleets to sector 752's node at `[0, 0, 8.0]` | AR's recipe |
| G3b raider's bit in `tscr` | `tscr = 127`, bit 0 set — player 0 has `CnRad = True` | §1.1 |
| G3c `FtFlg & 0x800` where armed | **not armed**: `tsct = 2` is the AI's bit, so `TradeSector_PlayerTradesHere(752, player 0)` is false | §1.1 |
| G4 `Pos` bit-equal | arrival at the node | AR §3.2 |
| `B` `flag` arm dead | `PlayerTradesHere` false ⇒ `FUN_0080ecf0` false ⇒ the diplomacy arm | AG §3.3 |
| G_B1b route sourced in this sector | `trfr = 4` lies in 752 | above |
| **G_B3 relation is 0 = war** | route owner is PID 32; player 0 and 32 both carry `AL = NA = CF = 0` | §1.4 |
| `SC1` route owner not Hiver | PID 32 is **species 2** | AZ §3.2 |
| `SC2` raider crew species not Hiver | player 0 is **species 2**, and `0x0081a2d0` returns the owner's species for a non-`_NPC` owner | AZ §1.2 |
| `SC3` `SpeciesDef[2]+0x144 == 0` | the flag word is written only for species 0 and 5 | AR §4.2 |
**Every conjunct is satisfied by a fleet move I control.** The build site is chosen from the save's
own geometry — distance from each player-0 system to sector 752's node, all three floats:
| player-0 system | idx | to 752's node | to 800's node |
|---|---|---|---|
| **`Ke'Rassak`** | **20** | **7.36** | 13.41 |
| `Kuo’Soro` | 16 | 9.85 | 15.08 |
| `Ku’Paaka` | 17 | 9.99 | 17.21 |
| `Kao’Thraal` | 0 | 12.31 | 17.33 |
| `Kea’Donum` | 3 | 13.16 | 17.15 |
| `Ka'Palum` | 26 | 13.93 | 18.24 |
A Tarka stock `Extended Range` destroyer has **Range 20.0** (lane AZ, from this game's own Build
screen), so **every one of the six is in range of 752's node** and five are in range of 800's.
Design `Extended Range` (`DesID 624`) is already in player 0's design list, and all six systems have
an **empty build queue**.
**The roll is per fleet, so I will build in several systems rather than splitting one stack.** A
fleet's odds are `0.05 + 0.01·nShips` for destroyers, doubled if a freighter shares the sector
(AC's read, undecided) — and **sector 752 holds the AI's `Freighters` fleet 3248**, so the doubling
is live here if it is live at all. Four two-ship fleets roll `4 × 0.07 = 0.28` per turn, or `0.56`
doubled; one eight-ship stack rolls `0.13`. Building two ships in each of four systems produces four
fleets **with no splitting at all**, which removes the right-click-menu step that cost lane AR time
and the staging error that cost lane AZ a 16-ship fleet.
**And I will not stage through a member system.** Lane AZ ordered a fleet to `Ku'Valt`, a sector-752
*member* system that is an AI colony, and lost sixteen destroyers to a real battle. The node is the
destination; nothing else is.
### 1.3 The numbered predictions
| # | prediction |
|---|---|
| **P0** | **`k = 2` on `az-turn23-tarka-comraid.sav` with the player owning no fleets** — lane AZ's committed prediction, taken as briefed. `Slot13RngCalleeA` is entered exactly **twice** on the first End Turn and `NextFloat 0x00820e18` reads `calls == words == 2`. The two are `Rho Fleet` (274, `LocID 768`, `FtFlg 0x80c`) and `Escorts` (3344, `LocID 752`, `FtFlg 0xc00`); `Freighters` 3248 and 4160 and `Nu Fleet IV` 1490 are rejected at G3c for lacking `FtFlg & 0x800` in a sector whose `tsct` bit is their owner's. Re-read from the save by this lane, not inherited |
| **P1** | **Neither AI raider can ever cost a word past `A`.** `Rho Fleet` stands on 768, which sources **no** route (`trfr ∈ {4, 27, 27}` lie in 752 and 800), so its `B` returns at **G_B4**. `Escorts` stands on 752, which sources one route — owned by its own player 32, so `PlayerAlliances_Relation` returns **3 for self**, `B` rejects at **G_B3** and the list empties, also G_B4. Two fleets, two different failed conjuncts, both at zero words |
| **P2** | **`tssec` gates route creation, and sector 768's is 0 for everyone**, so the briefed Target 1 is unreachable. Decoded at `0x00820a3b`; the mask's writer is `0x0088e0af`; the predicate is `TradeSector_GetPlayerStatus 0x00819f00`; and it reproduces all five sectors × seven players from the save (§1.1) |
| **P3** | **`tscr` is the `CnRad` roster.** Bit `p` is `players[p]->CnRad`, written at `0x0088e733`. 127 here (7 players, all true), 253 in the AC lineage (8 players, the AI false), 252 the turn before the human's tech landed. AZ's "the writer is unfound" is closed and the "NPC slots default set" reading is retired |
| **P4** | **`z` draws.** On the first turn one of my parked Tarka fleets succeeds, `draw_sites` shows **`NextFloat 0x00820c1b` `calls == words == 1`**, and the tail costs **`k + 2`** rather than `k + 1`. This is the first observation of that site through `Slot13RngCalleeB` in the campaign |
| **P5** | **The attribution is proved at the site, not at the bracket** (rule 23's corollary). A `0x00820c1b` row belongs to `B` on a turn where **`NextInt 0x008939ee` reads 0** — lane AZ's P6, whose static half is an image-wide `E8 rel32` scan finding exactly two callers of `FUN_00820af0`, with `0x0082cf65` reachable only through `CreateRaidEncounter`'s straight-line path past `0x008939ee`. If `0x008939ee` is non-zero on a turn I want to read, I will call that turn ambiguous and use another, not argue |
| **P6** | **The general model is `k + s + z` with `z = s`**, i.e. `OnAllCombatDone_Tail` costs **`k + 2s`** on this state, where `s` counts successes **by my fleets only** (P1 says the AI's two contribute nothing past `A`). Bracket residual **0** on every turn |
| **P7** | **`NextInt 0x0088b613` costs exactly one word per success.** Sector 752 sources one route, so `n = 1`, `bound = 0`, the mask smear gives 0 and `RNG_NextInt` cannot reject (AG §3.3, read in full). On sector 800, `n = 2`, `bound = 1`, mask 1, also exactly one. **If the AI's route set changes under me so that a target sector sources three or more, the rejection loop goes live and I will report `words > calls` rather than hide it** |
| **P8** | **A success displaces the raider off the node** (`fleet->Pos = out` at `0x0088b672`, AR §4.3), so `k` **falls by one** on the turn after each of my successes, and each fleet contributes at most one success per parking. The same applies to the AI's `Escorts` on 752 — if it succeeds, `k` drops to 1 + mine even before I arrive |
| **P9** | **The `hooks=off` control will NOT agree with itself.** Rule 26 corollary (c)'s screen fires: `Player[32]` (`Fane Kona`, `PlyrIdx 1`, `NumOwn 17`, `NumDes 58`, an AI player that runs) enters the turn with **`ResTNm == ''`**, which is a research-pick turn. I expect variation confined to `Player[32]`'s `TechTree` leaves and the derived `/Summary/Checksum`, and I expect **`/Sim/trdmgr`, the five `Trade` records and `/Sim/fleets` to be sub-tree identical** across processes — which would leave `k`, the route set and the raid arm reproducible even though the whole-save hash is not. **I am predicting the sub-trees my measurement depends on, in advance, so that "the control varied" cannot be turned into "the measurement is fine" after the fact.** Note (c)'s own restatement: `ResTNm == ''` predicts that a pick is *made*, not that it *varies*, so an agreeing control would not surprise me either |
| **P10** | **The AI-client roster is `{32, 496, 512}`** — three clients out of six non-human players — by lane BP's rule that the NPC factions at net ids 528 and above get no client. `528 Peacekeeper Enforcer`, `544 Von Neumann` and `560 Independent Colony` carry `ResTNm == ''` **and** `NumOwn == 0` **and** no client, so their signature is inert. **This is an inference from another save and I will label it as one unless I measure it** with an `aiseed=log` process |
| **P11** | **Not settled by this lane, predicted in advance** (rule 15): the freighter doubling of the raid odds (one or two successes cannot separate 0.07 from 0.14); `design+0x12c > 1`; `bestScale`; the `0x0088b613` rejection loop at `n >= 3`; whether `SC2` (a Hiver raider) is reachable at all; and the leaf semantics of `FUN_00819d20` / `FUN_00819dd0`, the two route-count sources `GetNumRoutesSupportedFrom` takes the `min` of |
### 1.4 The falsifier, stated as the sentence I will have to write
> **If one of my parked Tarka fleets succeeds — `Slot13RngCalleeB` is entered, `NextInt 0x0088b613`
> fires for one word, `0x008939ee` reads 0 — and there is still no `NextFloat 0x00820c1b` row,
> then `SpeciesDef+0x144` is not the whole gate**, and the two short-circuits ahead of it become
> the live suspects: `owner->+0x5c == 1` on the route owner (checkable in the save: PID 32 carries
> `Species 2`) and `FUN_0081a2d0(fleet) == 1` on the raider's crew species (checkable the same way:
> player 0 carries `Species 2`). If both check out, the static decode that three lanes have now
> read the same way is retired, and the interesting question becomes what `0x0053baf0` actually
> reads.
And the other direction, which is the more likely negative:
> **If `Slot13RngCalleeB` is never entered with a non-empty candidate list**, I will report *which*
> conjunct failed by reading it out of the turn's own autosave — `LocID`, all three floats of `Pos`
> against the sector's, `FtFlg`, the sector's `tsct`/`tscr`, and the live `trfr` set — and not as
> "the species word did not fire". A negative without a named conjunct is "not reached in the states
> measured", which is a different sentence (rule 28 practice 1).
### 1.5 What could go wrong with the workload, and the symptom of each
| way it fails | symptom |
|---|---|
| the AI's route set moves and 752 stops sourcing a route | `trfr` in the per-turn autosave no longer contains 4; `B` entered, 0 words, G_B4 — mitigated by also sending fleets to 800's node |
| a sector I target starts sourcing three or more routes | `0x0088b613` `words > calls`; reported, not hidden (P7) |
| the node cannot be aimed at from the default camera | AZ's §6: the Move-mode hover snaps to the nearest **system** over >100 px. Sector 752's node is 2.4 ly from `Ke'Sothram`. **`rdrag` rotates the star map** — rotate first, then aim, and confirm from the hover readout (`Sector 2 / Range / ETA`) before committing |
| a fleet is intercepted in transit | it dies; the arm shrinks. Fleets are ordered **to the node**, never through a member system (AZ's §6 cause 1) |
| an encounter query blocks a scripted End Turn | the driver polls the **autosave mtime**, never a button colour, and **stalls with a screenshot** rather than clicking blind. AZ lost a 16-ship fleet to a blind `Auto Resolve Peacefully` sequence, and `Auto Resolve Peacefully` means "I commit no ships", not "no combat occurs" |
| the control varies | expected (P9). Localise by sub-tree with `state_checksum.py --tree` and state which leaves the measurement depends on |
---
*Results, and the verdicts, follow below. Nothing above this line is edited after the run.*

View file

@ -0,0 +1,36 @@
{
"entries": [
{
"name": "TradeSector_RecomputeStatusMasks",
"addr": "0x0088df90",
"convention": "thiscall",
"prototype": "void TradeSector::RecomputeStatusMasks(TradeSector* this, bool full) -- the writer of all three of the serialised sector masks, and of their `p`-prefixed previous copies. At 0x0088e031-0x0088e058 it copies tssec/tsct/tscr (+0x88/+0x8c/+0x90) into ptssec/ptsct/ptscr (+0x94/+0x98/+0x9c). Then: (1) tssec, written at 0x0088e0af, is the mask over players p of `TradeSector_GetPlayerStatus(this, players[p]) == 0`; (2) tscr, written at 0x0088e733, is the mask over players p of `players[p]->+0x100 != 0` -- the `CnRad` (Commerce Raiding) bool, so tscr is a PLAYER-TECH ROSTER and not a sector property at all; (3) tsct, written at 0x0088e73c, is the mask over players p of `players[p]->+0xff (CnTrd) != 0 AND (tssec >> p) & 1 AND (localMask >> p) & 1`, where localMask is OR'd over the triples FUN_00864670 returns for this sector. Players are StrategyServer+0x50..+0x54 and the bit index is the player's own index field, matching the news-post loop in Slot13RngCalleeB",
"status": "verified",
"source": "findings/control-flow/raid-intercept-z-word.md (lane CZ 2026-09-09); disassembled from dumps/sots.exe, cross-checked live against az-turn23-tarka-comraid.sav (tscr=127 over seven players all carrying CnRad=True) and against the 24-save AC lineage (tscr=253 over eight players with exactly one CnRad=False)"
},
{
"name": "TradeSector_GetPlayerStatus",
"addr": "0x00819f00",
"convention": "thiscall",
"prototype": "int TradeSector::GetPlayerStatus(TradeSector* this, ServerPlayer* p) -- returns a status code for one player in one sector; 0 means the player may trade here and is the value TradeSector_RecomputeStatusMasks turns into a tssec bit. 1 = p is null; 5 = p->+0xff (CnTrd) is false; 4 = the sector's member-system vector (this+0x78..+0x7c) is empty, or any member system fails FUN_00743880(sys, p); 2 = no member system counted as friendly; 3 = hostile-owned members outnumber friendly ones; 0 otherwise. Per member system: FUN_007469e0(sys) and sys->+0xc6 skip it; owner = sys->+0x100; FUN_00743fa0(sys) or FUN_0080ecb0(p, owner) counts it friendly, else hostile",
"status": "verified",
"source": "findings/control-flow/raid-intercept-z-word.md (lane CZ 2026-09-09); disassembled to the next function start from dumps/sots.exe"
},
{
"name": "TradeManager_GetNumRoutesSupportedFrom",
"addr": "0x008209e0",
"convention": "thiscall",
"prototype": "int TradeManager::GetNumRoutesSupportedFrom(TradeManager* this, TradeEndpoint* ep, ServerPlayer* owner, bool ignoreInfra) -- the top-of-loop gate in the route rebuilder's per-source helper (called at 0x0086494c, whose caller returns immediately when this is <= 0). Returns 0 unless: owner is non-null; the containing-sector table this->+0x0c indexed by ep->+0x4 (a system index) is non-null; and, for an owner whose Species != 4, the containing sector's tssec (+0x88) has the owner's index bit set. With ep->+0x8 == 0 the source is a SYSTEM and the system's own owner (sys->+0x100) must be this player, and the answer is min(FUN_00819d20(sys), FUN_00819dd0(sys, ignoreInfra ? 1.0f : sys->+0x88)); with ep->+0x8 != 0 the source is a station looked up in StrategyServer+0x80 and the answer is the global at 0x00af0c48",
"status": "verified",
"source": "findings/control-flow/raid-intercept-z-word.md (lane CZ 2026-09-09); disassembled from dumps/sots.exe"
},
{
"name": "TradeManager_BuildRoutesForSource",
"addr": "0x008648e0",
"convention": "thiscall",
"prototype": "void TradeManager::BuildRoutesForSource(TradeManager* this, vector<TradeRoute>* out, TradeEndpoint* from, vector<TradeEndpoint>* candidates) -- called once per source endpoint by ServerTradeManagerImpl_vslot9 (0x00868060). Returns with nothing appended when TradeManager_GetNumRoutesSupportedFrom(from, owner, true) <= 0 at 0x00864956. Otherwise, for each candidate `to`: FUN_0083a690(from, to) must pass and FUN_008194a0(&this->+0x3c, from, to) must find no existing route, and only then is a TradeRoute (vtable 0x00a2d848, 0x38 bytes) constructed and appended",
"status": "verified",
"source": "findings/control-flow/raid-intercept-z-word.md (lane CZ 2026-09-09); disassembled from dumps/sots.exe"
}
]
}

View file

@ -1,5 +1,5 @@
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
// Source: sots-re ghidra/addresses.json @ 2ad6652, generated 2026-09-09 by tools/gen_addresses.py
// Source: sots-re ghidra/addresses.json @ 77b5dd0, generated 2026-09-09 by tools/gen_addresses.py
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
#pragma once
#include <cstdint>
@ -1325,6 +1325,20 @@ constexpr uint32_t ServerSpyManager_DetectionRoll_DrawSite = 0x00487c8a;
constexpr uint32_t SpyCraft_ResetMission = 0x00438070;
// data void*[7], the switch jump table SpeciesDef_InitTable 0x005453a0 dispatches through at 0x005453eb (`jmp [eax*4+0x545b60]`) to choose the `Species/%s/...` data-path prefix. READ AS BYTES, the seven entries are 0x005453f2 "Human", 0x005453f9 "Hiver", 0x00545400 "Tarkas", 0x00545407 "Liir", 0x0054541c "_NPC", 0x0054540e "Zuul", 0x00545415 "Morrigi" -- so the species enum is 0 Human, 1 HIVER, 2 TARKAS, 3 LIIR, 4 _NPC, 5 Zuul, 6 MORRIGI. NOTE the case bodies are NOT laid out in case order (index 4 jumps PAST index 5's body), so reading the disassembly top to bottom gives the wrong enum; only the table bytes settle it. SpeciesDef_Get 0x00545cc0 bounds-checks k <= 6, so there are exactly seven. This confirms lane AR's species 0 = Human / species 5 = Zuul from an independent direction and names 1, 2, 3, 4 and 6 for the first time [verified]
constexpr uint32_t SpeciesDef_NameJumpTable = 0x00145b60;
// thiscall void __thiscall Game::StrategyAIAgent::ClaimShipsOfFleet(StrategyAIAgent* agent /*ecx*/, StarFleet* fleet /*stack*/) -- EXACTLY ONE CALLER in the whole image, 0x006c1735, the FIRST loop of AssignFleetsAndIssueOrders (0x006c1730-0x006c1740), which walks `param_4` once at function entry before any pass gate. Body: n = ([fleet+0xa8] - [fleet+0xa4]) (the ships vector, StarFleet_off_Ships), and for each StarShip* s in it, push_back(s ? [s+4] : 0) onto the agent's int vector at agent+0x2d8 -- a linear scan of that vector first (0x006a42e0) so an id already present is not added twice, then the MSVC push_back grow path (capacity check against 0x3ffffffe, 0x00483410 to reallocate). `[s+4]` is StarShip's id, the same word StarFleet_off_Id names on a fleet. BECAUSE IT HAS ONE CALLER AND IS CALLED ONCE PER ELEMENT AT THE HEAD OF THE WALK, a detour on it records the acquired-fleet vector in visit order with no return-address filter and no mid-function patch: it is the whole instrument of lane BU's stage-2 probe [verified]
constexpr uint32_t StrategyAIAgent_ClaimShipsOfFleet = 0x002a4290;
// thiscall void __thiscall Game::StrategyAIAgent::ReleaseShipsOfFleet(StrategyAIAgent* agent /*ecx*/, StarFleet* fleet /*stack*/) -- RET 4. EXACTLY ONE CALLER, 0x006c1765, the second loop of AssignFleetsAndIssueOrders (0x006c1760-0x006c1770), which is entered at 0x006c1753 AFTER the element loop has walked the vector to exhaustion (`jmp 0x6c1753` at 0x006c21c4). The exact inverse of ClaimShipsOfFleet: for each StarShip* s in the fleet's ships vector it finds [s+4] in the agent's int vector at agent+0x2d8, memmoves the tail down (import 0x009dd30c) and does `[agent+0x2dc] -= 4`. Same one-call-per-element shape over the SAME vector in the SAME order, so it is a free second witness of the visit order taken after the loop rather than before it [verified]
constexpr uint32_t StrategyAIAgent_ReleaseShipsOfFleet = 0x0029da10;
// offset std::vector<int> claimed ship ids (_Myfirst @+0x2d8, _Mylast @+0x2dc, _Myend @+0x2e0). Read and written ONLY by the claim/release pair above, both of which are exclusive to AssignFleetsAndIssueOrders: the fleets acquired for a task have their ships' ids parked here for the duration of the assignment walk and removed when it is done. Enumerated from the push_back grow path at 0x006a4338-0x006a4356 (which reads +0x2e0 as _Myend via `mov ecx,[esi+8]` with esi = agent+0x2d8) and the erase path at 0x0069da63-0x0069da81 [verified]
constexpr uint32_t StrategyAIAgent_off_ClaimedShipIds = 0x000002d8;
// thiscall void TradeSector::RecomputeStatusMasks(TradeSector* this, bool full) -- the writer of all three of the serialised sector masks, and of their `p`-prefixed previous copies. At 0x0088e031-0x0088e058 it copies tssec/tsct/tscr (+0x88/+0x8c/+0x90) into ptssec/ptsct/ptscr (+0x94/+0x98/+0x9c). Then: (1) tssec, written at 0x0088e0af, is the mask over players p of `TradeSector_GetPlayerStatus(this, players[p]) == 0`; (2) tscr, written at 0x0088e733, is the mask over players p of `players[p]->+0x100 != 0` -- the `CnRad` (Commerce Raiding) bool, so tscr is a PLAYER-TECH ROSTER and not a sector property at all; (3) tsct, written at 0x0088e73c, is the mask over players p of `players[p]->+0xff (CnTrd) != 0 AND (tssec >> p) & 1 AND (localMask >> p) & 1`, where localMask is OR'd over the triples FUN_00864670 returns for this sector. Players are StrategyServer+0x50..+0x54 and the bit index is the player's own index field, matching the news-post loop in Slot13RngCalleeB [verified]
constexpr uint32_t TradeSector_RecomputeStatusMasks = 0x0048df90;
// thiscall int TradeSector::GetPlayerStatus(TradeSector* this, ServerPlayer* p) -- returns a status code for one player in one sector; 0 means the player may trade here and is the value TradeSector_RecomputeStatusMasks turns into a tssec bit. 1 = p is null; 5 = p->+0xff (CnTrd) is false; 4 = the sector's member-system vector (this+0x78..+0x7c) is empty, or any member system fails FUN_00743880(sys, p); 2 = no member system counted as friendly; 3 = hostile-owned members outnumber friendly ones; 0 otherwise. Per member system: FUN_007469e0(sys) and sys->+0xc6 skip it; owner = sys->+0x100; FUN_00743fa0(sys) or FUN_0080ecb0(p, owner) counts it friendly, else hostile [verified]
constexpr uint32_t TradeSector_GetPlayerStatus = 0x00419f00;
// thiscall int TradeManager::GetNumRoutesSupportedFrom(TradeManager* this, TradeEndpoint* ep, ServerPlayer* owner, bool ignoreInfra) -- the top-of-loop gate in the route rebuilder's per-source helper (called at 0x0086494c, whose caller returns immediately when this is <= 0). Returns 0 unless: owner is non-null; the containing-sector table this->+0x0c indexed by ep->+0x4 (a system index) is non-null; and, for an owner whose Species != 4, the containing sector's tssec (+0x88) has the owner's index bit set. With ep->+0x8 == 0 the source is a SYSTEM and the system's own owner (sys->+0x100) must be this player, and the answer is min(FUN_00819d20(sys), FUN_00819dd0(sys, ignoreInfra ? 1.0f : sys->+0x88)); with ep->+0x8 != 0 the source is a station looked up in StrategyServer+0x80 and the answer is the global at 0x00af0c48 [verified]
constexpr uint32_t TradeManager_GetNumRoutesSupportedFrom = 0x004209e0;
// thiscall void TradeManager::BuildRoutesForSource(TradeManager* this, vector<TradeRoute>* out, TradeEndpoint* from, vector<TradeEndpoint>* candidates) -- called once per source endpoint by ServerTradeManagerImpl_vslot9 (0x00868060). Returns with nothing appended when TradeManager_GetNumRoutesSupportedFrom(from, owner, true) <= 0 at 0x00864956. Otherwise, for each candidate `to`: FUN_0083a690(from, to) must pass and FUN_008194a0(&this->+0x3c, from, to) must find no existing route, and only then is a TradeRoute (vtable 0x00a2d848, 0x38 bytes) constructed and appended [verified]
constexpr uint32_t TradeManager_BuildRoutesForSource = 0x004648e0;
// thiscall void (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) // the body of every `Player.<id>.AIAgent` CD block. 36 wire items, NO conditionals: the only `if` the decompiler shows around `lnat` is an inlined std::vector destructor whose operator delete is marked noreturn, and both paths converge at 0x006c72e8. The agent object is *(this+4) [verified]
constexpr uint32_t Game_StrategyAIAgent_Streamable_Write = 0x002c6f00;
// thiscall bool (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) [verified]