diff --git a/findings/subsystems/spy-detection-roll.md b/findings/subsystems/spy-detection-roll.md new file mode 100644 index 0000000..e4aa8e2 --- /dev/null +++ b/findings/subsystems/spy-detection-roll.md @@ -0,0 +1,694 @@ +# The spy detection roll — decoded, its gate turned into a save predicate, and measured + +- **Type:** subsystem (static decode + live measurement) +- **Owner / date:** lane AS · 2026-09-09 · guest **VM144** (`sots-re-win10-144`, `re@192.168.10.144`) +- **Instrument:** lane Z's `draw_sites` return-address ledger at **`probes=8`**, shim build + `as-c172c99-20260909T0205Z`, dist `C:\SOTS\shimdist-as` +- **Predictions, committed before the build:** `sots-engine` `docs/AS-predictions.md` + (commit `c172c99`, addendum `61875a4`) +- **Answers:** the resolution `2026-09-09-tail-draws.md` §8 item 2 (the spy half of the tail gate) +- **Corrects:** the resolution's "the detection roll is inline in `vslot13`" (it is a direct call); + lane L3 §6's "`spies2` is not the spy list"; board row 399's V2 misquote is *not* touched here + +*(Sections 1–3 were written and committed before any measurement. Sections 4 onward are the +measurement.)* + +--- + +## 0. Headline + +> **The detection roll fires, it costs one word, and its gate is an asteroid belt.** +> +> `Mars::RNG::Chance` at **`0x00887c8a`** — in the body of `ServerSpyManager::vslot13`, phase 23 +> call 9 of 9 of `OnAllCombatDone_Tail` — rolls once per deployed, undetected spy per turn, on the +> strategic generator, at a probability `sdo` that starts at **0.0084 as measured** and accumulates +> by the same amount every turn. `OnAllCombatDone_Tail` went **363 → 364**: one word, against 0 on +> every turn any lane has ever measured. +> +> The predicate is `spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1)`. +> **Corpus count: 0 of 22.** The failed conjunct is **`deat != 0`** — every spy this campaign has +> ever built was still docked to its tender. Not trade routes, not freighters, not `spyon`, and not +> the asteroid belt: **all 22 corpus saves already had a usable belt somewhere**, and two of them +> already had four AI-owned ones. +> +> And the polarity is the opposite of the intuitive one: **when the belt is gone the roll does not +> happen and the spy is detected anyway.** A zero at this site is two different results. + +--- + +## 1. The chain, decoded from the instruction stream + +Program `sots` / "Sword of the Stars.exe", ImageBase `0x00400000`, all addresses VAs. Every body +below was disassembled from its start **to the next function start** (rule 17); no Ghidra size was +used. ReVa/CT111's MCP endpoint was down for this session (`CONNECTION_CLOSED`), so nothing was read +in the decompiler — this is all instruction stream, which for these bodies is a feature. + +`Game::ServerSpyManager::vslot13` = **`0x008877b0`** (lane V2's name), dispatched at `0x007d9811` +from `OnAllCombatDone_Tail` on receiver `S+0x15c` slot 13. Body `0x008877b0 .. 0x00887f30`. +Throughout: `ebx = 0`, `edi = this` (the manager), `esi` = the current `Game::SpyCraft`. The loop is +over the vector at `this+0x10 .. this+0x14` — the same vector lane L3 watched grow to 1 at +**Build Spy**. + +``` +0088781d je 0x887e92 G1 spy.deat (+0x10) == 0 -> next spy +0088782d call 0x8b9240 sysA = registry(server+0x84, spy.deat) -> [ebp-0xbc] +00887845 call 0x8b9240 ownerO = registry(server+0x84, spy.sown) -> [ebp-0xc0] +00887856 je 0x887c5b G2 spy.sdet (+0x40) == -1 -> BRANCH D (detection) +00887867 jl 0x887e92 (server.turn - sdet) < 3 -> next spy +0088787a je 0x887c4f sysA.PID == 0 -> sdet := -1, next spy +00887891 je 0x887c4f sysA.PID == ownerO -> sdet := -1, next spy +00887af2 call 0x8408e0 <-- P, the false-flag draw (three sites inside) +``` + +**Branch D — the detection roll:** + +``` +00887c5b push eax ; ownerO +00887c62 push [ebp-0xbc] ; sysA +00887c63 push esi ; the spy +00887c66 call 0x81f570 D1 SpyCraft_AccumulateDetectionOdds -> writes spy.sdo (+0x3c) +00887c6b mov ecx,[ebp-0xbc] +00887c71 call 0x743f80 D2 t = ServerSystem_BeltUsableFlags(sysA) +00887c76 test al,1 +00887c78 je 0x887c97 (t & 1) == 0 -> SKIP THE ROLL, straight to D4 +00887c7a mov ecx,[edi+8] ; the StrategyServer +00887c7d fld [esi+0x3c] ; spy.sdo +00887c80 mov ecx,[ecx+0x16c] ; THE STRATEGIC GENERATOR +00887c86 push ecx +00887c87 fstp [esp] +00887c8a call 0x8e6dd0 D3 Mars::RNG::Chance(strategic, sdo) <-- THE DETECTION ROLL + return address 0x00887c8f +00887c8f test al,al +00887c91 je 0x887e92 roll failed -> next spy, still undetected +00887c97 mov edx,[edi+8] +00887c9a mov eax,[edx+0xc] ; the current turn +00887c9d mov [esi+0x40],eax D4 spy.sdet := turn (DETECTED; then the event strings) +``` + +### 1.1 It is a direct call, not an inlined draw — the instrument advice was right for the wrong reason + +The resolution that created this lane says the roll is *inline* in `vslot13`. It is not. The bytes +are `e8 41 f1 05 00` — a plain `E8 rel32` to the `Chance` **entry point** `0x008e6dd0`. Nothing +resembling rule 16's inlined tempering sequence is present. + +What *is* true, and is the whole reason `draw_sites` is the right instrument, is that the call sits +in the **caller** rather than in `SpyManager::Slot13RngCallee 0x008408e0`. An entry probe on +`0x008408e0` therefore reads zero whether or not the roll fires — but that is instrument *choice*, +not inlining, and the distinction matters because an inlined draw would be invisible to +`draw_sites` too, and this one is not. Lane AG read this independently and reached the same +conclusion; the correction is recorded in both places. + +### 1.2 The gate is an asteroid belt, and it is sixteen bytes + +``` +00743f80 cmp dword ptr [ecx + 0x6c], 0 +00743f84 mov eax, dword ptr [ecx + 0x19c] +00743f8a jg 0x743f8f +00743f8c and eax, 0xfffffffe ; clear bit 0 when [ecx+0x6c] <= 0 +00743f8f ret +``` + +On `Game::ServerSystem` (`objects/layouts.md`): **`+0x6c` = `ARes2`**, **`+0x19c` = `TerrFl`**, +`+0x100` = `PID` (which `ServerSystem_GetOwner 0x007437e0`, used three times above, returns). So + +> **bit 0 of the return = `(TerrFl & 1) AND (ARes2 > 0)` — "this system still has a usable +> asteroid belt".** + +`ARes2` is already the field lane AC identified as the asteroid-belt resource (values 1500–3000, +matching `SYSTEM_MIN/MAX_ASTEROID_RESOURCES`). `TerrFl` bit 0 is its flag; §2 checks that reading +against the corpus, which is the only place a static reading of an unnamed bit can be checked +without a debugger. + +**The polarity, stated because everyone's intuition gets it backwards.** The branch is a `je` that +jumps **past** the roll. Belt usable ⇒ roll. Belt gone ⇒ **no draw at all, and `sdet := turn` +unconditionally.** The spy hides in the belt; if the belt is mined out it has nowhere to hide and is +spotted for free. So at this site, a `draw_sites` row with `calls = 0` and a `sdet` that has moved +is a *positive* result about a different arm, and must never be reported as "the roll did not fire". + +### 1.3 `sdo` — where the probability comes from, and why the roll should cost a word + +`0x0081f570` (`SpyCraft_AccumulateDetectionOdds(spy, sys, spyOwner)`, `__stdcall`, `ret 0xc`, 512 +bytes) is called **unconditionally** on branch D, immediately before the gate, and writes +`spy.sdo (+0x3c)`: + +* if `sys.PID == 0` (unowned): `fldz; fstp [esi+0x3c]` — **`sdo := 0.0` and return**. A spy at an + unowned system rolls at p = 0, which costs no word. +* otherwise it walks the system's fleet vector accumulating three counters (`[ebp-4]`, `[ebp-8]`, + `[ebp-0xc]`) and computes, with every literal read as the **four bytes in the image** (rule 23 — + these are widened floats, not decimals): + +``` +p = 0.01f (0x009e31c0) +p += (countA > 0) ? 0.01f : 0 (0x009e3e14) +p += (countB > 0) ? 0.02f : 0 (0x00a1d048) +p += 0.001 * countC (0x009e23c0) +p *= (spyOwner->[0x5c] == 5) ? 0.75f (0x009e5ac4) + : (spyOwner->[0x5c] == 6) ? 0.5f (0x00a2c788) + : 1.0f +p *= 0.7 (0x009e5df8) +sdo = min(sdo + p, 1.0) (0x009e1ef0) +``` + +Two properties matter more than the arithmetic. It **accumulates** onto the previous `sdo`, and it +is **clamped at 1.0**. `Mars::RNG::Chance` costs **0 words at `p <= 0` and at `p >= 1`** (board row +367). So: + +* a fresh deploy at a quiet foreign system gives `sdo = 0.7 × 0.01 = 0.007` — strictly inside + (0, 1), so the roll costs **exactly one word**; +* `sdo` climbs by ~0.007/turn, so a spy left in place for ~143 turns reaches `p >= 1`, at which + point the site **costs nothing and always succeeds**. That is a second way a zero at this site is + not a negative, and it is the same shape as lane AG's `cmo` early-out. + +*(Measured afterwards: **0.0084**, i.e. `0.7 × (0.01 + 0.001 × 2)` — `countC = 2` rather than the +0 assumed here. The one-word conclusion is unaffected and the form of the expression reproduced +exactly. §4.2a.)* + +### 1.4 What a deploy actually writes — and one draw site nobody had + +`ServerSpyManager::DeploySpy` = **`0x00887410`**, vftable `0x00a3073c` slot 7, zero direct call +sites, reached only from the `SHIPACTION_DEPLOYSPY` handler `0x0078c930` through +`[[dispatcher+0x15c]+0x1c]` — the stack-built ship-action function-pointer table that lane B6 +identified as a distinct indirection class. On the success path it writes: + +``` +spy->Reset() (0x00838070) +spy->sdo (+0x3c) := 0.0f +spy->sdet (+0x40) := -1 +spy->cbh (+0x18) := +spy->tdep (+0x14) := server->[+0xc] ; the current turn +spy->atto (+0xc) := 0 ; carrierShip->[+0xa8] := 0 ; detach from the tender +ServerSystem_AddSpy(system, spy) at 0x008874d4 +``` + +and `AddSpy` is where `deat` is born (§1.5). Because the deploy leaves `sdet == -1` and `sdo == 0`, +**the detection branch is entered on the very next `OnAllCombatDone_Tail`** — which is the same End +Turn that applied the order, since `ApplyAllTurnCommands` runs before the tail. + +The float argument is computed in the handler, and it is a draw: + +``` +0078c975 mov ecx,[esi+0x16c] ; esi = StrategyServer -- THE STRATEGIC GENERATOR +0078c97c add ecx,4 ; the +4 sub-object, same one the trade-raid roll uses +0078c97f call 0x47d830 ; Mars::RNG::NextFloat <-- 1 word, ret 0x0078c984 +0078c984 fld qword [0x009e21b0] ; 6.283185482025146 == 2*pi + cbh = NextFloat() * 2*pi +``` + +> **`0x0078c97f` is a strategic-generator draw site in no existing inventory.** It is not one of +> lane V2's eight, and it is not in lane I's 22, because it hangs off the ship-action table and no +> call-graph sweep reaches it. It fires **once per Deploy Spy order applied**, inside +> `ApplyAllTurnCommands` — so it lands in the `ProcessTurn` bracket, not the tail's. Any gate-indexed +> audit (lane AG) should carry it, and its predicate is "a `SHIPACTION_DEPLOYSPY` command in the +> turn's command stream", which is a *stream* predicate rather than a save predicate — a category +> the audit does not yet have a column for. + +### 1.5 `deat` and `spies2` are written by the same two instructions — `spies2` is closed + +`ServerSystem::AddSpy` = **`0x007514c0`**, 48 bytes, complete: + +``` +007514c3 mov edx,[ebp+8] ; the spy +007514c6 test edx,edx / je ret +007514ca test ecx,ecx / jne +007514ce xor eax,eax ; this == NULL -> store 0 +007514d2 mov eax,[ecx+4] ; the system's own registry handle +007514d5 mov [edx+0x10],eax ; *** spy->deat = system handle *** +007514d8 add edx,4 ; &spy->sid +007514db push edx +007514dc add ecx,0x1cc ; &system->spies2 +007514e2 call 0x59f1a0 ; push_back (lane AI3's wrapper) +``` + +and its exact mirror `ServerSystem::RemoveSpy` **`0x0074f550`** sets `spy->deat = 0` and erases +`spy->sid` from the same `+0x1cc` vector. `ServerSystem +0x1cc` is `spies2` in `objects/layouts.md`. +`AddSpy` has exactly one caller (`0x008874d4`, inside `DeploySpy`) and no vtable slot, so **this is +the only way `deat` ever becomes non-zero at runtime.** + +> **`spies2` is the per-system vector of DEPLOYED spy ids**, and `deat` is its reciprocal +> back-pointer. They are written by the same function, two instructions apart. +> +> - Lane L3 §6: *"`spies2` is therefore not the spy list"* — **half right and worth correcting in +> place.** It is not the spy *manager's craft* list; it is the *system's deployed-spy* list. L3's +> inference from "this save has a spy and `spies2` is still empty" was sound about what `spies2` +> is not, and the alternative it offered second ("or it only fills for a deployed spy") is the +> right one. +> - Lane AC's P4 (*"fills only for a deployed spy"*) is **confirmed, and now from the writer rather +> than by elimination.** +> +> This retires one third of lane W's rule-6 flag on `spies2` / `SysMem` / `mts` **statically**, and +> §5 exercises it live. + +### 1.6 `vslot14`'s roll is a different gate — and it excludes independent colonies + +For the record, because target choice depends on it. `ServerSpyManager::vslot14` `0x0088db80` +(phase 33) loops the same vector and requires `spy.deat != 0`, `sys.PID != 0`, and +**`sys.indi (+0x1c8) == 0`** (`ServerSystem_IsIndependent 0x00743fa0`) — *not* an independent +colony — plus `spy.cm (+0x24)` in 1..4, before its `Chance` at `0x0088dc43`. Lane AG has decoded +that site fully; the consequence here is only that **an AI-empire target exercises strictly more +than an Independent Colony**, which is why this lane went looking for an AI-owned belt rather than +taking the easy NPC one. + +--- + +## 2. The corpus, counted before measuring (rule 28b) + +`verify/save-reader/save_reader.py --json` over all **22** saves in `verify/results/saves/`. + +| | count | +|---|---| +| saves with any `spymgr.spy` record | **3** (L3's turn-15, AC's turn-20 and turn-22) | +| of those, `deat != 0` | **0** | +| saves satisfying the full detection-roll predicate | **0 of 22** | +| **the failed conjunct** | **`spy.deat != 0`** | + +The integrator's value-domain census (`value-domain-census.md`) reaches the same 0 independently and +more strongly: `deat`, `sdet`, `sdo`, `cbh`, `cm`, `cmo` are all in the 234 fields that have only +ever held one value, so the count is 0 **by construction**. + +**Two things fell out of the same pass, and the first is the load-bearing one.** + +* **`ARes2 > 0` and `TerrFl & 1` agree on every one of 616 system records** (22 saves × 28 systems), + with no exception in either direction. That is a save-side confirmation of §1.2's reading of an + unnamed flag bit, obtained without a debugger, over a sample large enough to be worth something. + It got stronger on the state this lane built (§4): there, `TerrFl` also takes the values **2** and + **3**, and `ARes2 > 0` still coincides exactly with `TerrFl & 1` — so bit 1 is a second, + independent terrain feature and bit 0 is not merely "TerrFl is non-zero". +* **The belt was never the missing conjunct.** **All 22** corpus saves have at least one system + with a usable belt (5 to 15 of 28), and **two of them — `human-turn2-orders.sav` and + `human-turn3-noderoute.sav` — already have four AI-owned belt systems.** Lane AC's map had none, + and the campaign generalised from it. Those two saves are `PTech 0 / ResM 1.0`, so the spy tech + chain is tens of turns away and they were not a usable shortcut; but "no AI colony can have a + belt" was a fact about lane L3's one map, exactly the rule-28 practice-3 trap. + +--- + +## 3. Predictions + +Committed to `sots-engine` `docs/AS-predictions.md` at `c172c99` before the build, with the +addendum at `61875a4` before the measurement. Summarised here; the falsification table is in that +file. + +| | prediction | +|---|---| +| **P1** | an AI-owned belt system exists within ≤ 5 map regenerations at lane L3's custom settings | +| **P2** | the predicate of §2; corpus count 0 of 22; failed conjunct `deat != 0` | +| **P3** | the roll fires: one `draw_sites` row at `ret_rva 0x00887c8f`, `Chance`, `calls = 1`, `strategic = true` | +| **P4** | it costs **1** word; `no_draw_calls = 0` | +| **P5** | the tail bracket goes 0 → **1**; the trade half contributes 0 (`tscr` 252, no fleet at a sector node) | +| **P6** | `sdet` most likely stays −1 (p ≈ 0.993); **`sdo` moves off its corpus-constant 0 to ≈ 0.007**, and that is the save-side confirmation | +| **P7** | `spies2` non-empty in the target system only | +| **P8** | the `hooks=off` pair reproduces byte-identically in two fresh processes | +| **P9** | `probes=8` is byte-neutral on this workload too | + +--- + +## 4. The workload — built from a cold main menu in one session + +Shim build **`as-c172c99-20260909T0205Z`**, cross-built on CT111 in this lane's own directory +`/srv/re-lab/build/sots-engine-as` (`rm -rf build-host build-shim` before configure — rule 24), +staged to `/srv/re-lab/shim/dist-as`, deployed to `C:\SOTS\shimdist-as` on VM144. Exports checked +identical to the real `binkw32.dll` (66 names). + +**Guest adoption check, first, before anything else.** `ref-turn2.sav`, one End Turn, `hooks=off`, +this lane's own build: + +``` +(Autosave EndTurn).sav bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 66,732 +(Autosave).sav 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 67,219 +``` + +Both **identical to the published oracle** in `verify/results/saves/determinism-hashes.txt`. The +binary and the guest are sane. + +### 4.1 The map — zero regenerations were needed + +New custom game at lane L3's settings (28 stars, 5 ly, 2 players Human vs Tarkas, treasury +9,850,000, **10 initial colonies, 15 initial technologies**, econ 148 %, research 146 %), species +set explicitly to Human on the launch card (it defaults to Random — L3's note holds). + +**The first map generated had two AI-owned belt systems.** The brief expected several +regenerations; P1's estimate of ≤ 5 was never tested because none were needed. + +| SysID | name | PID | `ARes2` | `TerrFl` | note | +|---|---|---|---|---|---| +| **400** | **Kepler** | **32 (AI)** | **2435** | **1** | the target — 3.5 ly from my colony Midway | +| 224 | Delta Pavonis | 32 (AI) | 1537 | 1 | the spare | +| 272 | Solaris | 0 | 2773 | 1 | unowned — `sdo` would be forced to 0 here (§1.3) | +| 160 / 480 | Piscium / Pascal | 16 (me) | 2448 / 2522 | 1 | mine | +| 336 | Copernicus | 0 | 2359 | **3** | bit 1 also set | +| 128 | Downbelow | 0 | 1965 | 1 | | +| 384 / 448 / 464 | Nunki / Mu Bootis / Jinx | — | **0** | **2** | **bit 1 set, bit 0 clear, no belt** | + +That last row is the one that matters for §1.2. In the whole 22-save corpus `TerrFl` had only ever +been 0 or 1, so "bit 0" and "non-zero" were indistinguishable. This map has three systems at +`TerrFl = 2` with `ARes2 = 0` and one at `TerrFl = 3` with `ARes2 = 2359`. **`ARes2 > 0` still +coincides exactly with `TerrFl & 1`, and now it is bit 0 specifically, not "TerrFl is set".** + +### 4.2 The path to a deployed spy, with what each turn cost + +The four techs came faster than lane L3's recipe suggested because `CCC_FTLBrdB` was in the +15-technology roll again (it appears to be common) and because the top-of-screen +**Savings ↔ Research slider at its right end turns a 2-turn tech into a 1-turn tech.** + +| turn | what | +|---|---| +| 2 | new game; research `CCC_SpyBm` (`CCC_FTLBrdB` already researched from the initial roll) | +| 4 | `CCC_SpyBm` done; research `IND_OrbFound` | +| 6 | done; research `IND_CruisCon` (2 turns → 1 with the research slider at max) | +| 7 | done; research `IND_SlvgTech` | +| 8 | **all four techs**; design a Cruiser with the `Repair and Salvage` mission section; queue it | +| 9 | the tender exists; fleet panel's `Special` → `Build Spy` → `Select All` → `OK` | +| 10 | **the spy craft exists**: `nspy 1`, `deat 0`, `sdet -1`, `sdo 0`, `atto` = the tender — the corpus state | +| 10 | `Move` the fleet to Kepler (`Dest: Kepler, 4 Turns`) | +| 14 | **fleet arrived**: `Flt.LocID = 400`, read from the autosave, not the panel | +| 14 | `Special` → **`Deploy Spy`** — the menu entry is present, which is itself the belt gate answering | +| 15 | order applied. **`deat = 400`, `sdet = -1`, `sdo = 0.0084`, `spies2 = [1]` at Kepler** | + +Two things about turn 15 are worth stating separately, because they are results rather than steps. + +**(a) `sdo = 0.0084`, and that is §1.3's formula evaluated, not fitted.** The prediction was 0.007 +for a quiet system (`0.7 × 0.01`). The measured value is `0.7 × 0.012 = 0.0084`, i.e. `countC = 2` +in the third term. So the *form* — a 0.7 multiplier over a 0.01 base plus 0.001 per unit of a +counter — reproduces exactly, at a value no run of this campaign has ever produced, on a state +nobody had built. The identity of `countC` is not established (it is 2 at a system holding one +foreign colony and my one-ship fleet); the constant and the shape are. + +**(b) `spies2` filled, in exactly the system the writer says.** Reading all 28 systems of the +turn-15 autosave: `spies2` is `count = 0` in 27 of them and `count = 1, element = 1` at **Kepler**, +where `1` is the spy's `sid`. This is the **first non-empty `spies2` in the corpus** and it matches +`ServerSystem::AddSpy` instruction for instruction (§1.5). + +--- + +## 5. The measurement + +All three runs load the **same file**, `MyGameas1spydep.sav` +(sha256 `854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08`, 76,705 B, turn 15, +`--strict` clean, 0 errors / 0 warnings), in a **freshly launched process**, and press End Turn +**once**. Repo copy: `verify/results/saves/as-turn15-spydeployed.sav`. + +### 5.1 The oracle pair (rule 26) — and what it says honestly + +| run | config | `(Autosave EndTurn).sav` (the input, re-saved) | `(Autosave).sav` (post-turn) | +|---|---|---|---| +| **A** | `hooks=off` | `98e45d3745b91450…` 76,706 | `262f8bda97c9511d…` 77,685 | +| **B** | `hooks=off`, fresh process | `98e45d3745b91450…` **identical** | `e34775a757e6fb3d…` 77,649 **differs** | +| **M** | `probes=8` | `98e45d3745b91450…` **identical** | `262f8bda97c9511d…` **identical to A** | + +**The pre-turn save is byte-identical in all three**, which proves the input state — including any +pending order — was the same in every run. The post-turn saves are not. + +**A vs B differs in exactly 22 leaves, and every one of them is inside one AI player.** +`state_checksum --no-audit` localises them: + +``` +/Summary/Checksum 985948837 -> 985948925 +/Sim/turnstats/history/hist[1]/stats[15]/tch 46 -> 45 +/Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[62] 2 -> 3 +/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[62] 0 -> 22469 +/Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[64] 4 -> 2 +/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[64] 22469 -> 0 +/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TAcq[64] 16 -> -1 +/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TiAcq[64] 45 -> -1 +/Sim/players/Player[32 "Revenge Fleet"]/ResTNm '' -> 'DRV_NodFoc' +… plus that player's Events/EvNxID/otch bookkeeping for the same pick +``` + +In A the AI completed `Overthrusting`; in B it is part-way through `DRV_NodFoc`. **Nothing else +moved** — not one leaf of my empire, not `deat`, `sdet`, `sdo`, `spies2`, `trdmgr` or any fleet. + +That is precisely the mechanism `2026-09-08-ai-seed-per-process.md` established: each AI +`StrategyClient`'s generator is seeded with a word that differs in every process, so its research +pick is a per-process outcome. Board row 326 recorded the same signature on +`turn1-state → turn2` — *"one shadow empire's research pick plus the derived checksum"*. + +**So the control agrees with itself modulo the known non-deterministic leaves, which is the bar the +brief set — and it is a weaker bar than byte-identity, which must be said plainly.** What is +established is: on this input, in two fresh un-instrumented processes, every leaf outside one AI +player's research bookkeeping reproduced exactly. What is **not** established is byte-identity, and +this workload cannot establish it until the client seeds are pinned (the resolution's Rung C). + +**And the instrumented run M came out byte-identical to A.** Read carefully, that is: + +* strong for the thing that matters — `probes=8` changed **no leaf** of the sim, the spy, or my + empire, on the same input, which is the byte-neutrality claim P9 makes; +* and, for the AI-pick leaf specifically, a **coincidence of size 1/k** (rule 26's corollary): M + happened to draw the same AI seed outcome as A. It is not evidence about the instrument in that + one leaf class, and I am not treating it as such. + +### 5.2 The instrument was armed, and one probe reading zero is the point + +`shim.log`: seven draw-site detours `create=MH_OK enable=MH_OK`; `probe: installing 8 of 12`; all +eight `MH_OK`; **no `COVERAGE:` line**. `draw_site_overflow = 0`. + +| probe | entries this turn | +|---|---| +| `Game::ServerSpyManager::vslot13` `0x008877b0` | **1** | +| `Game::ServerSpyManager::vslot14` `0x0088db80` | 1 | +| `Game::ServerTradeManagerImpl::vslot13` `0x0088ef80` | 1 | +| `Game::ServerTradeManagerImpl::vslot15` `0x0082cca0` | 1 | +| **`Game::SpyManager::Slot13RngCallee` `0x008408e0`** | **0** | +| `Game::TradeManager::Slot13RngCalleeA` `0x00820ca0` | 0 | +| `Game::TradeManager::Slot13RngCalleeB` `0x0088b440` | 0 | +| `Game::ServerTradeManager::CreateRaidEncounter` `0x008938a0` | 0 | +| indices 8–11 (`GenerateTradeRaidEncounters`, three controls) | **NOT INSTALLED** — `probes=8` installs 0–7 | + +> **That table is the lane's methodological point in one line.** The detection roll fired, and the +> entry probe on `SpyManager::Slot13RngCallee` read **0** on the same turn. Four lanes read that +> zero and concluded the spy subtree was draw-free. It was never a statement about the subtree; it +> was a statement about which function the call sits in. + +### 5.3 The detection roll — it fires, and it costs one word + +`draw_sites`, the post-turn autosave marker, the row verbatim: + +``` +entry = Chance +ret_rva = 0x0048_7c8f (VA 0x00887c8f — the instruction after `call 0x8e6dd0` at 0x00887c8a) +calls = 1 +words = 1 +no_draw_calls = 0 +strategic = true +``` + +and the same site in the per-call-site report, with the owner resolved: + +``` +* STRAT Chance call 0x00887c8a +0x4da calls=1 words=1 +``` + +The boundary ledger for the same turn: + +``` +BeginProcessTurn 345 -> 345 0 + ProcessTurn 345 -> 363 18 + OnAllCombatDone_Tail 363 -> 364 1 +Autosave 364 -> 364 0 +``` + +> **`OnAllCombatDone_Tail` costs 1 word, against 0 on every turn any lane has ever measured, and the +> whole of it is the detection roll.** + +Turn total 19 words; the per-site sum after removing helper-internal rows is **19**; **residual 0**. +The tail's one word is the only site in the tail that fired: no row at `0x00820e18` or `0x0088b613` +(trade 13 — `tscr` is 252 in this game and no fleet stands on a sector node, so lane AC's predicate +says 0 and it is 0), none at `0x0082cdb8` (trade 15), none at `0x0088dc43` (spy 14 — `cm = 0`, lane +AG's failed conjunct), and none of `P`'s three. + +**Predictions, scored.** P1 held trivially (0 regenerations, not ≤ 5). **P2 held.** **P3 held +exactly.** **P4 held exactly** — one word, `no_draw_calls = 0`, and the reasoning behind it (four +widened-float literals plus `Chance`'s cost at interior *p*) was written down before the run. +**P5 held** — 0 → 1. **P6 held in both halves**: `sdet` stayed −1 (the roll failed, as a 1.68 % +roll should), and `sdo` moved from its corpus-constant 0 to **0.0084** on the deploy turn and to +**0.0168** after the measured turn — an increment of exactly 0.0084 both times, which is the +accumulator running. **P7 held.** **P8 held only modulo the AI-seed leaves** (§5.1). **P9 held.** + +### 5.4 The second instrument agreed — which is what makes this more than one number + +The coordinator asked for a save-visible confirmation independent of the bracket. There are two: + +* **`sdo` moved and by the right amount.** 0 → 0.0084 → 0.0168. Only branch D writes `sdo`, and it + writes it immediately before the gate, so a moving `sdo` proves the branch ran; the *equal* + increments prove the accumulator, not a one-off. +* **`spies2` filled in exactly one system.** That is the deploy half rather than the roll, but it + confirms the `deat` reading the whole predicate rests on. + +`sdet` did **not** move, and per P6 that was the predicted outcome, not a failure. + +--- + +## 6. Coverage — what this did not touch, said as loudly as what it did + +* **One measured turn, one spy, one target system, one fleet of one ship.** The cost is 1 word on + that turn. The loop is over the spy vector, so the cost should be one word per deployed, + undetected spy at a system with a usable belt — **that is a reading, not a measurement.** Rule 20: + do not fit a constant to one observation. A second spy at a second belt system separates + per-spy from per-turn and costs one more deploy. +* **The `sdo` arithmetic is validated only in its first two terms.** `countA`, `countB` and the + species multiplier were 0/0/1.0 here, and `countC = 2` is inferred from the value, not observed. + A Zuul or Liir owner (`+0x5c` = 5 or 6) would exercise the 0.75/0.5 branches; a system with enemy + ships present would exercise `countA`/`countB`. +* **The auto-detect arm was never taken.** `(TerrFl & 1) == 0` at the target has not been reached — + it would need a belt to be mined out from under a deployed spy. So §1.2's polarity claim rests on + the instruction stream plus the deploy-time refusal, not on a measured firing. It is the single + most load-bearing unmeasured branch in this document. +* **`P` (`0x008408e0`) and its three draws stay unfired**, and the failed conjunct is now `sdet`, + not `deat`. From this save the recipe is arithmetic rather than a guess: `sdo` grows 0.0084/turn, + so `P(detected)` is 12 % after 5 more End Turns, **38 % after 10, 65 % after 15, 85 % after 20** — + then `P` runs 3 turns after `sdet` is stamped, provided the system is still foreign-owned. + About 20 End Turns from `as-turn15-spydeployed.sav`, no clicking. +* **`0x0078c97f` was decoded and not measured** (§6.1). +* **`probes=11` was not used anywhere in this lane**, and no number here is comparable to lane AC's + turn totals, which were taken at that configuration. +* **No `Guard` region is declared by any hook in this family**, the same gap lanes Z and H reported: + `undeclared = 0` in this trace is vacuous. +* **One control run was lost to the harness and is reported rather than hidden.** A third + `hooks=off` run mis-detected the post-turn End Turn button as "turn still running" and clicked + End Turn three times, advancing the line to turn 18. It was discarded; nothing in §5 uses it. The + defect was in this lane's own click automation (the brightness test had the wrong polarity: after + a completed turn the button is *brighter*, not dimmer), which is rule 19 pointing at the operator + rather than at the detour. Its lesson is worth carrying: **verify the turn number changed, not a + button's colour.** + +### 6.1 A draw site nobody has — the deploy's own `NextFloat` + +Decoded here and **not measured**, so it is a rule-6 hypothesis with a hook site attached: + +``` +0078c975 mov ecx,[esi+0x16c] ; esi = StrategyServer -- the STRATEGIC generator +0078c97c add ecx,4 +0078c97f call 0x0047d830 ; Mars::RNG::NextFloat -- 1 word, return address 0x0078c984 +0078c984 fld qword [0x009e21b0] ; 6.283185482025146 == 2*pi + spy.cbh = NextFloat() * 2*pi +0078c9bb call edx ; -> ServerSpyManager::DeploySpy, vtable slot 7 +``` + +inside the `SHIPACTION_DEPLOYSPY` handler `0x0078c930`, which is entry 9 of the stack-built +ship-action function-pointer table (lane B6's indirection class — no direct callers, no vtable +slot, invisible to every call-graph sweep this campaign has run). + +It fires **once per Deploy Spy order applied**, inside `ApplyAllTurnCommands`, so it lands in the +**`ProcessTurn`** bracket, not the tail's. Its predicate is not a predicate on save fields at all — +it is *"a `SHIPACTION_DEPLOYSPY` command in this turn's command stream"*, a **stream** predicate, +which lane AG's gate-indexed audit has no column for and should grow one. + +To measure it costs one run: load `verify/results/saves/as-turn14-predeploy.sav` under `probes=8`, +select Kepler, `Special → Deploy Spy → → OK`, End Turn, and read `draw_sites` at +`ret_rva 0x0078c984`. This lane attempted it and lost the run to the map UI (Kepler and Midway sit +within four pixels of each other at the zoom the map opens at, and Midway wins the hit test), then +chose to report the site honestly rather than rush a fourth process. Prediction, committed here for +whoever takes it: **one call, one word, `strategic = true`, in the `ProcessTurn` bracket**, and the +same turn's tail still costs exactly 1 — the deploy and the first detection roll happen on the same +End Turn, because `ApplyAllTurnCommands` runs before `OnAllCombatDone_Tail`. + +--- + +## 7. VM144 as left + +**Restored and verified by screenshot at the main menu**, profile `re`, 2026-09-09 00:03 local. + +* `C:\SOTS\binkw32.dll` restored to lane L3's build from `C:\SOTS\shimdist-l3\binkw32.dll` + (sha256 `479B8614D2417603…`, byte-identical to what this lane found in place). +* `C:\SOTS\shim.cfg` restored from `C:\SOTS\ui\preAS-shim.cfg` (L3's `shim.cfg.l3probe`). +* The three autosaves restored **byte-identical** from `C:\SOTS\ui\preAS-SavedGames` + (`1985E6F4…` / `24B2E072…` / `5EC80C1E…`). +* `C:\SOTS\shim.trace.jsonl` removed. + +**`SavedGames` is now 15 files, not 13**, so **every Load-dialog row position has moved again** — +screenshot the dialog, do not reuse a remembered row. The two additions are this lane's, and they +are worth leaving because they are the only deployed-spy states in existence: + +| file | turn | why it is worth keeping | +|---|---|---| +| `MyGameas1predeploy.sav` | 14 | fleet + tender + docked spy standing **at** an AI belt colony, one click from a deploy — the input for §6.1 | +| `MyGameas1spydep.sav` | 15 | **the deployed spy**: `deat 400`, `sdet -1`, `sdo 0.0084`, `spies2 = [1]` at Kepler | + +At the 15-file set, verified by screenshot this session: `MyGameas1predeploy` is at **(400, 347)** +and `MyGameas1spydep` at **(400, 376)**; `OK` is unmoved at (682, 624). + +Also left in place, all harmless: `C:\SOTS\shimdist-as\` (build +`as-c172c99-20260909T0205Z`, 64 files), `C:\SOTS\ui\asgo.ps1`, `C:\SOTS\ui\ashash.ps1`, +`C:\SOTS\ui\preAS-shim.cfg`, `C:\SOTS\ui\preAS-SavedGames\` (13 files), +`C:\SOTS\ui\asfetch.sav` / `asgrab.jsonl` / `asgrab.log` (scratch). **`click_helper.ps1` was not +touched** — this lane needed no new verbs, which is the first VM lane in a while that did not. + +### 7.1 Guest notes worth carrying forward + +* **The map's hover readout works with a plain `move`** on VM144 — lane AC's `jmove` jiggle was not + needed here. Enter Move mode first; the readout renders at the hovered star, not at the cursor. +* **Finding a named system on the star map is solvable arithmetically instead of by hunting.** + Hover four systems, read their names, and least-squares-fit an affine map from the save's `Pos` + vectors to screen coordinates; the residuals came out ≤ 8 px on 5 points, which is enough to click + a specific star first time. The script is in this lane's scratch notes and the method is the + reusable part. Its failure mode is systems that project within a few pixels of each other + (Kepler and Midway), where the nearer one wins the hit test. +* **The End Turn button is BRIGHTER after a turn completes, not dimmer.** Any "is the turn still + running?" test built on that button's colour has the polarity backwards. Poll the **autosave + mtime** and the **turn number in the ticker**, never the button. +* Accepting the AI's non-aggression offer (turn 6 here) stopped the combat encounters that were + interrupting every third End Turn, and cost nothing this lane needed. + +--- + +## 8. Corrections to earlier findings (rule 11) + +* **`2026-09-09-tail-draws.md` §0.7 and §7, and lane AC §2.2: "the detection roll is inline in + `vslot13`".** It is a direct `E8` call to the `Chance` entry point sitting in `vslot13`'s body. + The instrument advice that followed from it is right; the stated reason is not. §1.1. +* **Lane L3 §6: "`spies2` is therefore not the spy list".** Half right. It is not the spy manager's + craft list; it is the **system's deployed-spy id vector**, written by `ServerSystem::AddSpy` in + the same two instructions that set `deat`. L3's own second alternative was the correct one. §1.5. +* **Lane AC §5: "the textbook target — an enemy colony with a belt — does not exist in this game."** + True of that map and read across the campaign as if it were true of the game. Two corpus saves + already had four AI-owned belt systems, and the first map this lane generated had two. §2. +* **Lane V2 §3.1's spy-13 row** listed `0x00887c8a` correctly and is **vindicated**, like its + trade-13 row before it. Its "never observed firing" note is now discharged for this one site. +* **Ghidra's size for `0x008877b0` is 1,869 bytes; the next function starts at `0x00887f30`, i.e. + 1,920.** The detection roll at `0x00887c8a` is inside both, but the 51-byte shortfall is rule 17 + again, on the very function this lane exists to read. + +--- + +## 9. Artifacts + +| what | where | +|---|---| +| **the deployed-spy save** — first `deat != 0` and first non-empty `spies2` in the corpus | `verify/results/saves/as-turn15-spydeployed.sav` (76,705 B, `--strict` 0 errors / 0 warnings, sha256 `854a10fa1ea602f0…`) | +| the pre-deploy save, one click from a deploy at an AI belt colony | `verify/results/saves/as-turn14-predeploy.sav` (75,908 B, sha256 `53986f8511cbf992…`) | +| the instrumented trace (`draw_sites` + `probe_entries` + the boundary ledger) | `verify/traces/as-probes8-turn15-turn16.jsonl.gz` | +| shim log for that run (probe and detour install status) | `verify/results/shim/as/as-probes8-run.shim.log` | +| addresses this lane mints | `ghidra/addresses.d/as.json` (7 entries; 6 `verified`, 1 `mapped`) | +| predictions, committed before the build | `sots-engine` `docs/AS-predictions.md` (`c172c99`, addendum `61875a4`) | +| instrument | `sots-engine` `src/shim/shim.cfg.hp8` and `shim.cfg.hoff`, **both unchanged** — this lane wrote no engine code | + +### 9.1 The hashes, for the record + +``` +input MyGameas1spydep.sav 854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08 +A/B/M (Autosave EndTurn).sav 98e45d3745b914506fc4c409ad5a5ad0938a2003ab4c845421a99a8065149c3a +A (Autosave).sav 262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe +B (Autosave).sav e34775a757e6fb3df647d7a589e8b1819b90bea094196a9e3cc819e51ed63257 +M (Autosave).sav 262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe +``` + +**This is not a calibration pair for the standalone**, and it must not be entered in +`determinism-hashes.txt` as one. A ≠ B, in the AI-research-pick leaf class, so the turn is +reproducible only once the client seeds are pinned. What it *is* is a reproducible **input** and a +turn whose every non-AI leaf reproduces across three processes and two configurations. + +--- + +## Proposed `campaign/board.md` rows + +New row: + +``` +| THE SPY DETECTION ROLL FIRES - 1 word, gated on an asteroid belt; `spies2` and `deat` closed | verify | verified | high | 100% | 2026-09-09 | **Lane AS, VM144, build `as-c172c99-20260909T0205Z`, `probes=8` (never 11).** `Mars::RNG::Chance` at **0x00887c8a**, in the BODY of `ServerSpyManager::vslot13` (tail phase 23 call 9), fired **1 call / 1 word / no_draw_calls=0 / strategic=true** at `ret_rva 0x00887c8f`; `OnAllCombatDone_Tail` went **363 -> 364**, against 0 on every turn any lane has ever measured. Turn total 19, per-site sum 19, **residual 0**. THE ENTRY PROBE ON `SpyManager::Slot13RngCallee 0x008408e0` READ **0 ON THE SAME TURN** - that zero was never a statement about the subtree, only about which function the call sits in (rule 28 practice 4, demonstrated rather than argued). **CORRECTION: the roll is NOT inline** (the resolution and AC §2.2 both say so) - it is a plain `E8` to the `Chance` entry point in the caller. **THE PREDICATE**, decoded to save fields: `spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1)`; **corpus count 0 of 22, failed conjunct `deat != 0`**. `ServerSystem_BeltUsableFlags 0x00743f80` (16 bytes) returns `TerrFl` with bit 0 cleared when `ARes2 <= 0`; `ARes2>0` and `TerrFl&1` agree on all 616 corpus system records AND on the new map, which also has `TerrFl = 2` and `3`, so bit 0 is the belt specifically. **POLARITY: belt gone => NO DRAW AND THE SPY IS DETECTED ANYWAY** (`je` past the roll to `sdet := turn`) - a zero here is two different results. Cost: `sdo` starts at 0, `SpyCraft_AccumulateDetectionOdds 0x0081f570` ACCUMULATES `0.7*(0.01 + [cA]0.01 + [cB]0.02 + 0.001*cC) * species(1.0/0.75/0.5)` and CLAMPS AT 1.0, so `Chance` costs 1 word until sdo reaches 1 and 0 thereafter. Measured `sdo` 0 -> 0.0084 -> 0.0168 (= 0.7*0.012 exactly, a value no run had produced). **`spies2` IS CLOSED**: `ServerSystem::AddSpy 0x007514c0` sets `spy.deat = system handle` and pushes `spy.sid` into `ServerSystem+0x1cc` in the same two instructions - `spies2` is the per-system DEPLOYED-spy id vector; L3's "not the spy list" was half right, AC's P4 was right. Measured: `count=1 element=1` at Kepler, 0 in the other 27. **NEW DRAW SITE NOBODY HAS**: `0x0078c97f` NextFloat in the SHIPACTION_DEPLOYSPY handler `0x0078c930` (`cbh = NextFloat()*2pi`), strategic generator, in the ProcessTurn bracket - decoded, NOT measured, and its predicate is on the COMMAND STREAM not the save, a column the gate-indexed audit lacks. Oracle: input byte-identical in 3 processes; A vs B differ in **22 leaves, all inside one AI player's research pick** (the known per-process client seed, resolution 2026-09-08) - so this is NOT a calibration pair and must not enter `determinism-hashes.txt`. `probes=8` run came out byte-identical to control A. Saves: `as-turn15-spydeployed.sav`, `as-turn14-predeploy.sav`; findings `findings/subsystems/spy-detection-roll.md` | +``` + +Edits to existing rows: + +- **Row 62** (guest holders) — `VM144 = FREE (lane AS released 2026-09-09; restored to L3's build + shim.cfg + the three pre-AS autosaves byte-identical, main menu verified by screenshot). SavedGames is now 15 FILES - AS added MyGameas1predeploy (400,347) and MyGameas1spydep (400,376), so ROW POSITIONS HAVE MOVED AGAIN; screenshot the dialog. AS left C:\SOTS\shimdist-as + ui\as{go,hash}.ps1 + ui\preAS-*; click_helper.ps1 untouched.` +- **Row 207** (V2's eight sites) — append: `FIRED 2026-09-09: 0x00887c8a (spy 13), lane AS, 1 word, behind a deployed spy at a foreign colony with a usable asteroid belt. V2 vindicated a second time. Five of the eight still unfired: P's three (0x00840929 / 0x008409c7 / 0x00840a3c, gate now `sdet != -1` and turn-sdet>=3, ~20 End Turns from as-turn15-spydeployed.sav), 0x0088dc43 (spy 14, gate `cm in 1..4`, lane AG), 0x0082cdb8 (trade 15). AND THE INVENTORY IS SHORT ONE: 0x0078c97f in the SHIPACTION_DEPLOYSPY handler draws a NextFloat on the strategic generator and is in no sweep, because it hangs off the stack-built ship-action table.` +- **Row 373 / the `spies2` rule-6 flag** — `CLOSED 2026-09-09 by lane AS, from the writer and then live. spies2 (ServerSystem+0x1cc) is the per-system vector of DEPLOYED spy ids: ServerSystem::AddSpy 0x007514c0 sets SpyCraft+0x10 (`deat`) to the system handle and push_backs SpyCraft+0x4 (`sid`) into +0x1cc; RemoveSpy 0x0074f550 is the exact mirror. Measured non-empty for the first time in the corpus: count=1, element=1, in the deploy system only. `SysMem` and `mts` are untouched and their flags stay up.` +- **Row 186 / `tail-rng-ledger.md`** — append to the existing qualification: `THIRD site outside the firing-indexed table has now fired: the spy detection roll (tail T23c9, lane AS). The pattern is now three for three - hives, the raid roll, the detection roll - and each fired on the first state built for it. The gate-indexed audit is the fix, not a longer table.` diff --git a/ghidra/addresses.d/as.json b/ghidra/addresses.d/as.json new file mode 100644 index 0000000..833cd66 --- /dev/null +++ b/ghidra/addresses.d/as.json @@ -0,0 +1,61 @@ +{ + "_note": "Lane AS (2026-09-09). The spy-deployment and detection-roll chain, read from the instruction stream of dumps/sots.exe (disassembled to the NEXT FUNCTION START, rule 17 -- Ghidra's reported ends are not used anywhere below). Every entry marked `verified` was read instruction by instruction and its operand offsets cross-checked against objects/layouts.md for Game::ServerSystem; entries marked `mapped` were reached but not read. Two of these -- ServerSystem_AddSpy and ServerSystem_RemoveSpy -- also settle the campaign's long-standing `spies2` rule-6 flag: they are the only writers of both SpyCraft+0x10 (`deat`) and ServerSystem+0x1cc (`spies2`), and they write them together. Evidence: findings/subsystems/spy-detection-roll.md. NOT minted here because another fragment already has them: ServerSystem_GetOwner 0x007437e0 (lane B5) and ServerSystem_IsIndependent 0x00743fa0 (lane AG), both of which this chain calls.", + "entries": [ + { + "name": "ServerSystem_AddSpy", + "addr": "0x007514c0", + "convention": "__thiscall", + "prototype": "void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = this ? this->[+4] : 0; push_back(&this->spies2(+0x1cc), &spy->sid(+0x4))` via lane AI3's 0x0059f1a0. THE ONLY WRITER THAT EVER MAKES `deat` NON-ZERO AT RUNTIME (the other stores to SpyCraft+0x10 are the zero-init in CreateSpyCraft, the deserializer, and three field copies). Exactly one caller, 0x008874d4 inside ServerSpyManager_DeploySpy, and no vtable slot. THIS SETTLES `spies2`: ServerSystem+0x1cc is the per-system vector of DEPLOYED spy ids, written in the same two instructions as `deat`, which is why it is 0 in every save whose only spy is docked to its tender */", + "status": "verified", + "source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)" + }, + { + "name": "ServerSystem_RemoveSpy", + "addr": "0x0074f550", + "convention": "__thiscall", + "prototype": "void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = 0; erase(&this->spies2(+0x1cc), &spy->sid(+0x4))` via 0x0059ec00. The exact mirror of ServerSystem_AddSpy; the two together are the whole life cycle of `deat` and of `spies2` */", + "status": "verified", + "source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)" + }, + { + "name": "ServerSpyManager_DeploySpy", + "addr": "0x00887410", + "convention": "__thiscall", + "prototype": "bool (Game::ServerSpyManager* this, int spyHandle, float cbh) /* Game::ServerSpyManager vftable 0x00a3073c SLOT 7; zero direct call sites -- reached only from the SHIPACTION_DEPLOYSPY handler 0x0078c930 through [[dispatcher+0x15c]+0x1c]. Read at the field writes: spy->Reset() (0x00838070), then sdo(+0x3c) := 0.0f, sdet(+0x40) := -1, cbh(+0x18) := arg, tdep(+0x14) := server->[+0xc] (the current turn), atto(+0xc) := 0 and carrierShip->[+0xa8] := 0 (detach), then ServerSystem_AddSpy(system, spy) at 0x008874d4, then a player message built from system->Name (+0xa8, capacity at +0xbc -- which is what proves the receiver is a ServerSystem). CONSEQUENCE FOR THE TAIL: a spy is deployed with sdet == -1 and sdo == 0, so ServerSpyManager_vslot13's detection branch is entered on the very next OnAllCombatDone_Tail */", + "status": "verified", + "source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)" + }, + { + "name": "ServerSystem_BeltUsableFlags", + "addr": "0x00743f80", + "convention": "__thiscall", + "prototype": "int (Game::ServerSystem* this) /* 16 B, complete: `eax = this->TerrFl(+0x19c); if (this->ARes2(+0x6c) <= 0) eax &= ~1; return eax`. BIT 0 OF THE RETURN IS \"this system still has a usable asteroid belt\" -- TerrFl bit 0 masked by the belt's remaining resources. Corroborated from the save side without a debugger: ARes2 > 0 and TerrFl & 1 agree on all 616 system records of the 22-save corpus and on all 28 of the state this lane built, where TerrFl also takes the values 2 and 3, so bit 1 is a second, independent terrain feature. Called at 0x00887c71 (the detection roll's gate) and at 0x0083d15d inside the shared placement validator 0x0083ce50, where a clear bit 0 sets refusal bit 0x1000000 */", + "status": "verified", + "source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)" + }, + { + "name": "SpyCraft_AccumulateDetectionOdds", + "addr": "0x0081f570", + "convention": "__stdcall", + "prototype": "void (Game::SpyCraft* spy, Game::ServerSystem* sys, Game::ServerPlayer* spyOwner) /* 512 B, `ret 0xc`. Writes spy->sdo (+0x3c) and is called unconditionally at 0x00887c66, immediately before the detection roll's gate. EARLY OUT: if sys->PID == 0 (unowned) it stores 0.0f and returns, so a spy at an unowned system rolls at p = 0 and costs no word. Otherwise it walks the system's fleet vector accumulating three counters and computes, with every literal read as the four bytes in the image (rule 23): p = 0.01f + (cA>0 ? 0.01f : 0) + (cB>0 ? 0.02f : 0) + 0.001*cC, then p *= (spyOwner->[+0x5c]==5 ? 0.75f : spyOwner->[+0x5c]==6 ? 0.5f : 1.0f), then p *= 0.7, then sdo = min(sdo + p, 1.0) -- IT ACCUMULATES onto the old value and is CLAMPED AT 1.0. So a quiet foreign target gives sdo = 0.007 on the first turn and ~0.007/turn after, and a spy left in place long enough reaches p >= 1, at which point RNG_Chance costs 0 words and always succeeds */", + "status": "verified", + "source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)" + }, + { + "name": "ServerSpyManager_DetectionRoll_DrawSite", + "addr": "0x00887c8a", + "convention": "site", + "prototype": "/* THE SPY DETECTION ROLL. A plain E8 rel32 `call Mars_RNG_Chance 0x008e6dd0` in the BODY of ServerSpyManager_vslot13 0x008877b0 -- NOT an inlined draw (the campaign carried `inline` for a day; the draw is a normal entry-point call, it simply sits in the caller rather than in SpyManager_Slot13RngCallee 0x008408e0, which is why an entry probe on that callee reads zero either way). Generator: `mov ecx,[server+0x16c]` at 0x00887c80 -- the STRATEGIC generator. Probability: `fld [spy+0x3c]` -- sdo. Return address for lane Z's draw_sites ledger: 0x00887c8f. Gates, in order: spy.deat != 0, spy.sdet == -1, and ServerSystem_BeltUsableFlags(sys(deat)) & 1. WHEN THAT LAST BIT IS CLEAR THE ROLL IS SKIPPED AND THE SPY IS DETECTED UNCONDITIONALLY (`je 0x887c97`, which stores sdet := turn) -- a zero here is therefore not always a negative */", + "status": "verified", + "source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)" + }, + { + "name": "SpyCraft_ResetMission", + "addr": "0x00838070", + "convention": "__thiscall", + "prototype": "void (Game::SpyCraft* this) /* Called by ServerSpyManager_DeploySpy at 0x0088749e and by ServerSpyManager_vslot14 at 0x0088dc11 when the target system changes hands. NOT READ: only its position in those two chains and the fields its callers write immediately afterwards (sdo := 0, sdet := -1) are established here */", + "status": "mapped", + "source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)" + } + ] +} diff --git a/ghidra/generated/sots_addresses.h b/ghidra/generated/sots_addresses.h index f533f38..217553d 100644 --- a/ghidra/generated/sots_addresses.h +++ b/ghidra/generated/sots_addresses.h @@ -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 @ 48db3cc, generated 2026-09-08 by tools/gen_addresses.py +// Source: sots-re ghidra/addresses.json @ 712d184, generated 2026-09-08 by tools/gen_addresses.py // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). #pragma once #include @@ -1281,6 +1281,20 @@ constexpr uint32_t RaidEncounter_Helper848e50 = 0x00448e50; constexpr uint32_t RaidEncounter_Helper848e50_DrawSite = 0x00448fd9; // thiscall Called only from TradeManager_BuildRaidEncounter at 0x00892700; calls TradeManager_ComputeRaidInterceptPoint at 0x0082cf65, which is where the NextFloat at 0x00820c1b is spent on this path. Body not read [mapped] constexpr uint32_t RaidEncounter_Helper82ce00 = 0x0042ce00; +// __thiscall void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = this ? this->[+4] : 0; push_back(&this->spies2(+0x1cc), &spy->sid(+0x4))` via lane AI3's 0x0059f1a0. THE ONLY WRITER THAT EVER MAKES `deat` NON-ZERO AT RUNTIME (the other stores to SpyCraft+0x10 are the zero-init in CreateSpyCraft, the deserializer, and three field copies). Exactly one caller, 0x008874d4 inside ServerSpyManager_DeploySpy, and no vtable slot. THIS SETTLES `spies2`: ServerSystem+0x1cc is the per-system vector of DEPLOYED spy ids, written in the same two instructions as `deat`, which is why it is 0 in every save whose only spy is docked to its tender */ [verified] +constexpr uint32_t ServerSystem_AddSpy = 0x003514c0; +// __thiscall void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = 0; erase(&this->spies2(+0x1cc), &spy->sid(+0x4))` via 0x0059ec00. The exact mirror of ServerSystem_AddSpy; the two together are the whole life cycle of `deat` and of `spies2` */ [verified] +constexpr uint32_t ServerSystem_RemoveSpy = 0x0034f550; +// __thiscall bool (Game::ServerSpyManager* this, int spyHandle, float cbh) /* Game::ServerSpyManager vftable 0x00a3073c SLOT 7; zero direct call sites -- reached only from the SHIPACTION_DEPLOYSPY handler 0x0078c930 through [[dispatcher+0x15c]+0x1c]. Read at the field writes: spy->Reset() (0x00838070), then sdo(+0x3c) := 0.0f, sdet(+0x40) := -1, cbh(+0x18) := arg, tdep(+0x14) := server->[+0xc] (the current turn), atto(+0xc) := 0 and carrierShip->[+0xa8] := 0 (detach), then ServerSystem_AddSpy(system, spy) at 0x008874d4, then a player message built from system->Name (+0xa8, capacity at +0xbc -- which is what proves the receiver is a ServerSystem). CONSEQUENCE FOR THE TAIL: a spy is deployed with sdet == -1 and sdo == 0, so ServerSpyManager_vslot13's detection branch is entered on the very next OnAllCombatDone_Tail */ [verified] +constexpr uint32_t ServerSpyManager_DeploySpy = 0x00487410; +// __thiscall int (Game::ServerSystem* this) /* 16 B, complete: `eax = this->TerrFl(+0x19c); if (this->ARes2(+0x6c) <= 0) eax &= ~1; return eax`. BIT 0 OF THE RETURN IS "this system still has a usable asteroid belt" -- TerrFl bit 0 masked by the belt's remaining resources. Corroborated from the save side without a debugger: ARes2 > 0 and TerrFl & 1 agree on all 616 system records of the 22-save corpus and on all 28 of the state this lane built, where TerrFl also takes the values 2 and 3, so bit 1 is a second, independent terrain feature. Called at 0x00887c71 (the detection roll's gate) and at 0x0083d15d inside the shared placement validator 0x0083ce50, where a clear bit 0 sets refusal bit 0x1000000 */ [verified] +constexpr uint32_t ServerSystem_BeltUsableFlags = 0x00343f80; +// __stdcall void (Game::SpyCraft* spy, Game::ServerSystem* sys, Game::ServerPlayer* spyOwner) /* 512 B, `ret 0xc`. Writes spy->sdo (+0x3c) and is called unconditionally at 0x00887c66, immediately before the detection roll's gate. EARLY OUT: if sys->PID == 0 (unowned) it stores 0.0f and returns, so a spy at an unowned system rolls at p = 0 and costs no word. Otherwise it walks the system's fleet vector accumulating three counters and computes, with every literal read as the four bytes in the image (rule 23): p = 0.01f + (cA>0 ? 0.01f : 0) + (cB>0 ? 0.02f : 0) + 0.001*cC, then p *= (spyOwner->[+0x5c]==5 ? 0.75f : spyOwner->[+0x5c]==6 ? 0.5f : 1.0f), then p *= 0.7, then sdo = min(sdo + p, 1.0) -- IT ACCUMULATES onto the old value and is CLAMPED AT 1.0. So a quiet foreign target gives sdo = 0.007 on the first turn and ~0.007/turn after, and a spy left in place long enough reaches p >= 1, at which point RNG_Chance costs 0 words and always succeeds */ [verified] +constexpr uint32_t SpyCraft_AccumulateDetectionOdds = 0x0041f570; +// site /* THE SPY DETECTION ROLL. A plain E8 rel32 `call Mars_RNG_Chance 0x008e6dd0` in the BODY of ServerSpyManager_vslot13 0x008877b0 -- NOT an inlined draw (the campaign carried `inline` for a day; the draw is a normal entry-point call, it simply sits in the caller rather than in SpyManager_Slot13RngCallee 0x008408e0, which is why an entry probe on that callee reads zero either way). Generator: `mov ecx,[server+0x16c]` at 0x00887c80 -- the STRATEGIC generator. Probability: `fld [spy+0x3c]` -- sdo. Return address for lane Z's draw_sites ledger: 0x00887c8f. Gates, in order: spy.deat != 0, spy.sdet == -1, and ServerSystem_BeltUsableFlags(sys(deat)) & 1. WHEN THAT LAST BIT IS CLEAR THE ROLL IS SKIPPED AND THE SPY IS DETECTED UNCONDITIONALLY (`je 0x887c97`, which stores sdet := turn) -- a zero here is therefore not always a negative */ [verified] +constexpr uint32_t ServerSpyManager_DetectionRoll_DrawSite = 0x00487c8a; +// __thiscall void (Game::SpyCraft* this) /* Called by ServerSpyManager_DeploySpy at 0x0088749e and by ServerSpyManager_vslot14 at 0x0088dc11 when the target system changes hands. NOT READ: only its position in those two chains and the fields its callers write immediately afterwards (sdo := 0, sdet := -1) are established here */ [mapped] +constexpr uint32_t SpyCraft_ResetMission = 0x00438070; // thiscall void (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) // the body of every `Player..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] diff --git a/verify/results/saves/as-turn14-predeploy.sav b/verify/results/saves/as-turn14-predeploy.sav new file mode 100644 index 0000000..278c4e8 Binary files /dev/null and b/verify/results/saves/as-turn14-predeploy.sav differ diff --git a/verify/results/saves/as-turn15-spydeployed.sav b/verify/results/saves/as-turn15-spydeployed.sav new file mode 100644 index 0000000..73ea5a0 Binary files /dev/null and b/verify/results/saves/as-turn15-spydeployed.sav differ diff --git a/verify/results/shim/as/as-probes8-run.shim.log b/verify/results/shim/as/as-probes8-run.shim.log new file mode 100644 index 0000000..d35655c --- /dev/null +++ b/verify/results/shim/as/as-probes8-run.shim.log @@ -0,0 +1,114 @@ +23:43:10.838 [tid 7184] ==== sots-engine shim (binkw32 proxy) build as-c172c99-20260909T0205Z ==== +23:43:10.838 [tid 7184] exe: C:\SOTS\Sword of the Stars.exe +23:43:10.838 [tid 7184] exe base=0x006b0000 (link-time image base 0x00400000, ASLR delta +2818048) pid=3060 shim=72b70000 +23:43:10.838 [tid 7184] addresses: Source: sots-re ghidra/addresses.json @ bdaa26f, generated 2026-09-08 by tools/gen_addresses.py +23:43:10.854 [tid 7184] config: hooks=trace +23:43:10.854 [tid 7184] config: hook.Shim::SelfTest::Fill=off +23:43:10.854 [tid 7184] config: hook.Mars::GlobalConsts::LoadFile=off +23:43:10.854 [tid 7184] config: hook.Game::WeaponDictionary::Init=off +23:43:10.854 [tid 7184] config: hook.Game::SectionDictionary::SectionDictionary=off +23:43:10.854 [tid 7184] config: hook.Game::ServerPlayer::ComputeBudget=off +23:43:10.854 [tid 7184] config: hook.Game::TechTree::ProcessResearch=off +23:43:10.854 [tid 7184] config: hook.Game::ServerPlayer::OnTechResearched=off +23:43:10.854 [tid 7184] config: hook.Game::ServerSystem::ProcessTurn=off +23:43:10.854 [tid 7184] config: hook.Game::ServerPlayer::ProcessTurn=off +23:43:10.854 [tid 7184] config: hook.Game::ServerSystem::GroupOutput=off +23:43:10.854 [tid 7184] config: hook.Game::ServerSystem::ComputeTotalOutput=off +23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::MoveFleet=off +23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::ProcessFleetMovement=off +23:43:10.854 [tid 7184] config: hook.Game::StrategyHost::Autosave=trace +23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::ProcessTurn=trace +23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::OnAllCombatDone_Tail=trace +23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::ApplyEncounterResult=trace +23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::NodeLineDecay=trace +23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace +23:43:10.854 [tid 7184] config: hook.Game::EncounterDetect::AssignContacts=trace +23:43:10.854 [tid 7184] config: hook.Game::EncounterDetect::ProcessTeamRecord=trace +23:43:10.854 [tid 7184] config: fpu.sample_turn=off +23:43:10.854 [tid 7184] config: fpu.sample_ticks=off +23:43:10.854 [tid 7184] config: trace.inline_max=64 +23:43:10.854 [tid 7184] config: trace.path=C:\SOTS\shim.trace.jsonl +23:43:10.854 [tid 7184] config: trace.flush=always +23:43:10.854 [tid 7184] config: probes=8 -> 8 lane-H entry probes +23:43:10.947 [tid 7184] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 64, flush always) +23:43:10.947 [tid 7184] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=00b50e50 +23:43:10.947 [tid 7184] hook: MH_Initialize -> MH_OK +23:43:10.947 [tid 7184] hook: MH_CreateHook -> MH_OK (trampoline=01930fe0) +23:43:10.947 [tid 7184] hook: MH_EnableHook -> MH_OK +23:43:10.947 [tid 7184] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +23:43:10.947 [tid 7184] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +23:43:10.947 [tid 7184] dict: dictionaries hook ready (crt new=74c4232b delete=74c40174) +23:43:10.947 [tid 7184] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +23:43:10.947 [tid 7184] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +23:43:10.963 [tid 7184] research: ProcessResearch hook ready (Cost=0082da00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +23:43:10.963 [tid 7184] hook: Game::TechTree::ProcessResearch rva=0x001876c0 mode=off (not installed) +23:43:10.963 [tid 7184] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +23:43:10.963 [tid 7184] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed) +23:43:10.963 [tid 7184] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed) +23:43:10.963 [tid 7184] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed) +23:43:10.963 [tid 7184] hook: Game::ServerSystem::GroupOutput rva=0x0034b7a0 mode=off (not installed) +23:43:10.963 [tid 7184] hook: Game::ServerSystem::ComputeTotalOutput rva=0x00350480 mode=off (not installed) +23:43:10.963 [tid 7184] player_turn: ServerPlayer::ProcessTurn hook armed (ratio helper at 0082e950) +23:43:10.963 [tid 7184] hook: Game::ServerPlayer::ProcessTurn rva=0x00491340 mode=off (not installed) +23:43:10.963 [tid 7184] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed) +23:43:10.963 [tid 7184] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +23:43:10.963 [tid 7184] hook: Game::StrategyHost::Autosave rva=0x00495210 -> va=00b45210 MH_CreateHook -> MH_OK (trampoline=01930fc0) +23:43:10.979 [tid 7184] hook: Game::StrategyHost::Autosave MH_EnableHook -> MH_OK mode=trace +23:43:10.979 [tid 7184] hook: Game::StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=00a8c6c0 MH_CreateHook -> MH_OK (trampoline=01930fa0) +23:43:10.994 [tid 7184] hook: Game::StrategyServer::ProcessTurn MH_EnableHook -> MH_OK mode=trace +23:43:10.994 [tid 7184] hook: Game::StrategyServer::OnAllCombatDone_Tail rva=0x003d92a0 -> va=00a892a0 MH_CreateHook -> MH_OK (trampoline=01930f80) +23:43:11.010 [tid 7184] hook: Game::StrategyServer::OnAllCombatDone_Tail MH_EnableHook -> MH_OK mode=trace +23:43:11.010 [tid 7184] hook: Game::StrategyServer::ApplyEncounterResult rva=0x003d8920 -> va=00a88920 MH_CreateHook -> MH_OK (trampoline=01930f60) +23:43:11.026 [tid 7184] hook: Game::StrategyServer::ApplyEncounterResult MH_EnableHook -> MH_OK mode=trace +23:43:11.026 [tid 7184] hook: Game::StrategyServer::NodeLineDecay rva=0x003ae010 -> va=00a5e010 MH_CreateHook -> MH_OK (trampoline=01930f40) +23:43:11.041 [tid 7184] hook: Game::StrategyServer::NodeLineDecay MH_EnableHook -> MH_OK mode=trace +23:43:11.041 [tid 7184] hook: Game::StrategyServer::ProcessNodeSpaceTravel rva=0x003a0e20 -> va=00a50e20 MH_CreateHook -> MH_OK (trampoline=01930f20) +23:43:11.057 [tid 7184] hook: Game::StrategyServer::ProcessNodeSpaceTravel MH_EnableHook -> MH_OK mode=trace +23:43:11.057 [tid 7184] hook: Game::EncounterDetect::AssignContacts rva=0x003aa240 -> va=00a5a240 MH_CreateHook -> MH_OK (trampoline=01930f00) +23:43:11.072 [tid 7184] hook: Game::EncounterDetect::AssignContacts MH_EnableHook -> MH_OK mode=trace +23:43:11.072 [tid 7184] hook: Game::EncounterDetect::ProcessTeamRecord rva=0x003ca640 -> va=00a7a640 MH_CreateHook -> MH_OK (trampoline=01930ee0) +23:43:11.088 [tid 7184] hook: Game::EncounterDetect::ProcessTeamRecord MH_EnableHook -> MH_OK mode=trace +23:43:11.088 [tid 7184] hook: Game::StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=00a898e0 MH_CreateHook -> MH_OK (trampoline=01930ec0) +23:43:11.104 [tid 7184] hook: Game::StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK mode=trace +23:43:11.104 [tid 7184] hook: Game::SVSOSwarmQueen::OnTurnBegin rva=0x00129930 -> va=007d9930 MH_CreateHook -> MH_OK (trampoline=01930ea0) +23:43:11.135 [tid 7184] hook: Game::SVSOSwarmQueen::OnTurnBegin MH_EnableHook -> MH_OK mode=trace +23:43:11.135 [tid 7184] hook: Game::SVSOSwarmQueen::RegisterHives rva=0x00127630 -> va=007d7630 MH_CreateHook -> MH_OK (trampoline=01930e80) +23:43:11.151 [tid 7184] hook: Game::SVSOSwarmQueen::RegisterHives MH_EnableHook -> MH_OK mode=trace +23:43:11.151 [tid 7184] hook: Game::SVSOSwarmQueen::TickHives rva=0x00127770 -> va=007d7770 MH_CreateHook -> MH_OK (trampoline=01930e60) +23:43:11.166 [tid 7184] hook: Game::SVSOSwarmQueen::TickHives MH_EnableHook -> MH_OK mode=trace +23:43:11.166 [tid 7184] hook: Game::SVSOSlaversRefuel::UpdateDifficultyTier rva=0x00115820 -> va=007c5820 MH_CreateHook -> MH_OK (trampoline=01930e40) +23:43:11.182 [tid 7184] hook: Game::SVSOSlaversRefuel::UpdateDifficultyTier MH_EnableHook -> MH_OK mode=trace +23:43:11.182 [tid 7184] hook: Mars::RNG::Seed rva=0x0009fdf0 -> va=0074fdf0 MH_CreateHook -> MH_OK (trampoline=01930e20) +23:43:11.197 [tid 7184] hook: Mars::RNG::Seed MH_EnableHook -> MH_OK mode=trace +23:43:11.197 [tid 7184] hook: Game::StrategyApp::RunAI rva=0x004706f0 -> va=00b206f0 MH_CreateHook -> MH_OK (trampoline=01930e00) +23:43:11.213 [tid 7184] hook: Game::StrategyApp::RunAI MH_EnableHook -> MH_OK mode=trace +23:43:11.229 [tid 7184] drawsite: Mars::RNG::NextFloat rva=0x0007d830 -> va=0072d830 create=MH_OK enable=MH_OK +23:43:11.244 [tid 7184] drawsite: Mars::RNG::NextInt rva=0x000271c0 -> va=006d71c0 create=MH_OK enable=MH_OK +23:43:11.260 [tid 7184] drawsite: Mars::RNG::Chance rva=0x004e6dd0 -> va=00b96dd0 create=MH_OK enable=MH_OK +23:43:11.276 [tid 7184] drawsite: Mars::RNG::NextUInt rva=0x000f7670 -> va=007a7670 create=MH_OK enable=MH_OK +23:43:11.291 [tid 7184] drawsite: Mars::RNG::FloatRange rva=0x0007d8a0 -> va=0072d8a0 create=MH_OK enable=MH_OK +23:43:11.307 [tid 7184] drawsite: Mars::RNG::IntRangeBell rva=0x004e6d80 -> va=00b96d80 create=MH_OK enable=MH_OK +23:43:11.322 [tid 7184] drawsite: Mars::RNG::GaussianRange rva=0x004e6e30 -> va=00b96e30 create=MH_OK enable=MH_OK +23:43:11.322 [tid 7184] probe: installing 8 of 12 (probes= in shim.cfg) +23:43:11.338 [tid 7184] probe: Game::ServerSpyManager::vslot13 rva=0x004877b0 -> va=00b377b0 create=MH_OK enable=MH_OK +23:43:11.354 [tid 7184] probe: Game::ServerSpyManager::vslot14 rva=0x0048db80 -> va=00b3db80 create=MH_OK enable=MH_OK +23:43:11.369 [tid 7184] probe: Game::ServerTradeManagerImpl::vslot13 rva=0x0048ef80 -> va=00b3ef80 create=MH_OK enable=MH_OK +23:43:11.385 [tid 7184] probe: Game::ServerTradeManagerImpl::vslot15 rva=0x0042cca0 -> va=00adcca0 create=MH_OK enable=MH_OK +23:43:11.401 [tid 7184] probe: Game::SpyManager::Slot13RngCallee rva=0x004408e0 -> va=00af08e0 create=MH_OK enable=MH_OK +23:43:11.416 [tid 7184] probe: Game::TradeManager::Slot13RngCalleeA rva=0x00420ca0 -> va=00ad0ca0 create=MH_OK enable=MH_OK +23:43:11.447 [tid 7184] probe: Game::TradeManager::Slot13RngCalleeB rva=0x0048b440 -> va=00b3b440 create=MH_OK enable=MH_OK +23:43:11.463 [tid 7184] probe: Game::ServerTradeManager::CreateRaidEncounter rva=0x004938a0 -> va=00b438a0 create=MH_OK enable=MH_OK +23:43:11.463 [tid 7184] watch: disabled (watch=off) +23:43:11.463 [tid 7184] aiorders: disabled (aiorders=off) +23:43:11.463 [tid 7184] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=off value=0x0000 sample_ticks=off +23:43:11.463 [tid 7184] fpu: sample_turn=off (off releases StrategyServer::ProcessTurn for another hook) +23:43:11.463 [tid 7184] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=00a33be0 MH_CreateHook -> MH_OK (trampoline=01930c00) +23:43:11.479 [tid 7184] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +23:43:11.479 [tid 7184] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=00a898e0 MH_CreateHook -> MH_ERROR_ALREADY_CREATED (trampoline=00000000) +23:43:11.479 [tid 7184] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 not installed (sampler off) +23:43:11.479 [tid 7184] fpu: DemoApp::OnTick rva=0x0049a640 not installed (sampler off) +23:43:11.479 [tid 7184] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +23:43:11.479 [tid 7184] Application::Initialize called (this=01968128) +23:48:37.590 [tid 7184] fpu: sample at StrategyClient::EndTurn (this=1e031680): cw=0x127f 53bit-double/nearest [no fpu.force configured] +23:48:40.778 [tid 7184] fpu: sample at StrategyClient::EndTurn (this=32a91610): cw=0x127f 53bit-double/nearest [no fpu.force configured] +23:48:40.778 [tid 7184] fpu: sample at StrategyClient::EndTurn (this=32a96d70): cw=0x127f 53bit-double/nearest [no fpu.force configured] diff --git a/verify/traces/as-probes8-turn15-turn16.jsonl.gz b/verify/traces/as-probes8-turn15-turn16.jsonl.gz new file mode 100644 index 0000000..5e12431 Binary files /dev/null and b/verify/traces/as-probes8-turn15-turn16.jsonl.gz differ