# What an AI turn emits, and what each command costs in `ModCount` Lane AI4, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs. Static reading of the image plus the corpus saves and **lane W2's watchpoint log**; no VM time of my own. Continues `ai-stepping-and-passes.md` (AI3), `ai-task-system.md` (AI2), `ai-turn-logic.md` (AI1) on the emission side, and `alliance-mask-and-modcount.md` (A2) / `watchpoints-modcount-status.md` (W2) on the counting side. **The join between those two halves is what this lane is.** Addresses: `ghidra/addresses.d/lane-ai4.json` (validated with `tools/gen_addresses.py` to a scratch path; 1,138 → 1,152, no duplicate names). --- ## 0. The three answers, up front **1. `ModCount`'s per-command cost is a property of the command's list, and there is a clean boundary at sixteen: applying an element of lists 1-16 bumps the counter, applying an element of lists 17-27 does not.** `ApplyTurnCommandBatch 0x0088f9b0` is not a loop over players containing a loop over lists — it is a **flat, hand-written sequence of twenty-seven per-list loops** (`for each player block: for each element: apply`), followed by three per-player gate loops, in a fixed order that is neither list order nor offset order. Reading the sequence out and asking, at each of the 27 list sites and 6 gate sites, whether a `ModCount` bump is on that path gives a complete cost table (§1). A2's rule "one bump per command applied" is right in shape and **wrong in detail**: applying a list-17…27 element applies the command and bumps nothing, and one of the six prologue gates is free as well. **2. Lane W2's four unnamed handler EIPs are all named, and so are the two inlined ones — and the answer makes the ten measured bumps decompose exactly, with no residual.** On `ref-turn2.sav` — which is **`turn2-state.sav` under another name**, so this *is* the reference game — the ten command bumps are: | bumps | EIP W2 recorded | what it is | |---:|---|---| | **4** | `0x00821a87` | the **research-rate gate**, `0x00821a80`, applied once per submitted block | | 1 | `0x0086c3e9` | **list 5**, system rates — `0x0086c3e0` | | 1 | `0x0084946e` | **list 3**, build order — `0x00849460` | | 1 | `0x0088bf01` | **list 10** — `0x0088bed0` | | **2** | `0x0089046f` | **list 14**, inlined at `0x0089046c` — the AI's fleet order | | 1 | `0x008657ad` | **list 8**, fleet move — `StrategySim::MoveFleetCommand` | **3. That confirms AI2's P1 from the `ModCount` side, at zero VM cost.** AI2 predicted that `AI_IssueFleetTask` deposits **two** list-14 elements per fleet where the UI deposits one. List 14's applier fired **exactly twice** in a turn in which exactly **one** fleet was moved (one list-8 bump). The UI control is `human-turn2-orders.sav`: one UI fleet move, **one** list-14 element. Two producers, two different element counts, in the direction predicted. **P1 holds.** --- ## 1. `ApplyTurnCommandBatch` — the order the twenty-seven lists are applied in `ApplyAllTurnCommands 0x0078f6a0` computes `n = (S->+0x178 - S->+0x174)/0x1b4` and tail-calls `0x0088f9b0(blocks, n)` with `ecx = S+4` (the `StrategySim`). The batch's body is a run of near-identical blocks: ``` edi = blocks + loop over player blocks (stride 0x1b4): for (node = list head; node != head; node = node->next) playerId = *(int*)(edi - (listMemberOffset - 4)) ; block+0x04 (sim, playerId, &node->+0x8) edi += 0x1b4 ``` so the member offset is written literally in an `add edi, imm8/imm32`, and the player id is fetched with the matching negative displacement. That pair identifies each block's list unambiguously. Applied order: | # | list | member | per-element handler | bumps `ModCount`? | |---:|---:|---|---|---| | 1 | 6 | +0xac | `0x008784e0` | **yes** @`0x008784e3` | | 2 | 11 | +0xe8 | `0x00821c30` | **yes** @`0x00821c33` | | 3 | 20 | +0x154 | `0x008202e0` | no | | 4 | 19 | +0x148 | `0x00821ed0` | no | | 5 | 17 | +0x130 | `0x00865a70` | no | | 6 | 18 | +0x13c | `0x00865bd0` | no | | 7 | 5 | +0xa0 | `0x0086c3e0` | **yes** @`0x0086c3e6` | | 8 | 23 | +0x178 | `0x008220b0` | no | | 9 | 24 | +0x184 | `0x00822180` | no | | — | **gate loop A** | +0x38 | §1.1 | mixed | | 10 | 1 | +0x70 | `0x00882910` | **yes** @`0x0088291a` | | 11 | 4 | +0x94 | `0x00842f40` | **yes** @`0x00842f43` | | 12 | 3 | +0x88 | `0x00849460` | **yes** @`0x0084946b` | | 13 | 21 | +0x160 | `0x008659a0` | no | | 14 | 2 | +0x7c | `0x0083d5d0` | **yes** @`0x0083d5d3` | | 15 | 22 | +0x16c | *(inline)* | no | | 16 | 9 | +0xd0 | `0x0085b6d0` | **yes** @`0x0085b70a` | | 17 | 10 | +0xdc | `0x0088bed0` | **yes** @`0x0088befe` | | 18 | 12 | +0xf4 | *(inline)* | **yes** @`0x008902fe` | | 19 | 13 | +0x100 | *(inline)* | **yes** @`0x008903b9` | | 20 | **14** | +0x10c | *(inline)* | **yes** @`0x0089046c` | | 21 | 15 | +0x118 | `0x00821d70` | **yes** @`0x00821d73` | | 22 | 16 | +0x124 | `0x00821e20` | **yes** @`0x00821e23` | | 23 | 7 | +0xb8 | *(inline)* + `0x00849280` | **yes** @`0x008905c8` | | 24 | 8 | +0xc4 | `0x00865780` `MoveFleetCommand` | **yes** @`0x008657aa` | | 25 | 25 | +0x190 | `0x00865d30` | no | | 26 | 27 | +0x1a8 | `0x00867980` | no | | 27 | 26 | +0x19c | `0x00871030` | no | | — | **gate loops B, C** | +0x20, +0x24 | §1.1 | **yes** both | **The rule is clean, and it is a boundary at sixteen: lists 1-16 each cost one bump per element, lists 17-27 cost nothing.** Not one exception in either direction. Eleven free lists, not the ten a partial read suggested; the sixteen paying ones include four whose bump is *inlined into the batch* rather than sitting in a handler (7, 12, 13, 14), which is why a call-graph sweep for `ModCount` writers under-counts them. The 1-16 / 17-27 split looks like a real design boundary rather than an accident — the paying half is the half whose members lane Q's element records describe as orders against a game object, and the free half is where the settings-shaped payloads (weapon groups, defence layouts, populations, raid targets, civilian ratios) live. *Honest bound:* "no bump" is read from the handler's own body swept to the next function start (rule 17) plus the absence of an inline `inc [reg+4]` in that block. A bump inside a deeper callee of one of those handlers would be invisible to this reading; the live control is that on two measured turns exactly six distinct sites trapped and all six are in the "yes" column. ### 1.1 The three per-player gate loops, and which gates are free The six prologue gates are **not** applied together. Three loops, at three different points in the batch: | loop | at | gate tested | payload | bumps? | |---|---|---|---|---| | **A** | `0x0088fdb0`, `esi = block+0x38` | `+0x3c` — the three floats | written straight to `player+0x224/+0x228/+0x22c` | **no** | | A | `0x0088fdfa` | `+0x14` — **research target** | `+0x10` techId | **yes**, inline @`0x0088fe0a` | | A | `0x0088ffb6` | `+0x0c` — **research rate** | `+0x08` float → `0x00821a80` | **yes** @`0x00821a84` | | **B** | `0x008907b1`, `esi = block+0x20` | `+0x20` — **research boost** | `+0x18` spend, `+0x1c` fraction → `0x00820560` | **yes**, inline @`0x008907bc` | | **C** | `0x0089080a`, `esi = block+0x24` | `+0x2c` — group 4 | `+0x24` bool, `+0x28` int → `0x00821b90` | **yes** @`0x00821b93` | | — | *not found in this function* | `+0x6c` — `CivilianRatios` | — | **unknown** | Two things worth pulling out. **The `+0x3c` gate is free, it is AI-only, and it is Hiver-only.** AI2 §4 records `0x00762ca0` (which sets it) as having no UI caller, and AI1 §3.1 puts its producer at **Process Turn phase 2**. Reading phase 2's body, `0x006c57e0`, adds the third restriction: its **fourth** instruction after the prologue is ``` 006c580a mov eax,[esi+0x10] ; the StrategyClient 006c580d mov eax,[eax+0x150] ; the ClientPlayer 006c5813 cmp DWORD PTR [eax+0x5c],1 ; Species 006c5817 jne 0x006c5a28 ; -> straight to the epilogue ``` `player+0x5c` is `Species` and 1 is **Hiver**, so the whole of phase 2 — including a `cl_Chance` draw at `0x006c592a` and the `0x00762ca0` call at `0x006c59b6` — is skipped for every other species. That is a **fifth** independent cross-check on AI2's species reading (after the Hiver gate creators, the Zuul node-bore, the empty NPC arm and AI3's Zuul range exemption), and it explains cleanly why no save has ever carried the `+0x3c` gate: the corpus has no Hiver player. **The `+0x6c` `CivilianRatios` gate has no applier in `ApplyTurnCommandBatch`.** AI2 §4 already found it is UI-only (`0x007813b0`, from `0x007f1820`). Where it is consumed is open; its `ModCount` cost is **unknown**, not zero, and §4 flags it. --- ## 2. The reference game, decomposed `turn2-state.sav` and `ref-turn2.sav` are **the same file** (`determinism-oracle.md`, and both sha256 `ab4ac2d7e2977260…`, 66,739 B). So W2's watchpoint run is a measurement of *this* game, and the corpus deltas and the trap log describe the same turns. Eight players. Read straight off `turn1-state.sav`: | idx | id | name | `Species` | `Elim` | `Status` | `plcy` | `Status` in `turn2-state` | |---:|---:|---|---:|---|---:|---:|---| | 0 | 16 | `re` | 0 | F | 0 | 2 | **4** | | 1 | 32 | Fane Lao | 2 | F | 0 | 2 | **4** | | 2 | 496 | Singularity | 0 | F | 0 | 2 | **4** | | 3 | 512 | Singularity | 2 | F | 0 | 2 | **4** | | 4-7 | 528/544/560/576 | Alien Menace / Peacekeeper / Von Neumann / Independent Colony | **4** | F | 0 | 2 | 0 | **Exactly four players end their turn, and exactly four research-rate bumps fire.** The four that do are indices 0-3; the four that do not are exactly the four `Species == 4` factions. That is a fourth independent cross-check on AI2's species reading, from a completely different instrument (W2 §4 recorded `MarkPlayerTurnEnded` for players 0 and 1 and no `Status` write for 4-7), and it is the first direct evidence that **the NPC/monster factions have no `StrategyClient` at all** — not merely an empty task list. **Three of those four are AI players, not one.** `ai-turn-logic.md` and `alliance-mask-and-modcount.md` both describe the reference game as having "one AI player". It has three: 32, 496 and 512. Players 496 and 512 own nothing (`Sav = 0`, no colonies, no fleets, no systems — `events.md` §5 records that they never hold an event) but they **run**: on turn 1→2 all three set `ResRate 0.25 → 0.8` and picked a research target (`IND_Waldo`, `DRV_PlsFiss`, `BIO_GnMod`). That is a correction to two published findings, and it is the term that makes the arithmetic close. ### 2.1 Turn 2→3 (W2's measured window), fully attributed | bumps | source | who | |---:|---|---| | 4 | research-rate gate | one per submitted block: 16, 32, 496, 512 | | 1 | list 5 — system rates | 32 | | 1 | list 3 — build order | 32 | | 1 | list 10 | 32 | | 2 | list 14 — one AI fleet order | 32 | | 1 | list 8 — that order's route | 32 | | 2 | `ProcessTurn` entry, `OnAllCombatDone_Tail` | — | | **12** | | | Zero residual. The human (16) issued nothing and still costs 1, because `BuildTurnCommands 0x00783780` sets the research-rate gate **unconditionally** from live player state (AI1 §4.1). 496 and 512 cost 1 each for the same reason and nothing else — they have nothing to command. The save agrees: `turn2-state → turn3-state` shows `NMnx 109 → 111` (two ids: ship 1760, fleet 1776), `NumDes` unchanged, `srb[0] 1 → 2`, `Maint 500 → 1000`. **A build of an existing design.** No `ResTNm` change on any player, which is why the research-target gate never traps in W2's log. --- ## 3. Predictions (rule 2) — written before the engine module was built ### P1 — the cost table generalises: lists 1-16 pay, lists 17-27 are free **Prediction:** on any turn, `ModCount` delta `= 2 + A + Σ_blocks [ rate + target + boost + group4 + Σ_{L = 1..16} |list L| ]`, where `A` is the number of systems with `Abdn` set, each gate term is 1 when set and 0 when clear, and **lists 17-27 contribute nothing**. *Falsified if:* a turn whose block carries an element in one of the eleven free lists moves `ModCount` by more than this. *Cheapest test:* not a VM run — a save whose block populates one of the eleven. None exists; lists 17-27 have never been observed non-empty (lane Q), so that half of the table is a **hypothesis in the rule-6 sense** even though it is read from the instruction stream. *Boundary case to check by hand (rule 23):* a block whose only content is the always-set rate gate must cost exactly **1**, not 0 — and that is the case that carries four of the ten bumps on this save. ### P2 — the composition of `turn1-state`'s twelve `turn1-state → turn2-state` is also 12, but it is **not the same twelve**, and that is the interesting part. Three terms are forced by the saves themselves: * **4** research-rate gates — the same four submitting players (all four go `Status 0 → 4`); * **3** research-target gates — 32, 496 and 512 all move `ResTNm '' → `; the human does not; * **2** drivers. That is 9, leaving **exactly three** for player 32's real orders. Its observable outcome that turn is a new design (`Des[18 "Honor Lance"]`, `NumDes 5 → 6`), one destroyer built at its home system Ke'Dolarra (`srb[0] 0 → 1`, `Maint 0 → 500`, `PvSav 50,000 → 38,100`, i.e. 11,900 spent before spine phase 0 — lane B6's missing money), and one new fleet (1744, "Alpha Fleet"). **Prediction: the three are `list 1` (the new design), `list 3` (the build order) and `list 5` (system rates) — and player 32 issues NO fleet order on turn 1: no list 8, no list 14, no list 10.** Equivalently, the AI's turn-1 emission is its turn-2 emission minus the whole fleet-order group (list 8 + 2 × list 14 + list 10) plus the design command. *Test:* the W2 watchpoint module, unchanged, armed on one End Turn from `turn1-state.sav`. The predicted trap multiset is `{0x0088fe0a ×3, 0x00821a87 ×4, 0x0088291a ×1, 0x0084946e ×1, 0x0086c3e9 ×1, 0x007dc6f3 ×1, 0x007d92cd ×1}` — note it contains **two EIPs W2 has never seen** (`0x0088fe0a`, the research-target gate, and `0x0088291a`, list 1) and **omits three it did** (`0x0088bf01`, `0x0089046f`, `0x008657ad`). *Falsified if:* (a) `0x00821a87` fires other than 4× ⇒ "one block per turn-ending player" is wrong, and the whole per-player base cost is wrong with it; (b) `0x0088fe0a` fires other than 3× ⇒ the AI's research target is not a `TurnCommands` gate, and `ResTNm` is being written by something server-side; (c) `0x0089046f` or `0x008657ad` fires ⇒ the AI *did* move a fleet on turn 1, and one of my three is wrong — most likely list 5, which is the softest of the three; (d) `0x0088291a` does not fire ⇒ **the AI's new design is not a command at all**; it would then be created client-side and carried inside the build order, and a design costs zero. *Why (d) is the valuable one:* it is the difference between "the engine must emit a design command" and "the engine must emit a design *object* the build order names", which are very different amounts of work. ### P3 — a Hiver AI writes a command every turn that `ModCount` cannot see Process Turn phase 2 (`0x006c57e0` → `0x00762ca0`) sets prologue gate `+0x3c`; §1.1 shows that gate's applier does not bump, and that phase 2 returns immediately unless the player is **Hiver**. **Prediction:** a **Hiver** AI player's `TurnCommands` block carries gate `+0x3c` set with three floats, every turn, and no `ModCount` accounting will ever reveal it; a non-Hiver AI never sets it. Corollary: the corpus can never show this gate, because it has no Hiver player — which is *why* lane Q has it as "never seen set". The workload that would settle it is a Hiver AI game, and lane V has already shown (rule 6) that manufacturing a save for an unexercised path beats weakening the claim. *Falsified if:* a non-Hiver AI capture shows it set ⇒ some other producer reaches `0x00762ca0`; or a Hiver AI capture shows it clear ⇒ there is a second gate inside phase 2 that I read past. ### P4 — Process Turn phases 29-33 emit nothing, ever `StrategyClient::EndTurn 0x00783be0` sets `this->+0x15c = 1` at **`0x00783c59`**, *before* it calls `BuildTurnCommands 0x00783780` (at `0x00783cd7`) and `SendEndTurn`. Every order method opens `if (this->+0x15c) return false` (AI1 §4.2). `cl_EndTurn` is Process Turn **phase 28**. **Prediction:** phases 29-33 — including phase 32's `0x006af790 → 0x00769640`, the **colonize** order — are dead: they run, they call the order method, and it refuses. The AI's colonise orders therefore come only from phase 20's task loop (AI3 §5's `AITColonize` path), never from phase 32. *Falsified if:* an AI-turn capture shows a list-7 element that can only have come from phase 32, or a hook on `0x00769640` shows it returning true after `cl_EndTurn`. *Why it matters:* a reimplementation that ports the phase list literally will emit an extra colonize command per AI per turn and be one `ModCount` over on every turn with a colony ship. ### P5 — `cl_SetResearchTarget` is AI-only, and it is phase 18 `0x00578f60` has **exactly one caller in the image**: `0x006caf70`, which is Process Turn phase 18. The UI reaches the same order methods (`0x00763110` / `0x00763180`) directly from `0x005d20a0` / `0x005d2140`. **Prediction:** extends AI1's P4 — the *façade entry* `0x00578f60` is AI-only surface, and the number of research-target bumps in a turn equals the number of AI players whose phase-18 gate opens. On the reference game that is 3 on turn 1 and 0 on turns 2 and 3. *Falsified if:* the trap count for `0x0088fe0a` on turn 1 is not 3 (this is falsifier (b) of P2, seen from the other end). --- ## 4. What this lane did **not** do 1. **Nothing ran under an instrument.** Every new claim is read from the instruction stream or from the corpus saves; the live inputs are lane W2's log, not a run of mine. §3 is the measurement plan and it reuses W2's module **unchanged** — one save swap and one End Turn. 2. **The `+0x6c` `CivilianRatios` gate has no located applier** (§1.1). Its `ModCount` cost is unknown, and the engine's cost function reports "not exact" when that gate is set rather than guessing zero. 3. **The eleven "free" lists are free by direct reading only.** No save populates any of them (rule 6), and a bump inside a deeper callee would be invisible. The claim is a lower bound on cost, stated as one. 4. **Which task emits which command is still not modelled** — this lane closes the *cost* of a command and the *shape* of a block, not the decision that produces it. Every task's `Execute` body past its first few instructions remains unread (AI2 §10.1, AI3 §8), so the engine module emits nothing on its own: it provides the order API, the two-pass gate and the cost function, and a caller supplies the decisions. 5. **List 10 is named only by position.** Its applier `0x0088bed0` sits next to `DestroyFleet` and its element is `{i32, i32, counted i32}`; AI3 §0 has `AssignFleetsAndIssueOrders` reaching lists 14, 8 **and 10** together, and the turn that produced it also produced a brand-new fleet. "Assign these ships to this fleet" fits every one of those, and I am **not** naming it on that basis. 6. **`turn3-state`'s successor was not measured.** W2's second window (values 25→36) is turn 3→4 and no save of turn 4 exists in the corpus, so that window's composition is assumed to match turn 2→3 on W2's own statement that the two windows agree. --- ## 5. Corrections to earlier findings - **`alliance-mask-and-modcount.md` §2.2 — "one per command applied out of every player's `TurnCommands` block".** Right in shape, wrong in detail: **eleven of the twenty-seven lists apply their command without bumping** (§1), and four of the six prologue gates bump while a fifth does not and the sixth has no located applier. - **`alliance-mask-and-modcount.md` §2.5 — "the 10 non-driver bumps are the AI's commands on a two-colony board".** Four of the ten are the **research-rate gate**, one per submitted block, and one of those four is the *human*'s. Six are AI commands, from one AI player of three. - **`ai-turn-logic.md` §8.1 / `watchpoints-modcount-status.md`** — the reference game has **three** AI players (32, 496, 512), not one; `ref-turn2.sav` is `turn2-state.sav`; and the four `Species == 4` factions submit no `TurnCommands` block at all (§2). - **`watchpoints-modcount-status.md` §5 — "four of the twelve `ModCount` writers are unnamed".** All four are named (§0.2), as are the two inlined ones. - **`ai-turn-logic.md` §3.1 — the Process Turn phase table.** Phases 29-33 cannot emit; `cl_EndTurn` at phase 28 latches `client+0x15c` before `BuildTurnCommands` runs (§3, P4).