diff --git a/findings/control-flow/tail-probes.md b/findings/control-flow/tail-probes.md new file mode 100644 index 0000000..d616740 --- /dev/null +++ b/findings/control-flow/tail-probes.md @@ -0,0 +1,438 @@ +# Five probes on the running game — and a harness defect the probes found in themselves + +Lane H, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs. +Engine worktree `wip/probes`, predictions in `sots-engine/docs/H-probes.md`, **committed before the build** +(method rule 2). VM140 held for the duration; released at the end. + +**The headline, in order of what depends on what.** + +1. **The determinism oracle reproduces exactly.** `bb4fd9ac…` and `978041ac…`, byte for byte, after + everything this session changed. §1. +2. **A hook in this lane's own instrument changed the game's output**, and it was caught by comparing + against a save lane Z had already produced. Bisected to **one address**. Every measurement below + was then re-taken on a configuration proved byte-neutral against that save. §2. +3. The four tail callees that can draw are **entered on every single turn** and gated *inside*; the + three inner functions that hold the draws are entered **zero** times. "Never observed firing" was + about a gate, not about reachability. §3. +4. `CreateRaidEncounter` **is called** — twice on one measured turn — and **draws nothing**. The two + hypotheses a word count could not separate are separated: the candidate list is empty. §4. +5. The Zuul prediction holds exactly: **7 calls and 7 words per site, 14 not 16.** The raid loop + iterates the player vector. §5. +6. `EncounterDetect::AssignContacts` cannot fire on our corpus for a reason nobody had measured: its + caller is never called, because `EncounterDetect_Run` receives an **empty** record vector. The + 2-word detection residual is somewhere else entirely, and §6 names the function it is in. + +--- + +## 1. The oracle, regenerated — and it reproduces + +Lane N's End Turns overwrote the three autosave *contents* on VM140 and said so. The file set was +never disturbed, `ref-turn2.sav` still hashed `ab4ac2d7e2977260…` (checked before the run), and all 11 +curated saves were in the repo, so nothing was lost — but the reference pair had to be remade. + +Load `ref-turn2.sav`, one End Turn, build `recap-7584bad-20260908T0615Z`, `hooks=trace`: + +| file | size | sha256 | historical value | +|---|---|---|---| +| `(Autosave EndTurn).sav` | 66,732 | `bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948` | **identical** | +| `(Autosave).sav` | 67,219 | `978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921` | **identical** | + +**Both reproduce.** The turn-2 state on screen (Imperial Savings 289,688) and the turn-3 state +(532,369) match `determinism-oracle.md` as well. + +This is worth more than a repeat. The process the oracle now runs in is not the one it was measured +in: nineteen template hooks and seven entry-point detours installed instead of one asm stub, an x87 +control-word module, eight save files where there were four, and a Zuul game played to turn 64 in the +same directory in between. **The oracle is revalidated across all of that**, which is what every +Phase-2 comparison downstream of it needed. + +Files: `verify/results/shim/probes/oracle-restore/h-oracle-{endturn,autosave}.sav`. + +**And the same two files were put back on the VM at the end**, so the three autosaves once again carry +the documented oracle bytes. That is a restore of bytes this lane produced by an actual run today, not +a substitution of an older snapshot — the distinction lane N was right to insist on. + +--- + +## 2. The instrument perturbed the game. Bisected to one address. + +This section is first among the probes because it invalidates a run and because nothing in this +campaign has ever tested it. + +### 2.1 How it was caught + +Three End Turns from `ref-turn2` under the full lane-H build gave per-turn word counts of +**19, 18, 19**. Lane Z's per-site table for the same three turns of the same save reads **19, 18, 20**. +One word apart on the third turn — the kind of difference that is easy to explain away as workload. + +It was not explainable, because the **input was byte-identical**. The run's `(Autosave EndTurn).sav` +(the turn-4 pre-turn state) hashed `8648957fecf5041233f9add4c8173f78981f026f133ecdbecdb89ddd10917048`, +which is exactly lane Z's `z2-endturn.sav`. Same bytes in; different bytes out: + +| | `(Autosave).sav` after the turn-4 → turn-5 End Turn | size | +|---|---|---| +| lane Z (`z2-autosave.sav`, and `z-t5-autosave.sav` from a second lane-Z run) | `b2124798470d85ea…` | 68,267 | +| lane H, full build | `058656787877cbee…` | 68,263 | + +Both sides are reproducible: lane Z's value appears in two of its runs, and lane H's appeared in two +of its own (three consecutive End Turns, and a fresh load of `z2-endturn.sav` followed by one End +Turn). So this is not noise in either direction. + +### 2.2 The bisect + +`z2-endturn.sav` was pushed to the VM and the same single End Turn run five more times, changing one +thing each time. A `probes=` key was added to `shim.cfg` for exactly this purpose — an instrument that +can change what it measures has to be switchable, or the question cannot be asked at all. + +| configuration | `(Autosave).sav` | verdict | +|---|---|---| +| `hooks=off` — the shim installs **nothing** | `b2124798…` | the un-instrumented game | +| all template hooks + 7 draw-site detours, `probes=0` | `b2124798…` | **neutral** | +| + probes 0–3 (the four phase-23/33 vtable callees) | `b2124798…` | neutral | +| + probes 4–7 (three inner callees, `CreateRaidEncounter`) | `b2124798…` | neutral | +| + probe 8 (`ServerTradeManager_GenerateTradeRaidEncounters` 0x00893290) | **`058656…`** | **perturbs** | + +Two things fall out, and the first is the more important: + +* **The un-instrumented game agrees with lane Z, not with lane H.** So lane Z's ledger — 7 draw-site + detours and 6 nested template hooks — **is behaviour-neutral on this pair**, and its published + numbers stand. This is the first time any lane has checked that, and it came out the right way. +* **Hooking `GenerateTradeRaidEncounters` at 0x00893290 with a MinHook detour changes the turn.** + Specifically it *suppresses* the `ServerPlayer::RollResearchEvent` draw at 0x0088df4f: the clean run + spends 20 words with that site firing once, the perturbed run spends 19 with it not firing at all. + +### 2.3 What the mechanism is not + +The obvious relocation hazard is **absent**. The bytes at 0x00893290 are + +``` +55 push ebp +8b ec mov ebp,esp +6a ff push -1 <- the 5-byte patch boundary falls exactly here +68 37 c3 99 00 push 0x99c337 ; SEH scope table +64 a1 00 00 00 00 mov eax,fs:[0] ; ... standard MSVC __try + /GS prologue +``` + +so MinHook's five bytes land on a clean instruction boundary, the relocated prologue is three +position-independent instructions, and there is no branch target inside the patched range. A mangled +prologue is therefore **not** the explanation, which makes this more interesting rather than less. + +**The mechanism is undetermined and is recorded as an open item, not guessed at.** What is odd, and is +the thing to chase: `RollResearchEvent` runs *earlier in the turn* than trade-raid generation, so +hooking 0x00893290 changed a decision taken before the hooked function was reached. The cheapest next +probe is a compare-mode hook on `RollResearchEvent` in both configurations, reading the threshold and +the progress value it tests, on the same input save. + +### 2.4 What this costs, and what it does not + +* Every number in §§3–6 was re-taken at `probes=8`, the largest configuration proved byte-identical + to the un-instrumented game on this save. Nothing below rests on the perturbed run. +* The perturbed run's trace is kept — `verify/traces/h-probes-human-perturbed-t3-5.jsonl.gz`, report + `verify/results/shim/probes/human-t3-5-perturbed.txt` — labelled as such, because it is the evidence + for §2 and because its probe *entry counts* agree with the clean runs on every row. +* **The general lesson is a method rule waiting to be written.** A behavioural harness has to be + checked against the oracle, not just against itself. It cost one save comparison to find this; it + would have cost a published wrong number to find it later. The `probes=` key exists so the check is + cheap to repeat: any lane adding detours should run one End Turn from `z2-endturn.sav` and confirm + `b2124798470d85ea…` before trusting its own measurements. + +--- + +## 3. The four tail callees — reached on every turn, gated inside + +Lane V2 closed lane K's tier-4 blind spot: four of the eleven phase-23/33 vtable targets on +`StrategyServer+0x158`/`+0x15c` reach a draw on the strategic generator, at eight instructions. Lane Z +measured 0 tail words on eight turns. Between those two facts sat a question a word count cannot +answer — *not reached*, or *reached and gated*? + +Register-transparent asm stubs (`pushfl; pushal; push idx; call; popal; popfl; jmp trampoline`) count +entries without assuming a calling convention. Two saves, three measured turns: + +| address | what | Human t5 | Zuul t24 | Zuul t25 | +|---|---|---|---|---| +| 0x008877b0 | `ServerSpyManager` slot 13 | **1** | **1** | **1** | +| 0x0088db80 | `ServerSpyManager` slot 14 | **1** | **1** | **1** | +| 0x0088ef80 | `ServerTradeManagerImpl` slot 13 | **1** | **1** | **1** | +| 0x0082cca0 | `ServerTradeManagerImpl` slot 15 | **1** | **1** | **1** | +| 0x008408e0 | `SpyManager_Slot13RngCallee` | 0 | 0 | 0 | +| 0x00820ca0 | `TradeManager_Slot13RngCalleeA` | 0 | 0 | 0 | +| 0x0088b440 | `TradeManager_Slot13RngCalleeB` | 0 | 0 | 0 | + +**P2a and P2c both hold.** The phase-23/33 dispatch block is unconditional, exactly as lane V2 read +it; and the gate that keeps the draws from firing is **inside each outer body, above the inner call**. +No draw-site row appeared at any of the eight return addresses on any turn, and the tail's bracket +cost stayed 0 — reproducing lane Z's number under an instrument that can now say *why*. + +So the correct statement is no longer "the tail draws nothing" and not even "the tail draws nothing on +these turns". It is: **the tail's four draw-bearing callees run on every turn and return before +reaching their draws.** That is a much stronger negative, and it converts the open question from +"which turns reach these functions" to "what does the gate in each body test". + +### 3.1 Three of the four gates are an empty container; the fourth is not, and it was wrong + +Read from the bodies (each confirmed a real function start with exactly one caller): + +| callee | the loop it drives | empty when | +|---|---|---| +| `ServerTradeManagerImpl` slot 15 | vector at `tradeManager+0x3c .. +0x40` | no trade routes | +| `ServerSpyManager` slot 14 | vector at `spyManager+0x10 .. +0x14` | no spy programs | +| `ServerSpyManager` slot 13 | 1,869 bytes, same manager, not fully read | — | +| **`ServerTradeManagerImpl` slot 13** | **`GetServer()+0x64 .. +0x68` — the FLEETS vector** | **no fleets** | + +That last row produced a competing prediction, **P2b-alt**, committed after the bodies were read and +before any run: on a save with fleets, slot 13's loop body runs, `TradeManager_Slot13RngCalleeA` +fires, and the tail cost is non-zero. + +**P2b-alt is falsified, and cleanly.** `zuul-turn23-fleet23.sav` is named for its 23 fleets; slot 13 +was entered on both its turns and 0x00820ca0 was entered **zero** times. So the fleet loop is not the +gate — the per-fleet test chain that precedes the call at 0x0088f036 is, and it rejects every fleet on +a 23-fleet save. That is a narrower and more useful place to look than "the loop never runs", and it is +the kind of wrong prediction that pays for itself: had P2b been the only one written down, its +confirmation would have been read as "no trade routes, no spies" and the fleet loop would have gone +unexamined. + +### 3.2 What is still a hypothesis (rule 6) + +**Whether any of the eight draw sites ever fires** is still unexercised. The workload that would +settle it — a game with live trade routes and a live spy program — was **not built**, and it is worth +saying exactly why rather than vaguely: + +* SotS 1 trade income requires trade-station construction and the tech behind it; from `ref-turn2` + (turn 2, 1 colony, 0 techs) that is tens of End Turns at 30–60 s each plus a Build/Research click + path per turn. The newest Zuul save in the corpus (turn 23) still has **no** trade routes: slot 15's + loop body did not execute on it. +* The corresponding spy state is worse — no lane has yet identified which UI produces an entry in the + `spyManager+0x10` vector, so there is no click path to write down, only a container to watch. +* What this lane did instead was cheaper and, for the map, better: it established that the containers + are the gate for two of the four and that the fourth is gated per-fleet. A future lane that builds + the workload now knows which two vectors to watch fill, and can confirm the workload took *before* + spending a turn measuring. + +--- + +## 4. `CreateRaidEncounter` — the two hypotheses, separated + +A successful trade-raid roll calls `ServerTradeManagerImpl` slot 17, `CreateRaidEncounter` 0x008938a0, +which draws a `NextInt` at 0x008939ee to pick a target but returns at 0x0089391c **without drawing** +when the candidate vector is empty. It cost 0 words on all three of lane Z's turns, which lane Z read +as consistent with either "no roll succeeded" or "the candidate list is empty". + +**A correction to the prior first (rule 11).** Lane Z wrote that no roll succeeding is "≈11% on the +defaults, so three quiet turns in a row is unremarkable". 11% is the probability of a quiet *turn*: +`0.8^8 × 0.95^8 = 0.1113`. Three consecutive is `0.00138`, about **1 in 720**. The two hypotheses were +never close to equally likely, and the expected number of successes per turn is `8 × 0.2 + 8 × 0.05 = 2.0`. + +**Measured:** + +| turn | `CreateRaidEncounter` entries | words drawn | +|---|---|---| +| Human t4 → t5 | 0 | 0 | +| Zuul t23 → t24 | **2** | **0** | +| Zuul t24 → t25 | **1** | **0** | +| Human t3/t4/t5, perturbed run | 0 / 1 / 0 | 0 | + +**Settled: rolls succeed at about the predicted rate, and the candidate list is empty every time.** +Four successful rolls were observed across five measured turns and not one of them reached the +`NextInt` at 0x008939ee — no draw-site row at return address 0x008939f3 on any turn. + +For the standalone this is the useful form: on a corpus like ours the cost of a successful raid roll +is **0 further words**, and the row lane Y flagged as "the one that will bite" does not bite yet. It +will bite the first turn a raid candidate exists, and the candidate list is built by 0x0083b110 — a +function no lane has read. + +**One caveat this lane found and must state.** `CreateRaidEncounter` is slot 17 and is reached by +virtual dispatch, and `GenerateTradeRaidEncounters` is **not its only caller**: `0x007d5150`, the +other subtree of `DetectEncounters` (see §6), also dispatches slot 17 — +`(**(code **)(**(int **)(S + 0x158) + 0x44))(out, player, 1, records)` — on the branch where its own +per-player roll returns 0x12. So an entry count of 2 does not prove two *trade-raid* successes; it +proves two slot-17 calls from one of two known sites. Separating them needs the return address, which +an entry counter deliberately does not capture. `tail-rng-ledger.md` §11.1's "called from +`GenerateTradeRaidEncounters` at 0x0089345e / 0x00893548 / 0x00893603" is incomplete as a list of +callers. + +--- + +## 5. The Zuul word count — 14, exactly as predicted + +Lane Z's free prediction, restated by lane Y as a consequence of its `trade_raid.cpp` model: the two +`Chance` sites in `GenerateTradeRaidEncounters` cost one word per entry of `StrategyServer::Players`, +which holds 8 on the Human saves and 7 on the Zuul ones. Lane N ran a Zuul End Turn with the RNG hooks +off, so it could not answer. + +`zuul-turn23-fleet23.sav`, two End Turns, `probes=8`: + +| site | Human t5 | **Zuul t24** | **Zuul t25** | +|---|---|---|---| +| 0x00893426 `Chance` (`TRADE_RAID_ODDS_PLAYER`) | 8 calls / 8 words | **7 / 7** | **7 / 7** | +| 0x00893513 `Chance` (`TRADE_RAID_ODDS_NPC`) | 8 calls / 8 words | **7 / 7** | **7 / 7** | +| trade-raid block total | 16 | **14** | **14** | +| turn total (attributed = measured) | 20 | **18** | **18** | + +**P5 holds in full**, and it is a prediction that survived a changed workload rather than a repeat of a +recording: 7 is a number no earlier run produced. The loop iterates the server's player vector and +nothing else — not the `TeamRecord` vector it is handed, not a species list, not a fixed NPC count. +Lane Y's `trade_raid.cpp`, which counts `NumPlrs`, is right. + +The Zuul turn's remaining 4 words are 2 detection (§6) and **2 research** — both `ProcessResearch` +sites (0x00587888 and 0x005878bb) fired on both turns, which is the second research site's first +appearance in a site table. + +--- + +## 6. `DetectEncounters` — the residual is not the `AssignContacts` family, and here is where it is + +Lane Y's standalone leaves a 2-word residual per turn, attributed by lane Z to 0x0050329d +(`NextFloat`, depth 4) and 0x007929a4 (`NextInt`, depth 3), and refused to model it: *"fitting 2/turn +to three observations is not a model."* Lane I bounds the containing family at +`|contacts| × |detectors|` and showed `EncounterDetect_AssignContacts` draws one inlined `NextFloat` +per (contact, detector) trial. Lane Z found `AssignContacts` never ran. + +`EncounterDetect_ProcessTeamRecord` 0x007ca640 — its only caller — was hooked, with lane I's gate +predicate, contact count and detector count recomputed from the record at hook entry. + +**It never ran either. On any turn, on either save.** The hook installed (`MH_OK` for create and +enable, logged), was registered in the trace's meta line, and emitted **zero records** across five +End Turns, while the entry probe on `EncounterDetect_Run` recorded **one call per turn, every turn**. + +`EncounterDetect_Run` calls `ProcessTeamRecord` once per 0x74-byte record. One call, zero records +means the vector it receives is **empty**. So: + +* **`AssignContacts` cannot fire on our corpus, and the reason is one level higher than anyone + thought.** It is not that the `+0xfc` gate fails on the records — there are no records. Lane I's + `|contacts| × |detectors|` bound is correct and simply has nothing to bound yet. +* **P3b is falsified**, exactly as its falsification clause anticipated, and the depth reasoning that + produced it was wrong about the *path*, not the depth. + +### 6.1 Where the two words actually are + +A call-graph walk from both residual sites upward lands in the same place, and it is not +`EncounterDetect_Run`: + +``` +StrategyServer::DetectEncounters 0x007d7f70 (the last phase of ProcessTurn) + ├─ 0x007d8469 GenerateTradeRaidEncounters [virtual] 16 words (14 on Zuul) §5 + ├─ 0x007d8470 EncounterDetect_Run 0x007cb080 0 words -- empty record vector + └─ 0x007d860b 0x007d5150 ** the 2 words ** + ├─ 0x007d52b3 0x0078cc70 -> 0x0078ccb1 0x00503200 -> NextFloat 0x0050329d 1 word + └─ 0x007d5311 / 0x007d5338 0x00792750 -> NextInt 0x007929a4 1 word +``` + +Every one of those edges is a direct `call` with a **single** incoming reference, so the attribution +is not a guess: 0x00503200 has exactly one caller (0x0078cc70), which has exactly one caller +(0x007d5150); 0x00792750 has two call sites, both inside 0x007d5150; and 0x007d5150 has exactly one +caller, `DetectEncounters` at 0x007d860b. + +**The whole detection residual is inside one 744-byte function with one call site.** That is a far +better handle than a family bound over two counts nobody can observe, and it makes the residual a +tractable modelling target rather than a fitted constant. From its body: + +* it first builds a bitmask over the same `records` vector (`entry[0]->+0xf9 == 0` sets bit + `entry[0]->+0x28`) and returns immediately if the mask is 0; +* it then loops the **player vector** at `+0x54/+0x58`, skipping players whose mask bit is set or + whose `+0xf9` is non-zero, builds a per-player candidate vector (0x00795870), and for each + non-empty one calls 0x0078cc70 — **that is the `NextFloat`** — whose non-zero return leads on, and + whose return value of **0x12 dispatches slot 17, `CreateRaidEncounter`** (§4's caveat); +* the measured cost is 1 call and 1 word per site per turn on 8-player and 7-player saves alike, so + only one player reaches the draw per turn on both — the per-player gate, not the loop bound, is what + sets the count. + +That last observation is the one to hand lane Y: **the 2 words are not `2 × |players|` and not a +constant by luck — they are one per player that passes a gate, and on both corpus saves exactly one +player passes.** A model that assumes 2/turn will be right on our saves and wrong on a busier one, and +now there is a named function to read to find out when. + +### 6.2 The hook that measured nothing, reported as loudly as the ones that did (rule 15) + +`EncounterDetectProcessTeamRecordHook` compared nothing, because it never ran. Its coverage notes, its +gate predicate, its contact/detector classifier and its byte-vs-dword cross-check on `+0xfc` are all +**unexercised**. They are kept in the tree because the moment a save produces a non-empty record +vector they become the check that matters — but nothing in this report rests on them, and the +`fc_byte_vs_dword_disagreements` counter has never returned a value. Lane I's classification rules are +therefore still exactly as verified as they were: from the instruction stream, and from nowhere else. + +--- + +## 7. Coverage — what these runs did not touch + +* **Five End Turns on two saves.** Human `ref-turn2` turn 4 → 5 (once clean, three times in the + perturbed run) and Zuul turn 23 → 25. That is a thin workload measured precisely. +* **No trade routes, no spy programs, no manufactured workload for either.** §3.2 says what it would + take. Until then, every statement about the eight tail draw sites is "they are not reached on a game + with none of either", and the four callees' gates are located but not read. +* **`ServerSpyManager` slot 13 (1,869 bytes) was not read.** Its loop and its gate are unexamined; it + is grouped with slot 14 above only by manager, not by evidence. +* **The probe counters cannot say which caller.** They record entries, not return addresses — a + deliberate cost of being calling-convention-agnostic — and §4 shows that costing something real. +* **The perturbation's mechanism is unknown**, and it is the largest open item this lane produced. + A second, independent instance may already exist and be undetected in some other lane's build; the + `z2-endturn.sav` → `b2124798470d85ea…` check is cheap enough that every future VM lane should run it. +* **No `Guard` region is declared by any hook in this family**, the same deliberate gap lane Z + reported: these hooks measure the generator and claim nothing about game state. `undeclared = 0` in + these traces is vacuous and must be read that way. +* **`probes=8` is proved neutral on one save and one turn.** It is not proved neutral in general, and + the Zuul numbers in §3 and §5 inherit that limit — they were taken on a configuration validated + against a Human turn, not a Zuul one. + +## 8. Corrections to earlier findings + +* `tail-rng-ledger.md` §11.1: the caller list for `CreateRaidEncounter` is incomplete — 0x007d5150 + dispatches slot 17 as well (§4). +* `tail-rng-ledger.md` §11.1: "three quiet turns in a row is unremarkable" — it is a 1-in-720 event, + and the measurement shows the rolls do succeed (§4). +* `tail-rng-ledger.md` §11.3 and lane Y's residual table: the 2-word detection residual is **not** the + `AssignContacts` family and is not blocked on the `+0xfc` gate; `EncounterDetect_Run` receives an + empty vector, and the words are in 0x007d5150's subtree (§6). +* Lane V2's per-slot notes: "never observed firing" is confirmed for the draws and **wrong as a + statement about the callees** — all four run on every turn (§3). +* `combat-done-tail.md` tier 4 / lane V2: the trade slot-13 loop is over the **fleets** vector, and a + 23-fleet save still reaches no draw (§3.1). + +## 9. Files + +* Predictions (pre-registered): `sots-engine/docs/H-probes.md`. +* Traces: `verify/traces/h-probes-human-t5.jsonl.gz`, `h-probes-zuul-t24-25.jsonl.gz`, + `h-probes-human-perturbed-t3-5.jsonl.gz`. +* Reports: `verify/results/shim/probes/{human-t5,zuul-t24-25,human-t3-5-perturbed}.txt`; + tool `tools/probe_report.py`. +* Oracle pair: `verify/results/shim/probes/oracle-restore/h-oracle-{endturn,autosave}.sav`. +* Addresses: `ghidra/addresses.d/lane-h.json` (3 entries, all `mapped`). +* Shim: `sots-engine/src/shim/hooks/probe_entry.{h,cpp}`, the `probes=` config key, and + `EncounterDetectProcessTeamRecordHook` in `tail_rng.{h,cpp}`; configs `shim.cfg.hprobes*`, + `shim.cfg.hp{0,4,8,9,10,11}`, `shim.cfg.hoff`. + +--- + +## 10. Priority insert from lane AI1 — run before the VM was released + +Lane AI1 asked for its P1–P3 on the same VM session. Results are written up where they belong, in +`findings/subsystems/ai-turn-logic.md` §8.1; in brief: + +* **P2 held — and it is the roadmap-level one.** `ref-turn2.sav` loaded in **two separate process + launches** (pids 3400 and 2028, killed and relaunched between), shim at `hooks=off` so nothing is + installed, one End Turn each: both autosaves byte-identical to each other and to the 2026-09-07 + oracle. Byte-identity subsumes `ModCount` and `Summary.Checksum`. **The AI's turn is reproducible + from a save alone**, so `backlog.md` §2's Rung B stays as written. Seven fresh processes across two + saves agreed today in total, counting the `z2-endturn.sav` bisect runs of §2. +* **P1 could not be answered as specified.** `session.log` after a full load + End Turn of a game with + an AI player is 22 lines and contains no `RunAI:`, no `random seed` and no `AI Process Turn` line; + the game's stdout carries the same 22 and nothing more. The level-2 messages are not emitted by this + build at its default log level, so the "one log read, no build" route does not exist. A suggestive + counter-indication to the all-zero-seed prediction is recorded there as a hypothesis, with the + decisive test named (record the `this` pointer beside the return address in `draw_sites.cpp`). +* **P3 not run** — no `ModCount` watchpoint was armed and no debugger session was held. + +## 11. VM140 released + +Left at the **main menu**, verified by screenshot, build `recap-7584bad-20260908T0615Z`, +`shim.cfg` = the recap trace config, `SavedGames\` back to the documented **8-file** set +(`ref-turn2` still at (400,436), `zuul-turn5` at (400,465)) with the three autosaves carrying the +oracle bytes `bb4fd9ac…` / `978041ac…` / `978041ac…`. Lane H left `C:\SOTS\shimdist-h` and +`C:\SOTS\ui\h{deploy,release}.ps1` in place, plus `C:\SOTS\ui\h-oracle-{endturn,autosave}.sav` and +`ai-run{1,2}.log`. + +**One VM note worth carrying forward.** The board's advice to screenshot before clicking is right but +under-specified: the *intro* screens are bright white, so a brightness threshold fires on them and the +click batch lands nowhere. The reliable detector is **redness in the main menu's Exit button** — +crop `100x14+462+674`, require `mean.r > 0.2` and `mean.g < 0.15` (the menu reads 0.327 / 0.036; the +white intro reads 1.0 / 1.0; a black screen reads 0 / 0). With that, every one of this lane's ten +launches reached the menu on the first attempt, at t+48…54 s. diff --git a/findings/control-flow/tail-rng-ledger.md b/findings/control-flow/tail-rng-ledger.md index 589cd8e..4bcb1e0 100644 --- a/findings/control-flow/tail-rng-ledger.md +++ b/findings/control-flow/tail-rng-ledger.md @@ -593,6 +593,16 @@ word**. On these three turns it cost 0, which is consistent either with no roll image defaults, so three quiet turns in a row is unremarkable) or with an empty candidate list every time. A word count cannot separate those, and separating them is the cheapest experiment left on this path. +> **SETTLED BY LANE H, 2026-09-08 — and two corrections to this paragraph** +> (`findings/control-flow/tail-probes.md` §4). An entry counter on 0x008938a0 recorded **2 calls on +> Zuul turn 24 and 1 on turn 25, all drawing 0 words**: the rolls *do* succeed and **the candidate +> list is empty**. A successful raid roll costs 0 further words on our corpus. +> (a) "three quiet turns in a row is unremarkable" understates it by three orders of magnitude — +> 11% is the probability of a quiet *turn* (`0.8^8 × 0.95^8`), so three in a row is 0.14%, about +> 1 in 720. (b) The caller list below is **incomplete**: `0x007d5150`, the other subtree of +> `DetectEncounters`, also dispatches slot 17, so an entry count of 2 is two slot-17 calls from one +> of two known sites, not two trade-raid successes. + ### 11.2 Two bookkeeping corrections the raw numbers need The shim's raw totals are 35 / 34 / 36, not 19 / 18 / 20, and both differences are accounting rather than @@ -619,9 +629,29 @@ one object; the site instrument had to be told. which is consistent with the sums reconciling exactly. **The inlined-draw path is therefore still unexercised**, and if it fires on some other save the site sum will fall short of the bracket by exactly its cost — which is how it will announce itself. + + > **CORRECTED BY LANE H, 2026-09-08** (`tail-probes.md` §6). The gate is not the reason. Lane H + > hooked `EncounterDetect_ProcessTeamRecord` itself: it recorded **zero calls** over five End Turns + > on two saves, while an entry probe on `EncounterDetect_Run` recorded **one call per turn**. Run + > calls ProcessTeamRecord once per record, so **the record vector it receives is empty** — the + > `+0xfc` gate is never evaluated because there is nothing to evaluate it on. Lane I's + > `|contacts| × |detectors|` bound is untouched and still has nothing to bound. + > + > The same lane found where the two detection words really are, and it is not this subtree: + > `DetectEncounters` → **0x007d5150** (one call site, at 0x007d860b) → 0x0078cc70 → 0x00503200 + > (`NextFloat` 0x0050329d) and → 0x00792750 (`NextInt` 0x007929a4). Every edge on that path has a + > single caller. It is one word per player that passes a per-player gate, and on both corpus saves + > exactly one player passes — so "2 per turn" is a coincidence of these saves, not a constant. * Three turns of one save. The two `Chance` sites fired 8/8 every turn with no variation, so nothing here says what makes them fire *fewer* times, and `FUN_00893290`'s own gating is unmeasured. + > **LANE H, 2026-09-08.** The free prediction at the end of §11.3 was run and **held exactly**: on + > `zuul-turn23-fleet23` (7-entry player vector) both sites read **7 calls / 7 words**, for **14** + > instead of 16, on two consecutive turns. The loop iterates `StrategyServer::Players` and nothing + > else. Lane H also found that **hooking 0x00893290 itself perturbs the game** (`tail-probes.md` + > §2) — but the *un-instrumented* game reproduces this document's `z2-autosave.sav` byte for byte, + > so the lane-Z instrument is behaviour-neutral and every number here stands. + **A free prediction, for whoever runs this next.** If the 8 calls per site are one per entry of the server's player vector, then on any **Zuul** save — where that vector holds **7**, not 8 (§8) — the two sites should cost **14** words per turn instead of 16, and the turn total should drop by 2 for the same activity. The diff --git a/findings/subsystems/ai-turn-logic.md b/findings/subsystems/ai-turn-logic.md index 3a645ec..166434a 100644 --- a/findings/subsystems/ai-turn-logic.md +++ b/findings/subsystems/ai-turn-logic.md @@ -551,6 +551,64 @@ that never populates from the UI but does populate in an AI game is one of these --- +## 8.1 Results — lane H, VM140, 2026-09-08 + +Run by lane H at lane AI1's request while it held VM140. Full lane-H report: +`findings/control-flow/tail-probes.md`. + +### P2 — **HELD. Rung B stays as written.** + +`ref-turn2.sav` (2 players: human `re` Morrigi, AI `Fane Lao`) loaded in **two separate process +launches** — the game was killed and relaunched through the `SOTS` scheduled task between them, so +different pids (`3400`, then `2028`) and a fresh address space each time — with the shim at +**`hooks=off`**, i.e. the proxy DLL forwards Bink exports and installs **nothing**. One End Turn in +each. + +| run | pid | `(Autosave EndTurn).sav` | `(Autosave).sav` | +|---|---|---|---| +| 1 | 3400 | `bb4fd9ac89f41e3b…` 66,732 B | `978041acd168b56e…` 67,219 B | +| 2 | 2028 | `bb4fd9ac89f41e3b…` 66,732 B | `978041acd168b56e…` 67,219 B | + +**Byte-identical**, and equal to the 2026-09-07 oracle values from a third process on a different day. +Byte-identity subsumes the specific fields P2 named: `ModCount` and `Summary.Checksum` are inside the +compared bytes, and `Summary.Checksum` is derived from the whole payload, so a single differing AI +decision anywhere would have moved it. + +**So the AI's turn is reproducible from a save alone, in a fresh process.** `backlog.md` §2's Rung B +is a real target: implementing the AI and byte-matching is reachable, and `--turn-commands` injection +does not have to become the primary path. + +Corroborating evidence from the same day, same method, different save and different instrument +configurations — five further fresh-process launches of `z2-endturn.sav` (`hooks=off`, and four shim +configurations) all produced `(Autosave).sav` = `b2124798470d85ea…`, matching two lane-Z runs from a +different build. Seven fresh processes, two saves, one answer. + +### P1 — **not answerable from the log at the default level.** + +`session.log` after a full load + End Turn of a 2-player game with one AI is **22 lines** and contains +no `RunAI:` line, no `random seed` line and no `AI Process Turn` line; `dxvklog\run.txt` (the game's +stdout) carries the same 22 lines and nothing more. So the message exists in the image but is **not +emitted at the log level this build runs at** — P1's "one log read, no build" route does not exist as +described. Whatever gates level-2 logging has not been found; a `-log` / config switch was not +searched for, and that is the cheap next step before anyone builds a hook for this. + +**One suggestive observation, offered as a hypothesis and not as a result (rule 6).** Lane H's +per-call-site RNG instrument separates draws by generator instance. On every measured turn the +non-strategic rows include `FUN_005798e0`+0x30, a `NextInt` that lane Z attributed to a +`StrategyClient`'s generator at `client+0x134` — and it repeatedly consumes **more words than calls** +(3 calls / 5 words, 3 / 4, 3 / 3 across turns and saves). Extra words in a bounded `NextInt` mean its +**rejection loop ran**, which requires successive draws to differ — impossible for an all-zero MT +state, where every temper returns 0. If that generator is an *AI* client's, P1 is false. **It is not +established that it is**: the human player has a `StrategyClient` too, and lane H did not determine +which instance those rows belong to. The decisive version of this test is cheap now that the +machinery exists: record the `this` pointer alongside the return address in `draw_sites.cpp` and +compare it against each client's `+0x134`. + +### P3 — **not run.** No `ModCount` watchpoint was armed; lane H held no debugger session and lane A2's +DR0 spec was not set up on this VM. Nothing was measured for or against it. + +--- + ## 9. Ranked plan for `sots-engine/src/game/ai` Ordered by *evidence available now* over *cost*, not by architectural tidiness. diff --git a/findings/subsystems/determinism-oracle.md b/findings/subsystems/determinism-oracle.md index cfbed70..5990828 100644 --- a/findings/subsystems/determinism-oracle.md +++ b/findings/subsystems/determinism-oracle.md @@ -17,6 +17,29 @@ resets a per-player `Status` int (4 → 0) on load, which also moves the derived See "Round-trip stability" below for the canonicalization rule if a harness ever compares a loaded `(Autosave).sav` against a re-save of it. +## Revalidated 2026-09-08 (lane H) + +Lane N's End Turns overwrote the three autosave *contents* on VM140 (the file set was untouched). +Lane H regenerated the pair by the documented procedure — load `ref-turn2.sav` (`ab4ac2d7…`, verified +first), one End Turn, build `recap-7584bad-20260908T0615Z`, `hooks=trace` — and **both hashes below +reproduced exactly**: `(Autosave EndTurn).sav` 66,732 B `bb4fd9ac89f41e3b…`, `(Autosave).sav` +67,219 B `978041acd168b56e…`. + +That is a stronger result than a repeat, because the process is no longer the one this document +measured: nineteen template hooks and seven RNG entry-point detours are now installed instead of a +single asm stub, an x87 control-word module is present, and `SavedGames\` holds eight files rather +than four. The oracle survives all of it. Copies kept at +`verify/results/shim/probes/oracle-restore/h-oracle-{endturn,autosave}.sav`, and the same bytes were +put back on the VM as the three autosaves. + +**Related, and it belongs next to the oracle:** lane H found that a MinHook detour on +**0x00893290** changes the game's output — same input save, `(Autosave).sav` 4 bytes different and +one generator word short (`findings/control-flow/tail-probes.md` §2). The un-instrumented game and +the lane-Z instrument agree; that one hook does not. **A behavioural harness must be checked against +the oracle, not only against itself.** The cheap standing check is: load +`verify/results/shim/tailrng/z2-endturn.sav`, one End Turn, expect +`(Autosave).sav` = `b2124798470d85ea86b0e9e4abfd9e23f3be73caa1115dd7614021be76305939`. + ## Environment * VM140 `sots-re-win10`, `C:\SOTS\Sword of the Stars.exe` GOG v1.8.1 (`ver.1.8.1 Wed Dec 13 03:38:31 2017`), diff --git a/ghidra/addresses.d/lane-h.json b/ghidra/addresses.d/lane-h.json new file mode 100644 index 0000000..4bd891a --- /dev/null +++ b/ghidra/addresses.d/lane-h.json @@ -0,0 +1,28 @@ +{ + "entries": [ + { + "name": "SpyManager_Slot13RngCallee", + "addr": "0x008408e0", + "convention": "thiscall", + "prototype": "int (void* this, int, int) // the ONE callee through which Game::ServerSpyManager vftable 0x00a3073c slot 13 (0x008877b0) reaches the strategic generator. Exactly ONE incoming reference in the image: an unconditional call at 0x00887af2 inside that slot. Real body 0x008408e0..0x00840a59, 370 bytes. Contains three of lane V2's eight draw sites -- Chance 0x00840929, NextInt 0x008409c7, Chance 0x00840a3c -- all loading the generator as [reg+0x16c]. Named for the verified relationship (which slot reaches it, and that it draws), NOT for any semantics: its body was not read. Lane H hooks it with a register-transparent entry counter so that 'the spy path never fired' can be split into 'slot 13 was not entered' and 'slot 13 was entered and gated above this call'", + "status": "mapped", + "source": "findings/control-flow/tail-probes.md" + }, + { + "name": "TradeManager_Slot13RngCalleeA", + "addr": "0x00820ca0", + "convention": "thiscall", + "prototype": "uint (void* fleet, char) // first of the two callees through which Game::ServerTradeManagerImpl vftable 0x00a31b74 slot 13 (0x0088ef80) reaches the strategic generator. Exactly ONE incoming reference: an unconditional call at 0x0088f036. Real body 0x00820ca0..0x00820e53, 436 bytes, containing lane V2's NextFloat site 0x00820e18. Its own body loops the vector at arg+0xa4/+0xa8 testing per-element flags, and its boolean result is what gates the call to TradeManager_Slot13RngCalleeB one instruction later. THE CALLER'S LOOP IS OVER THE FLEETS VECTOR at GetServer()+0x64/+0x68, not over a trade-route list -- which is why 'the tail draws nothing because we have no trade routes' does not explain this slot", + "status": "mapped", + "source": "findings/control-flow/tail-probes.md" + }, + { + "name": "TradeManager_Slot13RngCalleeB", + "addr": "0x0088b440", + "convention": "thiscall", + "prototype": "void (void* this, int) // second of the two callees through which Game::ServerTradeManagerImpl slot 13 (0x0088ef80) reaches the strategic generator, called at 0x0088f042 ONLY when TradeManager_Slot13RngCalleeA returned non-zero. Exactly ONE incoming reference. Real body 0x0088b440..0x0088b976, 1323 bytes, containing lane V2's NextInt site 0x0088b613 (`mov ecx,[ecx+0x16c]; add ecx,4`). The two form a two-stage gate: A draws first and decides whether B runs, so a run where A fires and B does not is a MEASUREMENT, not a gap", + "status": "mapped", + "source": "findings/control-flow/tail-probes.md" + } + ] +} diff --git a/tools/probe_report.py b/tools/probe_report.py new file mode 100644 index 0000000..13f4677 --- /dev/null +++ b/tools/probe_report.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Lane H: the entry-counter table and the encounter-detection records from a probe trace. + +Two things the word-count reports cannot say, printed side by side with the words so they are read +together: + + * `probe_entries` -- how many times control ENTERED each of the tail callees lane V2 proved can + reach the strategic generator, and the two trade-raid functions. A row with `calls > 0` and no + matching draw-site row is "reached and gated"; a row with `calls == 0` is "not reached". Eight + turns of `tail words = 0` could not distinguish those, and they have opposite consequences. + `installed=false` is printed as **NOT INSTALLED**, never as a zero (method rule 1). + + * `ProcessTeamRecord` -- the gate predicate, contact count and detector count recomputed from the + record at hook entry, beside the words the call actually cost. When the gate is false, the + counts are a prediction of a call that will not happen; when it is true, `AssignContacts` fires + and prints the callee's own numbers, and the two can be compared. + + uv run python3 tools/probe_report.py +""" +import json +import sys + + +def val(tv): + return None if tv is None else tv.get("v") + + +def sv(struct_tv, key): + if struct_tv is None or struct_tv.get("t") != "struct": + return None + return val(struct_tv["v"].get(key)) + + +def words_of(rec): + rng = rec.get("side", {}).get("rng", {}) + wb, wa = sv(rng.get("before"), "words"), sv(rng.get("after"), "words") + return (wa - wb) if (wa is not None and wb is not None) else None + + +turn_no = 0 +for line in open(sys.argv[1]): + d = json.loads(line) + if "hook" not in d: + continue + hook = d["hook"] + args = {a.get("n"): a for a in d.get("args", []) if a.get("n")} + + if hook.endswith("ProcessTeamRecord") or hook.endswith("AssignContacts"): + w = words_of(d) + short = hook.split("::")[-1] + if short == "ProcessTeamRecord": + flags = [ + (sv(e, "fb"), sv(e, "fc"), sv(e, "fc_dword")) + for e in (val(args.get("entry_flags")) or []) + ] + print( + f" [{short}] call {d['call_id']} depth {d.get('depth')}: " + f"entries={val(args.get('entries'))} gate={val(args.get('gate'))} " + f"contacts={val(args.get('pred_contacts'))} " + f"detectors={val(args.get('pred_detectors'))} " + f"neither={val(args.get('pred_neither'))} " + f"max_trials={val(args.get('pred_max_trials'))} " + f"fc_byte_vs_dword_disagreements={val(args.get('fc_byte_vs_dword_disagreements'))} " + f"WORDS={w}" + ) + if flags: + print(" entry flags (fb, fc, fc_dword): " + ", ".join( + f"({a},{b},0x{c:x})" for a, b, c in flags)) + else: + print( + f" [{short}] call {d['call_id']} depth {d.get('depth')}: " + f"detectors={val(args.get('detectors'))} contacts={val(args.get('contacts'))} " + f"max_trials={val(args.get('max_trials'))} WORDS={w}" + ) + continue + + if not hook.endswith("Autosave"): + continue + if val(args.get("end_turn")) is not False: + continue + turn_no += 1 + probes = val(args.get("probe_entries")) + print(f"\n=== post-turn autosave #{turn_no} (call {d['call_id']}) ===") + if probes is None: + print(" probe_entries: ABSENT -- this trace is from a build without lane H's counters") + continue + for p in probes: + name = sv(p, "name") + calls = sv(p, "calls") + total = sv(p, "calls_since_launch") + inst = sv(p, "installed") + state = f"calls={calls:<4} since_launch={total}" if inst else "**NOT INSTALLED**" + print(f" 0x{sv(p, 'rva') + 0x400000:08x} {name:<52} {state}") diff --git a/verify/results/shim/probes/human-t3-5-perturbed.txt b/verify/results/shim/probes/human-t3-5-perturbed.txt new file mode 100644 index 0000000..cc19056 --- /dev/null +++ b/verify/results/shim/probes/human-t3-5-perturbed.txt @@ -0,0 +1,78 @@ +== human turns 3-5, ALL 12 probes = PERTURBED RUN, build h-probes-20260908T1636Z == +turn: strategic 35 words / 35 calls; other generators 10 words / 8 calls; overflow 0 + * STRAT NextFloat call 0x0050329d FUN_00503200+0x9d calls= 1 words= 1 + * STRAT NextFloat call 0x00587888 ProcessResearch+0x1c8 calls= 1 words= 1 + * STRAT NextInt call 0x007929a4 FUN_00792750+0x254 calls= 1 words= 1 + * STRAT Chance call 0x00893426 FUN_00893290+0x196 calls= 8 words= 8 + * STRAT Chance call 0x00893513 FUN_00893290+0x283 calls= 8 words= 8 + STRAT NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 16 words= 16 [internal to Chance: already counted on that row] + other Chance call 0x00578d10 FUN_00578cf0+0x20 calls= 1 words= 1 + other NextInt call 0x00579910 FUN_005798e0+0x30 calls= 3 words= 5 + other NextInt call 0x0069dc29 FUN_0069dbb0+0x79 calls= 3 words= 3 + other NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 1 words= 1 [internal to Chance: already counted on that row] + => attributed strategic words: 19 +turn: strategic 34 words / 34 calls; other generators 8 words / 8 calls; overflow 0 + * STRAT NextFloat call 0x0050329d FUN_00503200+0x9d calls= 1 words= 1 + * STRAT NextInt call 0x007929a4 FUN_00792750+0x254 calls= 1 words= 1 + * STRAT Chance call 0x00893426 FUN_00893290+0x196 calls= 8 words= 8 + * STRAT Chance call 0x00893513 FUN_00893290+0x283 calls= 8 words= 8 + STRAT NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 16 words= 16 [internal to Chance: already counted on that row] + other Chance call 0x00578d10 FUN_00578cf0+0x20 calls= 1 words= 1 + other NextInt call 0x00579910 FUN_005798e0+0x30 calls= 3 words= 3 + other NextInt call 0x0069dc29 FUN_0069dbb0+0x79 calls= 3 words= 3 + other NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 1 words= 1 [internal to Chance: already counted on that row] + => attributed strategic words: 18 +turn: strategic 35 words / 35 calls; other generators 9 words / 8 calls; overflow 0 + * STRAT NextFloat call 0x0050329d FUN_00503200+0x9d calls= 1 words= 1 + * STRAT NextFloat call 0x00587888 ProcessResearch+0x1c8 calls= 1 words= 1 + * STRAT NextInt call 0x007929a4 FUN_00792750+0x254 calls= 1 words= 1 + * STRAT Chance call 0x00893426 FUN_00893290+0x196 calls= 8 words= 8 + * STRAT Chance call 0x00893513 FUN_00893290+0x283 calls= 8 words= 8 + STRAT NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 16 words= 16 [internal to Chance: already counted on that row] + other Chance call 0x00578d10 FUN_00578cf0+0x20 calls= 1 words= 1 + other NextInt call 0x00579910 FUN_005798e0+0x30 calls= 3 words= 4 + other NextInt call 0x0069dc29 FUN_0069dbb0+0x79 calls= 3 words= 3 + other NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 1 words= 1 [internal to Chance: already counted on that row] + => attributed strategic words: 19 + +=== post-turn autosave #1 (call 3925) === + 0x008877b0 Game::ServerSpyManager::vslot13 calls=1 since_launch=1 + 0x0088db80 Game::ServerSpyManager::vslot14 calls=1 since_launch=1 + 0x0088ef80 Game::ServerTradeManagerImpl::vslot13 calls=1 since_launch=1 + 0x0082cca0 Game::ServerTradeManagerImpl::vslot15 calls=1 since_launch=1 + 0x008408e0 Game::SpyManager::Slot13RngCallee calls=0 since_launch=0 + 0x00820ca0 Game::TradeManager::Slot13RngCalleeA calls=0 since_launch=0 + 0x0088b440 Game::TradeManager::Slot13RngCalleeB calls=0 since_launch=0 + 0x008938a0 Game::ServerTradeManager::CreateRaidEncounter calls=0 since_launch=0 + 0x00893290 Game::ServerTradeManager::GenerateTradeRaidEncounters calls=1 since_launch=1 + 0x00887f30 Game::ServerSpyManager::vslot15 [control] calls=1 since_launch=1 + 0x008590d0 Game::ServerTradeManagerImpl::vslot14 [control] calls=1 since_launch=1 + 0x007cb080 Game::EncounterDetect::Run [control] calls=1 since_launch=1 + +=== post-turn autosave #2 (call 11130) === + 0x008877b0 Game::ServerSpyManager::vslot13 calls=1 since_launch=2 + 0x0088db80 Game::ServerSpyManager::vslot14 calls=1 since_launch=2 + 0x0088ef80 Game::ServerTradeManagerImpl::vslot13 calls=1 since_launch=2 + 0x0082cca0 Game::ServerTradeManagerImpl::vslot15 calls=1 since_launch=2 + 0x008408e0 Game::SpyManager::Slot13RngCallee calls=0 since_launch=0 + 0x00820ca0 Game::TradeManager::Slot13RngCalleeA calls=0 since_launch=0 + 0x0088b440 Game::TradeManager::Slot13RngCalleeB calls=0 since_launch=0 + 0x008938a0 Game::ServerTradeManager::CreateRaidEncounter calls=1 since_launch=1 + 0x00893290 Game::ServerTradeManager::GenerateTradeRaidEncounters calls=1 since_launch=2 + 0x00887f30 Game::ServerSpyManager::vslot15 [control] calls=1 since_launch=2 + 0x008590d0 Game::ServerTradeManagerImpl::vslot14 [control] calls=1 since_launch=2 + 0x007cb080 Game::EncounterDetect::Run [control] calls=1 since_launch=2 + +=== post-turn autosave #3 (call 18233) === + 0x008877b0 Game::ServerSpyManager::vslot13 calls=1 since_launch=3 + 0x0088db80 Game::ServerSpyManager::vslot14 calls=1 since_launch=3 + 0x0088ef80 Game::ServerTradeManagerImpl::vslot13 calls=1 since_launch=3 + 0x0082cca0 Game::ServerTradeManagerImpl::vslot15 calls=1 since_launch=3 + 0x008408e0 Game::SpyManager::Slot13RngCallee calls=0 since_launch=0 + 0x00820ca0 Game::TradeManager::Slot13RngCalleeA calls=0 since_launch=0 + 0x0088b440 Game::TradeManager::Slot13RngCalleeB calls=0 since_launch=0 + 0x008938a0 Game::ServerTradeManager::CreateRaidEncounter calls=0 since_launch=1 + 0x00893290 Game::ServerTradeManager::GenerateTradeRaidEncounters calls=1 since_launch=3 + 0x00887f30 Game::ServerSpyManager::vslot15 [control] calls=1 since_launch=3 + 0x008590d0 Game::ServerTradeManagerImpl::vslot14 [control] calls=1 since_launch=3 + 0x007cb080 Game::EncounterDetect::Run [control] calls=1 since_launch=3 diff --git a/verify/results/shim/probes/human-t5.txt b/verify/results/shim/probes/human-t5.txt new file mode 100644 index 0000000..d8d2e67 --- /dev/null +++ b/verify/results/shim/probes/human-t5.txt @@ -0,0 +1,28 @@ +== human turn 4->5, probes=8 (behaviour-neutral), build h-probes2-20260908T1658Z == +turn: strategic 36 words / 36 calls; other generators 10 words / 8 calls; overflow 0 + * STRAT NextFloat call 0x0050329d FUN_00503200+0x9d calls= 1 words= 1 + * STRAT NextFloat call 0x00587888 ProcessResearch+0x1c8 calls= 1 words= 1 + * STRAT NextInt call 0x007929a4 FUN_00792750+0x254 calls= 1 words= 1 + * STRAT NextFloat call 0x0088df4f FUN_0088df20+0x2f calls= 1 words= 1 + * STRAT Chance call 0x00893426 FUN_00893290+0x196 calls= 8 words= 8 + * STRAT Chance call 0x00893513 FUN_00893290+0x283 calls= 8 words= 8 + STRAT NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 16 words= 16 [internal to Chance: already counted on that row] + other Chance call 0x00578d10 FUN_00578cf0+0x20 calls= 1 words= 1 + other NextInt call 0x00579910 FUN_005798e0+0x30 calls= 3 words= 5 + other NextInt call 0x0069dc29 FUN_0069dbb0+0x79 calls= 3 words= 3 + other NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 1 words= 1 [internal to Chance: already counted on that row] + => attributed strategic words: 20 + +=== post-turn autosave #1 (call 7) === + 0x008877b0 Game::ServerSpyManager::vslot13 calls=1 since_launch=1 + 0x0088db80 Game::ServerSpyManager::vslot14 calls=1 since_launch=1 + 0x0088ef80 Game::ServerTradeManagerImpl::vslot13 calls=1 since_launch=1 + 0x0082cca0 Game::ServerTradeManagerImpl::vslot15 calls=1 since_launch=1 + 0x008408e0 Game::SpyManager::Slot13RngCallee calls=0 since_launch=0 + 0x00820ca0 Game::TradeManager::Slot13RngCalleeA calls=0 since_launch=0 + 0x0088b440 Game::TradeManager::Slot13RngCalleeB calls=0 since_launch=0 + 0x008938a0 Game::ServerTradeManager::CreateRaidEncounter calls=0 since_launch=0 + 0x00893290 Game::ServerTradeManager::GenerateTradeRaidEncounters **NOT INSTALLED** + 0x00887f30 Game::ServerSpyManager::vslot15 [control] **NOT INSTALLED** + 0x008590d0 Game::ServerTradeManagerImpl::vslot14 [control] **NOT INSTALLED** + 0x007cb080 Game::EncounterDetect::Run [control] **NOT INSTALLED** diff --git a/verify/results/shim/probes/oracle-restore/h-oracle-autosave.sav b/verify/results/shim/probes/oracle-restore/h-oracle-autosave.sav new file mode 100644 index 0000000..6b3e2e4 Binary files /dev/null and b/verify/results/shim/probes/oracle-restore/h-oracle-autosave.sav differ diff --git a/verify/results/shim/probes/oracle-restore/h-oracle-endturn.sav b/verify/results/shim/probes/oracle-restore/h-oracle-endturn.sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/shim/probes/oracle-restore/h-oracle-endturn.sav differ diff --git a/verify/results/shim/probes/zuul-t24-25.txt b/verify/results/shim/probes/zuul-t24-25.txt new file mode 100644 index 0000000..4cb4a5b --- /dev/null +++ b/verify/results/shim/probes/zuul-t24-25.txt @@ -0,0 +1,57 @@ +== zuul turn 23->25, probes=8, build h-probes2-20260908T1658Z == +turn: strategic 32 words / 32 calls; other generators 10 words / 8 calls; overflow 0 + * STRAT NextFloat call 0x0050329d FUN_00503200+0x9d calls= 1 words= 1 + * STRAT NextFloat call 0x00587888 ProcessResearch+0x1c8 calls= 1 words= 1 + * STRAT NextFloat call 0x005878bb ProcessResearch+0x1fb calls= 1 words= 1 + * STRAT NextInt call 0x007929a4 FUN_00792750+0x254 calls= 1 words= 1 + * STRAT Chance call 0x00893426 FUN_00893290+0x196 calls= 7 words= 7 + * STRAT Chance call 0x00893513 FUN_00893290+0x283 calls= 7 words= 7 + STRAT NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 14 words= 14 [internal to Chance: already counted on that row] + other Chance call 0x00578d10 FUN_00578cf0+0x20 calls= 1 words= 1 + other NextInt call 0x00579910 FUN_005798e0+0x30 calls= 3 words= 5 + other NextInt call 0x0069dc29 FUN_0069dbb0+0x79 calls= 2 words= 2 + other NextFloat call 0x006ad873 FUN_006ad700+0x173 calls= 1 words= 1 + other NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 1 words= 1 [internal to Chance: already counted on that row] + => attributed strategic words: 18 +turn: strategic 32 words / 32 calls; other generators 9 words / 8 calls; overflow 0 + * STRAT NextFloat call 0x0050329d FUN_00503200+0x9d calls= 1 words= 1 + * STRAT NextFloat call 0x00587888 ProcessResearch+0x1c8 calls= 1 words= 1 + * STRAT NextFloat call 0x005878bb ProcessResearch+0x1fb calls= 1 words= 1 + * STRAT NextInt call 0x007929a4 FUN_00792750+0x254 calls= 1 words= 1 + * STRAT Chance call 0x00893426 FUN_00893290+0x196 calls= 7 words= 7 + * STRAT Chance call 0x00893513 FUN_00893290+0x283 calls= 7 words= 7 + STRAT NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 14 words= 14 [internal to Chance: already counted on that row] + other Chance call 0x00578d10 FUN_00578cf0+0x20 calls= 1 words= 1 + other NextInt call 0x00579910 FUN_005798e0+0x30 calls= 3 words= 4 + other NextInt call 0x0069dc29 FUN_0069dbb0+0x79 calls= 2 words= 2 + other NextFloat call 0x006ad873 FUN_006ad700+0x173 calls= 1 words= 1 + other NextFloat call 0x008e6e04 FUN_008e6dd0+0x34 calls= 1 words= 1 [internal to Chance: already counted on that row] + => attributed strategic words: 18 + +=== post-turn autosave #1 (call 7) === + 0x008877b0 Game::ServerSpyManager::vslot13 calls=1 since_launch=1 + 0x0088db80 Game::ServerSpyManager::vslot14 calls=1 since_launch=1 + 0x0088ef80 Game::ServerTradeManagerImpl::vslot13 calls=1 since_launch=1 + 0x0082cca0 Game::ServerTradeManagerImpl::vslot15 calls=1 since_launch=1 + 0x008408e0 Game::SpyManager::Slot13RngCallee calls=0 since_launch=0 + 0x00820ca0 Game::TradeManager::Slot13RngCalleeA calls=0 since_launch=0 + 0x0088b440 Game::TradeManager::Slot13RngCalleeB calls=0 since_launch=0 + 0x008938a0 Game::ServerTradeManager::CreateRaidEncounter calls=2 since_launch=2 + 0x00893290 Game::ServerTradeManager::GenerateTradeRaidEncounters **NOT INSTALLED** + 0x00887f30 Game::ServerSpyManager::vslot15 [control] **NOT INSTALLED** + 0x008590d0 Game::ServerTradeManagerImpl::vslot14 [control] **NOT INSTALLED** + 0x007cb080 Game::EncounterDetect::Run [control] **NOT INSTALLED** + +=== post-turn autosave #2 (call 15) === + 0x008877b0 Game::ServerSpyManager::vslot13 calls=1 since_launch=2 + 0x0088db80 Game::ServerSpyManager::vslot14 calls=1 since_launch=2 + 0x0088ef80 Game::ServerTradeManagerImpl::vslot13 calls=1 since_launch=2 + 0x0082cca0 Game::ServerTradeManagerImpl::vslot15 calls=1 since_launch=2 + 0x008408e0 Game::SpyManager::Slot13RngCallee calls=0 since_launch=0 + 0x00820ca0 Game::TradeManager::Slot13RngCalleeA calls=0 since_launch=0 + 0x0088b440 Game::TradeManager::Slot13RngCalleeB calls=0 since_launch=0 + 0x008938a0 Game::ServerTradeManager::CreateRaidEncounter calls=1 since_launch=3 + 0x00893290 Game::ServerTradeManager::GenerateTradeRaidEncounters **NOT INSTALLED** + 0x00887f30 Game::ServerSpyManager::vslot15 [control] **NOT INSTALLED** + 0x008590d0 Game::ServerTradeManagerImpl::vslot14 [control] **NOT INSTALLED** + 0x007cb080 Game::EncounterDetect::Run [control] **NOT INSTALLED** diff --git a/verify/traces/h-probes-human-perturbed-t3-5.jsonl.gz b/verify/traces/h-probes-human-perturbed-t3-5.jsonl.gz new file mode 100644 index 0000000..57347a1 Binary files /dev/null and b/verify/traces/h-probes-human-perturbed-t3-5.jsonl.gz differ diff --git a/verify/traces/h-probes-human-t5.jsonl.gz b/verify/traces/h-probes-human-t5.jsonl.gz new file mode 100644 index 0000000..15c34a7 Binary files /dev/null and b/verify/traces/h-probes-human-t5.jsonl.gz differ diff --git a/verify/traces/h-probes-zuul-t24-25.jsonl.gz b/verify/traces/h-probes-zuul-t24-25.jsonl.gz new file mode 100644 index 0000000..3d22c03 Binary files /dev/null and b/verify/traces/h-probes-zuul-t24-25.jsonl.gz differ