From ae479713166c18057c0b2652c7adff4401a76a93 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 8 Sep 2026 16:27:58 -0400 Subject: [PATCH] AI4: what an AI turn emits, and what each command costs in ModCount Joins the emission side (lanes AI1-AI3) to the counting side (A2, W2). Three results. The cost of applying a command is a property of its list, and the boundary is sharp: lists 1-16 each advance ModCount once per element, lists 17-27 never do. Four of the six prologue gates bump, one is free, and the sixth has no applier anywhere in the application path -- its cost is unknown, not zero. The batch applies the twenty-seven lists in a fixed order that is neither list nor offset order, with the six gates split across three separate per-player loops at three different points, and four of the sixteen bumps inlined into the batch rather than living in a handler, which is why a call-graph sweep under-counts them. Lane W2's four unnamed handler EIPs are named, and so are its two inlined ones, and the ten measured command bumps then decompose with zero residual. Four of the ten are the research-rate gate, one per submitted block, and one of those four is the human's. Two are list 14, on a turn that moved exactly one fleet -- which confirms lane AI2's P1 (an AI fleet order deposits two fleet-task elements where the interface deposits one) from the counter side, at no VM cost. The reference game is not what the record says. ref-turn2.sav IS turn2-state.sav; there are THREE AI players, not one, and the two dormant ones do run -- all three set a research rate and picked a research target on turn 1; and the four monster-faction players submit no command block at all, which is the first direct evidence that they have no client rather than an empty task list. Prediction committed for turn1-state: the same 12, out of a different set of commands -- 4 rate gates, 3 research targets, and three orders from the one AI with an empire, which are predicted to be a new design, a build order and a system-rates command, with NO fleet order on turn 1. The trap multiset it predicts contains two EIPs W2 has never seen and omits three it did, so it is cheap to falsify: one save swap on W2's unchanged watchpoint module. Also: phase 2 of the AI's turn is Hiver-only (a fifth cross-check on the species reading, and the reason one prologue gate has never been observed set), phases 29-33 are dead because the submit latches the client before it builds the send buffer, and cl_SetResearchTarget is AI-only surface with exactly one caller. 14 addresses in ghidra/addresses.d/lane-ai4.json; validated to a scratch path, 1,138 -> 1,152, no duplicate names. --- findings/subsystems/ai-order-emission.md | 327 +++++++++++++++++++++++ ghidra/addresses.d/lane-ai4.json | 116 ++++++++ 2 files changed, 443 insertions(+) create mode 100644 findings/subsystems/ai-order-emission.md create mode 100644 ghidra/addresses.d/lane-ai4.json diff --git a/findings/subsystems/ai-order-emission.md b/findings/subsystems/ai-order-emission.md new file mode 100644 index 0000000..890e091 --- /dev/null +++ b/findings/subsystems/ai-order-emission.md @@ -0,0 +1,327 @@ +# 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). diff --git a/ghidra/addresses.d/lane-ai4.json b/ghidra/addresses.d/lane-ai4.json new file mode 100644 index 0000000..81654a8 --- /dev/null +++ b/ghidra/addresses.d/lane-ai4.json @@ -0,0 +1,116 @@ +{ + "entries": [ + { + "name": "TurnCommandApply_ResearchRate", + "addr": "0x00821a80", + "convention": "__thiscall", + "prototype": "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", + "status": "verified", + "source": "findings/subsystems/ai-order-emission.md §1.1 (lane AI4 2026-09-08, instruction stream); the EIP was measured live by lane W2, watchpoints-modcount-status.md §2 row 2-5" + }, + { + "name": "TurnCommandApply_SystemRates", + "addr": "0x0086c3e0", + "convention": "__thiscall", + "prototype": "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", + "status": "verified", + "source": "findings/subsystems/ai-order-emission.md §1 (lane AI4 2026-09-08); EIP measured by lane W2" + }, + { + "name": "TurnCommandApply_BuildOrder", + "addr": "0x00849460", + "convention": "__thiscall", + "prototype": "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", + "status": "verified", + "source": "findings/subsystems/ai-order-emission.md §1 (lane AI4 2026-09-08); EIP measured by lane W2" + }, + { + "name": "TurnCommandApply_List10", + "addr": "0x0088bed0", + "convention": "__thiscall", + "prototype": "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", + "status": "verified", + "source": "findings/subsystems/ai-order-emission.md §1 (lane AI4 2026-09-08); EIP measured by lane W2" + }, + { + "name": "TurnCommandApply_List14_Inline", + "addr": "0x0089046c", + "convention": "site", + "prototype": "`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", + "status": "verified", + "source": "findings/subsystems/ai-order-emission.md §0.3 (lane AI4 2026-09-08); EIP measured by lane W2 as 0x0089046f, x2" + }, + { + "name": "TurnCommandApply_ResearchTarget_Inline", + "addr": "0x0088fe0a", + "convention": "site", + "prototype": "`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", + "status": "mapped", + "source": "findings/subsystems/ai-order-emission.md §1.1 and §3 P2 (lane AI4 2026-09-08, instruction stream; not yet observed live)" + }, + { + "name": "TurnCommandApply_ResearchBoost_Inline", + "addr": "0x008907bc", + "convention": "site", + "prototype": "`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", + "status": "mapped", + "source": "findings/subsystems/ai-order-emission.md §1.1 (lane AI4 2026-09-08, instruction stream)" + }, + { + "name": "TurnCommandApply_Group4Gate", + "addr": "0x00821b90", + "convention": "__thiscall", + "prototype": "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", + "status": "mapped", + "source": "findings/subsystems/ai-order-emission.md §1.1 (lane AI4 2026-09-08, instruction stream)" + }, + { + "name": "TurnCommandApply_NewDesign", + "addr": "0x00882910", + "convention": "__thiscall", + "prototype": "void (StrategySim* this, int playerId, void* element) -- the applier for TurnCommands list 1 (`StreamableHelper` 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", + "status": "mapped", + "source": "findings/subsystems/ai-order-emission.md §1 and §3 P2 falsifier (d) (lane AI4 2026-09-08)" + }, + { + "name": "TurnCommandApply_Colonize_Inline", + "addr": "0x008905c8", + "convention": "site", + "prototype": "`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)", + "status": "mapped", + "source": "findings/subsystems/ai-order-emission.md §1 (lane AI4 2026-09-08, instruction stream)" + }, + { + "name": "AIProcessTurn_SubmitLatch", + "addr": "0x00783c59", + "convention": "site", + "prototype": "`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", + "status": "verified", + "source": "findings/subsystems/ai-order-emission.md §3 P4 (lane AI4 2026-09-08, instruction stream)" + }, + { + "name": "AIProcessTurn_Phase2_HiverGate", + "addr": "0x006c5813", + "convention": "site", + "prototype": "`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", + "status": "verified", + "source": "findings/subsystems/ai-order-emission.md §1.1 and §3 P3 (lane AI4 2026-09-08, instruction stream)" + }, + { + "name": "cl_SetResearchTarget", + "addr": "0x00578f60", + "convention": "__cdecl", + "prototype": "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", + "status": "verified", + "source": "findings/subsystems/ai-order-emission.md §3 P5 (lane AI4 2026-09-08, image-wide rel32 caller scan)" + }, + { + "name": "AIProcessTurn_Phase18_Research", + "addr": "0x006caf70", + "convention": "__thiscall", + "prototype": "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", + "status": "mapped", + "source": "findings/subsystems/ai-order-emission.md §3 P5 (lane AI4 2026-09-08)" + } + ] +}