lane H: five live probes, the oracle restored, and a harness perturbation bisected to one hook

- determinism oracle regenerated and byte-identical (bb4fd9ac / 978041ac)
- the four phase-23/33 draw-bearing tail callees run EVERY turn; the three inner
  functions holding the draws run zero times -- the gate is inside each outer body
- CreateRaidEncounter is called (2 on one turn) and draws nothing: candidate list empty
- Zuul: 7 calls / 7 words per trade-raid Chance site, 14 not 16, as predicted
- EncounterDetect_Run receives an EMPTY record vector, so ProcessTeamRecord and
  AssignContacts never run; the 2-word detection residual is in 0x007d5150's subtree
- a MinHook detour on 0x00893290 changes the game's output; bisected over six runs.
  The un-instrumented game and lane Z's instrument agree, so lane Z's numbers stand
- lane AI1 insert: P2 held across two fresh processes, Rung B stays as written
This commit is contained in:
alex 2026-09-08 13:53:36 -04:00
parent 3f014a42eb
commit 8ad9971f6e
14 changed files with 833 additions and 0 deletions

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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`),

View file

@ -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"
}
]
}

93
tools/probe_report.py Normal file
View file

@ -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 <trace.jsonl>
"""
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}")

View file

@ -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

View file

@ -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**

View file

@ -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**

Binary file not shown.

Binary file not shown.

Binary file not shown.