Compare commits
4 commits
7d49e216f1
...
5e4c27772d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e4c27772d | ||
|
|
aef3d487f3 | ||
|
|
4a212e25b4 | ||
|
|
712d184599 |
11 changed files with 1133 additions and 18 deletions
|
|
@ -1,16 +1,16 @@
|
|||
# SotS RE campaign — coverage dashboard
|
||||
|
||||
Generated 2026-09-09 03:02 UTC · `sots-re` @ 6d33a74,2026-09-08 · `sots-engine` @ 989c692,2026-09-08 (240 commits) · regenerate with `tools/dashboard.py`
|
||||
Generated 2026-09-09 04:14 UTC · `sots-re` @ aef3d48,2026-09-09 · `sots-engine` @ 989c692,2026-09-08 (240 commits) · regenerate with `tools/dashboard.py`
|
||||
|
||||
> **North star:** A functional reimplementation of the engine — behavior-equivalent, NOT byte-for-byte
|
||||
|
||||
## 1. Map coverage (campaign/board.md)
|
||||
|
||||
410 targets · mapped-or-better **363/410** `[█████████░] 89%` · verified **321/410** `[████████░░] 78%`
|
||||
414 targets · mapped-or-better **367/414** `[█████████░] 89%` · verified **325/414** `[████████░░] 79%`
|
||||
|
||||
| Status | Count | % |
|
||||
|---|---:|---:|
|
||||
| verified | 321 | 78% |
|
||||
| verified | 325 | 79% |
|
||||
| mapped | 42 | 10% |
|
||||
| in-progress | 4 | 1% |
|
||||
| backlog | 41 | 10% |
|
||||
|
|
@ -22,23 +22,23 @@ Generated 2026-09-09 03:02 UTC · `sots-re` @ 6d33a74,2026-09-08 · `sots-engine
|
|||
| control-flow | 29 | 2 | 0 | 0 | 0 | 31 |
|
||||
| subsystems | 4 | 8 | 0 | 2 | 1 | 15 |
|
||||
| engine | 30 | 0 | 0 | 0 | 0 | 30 |
|
||||
| verify | 101 | 15 | 3 | 35 | 0 | 154 |
|
||||
| verify | 102 | 15 | 3 | 35 | 0 | 155 |
|
||||
| phase2 | 13 | 3 | 1 | 0 | 0 | 17 |
|
||||
| meta | 79 | 6 | 0 | 1 | 0 | 86 |
|
||||
| meta | 82 | 6 | 0 | 1 | 0 | 89 |
|
||||
| other | 19 | 2 | 0 | 0 | 0 | 21 |
|
||||
|
||||
## 2. Binary understanding
|
||||
|
||||
- RTTI type descriptors: **1,924** (`Game::` 1,404, `Mars::` 194; serializable types 179)
|
||||
- Classes with recovered member layouts: **384** / 1,598 named classes `[██░░░░░░░░] 24%` — `objects/layouts.json` (serializer recovery) plus classes recovered by hand in `struct-recovery.md` + `schema-gaps-resolved.md`. Note 179 types are *serializable*; the recovery also reaches non-serializable ones, so this is not a subset of that
|
||||
- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1276**, verified **1145** `[█████████░] 90%`
|
||||
- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1283**, verified **1152** `[█████████░] 90%`
|
||||
|
||||
## 3. Data layer
|
||||
|
||||
- Catalogs: **1,595/1,595** files parsed (91 block kinds in `schema_stats.json`), dangling cross-refs **0** (`crosslink.json`)
|
||||
- Oracle `mars-parse`: **1,531/1,531** files agree `[██████████] 100%`
|
||||
- Oracle `mars-text`: **64/64** files agree `[██████████] 100%`
|
||||
- Saves: **25/25** real saves strict-clean — strict exit 0, 0 errors, 0 warnings
|
||||
- Saves: **29/29** real saves strict-clean — strict exit 0, 0 errors, 0 warnings
|
||||
- Design rules: **127/127** stock designs pass `[██████████] 100%`
|
||||
- Value domains: **490/724** typed fields have been seen to vary `[███████░░░] 68%` — the other **234** have only ever held one value across the corpus, so their typing is untested (`value-domain-census.md`)
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ Detail: `verify/results/standalone/report.txt`.
|
|||
|
||||
## 7. Verification ledger
|
||||
|
||||
- ✅ Saves strict: 25/25 (strict exit 0, 0 errors, 0 warnings)
|
||||
- ✅ Saves strict: 29/29 (strict exit 0, 0 errors, 0 warnings)
|
||||
- ✅ Design rules: 127/127
|
||||
- ✅ oracle mars-parse 1531/1531 · ✅ oracle mars-text 64/64
|
||||
- ✅ Compare harness present (`verify/harness/compare/`)
|
||||
|
|
@ -133,11 +133,11 @@ Most recent open:
|
|||
|
||||
## 9. Delta since previous dashboard
|
||||
|
||||
- verified targets: 317 → 321 (+4) · mapped-or-better: 359 → 363 (+4)
|
||||
- verified targets: 321 → 325 (+4) · mapped-or-better: 363 → 367 (+4)
|
||||
- engine LOC: 58,647 → 58,647 (+0) · test files: 122 → 122 (+0) · checks: 4,257 → 4,257 (+0)
|
||||
- addresses verified: 1,139 → 1,145 (+6) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0)
|
||||
- addresses verified: 1,145 → 1,152 (+7) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0)
|
||||
- standalone leaves closed: 45 → 45 (+0) · leaves still diverging: 63 → 63 (+0)
|
||||
|
||||
---
|
||||
warnings: board.md: unknown types subsystems; mars-rng.md: no oracle total row parsed; mars-stream.md: no oracle total row parsed; mars-vfs.md: no oracle total row parsed
|
||||
<!-- dashboard-metrics {"verified": 321, "mapped_plus": 363, "targets": 410, "loc": 58647, "tests": 122, "checks": 4257, "addr_verified": 1145, "addr_total": 1276, "layouts": 384, "open_q": 26, "sa_closed": 45, "sa_left": 63} -->
|
||||
<!-- dashboard-metrics {"verified": 325, "mapped_plus": 367, "targets": 414, "loc": 58647, "tests": 122, "checks": 4257, "addr_verified": 1152, "addr_total": 1283, "layouts": 384, "open_q": 26, "sa_closed": 45, "sa_left": 63} -->
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
198
findings/control-flow/raid-target-pick-verdict.md
Normal file
198
findings/control-flow/raid-target-pick-verdict.md
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# The raid target pick — a verdict on `TradeManager+0x0c`
|
||||
|
||||
- **Type:** control-flow (static decode of the writer + live measurement)
|
||||
- **Owner / date:** lane AR · 2026-09-08 · guest **VM141** (`sots-re-win10-b`, 192.168.10.143) —
|
||||
**held from the time this file was committed**
|
||||
- **Decides:** lane AG's `gate-indexed-rng-audit.md` §3.3 final paragraph — *per-system containing-sector
|
||||
table* versus *sector-indexed vector of six* — which lane AD (`raid-gate-multiplicity.md` §4) measured
|
||||
and correctly reported it could not separate on sector `TradeID 832`
|
||||
- **Instrument:** lane Z's return-address ledger (`draw_sites`) + lane H's entry probes at **`probes=8`**,
|
||||
the byte-neutral set. **Not `probes=11`** (lane H bisected index 8 as not byte-neutral)
|
||||
|
||||
---
|
||||
|
||||
## 1. Predictions, committed before the build (rule 2)
|
||||
|
||||
*Everything in §1 was written and committed before the shim was built, before the workload was played
|
||||
and before any counter was read. It is reproduced verbatim below when the results are added; nothing in
|
||||
it is edited after the fact, only annotated with verdicts.*
|
||||
|
||||
### 1.0 The static result this lane found first, stated as a falsifiable prediction
|
||||
|
||||
Lane AG's §7 lists **"`ServerTradeManager+0x0c`'s writer was not found"** as the one load-bearing
|
||||
inference in its document. This lane found the writer on the host before touching the guest, and it
|
||||
decides the question on its own. It is written here **as a prediction**, because a live run can falsify
|
||||
it, and because rule 3 says static reading and behavioural comparison are different instruments and
|
||||
neither is trusted alone.
|
||||
|
||||
**`FUN_00841700`** (`this` in `ecx` → `ebx`; 214 bytes, `0x00841700`–`0x008417d5`, decoded to the next
|
||||
function start per rule 17) does exactly three things:
|
||||
|
||||
```
|
||||
; 1. CLEAR the +0x0c vector
|
||||
0084170b lea ecx,[ebx+0xc] ; &this->vec0c
|
||||
0084170e call 0x459f70 ; vector<T*>::resize(0) -- arg pushed at 0x841709
|
||||
|
||||
; 2. RESIZE it to the length of the vector at [this+4]+0x40 .. +0x44
|
||||
00841713 mov ecx,[ebx+0x4]
|
||||
00841716 mov eax,[ecx+0x44]
|
||||
00841719 sub eax,[ecx+0x40]
|
||||
0084171c mov edx,[ebx+0x10] ; vec0c._Mylast
|
||||
0084171f mov edi,[ebx+0xc] ; vec0c._Myfirst
|
||||
00841726 sar eax,0x2 ; N = count([this+4]+0x40..+0x44)
|
||||
00841729 sar ecx,0x2 ; cur = count(vec0c)
|
||||
0084172c mov DWORD PTR [ebp-0x4],0x0 ; fill value = NULL
|
||||
00841733 cmp eax,ecx
|
||||
00841735 jbe 0x841749
|
||||
0084173f lea ecx,[ebx+0xc] ; grow: _Insert_n(_Mylast, N-cur, NULL)
|
||||
00841742 call 0x50e6a0
|
||||
00841754 lea ecx,[ebx+0xc] ; shrink: erase(_Myfirst+N, _Mylast)
|
||||
00841757 call 0x4ddfc0
|
||||
|
||||
; 3. FILL it by walking sectors x their member systems
|
||||
0084175c ecx = this->+0x1c ; eax = (this->+0x20 - ecx)/4 ; the SECTOR vector
|
||||
00841778 sect = sectors[i]
|
||||
0084177f inner count = (sect->+0x7c - sect->+0x78)/4 ; the sector's MEMBER vector
|
||||
00841798 m = sect->members[j]
|
||||
008417a2 idx = m->+0x5c ; the member's OWN INDEX field
|
||||
008417a5 edi = this->+0x0c
|
||||
008417a8 mov DWORD PTR [edi + idx*4], ecx ; table[idx] = sect
|
||||
```
|
||||
|
||||
Three things follow, and each is checkable by a reader who disagrees:
|
||||
|
||||
1. **The table's length is, by construction, the length of the container that `trfr` indexes.**
|
||||
`FUN_00841cd0` — the function that settles what `trfr` means, and the one AG cites for exactly that —
|
||||
resolves a route's source endpoint at `0x00841d36` as
|
||||
`mov eax,[edi+0x4]; mov ecx,[eax+0x40]; mov edx,[esi+0xc]; mov eax,[ecx+edx*4]`, i.e.
|
||||
**`([this+4]+0x40)[rt->trfr]`**. Step 2 above resizes the `+0x0c` vector to the length of *that same
|
||||
vector on that same object*. So `G_B1a` (`0 <= rt->trfr < count(this->+0x0c..+0x10)`) is satisfied by
|
||||
**every route with a valid `trfr`, by construction**, in the same way that AG showed `vslot15`'s G1 is
|
||||
satisfied by every `rt` in every save by construction.
|
||||
2. **The slot index is a system's own index field, and the slot value is a `TradeSector*`.** Step 3
|
||||
writes `table[m->+0x5c] = sect` where `sect` came out of the sector vector and `m` came out of that
|
||||
sector's member list. That is the definition of a **per-system containing-trade-sector table**.
|
||||
`FUN_00841c70` reads it back the same way (`arg->+0x5c` → bounds check against the same count →
|
||||
`table[idx]`, then `table[idx] + 0x78` — the member vector — is handed to `FUN_0059ec00`), which is the
|
||||
independent confirmation that `+0x5c` on the indexing object and `+0x78` on the table element are the
|
||||
two halves of one relation.
|
||||
3. **The table is rebuilt on load.** `FUN_00841700` has five callers; one of them is **`FUN_00858a10`**,
|
||||
which is full of `ReadInt`/`ReadFloat` calls and invokes `FUN_00841700` at `0x00858f07` as its last
|
||||
real act. So a loaded save has a populated table, and this is not a galaxy-generation-only structure.
|
||||
|
||||
> **P0 — the verdict, predicted.** `TradeManager+0x0c` is a `std::vector<TradeSector*>` of length
|
||||
> `numSystems`, indexed by system index. **AG's per-system reading is right; the sector-indexed-vector-of-six
|
||||
> reading is false**, and `0x0088b613` is therefore **reachable in principle**. `ad-turn27-two-raiders.sav`
|
||||
> has `systemIds` of length **28**, so the table has 28 slots and `trfr ∈ {14,16,18,20}` clears `G_B1a`
|
||||
> with room to spare.
|
||||
|
||||
**How P0 could be wrong, and the symptom of each way** (rule 2 requires this):
|
||||
|
||||
| way it could be wrong | symptom in the run |
|
||||
|---|---|
|
||||
| `[this+4]+0x40` is not the systems vector — some other container of a coincidentally similar length | `B` is entered on a success and still costs **0** words |
|
||||
| `m->+0x5c` is not a system index (e.g. the member list holds something else) | same symptom; `table[trfr]` would then be null or a wrong sector, and `G_B1b` rejects |
|
||||
| the members of sector 816 do not include the systems the routes are sourced at | `B` entered, 0 words; and the save's `tssys` list would contradict the route's `trfr` |
|
||||
| `FUN_00841700` is not called on this path after all (my caller read is wrong) | `B` entered, 0 words, table all-null |
|
||||
|
||||
**In every one of those cases the observable is the same: `B` entered, 0 words.** That single number is
|
||||
the whole experiment, and it is why the run is worth taking even though the static read looks closed.
|
||||
|
||||
### 1.1 The state, read fresh from the save (rule: do not inherit a number)
|
||||
|
||||
Read from `verify/results/saves/ad-turn27-two-raiders.sav` with
|
||||
`verify/save-reader/save_reader.py --dump`, before touching the guest.
|
||||
|
||||
**`trdmgr` holds FIVE `rt` records at turn 27, not three.** Lane AD's §4 reports *"three route records,
|
||||
`trfr = 18, 14, 14`"*; the save has **five**, `trfr = 18, 16, 14, 20, 14`, `tro = 32` (the AI) on all
|
||||
five, `trfrs = trtos = 0` on all five. This is a correction to `raid-gate-multiplicity.md` §4 and to its
|
||||
board row (rule 11), recorded before the run rather than after.
|
||||
|
||||
**System index ↔ system id is `index = id/16 − 3`**, pinned from `Sim.systemIds`
|
||||
`[48, 64, 80, …, 480]` (28 entries, stride 16, first = 48). Applying it to each sector's `tssys` list
|
||||
reproduces lane AG's index sets for all six sectors exactly, which is the check that the mapping is right:
|
||||
|
||||
| sector `TradeID` | grid | `tsct` | `tscr` | member system **indices** | fleets present |
|
||||
|---|---|---|---|---|---|
|
||||
| 752 | 1 | 2 | 253 | 1, 10, 20, 24, 17 | `6864` |
|
||||
| 768 | 2 | 2 | 253 | 2, 7, **14**, 21, 22, 27 | `6880` |
|
||||
| 784 | 3 | 0 | 253 | 4, 8, 12, 25 | — |
|
||||
| 800 | 4 | 0 | 253 | 6, 11, 5 | — |
|
||||
| **816** | **5** | **2** | **253** | 9, 15, **16**, **18**, 19, 26 | `7072` **`Freighters`** (PID 32, 11 ships), `7088` `Escorts` |
|
||||
| 832 | 6 | 0 | 253 | 13, 23, 0, 3 | `3744` Alpha, `6544` Beta — AD's two raiders |
|
||||
|
||||
So on the turn-27 route set:
|
||||
|
||||
- **sector 816 sources TWO routes** (`trfr` 18 and 16) → candidate list `n = 2`, `bound = 1`;
|
||||
- sector 768 sources two (`trfr` 14 twice) → `n = 2`;
|
||||
- sector 752 sources one (`trfr` 20) → `n = 1`, `bound = 0`;
|
||||
- sectors 784, 800, 832 source **none**. 832 is still the one sector no route is sourced in.
|
||||
|
||||
**Geometry, recomputed rather than inherited** (node positions from `trdmgr`, colony positions from
|
||||
`Sim.systems[].Sys.Pos`; player 0 = `PlyrIdx 0` = `PID 16`, `HomeSys 48`, `CnRad true`):
|
||||
|
||||
| sector | from Epsilon Eridani (idx 0) | from Downbelow (idx 12) | sources a route? |
|
||||
|---|---|---|---|
|
||||
| 752 | 12.85 | 8.43 | yes (1) |
|
||||
| **768** | **11.53** | **10.14** | **yes (2)** |
|
||||
| 784 | 9.36 | 3.49 | no |
|
||||
| 800 | 9.37 | 11.77 | no |
|
||||
| **816** | **15.66** | **12.12** | **yes (2)** |
|
||||
| 832 | 4.99 | 4.56 | no |
|
||||
|
||||
Reproduces lane AD's table to the last digit. Player-0 fleets at turn 27: `3744` *Alpha Fleet* and
|
||||
`6544` *Beta Fleet* on 832 (both `DesID 17`, one `CR Repair and Salvage` each, `Range` consumed to
|
||||
4.0125), and `6672` *Gamma Fleet* at `LocID 48` (home, `Range 9.0`).
|
||||
|
||||
### 1.2 The workload
|
||||
|
||||
Build **several separate single-ship `Extended Range` destroyer fleets** (stock hull, `Range 27.0`,
|
||||
cost 2,252, node speed 4.0 — no research required) and park them on **sector 816's node**, `Pos`
|
||||
bit-equal, for as many End Turns as the session allows. Sector **768** is a secondary target if the map
|
||||
allows it: it is *closer* (10.14 ly from Downbelow) and also sources two routes, so a fleet there is a
|
||||
second independent instance of the same test at no extra build cost.
|
||||
|
||||
**Separate fleets, not one big fleet, and this is arithmetic not taste.** `A` is per fleet (lane AD) and
|
||||
its odds are `ODDS_DE + PER_SHIP·nShips` for a destroyer fleet. One 5-destroyer fleet rolls **once** at
|
||||
`0.05 + 0.05 = 0.10`; five 1-destroyer fleets roll **five times** at `0.06` each — expected successes
|
||||
0.10/turn against 0.30/turn. Three times the signal for the same ships.
|
||||
|
||||
### 1.3 The numbered predictions
|
||||
|
||||
| # | prediction |
|
||||
|---|---|
|
||||
| **P0** | *(above)* `+0x0c` is the per-system containing-sector table; AG's reading is right and the sector-indexed reading is dead |
|
||||
| **P1** | `TradeManager::Slot13RngCalleeA` is entered **once per parked permitted fleet per turn** and costs **1 word** each — lane AD's per-fleet result, re-confirmed on a different sector and a different hull |
|
||||
| **P2** | On a turn `A` succeeds for a fleet on **816**, `Slot13RngCalleeB` is entered **and reaches `0x0088b613`**, costing **≥ 1 word** — the number lane AC and lane AD both measured as **0** |
|
||||
| **P3** | `B`'s full cost on such a success is **2 words**: `NextInt` at `0x0088b613` (exactly 1 — `RNG_NextInt` has no zero-bound early-out, and with `bound = 1` the mask is 1 so **no rejection is possible either**) plus `NextFloat` at `0x00820c1b` inside `FUN_00820af0`, unless that function's species short-circuit fires (`owner->+0x5c == 1`, or either of the two `SpeciesDef` tests) |
|
||||
| **P4** | Therefore `OnAllCombatDone_Tail` costs **k + 2s** words on a turn, where k = parked permitted fleets and s = successes among them. With one raider that is **1 on a failing turn and 3 on a succeeding turn** — AG's committed number in §3.3, tested |
|
||||
| **P5** | `draw_sites` shows a strategic row at call `0x0088b613` (`NextInt`) with `calls == words == s`, and one at `0x00820c1b` (`NextFloat`) with `calls == s`, `words == s − (species short-circuits)`. Both are sites **no lane has ever fired**; `0x00820c1b` is one of the six lane AG added to the inventory |
|
||||
| **P6** | `0x00820e18` (`A`'s `NextFloat`) shows `calls == words == k`, reproducing AD's per-fleet pricing on a hull and a sector AD did not use |
|
||||
| **P7** | The `hooks=off` control **will not agree with itself** — I expect AD's failure to repeat, because it is the per-process client-AI seed and the AI is larger by turn 33 than at 27, not smaller. I predict the variation is again confined to `Player[32]`'s designs / build queues / fleet ids, and that `/Sim/RNG` and `/Sim/trdmgr` are **bit-identical across all runs**. If a `/Sim/trdmgr` leaf, a trade-sector leaf or a player-0 fleet leaf moves, my numbers are **not** off a reproducible sub-tree and I will say exactly that instead of reporting them |
|
||||
| **P8** | If `A` is entered `k` times but `B` is never entered across the run, that is **not** a negative — it is a run of failed Bernoulli trials, and I will report the exact binomial probability rather than the word "unremarkable" (rule 20) |
|
||||
|
||||
### 1.4 The falsifier, stated as the sentence I will have to write
|
||||
|
||||
> **If a raider is parked on sector 816 with `Pos` bit-equal, `A` succeeds for it, `B` is entered, and
|
||||
> `B` still costs 0 words — then P0 is false, `TradeManager+0x0c` is not the per-system table, lane AG's
|
||||
> §3.3 decode is retired, and the campaign loses a finding it is currently leaning on.**
|
||||
|
||||
That is a real result and it is the one I am most interested in, because it would mean a careful static
|
||||
read of the writer produced a confident wrong answer — which is rule 3's warning pointing the other way
|
||||
from its usual direction.
|
||||
|
||||
### 1.5 What this lane will not be able to settle, predicted in advance (rule 15)
|
||||
|
||||
- **The freighter doubling.** Sector 816 holds the AI's `Freighters` fleet, so `param_2` is finally
|
||||
testable in principle — but separating `p = 0.06` from `p = 0.12` needs on the order of a hundred
|
||||
fleet-rolls. At the sample size a session affords, the success count is **consistent with both** and I
|
||||
will report it as consistency, not validation.
|
||||
- **The `design+0x12c > 1` short-circuit.** Still unpriced; the Dreadnought category was empty for lane
|
||||
AD and nothing in this workload changes that.
|
||||
- **`bestScale`.** `0x009f8d48 = 0.33f` applies when `ship+0x18 & 0x100000`. I will read the destroyer's
|
||||
flag from the save rather than assume it is clear.
|
||||
- **A rung-A calibration pair.** P7 predicts there is none to be had on this lineage.
|
||||
|
||||
---
|
||||
|
||||
*Results, and the verdict, follow below. Nothing above this line is edited after the run.*
|
||||
706
findings/subsystems/spy-detection-roll.md
Normal file
706
findings/subsystems/spy-detection-roll.md
Normal file
|
|
@ -0,0 +1,706 @@
|
|||
# The spy detection roll — decoded, its gate turned into a save predicate, and measured
|
||||
|
||||
- **Type:** subsystem (static decode + live measurement)
|
||||
- **Owner / date:** lane AS · 2026-09-09 · guest **VM144** (`sots-re-win10-144`, `re@192.168.10.144`)
|
||||
- **Instrument:** lane Z's `draw_sites` return-address ledger at **`probes=8`**, shim build
|
||||
`as-c172c99-20260909T0205Z`, dist `C:\SOTS\shimdist-as`
|
||||
- **Predictions, committed before the build:** `sots-engine` `docs/AS-predictions.md`
|
||||
(commit `c172c99`, addendum `61875a4`)
|
||||
- **Answers:** the resolution `2026-09-09-tail-draws.md` §8 item 2 (the spy half of the tail gate)
|
||||
- **Corrects:** the resolution's "the detection roll is inline in `vslot13`" (it is a direct call);
|
||||
lane L3 §6's "`spies2` is not the spy list"; board row 399's V2 misquote is *not* touched here
|
||||
|
||||
*(Sections 1–3 were written and committed before any measurement. Sections 4 onward are the
|
||||
measurement.)*
|
||||
|
||||
---
|
||||
|
||||
## 0. Headline
|
||||
|
||||
> **The detection roll fires, it costs one word, and its gate is an asteroid belt.**
|
||||
>
|
||||
> `Mars::RNG::Chance` at **`0x00887c8a`** — in the body of `ServerSpyManager::vslot13`, phase 23
|
||||
> call 9 of 9 of `OnAllCombatDone_Tail` — rolls once per deployed, undetected spy per turn, on the
|
||||
> strategic generator, at a probability `sdo` that starts at **0.0084 as measured** and accumulates
|
||||
> by the same amount every turn. `OnAllCombatDone_Tail` went **363 → 364**: one word, against 0 on
|
||||
> every turn any lane has ever measured.
|
||||
>
|
||||
> The predicate is `spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1)`.
|
||||
> **Corpus count: 0 of 22.** The failed conjunct is **`deat != 0`** — every spy this campaign has
|
||||
> ever built was still docked to its tender. Not trade routes, not freighters, not `spyon`, and not
|
||||
> the asteroid belt: **all 22 corpus saves already had a usable belt somewhere**, and two of them
|
||||
> already had four AI-owned ones.
|
||||
>
|
||||
> And the polarity is the opposite of the intuitive one: **when the belt is gone the roll does not
|
||||
> happen and the spy is detected anyway.** A zero at this site is two different results.
|
||||
|
||||
---
|
||||
|
||||
## 1. The chain, decoded from the instruction stream
|
||||
|
||||
Program `sots` / "Sword of the Stars.exe", ImageBase `0x00400000`, all addresses VAs. Every body
|
||||
below was disassembled from its start **to the next function start** (rule 17); no Ghidra size was
|
||||
used. ReVa/CT111's MCP endpoint was down for this session (`CONNECTION_CLOSED`), so nothing was read
|
||||
in the decompiler — this is all instruction stream, which for these bodies is a feature.
|
||||
|
||||
`Game::ServerSpyManager::vslot13` = **`0x008877b0`** (lane V2's name), dispatched at `0x007d9811`
|
||||
from `OnAllCombatDone_Tail` on receiver `S+0x15c` slot 13. Body `0x008877b0 .. 0x00887f30`.
|
||||
Throughout: `ebx = 0`, `edi = this` (the manager), `esi` = the current `Game::SpyCraft`. The loop is
|
||||
over the vector at `this+0x10 .. this+0x14` — the same vector lane L3 watched grow to 1 at
|
||||
**Build Spy**.
|
||||
|
||||
```
|
||||
0088781d je 0x887e92 G1 spy.deat (+0x10) == 0 -> next spy
|
||||
0088782d call 0x8b9240 sysA = registry(server+0x84, spy.deat) -> [ebp-0xbc]
|
||||
00887845 call 0x8b9240 ownerO = registry(server+0x84, spy.sown) -> [ebp-0xc0]
|
||||
00887856 je 0x887c5b G2 spy.sdet (+0x40) == -1 -> BRANCH D (detection)
|
||||
00887867 jl 0x887e92 (server.turn - sdet) < 3 -> next spy
|
||||
0088787a je 0x887c4f sysA.PID == 0 -> sdet := -1, next spy
|
||||
00887891 je 0x887c4f sysA.PID == ownerO -> sdet := -1, next spy
|
||||
00887af2 call 0x8408e0 <-- P, the false-flag draw (three sites inside)
|
||||
```
|
||||
|
||||
**Branch D — the detection roll:**
|
||||
|
||||
```
|
||||
00887c5b push eax ; ownerO
|
||||
00887c62 push [ebp-0xbc] ; sysA
|
||||
00887c63 push esi ; the spy
|
||||
00887c66 call 0x81f570 D1 SpyCraft_AccumulateDetectionOdds -> writes spy.sdo (+0x3c)
|
||||
00887c6b mov ecx,[ebp-0xbc]
|
||||
00887c71 call 0x743f80 D2 t = ServerSystem_BeltUsableFlags(sysA)
|
||||
00887c76 test al,1
|
||||
00887c78 je 0x887c97 (t & 1) == 0 -> SKIP THE ROLL, straight to D4
|
||||
00887c7a mov ecx,[edi+8] ; the StrategyServer
|
||||
00887c7d fld [esi+0x3c] ; spy.sdo
|
||||
00887c80 mov ecx,[ecx+0x16c] ; THE STRATEGIC GENERATOR
|
||||
00887c86 push ecx
|
||||
00887c87 fstp [esp]
|
||||
00887c8a call 0x8e6dd0 D3 Mars::RNG::Chance(strategic, sdo) <-- THE DETECTION ROLL
|
||||
return address 0x00887c8f
|
||||
00887c8f test al,al
|
||||
00887c91 je 0x887e92 roll failed -> next spy, still undetected
|
||||
00887c97 mov edx,[edi+8]
|
||||
00887c9a mov eax,[edx+0xc] ; the current turn
|
||||
00887c9d mov [esi+0x40],eax D4 spy.sdet := turn (DETECTED; then the event strings)
|
||||
```
|
||||
|
||||
### 1.1 It is a direct call, not an inlined draw — the instrument advice was right for the wrong reason
|
||||
|
||||
The resolution that created this lane says the roll is *inline* in `vslot13`. It is not. The bytes
|
||||
are `e8 41 f1 05 00` — a plain `E8 rel32` to the `Chance` **entry point** `0x008e6dd0`. Nothing
|
||||
resembling rule 16's inlined tempering sequence is present.
|
||||
|
||||
What *is* true, and is the whole reason `draw_sites` is the right instrument, is that the call sits
|
||||
in the **caller** rather than in `SpyManager::Slot13RngCallee 0x008408e0`. An entry probe on
|
||||
`0x008408e0` therefore reads zero whether or not the roll fires — but that is instrument *choice*,
|
||||
not inlining, and the distinction matters because an inlined draw would be invisible to
|
||||
`draw_sites` too, and this one is not. Lane AG read this independently and reached the same
|
||||
conclusion; the correction is recorded in both places.
|
||||
|
||||
### 1.2 The gate is an asteroid belt, and it is sixteen bytes
|
||||
|
||||
```
|
||||
00743f80 cmp dword ptr [ecx + 0x6c], 0
|
||||
00743f84 mov eax, dword ptr [ecx + 0x19c]
|
||||
00743f8a jg 0x743f8f
|
||||
00743f8c and eax, 0xfffffffe ; clear bit 0 when [ecx+0x6c] <= 0
|
||||
00743f8f ret
|
||||
```
|
||||
|
||||
On `Game::ServerSystem` (`objects/layouts.md`): **`+0x6c` = `ARes2`**, **`+0x19c` = `TerrFl`**,
|
||||
`+0x100` = `PID` (which `ServerSystem_GetOwner 0x007437e0`, used three times above, returns). So
|
||||
|
||||
> **bit 0 of the return = `(TerrFl & 1) AND (ARes2 > 0)` — "this system still has a usable
|
||||
> asteroid belt".**
|
||||
|
||||
`ARes2` is already the field lane AC identified as the asteroid-belt resource (values 1500–3000,
|
||||
matching `SYSTEM_MIN/MAX_ASTEROID_RESOURCES`). `TerrFl` bit 0 is its flag; §2 checks that reading
|
||||
against the corpus, which is the only place a static reading of an unnamed bit can be checked
|
||||
without a debugger.
|
||||
|
||||
**The polarity, stated because everyone's intuition gets it backwards.** The branch is a `je` that
|
||||
jumps **past** the roll. Belt usable ⇒ roll. Belt gone ⇒ **no draw at all, and `sdet := turn`
|
||||
unconditionally.** The spy hides in the belt; if the belt is mined out it has nowhere to hide and is
|
||||
spotted for free. So at this site, a `draw_sites` row with `calls = 0` and a `sdet` that has moved
|
||||
is a *positive* result about a different arm, and must never be reported as "the roll did not fire".
|
||||
|
||||
### 1.3 `sdo` — where the probability comes from, and why the roll should cost a word
|
||||
|
||||
`0x0081f570` (`SpyCraft_AccumulateDetectionOdds(spy, sys, spyOwner)`, `__stdcall`, `ret 0xc`, 512
|
||||
bytes) is called **unconditionally** on branch D, immediately before the gate, and writes
|
||||
`spy.sdo (+0x3c)`:
|
||||
|
||||
* if `sys.PID == 0` (unowned): `fldz; fstp [esi+0x3c]` — **`sdo := 0.0` and return**. A spy at an
|
||||
unowned system rolls at p = 0, which costs no word.
|
||||
* otherwise it walks the system's fleet vector accumulating three counters (`[ebp-4]`, `[ebp-8]`,
|
||||
`[ebp-0xc]`) and computes, with every literal read as the **four bytes in the image** (rule 23 —
|
||||
these are widened floats, not decimals):
|
||||
|
||||
```
|
||||
p = 0.01f (0x009e31c0)
|
||||
p += (countA > 0) ? 0.01f : 0 (0x009e3e14)
|
||||
p += (countB > 0) ? 0.02f : 0 (0x00a1d048)
|
||||
p += 0.001 * countC (0x009e23c0)
|
||||
p *= (spyOwner->[0x5c] == 5) ? 0.75f (0x009e5ac4)
|
||||
: (spyOwner->[0x5c] == 6) ? 0.5f (0x00a2c788)
|
||||
: 1.0f
|
||||
p *= 0.7 (0x009e5df8)
|
||||
sdo = min(sdo + p, 1.0) (0x009e1ef0)
|
||||
```
|
||||
|
||||
Two properties matter more than the arithmetic. It **accumulates** onto the previous `sdo`, and it
|
||||
is **clamped at 1.0**. `Mars::RNG::Chance` costs **0 words at `p <= 0` and at `p >= 1`** (board row
|
||||
367). So:
|
||||
|
||||
* a fresh deploy at a quiet foreign system gives `sdo = 0.7 × 0.01 = 0.007` — strictly inside
|
||||
(0, 1), so the roll costs **exactly one word**;
|
||||
* `sdo` climbs by ~0.007/turn, so a spy left in place for ~143 turns reaches `p >= 1`, at which
|
||||
point the site **costs nothing and always succeeds**. That is a second way a zero at this site is
|
||||
not a negative, and it is the same shape as lane AG's `cmo` early-out.
|
||||
|
||||
*(Measured afterwards: **0.0084**, i.e. `0.7 × (0.01 + 0.001 × 2)` — `countC = 2` rather than the
|
||||
0 assumed here. The one-word conclusion is unaffected and the form of the expression reproduced
|
||||
exactly. §4.2a.)*
|
||||
|
||||
### 1.4 What a deploy actually writes — and one draw site nobody had
|
||||
|
||||
`ServerSpyManager::DeploySpy` = **`0x00887410`**, vftable `0x00a3073c` slot 7, zero direct call
|
||||
sites, reached only from the `SHIPACTION_DEPLOYSPY` handler `0x0078c930` through
|
||||
`[[dispatcher+0x15c]+0x1c]` — the stack-built ship-action function-pointer table that lane B6
|
||||
identified as a distinct indirection class. On the success path it writes:
|
||||
|
||||
```
|
||||
spy->Reset() (0x00838070)
|
||||
spy->sdo (+0x3c) := 0.0f
|
||||
spy->sdet (+0x40) := -1
|
||||
spy->cbh (+0x18) := <the float argument>
|
||||
spy->tdep (+0x14) := server->[+0xc] ; the current turn
|
||||
spy->atto (+0xc) := 0 ; carrierShip->[+0xa8] := 0 ; detach from the tender
|
||||
ServerSystem_AddSpy(system, spy) at 0x008874d4
|
||||
```
|
||||
|
||||
and `AddSpy` is where `deat` is born (§1.5). Because the deploy leaves `sdet == -1` and `sdo == 0`,
|
||||
**the detection branch is entered on the very next `OnAllCombatDone_Tail`** — which is the same End
|
||||
Turn that applied the order, since `ApplyAllTurnCommands` runs before the tail.
|
||||
|
||||
The float argument is computed in the handler, and it is a draw:
|
||||
|
||||
```
|
||||
0078c975 mov ecx,[esi+0x16c] ; esi = StrategyServer -- THE STRATEGIC GENERATOR
|
||||
0078c97c add ecx,4 ; the +4 sub-object, same one the trade-raid roll uses
|
||||
0078c97f call 0x47d830 ; Mars::RNG::NextFloat <-- 1 word, ret 0x0078c984
|
||||
0078c984 fld qword [0x009e21b0] ; 6.283185482025146 == 2*pi
|
||||
cbh = NextFloat() * 2*pi
|
||||
```
|
||||
|
||||
> **`0x0078c97f` is a strategic-generator draw site in no existing inventory.** It is not one of
|
||||
> lane V2's eight, and it is not in lane I's 22, because it hangs off the ship-action table and no
|
||||
> call-graph sweep reaches it. It fires **once per Deploy Spy order applied**, inside
|
||||
> `ApplyAllTurnCommands` — so it lands in the `ProcessTurn` bracket, not the tail's. Any gate-indexed
|
||||
> audit (lane AG) should carry it, and its predicate is "a `SHIPACTION_DEPLOYSPY` command in the
|
||||
> turn's command stream", which is a *stream* predicate rather than a save predicate — a category
|
||||
> the audit does not yet have a column for.
|
||||
|
||||
### 1.5 `deat` and `spies2` are written by the same two instructions — `spies2` is closed
|
||||
|
||||
`ServerSystem::AddSpy` = **`0x007514c0`**, 48 bytes, complete:
|
||||
|
||||
```
|
||||
007514c3 mov edx,[ebp+8] ; the spy
|
||||
007514c6 test edx,edx / je ret
|
||||
007514ca test ecx,ecx / jne
|
||||
007514ce xor eax,eax ; this == NULL -> store 0
|
||||
007514d2 mov eax,[ecx+4] ; the system's own registry handle
|
||||
007514d5 mov [edx+0x10],eax ; *** spy->deat = system handle ***
|
||||
007514d8 add edx,4 ; &spy->sid
|
||||
007514db push edx
|
||||
007514dc add ecx,0x1cc ; &system->spies2
|
||||
007514e2 call 0x59f1a0 ; push_back (lane AI3's wrapper)
|
||||
```
|
||||
|
||||
and its exact mirror `ServerSystem::RemoveSpy` **`0x0074f550`** sets `spy->deat = 0` and erases
|
||||
`spy->sid` from the same `+0x1cc` vector. `ServerSystem +0x1cc` is `spies2` in `objects/layouts.md`.
|
||||
`AddSpy` has exactly one caller (`0x008874d4`, inside `DeploySpy`) and no vtable slot, so **this is
|
||||
the only way `deat` ever becomes non-zero at runtime.**
|
||||
|
||||
> **`spies2` is the per-system vector of DEPLOYED spy ids**, and `deat` is its reciprocal
|
||||
> back-pointer. They are written by the same function, two instructions apart.
|
||||
>
|
||||
> - Lane L3 §6: *"`spies2` is therefore not the spy list"* — **half right and worth correcting in
|
||||
> place.** It is not the spy *manager's craft* list; it is the *system's deployed-spy* list. L3's
|
||||
> inference from "this save has a spy and `spies2` is still empty" was sound about what `spies2`
|
||||
> is not, and the alternative it offered second ("or it only fills for a deployed spy") is the
|
||||
> right one.
|
||||
> - Lane AC's P4 (*"fills only for a deployed spy"*) is **confirmed, and now from the writer rather
|
||||
> than by elimination.**
|
||||
>
|
||||
> This retires one third of lane W's rule-6 flag on `spies2` / `SysMem` / `mts` **statically**, and
|
||||
> §5 exercises it live.
|
||||
|
||||
### 1.6 `vslot14`'s roll is a different gate — and it excludes independent colonies
|
||||
|
||||
For the record, because target choice depends on it. `ServerSpyManager::vslot14` `0x0088db80`
|
||||
(phase 33) loops the same vector and requires `spy.deat != 0`, `sys.PID != 0`, and
|
||||
**`sys.indi (+0x1c8) == 0`** (`ServerSystem_IsIndependent 0x00743fa0`) — *not* an independent
|
||||
colony — plus `spy.cm (+0x24)` in 1..4, before its `Chance` at `0x0088dc43`. Lane AG has decoded
|
||||
that site fully; the consequence here is only that **an AI-empire target exercises strictly more
|
||||
than an Independent Colony**, which is why this lane went looking for an AI-owned belt rather than
|
||||
taking the easy NPC one.
|
||||
|
||||
---
|
||||
|
||||
## 2. The corpus, counted before measuring (rule 28b)
|
||||
|
||||
`verify/save-reader/save_reader.py --json` over all **22** saves in `verify/results/saves/`.
|
||||
|
||||
| | count |
|
||||
|---|---|
|
||||
| saves with any `spymgr.spy` record | **3** (L3's turn-15, AC's turn-20 and turn-22) |
|
||||
| of those, `deat != 0` | **0** |
|
||||
| saves satisfying the full detection-roll predicate | **0 of 22** |
|
||||
| **the failed conjunct** | **`spy.deat != 0`** |
|
||||
|
||||
The integrator's value-domain census (`value-domain-census.md`) reaches the same 0 independently and
|
||||
more strongly: `deat`, `sdet`, `sdo`, `cbh`, `cm`, `cmo` are all in the 234 fields that have only
|
||||
ever held one value, so the count is 0 **by construction**.
|
||||
|
||||
**Two things fell out of the same pass, and the first is the load-bearing one.**
|
||||
|
||||
* **`ARes2 > 0` and `TerrFl & 1` agree on every one of 616 system records** (22 saves × 28 systems),
|
||||
with no exception in either direction. That is a save-side confirmation of §1.2's reading of an
|
||||
unnamed flag bit, obtained without a debugger, over a sample large enough to be worth something.
|
||||
It got stronger on the state this lane built (§4): there, `TerrFl` also takes the values **2** and
|
||||
**3**, and `ARes2 > 0` still coincides exactly with `TerrFl & 1` — so bit 1 is a second,
|
||||
independent terrain feature and bit 0 is not merely "TerrFl is non-zero".
|
||||
* **The belt was never the missing conjunct.** **All 22** corpus saves have at least one system
|
||||
with a usable belt (5 to 15 of 28), and **two of them — `human-turn2-orders.sav` and
|
||||
`human-turn3-noderoute.sav` — already have four AI-owned belt systems.** Lane AC's map had none,
|
||||
and the campaign generalised from it. Those two saves are `PTech 0 / ResM 1.0`, so the spy tech
|
||||
chain is tens of turns away and they were not a usable shortcut; but "no AI colony can have a
|
||||
belt" was a fact about lane L3's one map, exactly the rule-28 practice-3 trap.
|
||||
|
||||
---
|
||||
|
||||
## 3. Predictions
|
||||
|
||||
Committed to `sots-engine` `docs/AS-predictions.md` at `c172c99` before the build, with the
|
||||
addendum at `61875a4` before the measurement. Summarised here; the falsification table is in that
|
||||
file.
|
||||
|
||||
| | prediction |
|
||||
|---|---|
|
||||
| **P1** | an AI-owned belt system exists within ≤ 5 map regenerations at lane L3's custom settings |
|
||||
| **P2** | the predicate of §2; corpus count 0 of 22; failed conjunct `deat != 0` |
|
||||
| **P3** | the roll fires: one `draw_sites` row at `ret_rva 0x00887c8f`, `Chance`, `calls = 1`, `strategic = true` |
|
||||
| **P4** | it costs **1** word; `no_draw_calls = 0` |
|
||||
| **P5** | the tail bracket goes 0 → **1**; the trade half contributes 0 (`tscr` 252, no fleet at a sector node) |
|
||||
| **P6** | `sdet` most likely stays −1 (p ≈ 0.993); **`sdo` moves off its corpus-constant 0 to ≈ 0.007**, and that is the save-side confirmation |
|
||||
| **P7** | `spies2` non-empty in the target system only |
|
||||
| **P8** | the `hooks=off` pair reproduces byte-identically in two fresh processes |
|
||||
| **P9** | `probes=8` is byte-neutral on this workload too |
|
||||
|
||||
---
|
||||
|
||||
## 4. The workload — built from a cold main menu in one session
|
||||
|
||||
Shim build **`as-c172c99-20260909T0205Z`**, cross-built on CT111 in this lane's own directory
|
||||
`/srv/re-lab/build/sots-engine-as` (`rm -rf build-host build-shim` before configure — rule 24),
|
||||
staged to `/srv/re-lab/shim/dist-as`, deployed to `C:\SOTS\shimdist-as` on VM144. Exports checked
|
||||
identical to the real `binkw32.dll` (66 names).
|
||||
|
||||
**Guest adoption check, first, before anything else.** `ref-turn2.sav`, one End Turn, `hooks=off`,
|
||||
this lane's own build:
|
||||
|
||||
```
|
||||
(Autosave EndTurn).sav bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 66,732
|
||||
(Autosave).sav 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 67,219
|
||||
```
|
||||
|
||||
Both **identical to the published oracle** in `verify/results/saves/determinism-hashes.txt`. The
|
||||
binary and the guest are sane.
|
||||
|
||||
### 4.1 The map — zero regenerations were needed
|
||||
|
||||
New custom game at lane L3's settings (28 stars, 5 ly, 2 players Human vs Tarkas, treasury
|
||||
9,850,000, **10 initial colonies, 15 initial technologies**, econ 148 %, research 146 %), species
|
||||
set explicitly to Human on the launch card (it defaults to Random — L3's note holds).
|
||||
|
||||
**The first map generated had two AI-owned belt systems.** The brief expected several
|
||||
regenerations; P1's estimate of ≤ 5 was never tested because none were needed.
|
||||
|
||||
| SysID | name | PID | `ARes2` | `TerrFl` | note |
|
||||
|---|---|---|---|---|---|
|
||||
| **400** | **Kepler** | **32 (AI)** | **2435** | **1** | the target — 3.5 ly from my colony Midway |
|
||||
| 224 | Delta Pavonis | 32 (AI) | 1537 | 1 | the spare |
|
||||
| 272 | Solaris | 0 | 2773 | 1 | unowned — `sdo` would be forced to 0 here (§1.3) |
|
||||
| 160 / 480 | Piscium / Pascal | 16 (me) | 2448 / 2522 | 1 | mine |
|
||||
| 336 | Copernicus | 0 | 2359 | **3** | bit 1 also set |
|
||||
| 128 | Downbelow | 0 | 1965 | 1 | |
|
||||
| 384 / 448 / 464 | Nunki / Mu Bootis / Jinx | — | **0** | **2** | **bit 1 set, bit 0 clear, no belt** |
|
||||
|
||||
That last row is the one that matters for §1.2. In the whole 22-save corpus `TerrFl` had only ever
|
||||
been 0 or 1, so "bit 0" and "non-zero" were indistinguishable. This map has three systems at
|
||||
`TerrFl = 2` with `ARes2 = 0` and one at `TerrFl = 3` with `ARes2 = 2359`. **`ARes2 > 0` still
|
||||
coincides exactly with `TerrFl & 1`, and now it is bit 0 specifically, not "TerrFl is set".**
|
||||
|
||||
### 4.2 The path to a deployed spy, with what each turn cost
|
||||
|
||||
The four techs came faster than lane L3's recipe suggested because `CCC_FTLBrdB` was in the
|
||||
15-technology roll again (it appears to be common) and because the top-of-screen
|
||||
**Savings ↔ Research slider at its right end turns a 2-turn tech into a 1-turn tech.**
|
||||
|
||||
| turn | what |
|
||||
|---|---|
|
||||
| 2 | new game; research `CCC_SpyBm` (`CCC_FTLBrdB` already researched from the initial roll) |
|
||||
| 4 | `CCC_SpyBm` done; research `IND_OrbFound` |
|
||||
| 6 | done; research `IND_CruisCon` (2 turns → 1 with the research slider at max) |
|
||||
| 7 | done; research `IND_SlvgTech` |
|
||||
| 8 | **all four techs**; design a Cruiser with the `Repair and Salvage` mission section; queue it |
|
||||
| 9 | the tender exists; fleet panel's `Special` → `Build Spy` → `Select All` → `OK` |
|
||||
| 10 | **the spy craft exists**: `nspy 1`, `deat 0`, `sdet -1`, `sdo 0`, `atto` = the tender — the corpus state |
|
||||
| 10 | `Move` the fleet to Kepler (`Dest: Kepler, 4 Turns`) |
|
||||
| 14 | **fleet arrived**: `Flt.LocID = 400`, read from the autosave, not the panel |
|
||||
| 14 | `Special` → **`Deploy Spy`** — the menu entry is present, which is itself the belt gate answering |
|
||||
| 15 | order applied. **`deat = 400`, `sdet = -1`, `sdo = 0.0084`, `spies2 = [1]` at Kepler** |
|
||||
|
||||
Two things about turn 15 are worth stating separately, because they are results rather than steps.
|
||||
|
||||
**(a) `sdo = 0.0084`, and that is §1.3's formula evaluated, not fitted.** The prediction was 0.007
|
||||
for a quiet system (`0.7 × 0.01`). The measured value is `0.7 × 0.012 = 0.0084`, i.e. `countC = 2`
|
||||
in the third term. So the *form* — a 0.7 multiplier over a 0.01 base plus 0.001 per unit of a
|
||||
counter — reproduces exactly, at a value no run of this campaign has ever produced, on a state
|
||||
nobody had built. The identity of `countC` is not established (it is 2 at a system holding one
|
||||
foreign colony and my one-ship fleet); the constant and the shape are.
|
||||
|
||||
**(b) `spies2` filled, in exactly the system the writer says.** Reading all 28 systems of the
|
||||
turn-15 autosave: `spies2` is `count = 0` in 27 of them and `count = 1, element = 1` at **Kepler**,
|
||||
where `1` is the spy's `sid`. This is the **first non-empty `spies2` in the corpus** and it matches
|
||||
`ServerSystem::AddSpy` instruction for instruction (§1.5).
|
||||
|
||||
---
|
||||
|
||||
## 5. The measurement
|
||||
|
||||
All three runs load the **same file**, `MyGameas1spydep.sav`
|
||||
(sha256 `854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08`, 76,705 B, turn 15,
|
||||
`--strict` clean, 0 errors / 0 warnings), in a **freshly launched process**, and press End Turn
|
||||
**once**. Repo copy: `verify/results/saves/as-turn15-spydeployed.sav`.
|
||||
|
||||
### 5.1 The oracle pair (rule 26) — and what it says honestly
|
||||
|
||||
| run | config | `(Autosave EndTurn).sav` (the input, re-saved) | `(Autosave).sav` (post-turn) |
|
||||
|---|---|---|---|
|
||||
| **A** | `hooks=off` | `98e45d3745b91450…` 76,706 | `262f8bda97c9511d…` 77,685 |
|
||||
| **B** | `hooks=off`, fresh process | `98e45d3745b91450…` **identical** | `e34775a757e6fb3d…` 77,649 **differs** |
|
||||
| **M** | `probes=8` | `98e45d3745b91450…` **identical** | `262f8bda97c9511d…` **identical to A** |
|
||||
|
||||
**The pre-turn save is byte-identical in all three**, which proves the input state — including any
|
||||
pending order — was the same in every run. The post-turn saves are not.
|
||||
|
||||
**A vs B differs in exactly 22 leaves, and every one of them is inside one AI player.**
|
||||
`state_checksum --no-audit` localises them:
|
||||
|
||||
```
|
||||
/Summary/Checksum 985948837 -> 985948925
|
||||
/Sim/turnstats/history/hist[1]/stats[15]/tch 46 -> 45
|
||||
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[62] 2 -> 3
|
||||
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[62] 0 -> 22469
|
||||
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[64] 4 -> 2
|
||||
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[64] 22469 -> 0
|
||||
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TAcq[64] 16 -> -1
|
||||
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TiAcq[64] 45 -> -1
|
||||
/Sim/players/Player[32 "Revenge Fleet"]/ResTNm '' -> 'DRV_NodFoc'
|
||||
… plus that player's Events/EvNxID/otch bookkeeping for the same pick
|
||||
```
|
||||
|
||||
In A the AI completed `Overthrusting`; in B it is part-way through `DRV_NodFoc`. **Nothing else
|
||||
moved** — not one leaf of my empire, not `deat`, `sdet`, `sdo`, `spies2`, `trdmgr` or any fleet.
|
||||
|
||||
That is precisely the mechanism `2026-09-08-ai-seed-per-process.md` established: each AI
|
||||
`StrategyClient`'s generator is seeded with a word that differs in every process, so its research
|
||||
pick is a per-process outcome. Board row 326 recorded the same signature on
|
||||
`turn1-state → turn2` — *"one shadow empire's research pick plus the derived checksum"*.
|
||||
|
||||
**So the control agrees with itself modulo the known non-deterministic leaves, which is the bar the
|
||||
brief set — and it is a weaker bar than byte-identity, which must be said plainly.** What is
|
||||
established is: on this input, in two fresh un-instrumented processes, every leaf outside one AI
|
||||
player's research bookkeeping reproduced exactly. What is **not** established is byte-identity, and
|
||||
this workload cannot establish it until the client seeds are pinned (the resolution's Rung C).
|
||||
|
||||
**And the instrumented run M came out byte-identical to A.** Read carefully, that is:
|
||||
|
||||
* strong for the thing that matters — `probes=8` changed **no leaf** of the sim, the spy, or my
|
||||
empire, on the same input, which is the byte-neutrality claim P9 makes;
|
||||
* and, for the AI-pick leaf specifically, a **coincidence of size 1/k** (rule 26's corollary): M
|
||||
happened to draw the same AI seed outcome as A. It is not evidence about the instrument in that
|
||||
one leaf class, and I am not treating it as such.
|
||||
|
||||
### 5.2 The instrument was armed, and one probe reading zero is the point
|
||||
|
||||
`shim.log`: seven draw-site detours `create=MH_OK enable=MH_OK`; `probe: installing 8 of 12`; all
|
||||
eight `MH_OK`; **no `COVERAGE:` line**. `draw_site_overflow = 0`.
|
||||
|
||||
| probe | entries this turn |
|
||||
|---|---|
|
||||
| `Game::ServerSpyManager::vslot13` `0x008877b0` | **1** |
|
||||
| `Game::ServerSpyManager::vslot14` `0x0088db80` | 1 |
|
||||
| `Game::ServerTradeManagerImpl::vslot13` `0x0088ef80` | 1 |
|
||||
| `Game::ServerTradeManagerImpl::vslot15` `0x0082cca0` | 1 |
|
||||
| **`Game::SpyManager::Slot13RngCallee` `0x008408e0`** | **0** |
|
||||
| `Game::TradeManager::Slot13RngCalleeA` `0x00820ca0` | 0 |
|
||||
| `Game::TradeManager::Slot13RngCalleeB` `0x0088b440` | 0 |
|
||||
| `Game::ServerTradeManager::CreateRaidEncounter` `0x008938a0` | 0 |
|
||||
| indices 8–11 (`GenerateTradeRaidEncounters`, three controls) | **NOT INSTALLED** — `probes=8` installs 0–7 |
|
||||
|
||||
> **That table is the lane's methodological point in one line.** The detection roll fired, and the
|
||||
> entry probe on `SpyManager::Slot13RngCallee` read **0** on the same turn. Four lanes read that
|
||||
> zero and concluded the spy subtree was draw-free. It was never a statement about the subtree; it
|
||||
> was a statement about which function the call sits in.
|
||||
|
||||
### 5.3 The detection roll — it fires, and it costs one word
|
||||
|
||||
`draw_sites`, the post-turn autosave marker, the row verbatim:
|
||||
|
||||
```
|
||||
entry = Chance
|
||||
ret_rva = 0x0048_7c8f (VA 0x00887c8f — the instruction after `call 0x8e6dd0` at 0x00887c8a)
|
||||
calls = 1
|
||||
words = 1
|
||||
no_draw_calls = 0
|
||||
strategic = true
|
||||
```
|
||||
|
||||
and the same site in the per-call-site report, with the owner resolved:
|
||||
|
||||
```
|
||||
* STRAT Chance call 0x00887c8a <ServerSpyManager::vslot13>+0x4da calls=1 words=1
|
||||
```
|
||||
|
||||
The boundary ledger for the same turn:
|
||||
|
||||
```
|
||||
BeginProcessTurn 345 -> 345 0
|
||||
ProcessTurn 345 -> 363 18
|
||||
OnAllCombatDone_Tail 363 -> 364 1
|
||||
Autosave 364 -> 364 0
|
||||
```
|
||||
|
||||
> **`OnAllCombatDone_Tail` costs 1 word, against 0 on every turn any lane has ever measured, and the
|
||||
> whole of it is the detection roll.**
|
||||
|
||||
Turn total 19 words; the per-site sum after removing helper-internal rows is **19**; **residual 0**.
|
||||
The tail's one word is the only site in the tail that fired: no row at `0x00820e18` or `0x0088b613`
|
||||
(trade 13 — `tscr` is 252 in this game and no fleet stands on a sector node, so lane AC's predicate
|
||||
says 0 and it is 0), none at `0x0082cdb8` (trade 15), none at `0x0088dc43` (spy 14 — `cm = 0`, lane
|
||||
AG's failed conjunct), and none of `P`'s three.
|
||||
|
||||
**Predictions, scored.** P1 held trivially (0 regenerations, not ≤ 5). **P2 held.** **P3 held
|
||||
exactly.** **P4 held exactly** — one word, `no_draw_calls = 0`, and the reasoning behind it (four
|
||||
widened-float literals plus `Chance`'s cost at interior *p*) was written down before the run.
|
||||
**P5 held** — 0 → 1. **P6 held in both halves**: `sdet` stayed −1 (the roll failed, as a 1.68 %
|
||||
roll should), and `sdo` moved from its corpus-constant 0 to **0.0084** on the deploy turn and to
|
||||
**0.0168** after the measured turn — an increment of exactly 0.0084 both times, which is the
|
||||
accumulator running. **P7 held.** **P8 held only modulo the AI-seed leaves** (§5.1). **P9 held.**
|
||||
|
||||
### 5.4 The second instrument agreed — which is what makes this more than one number
|
||||
|
||||
The coordinator asked for a save-visible confirmation independent of the bracket. There are two:
|
||||
|
||||
* **`sdo` moved and by the right amount.** 0 → 0.0084 → 0.0168. Only branch D writes `sdo`, and it
|
||||
writes it immediately before the gate, so a moving `sdo` proves the branch ran; the *equal*
|
||||
increments prove the accumulator, not a one-off.
|
||||
* **`spies2` filled in exactly one system.** That is the deploy half rather than the roll, but it
|
||||
confirms the `deat` reading the whole predicate rests on.
|
||||
|
||||
`sdet` did **not** move, and per P6 that was the predicted outcome, not a failure.
|
||||
|
||||
---
|
||||
|
||||
## 6. Coverage — what this did not touch, said as loudly as what it did
|
||||
|
||||
* **One measured turn, one spy, one target system, one fleet of one ship.** The cost is 1 word on
|
||||
that turn. The loop is over the spy vector, so the cost should be one word per deployed,
|
||||
undetected spy at a system with a usable belt — **that is a reading, not a measurement.** Rule 20:
|
||||
do not fit a constant to one observation. A second spy at a second belt system separates
|
||||
per-spy from per-turn and costs one more deploy.
|
||||
* **The `sdo` arithmetic is validated only in its first two terms.** `countA`, `countB` and the
|
||||
species multiplier were 0/0/1.0 here, and `countC = 2` is inferred from the value, not observed.
|
||||
A Zuul or Liir owner (`+0x5c` = 5 or 6) would exercise the 0.75/0.5 branches; a system with enemy
|
||||
ships present would exercise `countA`/`countB`.
|
||||
* **The auto-detect arm was never taken.** `(TerrFl & 1) == 0` at the target has not been reached —
|
||||
it would need a belt to be mined out from under a deployed spy. So §1.2's polarity claim rests on
|
||||
the instruction stream plus the deploy-time refusal, not on a measured firing. It is the single
|
||||
most load-bearing unmeasured branch in this document.
|
||||
* **`P` (`0x008408e0`) and its three draws stay unfired**, and the failed conjunct is now `sdet`,
|
||||
not `deat`. From this save the recipe is arithmetic rather than a guess: `sdo` grows 0.0084/turn,
|
||||
so `P(detected)` is 12 % after 5 more End Turns, **38 % after 10, 65 % after 15, 85 % after 20** —
|
||||
then `P` runs 3 turns after `sdet` is stamped, provided the system is still foreign-owned.
|
||||
About 20 End Turns from `as-turn15-spydeployed.sav`, no clicking.
|
||||
* **`0x0078c97f` was decoded and not measured** (§6.1).
|
||||
* **`probes=11` was not used anywhere in this lane**, and no number here is comparable to lane AC's
|
||||
turn totals, which were taken at that configuration.
|
||||
* **No `Guard` region is declared by any hook in this family**, the same gap lanes Z and H reported:
|
||||
`undeclared = 0` in this trace is vacuous.
|
||||
* **One control run was lost to the harness and is reported rather than hidden.** A third
|
||||
`hooks=off` run mis-detected the post-turn End Turn button as "turn still running" and clicked
|
||||
End Turn three times, advancing the line to turn 18. It was discarded; nothing in §5 uses it. The
|
||||
defect was in this lane's own click automation (the brightness test had the wrong polarity: after
|
||||
a completed turn the button is *brighter*, not dimmer), which is rule 19 pointing at the operator
|
||||
rather than at the detour. Its lesson is worth carrying: **verify the turn number changed, not a
|
||||
button's colour.**
|
||||
|
||||
### 6.1 A draw site nobody has — the deploy's own `NextFloat`
|
||||
|
||||
Decoded here and **not measured**, so it is a rule-6 hypothesis with a hook site attached:
|
||||
|
||||
```
|
||||
0078c975 mov ecx,[esi+0x16c] ; esi = StrategyServer -- the STRATEGIC generator
|
||||
0078c97c add ecx,4
|
||||
0078c97f call 0x0047d830 ; Mars::RNG::NextFloat -- 1 word, return address 0x0078c984
|
||||
0078c984 fld qword [0x009e21b0] ; 6.283185482025146 == 2*pi
|
||||
spy.cbh = NextFloat() * 2*pi
|
||||
0078c9bb call edx ; -> ServerSpyManager::DeploySpy, vtable slot 7
|
||||
```
|
||||
|
||||
inside the `SHIPACTION_DEPLOYSPY` handler `0x0078c930`, which is entry 9 of the stack-built
|
||||
ship-action function-pointer table (lane B6's indirection class — no direct callers, no vtable
|
||||
slot, invisible to every call-graph sweep this campaign has run).
|
||||
|
||||
It fires **once per Deploy Spy order applied**, inside `ApplyAllTurnCommands`, so it lands in the
|
||||
**`ProcessTurn`** bracket, not the tail's. Its predicate is not a predicate on save fields at all —
|
||||
it is *"a `SHIPACTION_DEPLOYSPY` command in this turn's command stream"*, a **stream** predicate,
|
||||
which lane AG's gate-indexed audit has no column for and should grow one.
|
||||
|
||||
To measure it costs one run: load `verify/results/saves/as-turn14-predeploy.sav` under `probes=8`,
|
||||
select Kepler, `Special → Deploy Spy → <ship> → OK`, End Turn, and read `draw_sites` at
|
||||
`ret_rva 0x0078c984`. This lane attempted it and lost the run to the map UI (Kepler and Midway sit
|
||||
within four pixels of each other at the zoom the map opens at, and Midway wins the hit test), then
|
||||
chose to report the site honestly rather than rush a fourth process. Prediction, committed here for
|
||||
whoever takes it: **one call, one word, `strategic = true`, in the `ProcessTurn` bracket**, and the
|
||||
same turn's tail still costs exactly 1 — the deploy and the first detection roll happen on the same
|
||||
End Turn, because `ApplyAllTurnCommands` runs before `OnAllCombatDone_Tail`.
|
||||
|
||||
---
|
||||
|
||||
## 7. VM144 as left
|
||||
|
||||
**Restored and verified by screenshot at the main menu**, profile `re`, 2026-09-09 00:03 local.
|
||||
|
||||
* `C:\SOTS\binkw32.dll` restored to lane L3's build from `C:\SOTS\shimdist-l3\binkw32.dll`
|
||||
(sha256 `479B8614D2417603…`, byte-identical to what this lane found in place).
|
||||
* `C:\SOTS\shim.cfg` restored from `C:\SOTS\ui\preAS-shim.cfg` (L3's `shim.cfg.l3probe`).
|
||||
* The three autosaves restored **byte-identical** from `C:\SOTS\ui\preAS-SavedGames`
|
||||
(`1985E6F4…` / `24B2E072…` / `5EC80C1E…`).
|
||||
* `C:\SOTS\shim.trace.jsonl` removed.
|
||||
|
||||
**`SavedGames` is now 15 files, not 13**, so **every Load-dialog row position has moved again** —
|
||||
screenshot the dialog, do not reuse a remembered row. The two additions are this lane's, and they
|
||||
are worth leaving because they are the only deployed-spy states in existence:
|
||||
|
||||
| file | turn | why it is worth keeping |
|
||||
|---|---|---|
|
||||
| `MyGameas1predeploy.sav` | 14 | fleet + tender + docked spy standing **at** an AI belt colony, one click from a deploy — the input for §6.1 |
|
||||
| `MyGameas1spydep.sav` | 15 | **the deployed spy**: `deat 400`, `sdet -1`, `sdo 0.0084`, `spies2 = [1]` at Kepler |
|
||||
|
||||
At the 15-file set, verified by screenshot this session: `MyGameas1predeploy` is at **(400, 347)**
|
||||
and `MyGameas1spydep` at **(400, 376)**; `OK` is unmoved at (682, 624).
|
||||
|
||||
Also left in place, all harmless: `C:\SOTS\shimdist-as\` (build
|
||||
`as-c172c99-20260909T0205Z`, 64 files), `C:\SOTS\ui\asgo.ps1`, `C:\SOTS\ui\ashash.ps1`,
|
||||
`C:\SOTS\ui\preAS-shim.cfg`, `C:\SOTS\ui\preAS-SavedGames\` (13 files),
|
||||
`C:\SOTS\ui\asfetch.sav` / `asgrab.jsonl` / `asgrab.log` (scratch). **`click_helper.ps1` was not
|
||||
touched** — this lane needed no new verbs, which is the first VM lane in a while that did not.
|
||||
|
||||
### 7.1 Guest notes worth carrying forward
|
||||
|
||||
* **The map's hover readout works with a plain `move`** on VM144 — lane AC's `jmove` jiggle was not
|
||||
needed here. Enter Move mode first; the readout renders at the hovered star, not at the cursor.
|
||||
* **Finding a named system on the star map is solvable arithmetically instead of by hunting.**
|
||||
Hover four systems, read their names, and least-squares-fit an affine map from the save's `Pos`
|
||||
vectors to screen coordinates; the residuals came out ≤ 8 px on 5 points, which is enough to click
|
||||
a specific star first time. The script is in this lane's scratch notes and the method is the
|
||||
reusable part. Its failure mode is systems that project within a few pixels of each other
|
||||
(Kepler and Midway), where the nearer one wins the hit test.
|
||||
* **The End Turn button is BRIGHTER after a turn completes, not dimmer.** Any "is the turn still
|
||||
running?" test built on that button's colour has the polarity backwards. Poll the **autosave
|
||||
mtime** and the **turn number in the ticker**, never the button.
|
||||
* Accepting the AI's non-aggression offer (turn 6 here) stopped the combat encounters that were
|
||||
interrupting every third End Turn, and cost nothing this lane needed.
|
||||
|
||||
---
|
||||
|
||||
## 8. Corrections to earlier findings (rule 11)
|
||||
|
||||
* **`2026-09-09-tail-draws.md` §0.7 and §7, and lane AC §2.2: "the detection roll is inline in
|
||||
`vslot13`".** It is a direct `E8` call to the `Chance` entry point sitting in `vslot13`'s body.
|
||||
The instrument advice that followed from it is right; the stated reason is not. §1.1.
|
||||
* **Lane L3 §6: "`spies2` is therefore not the spy list".** Half right. It is not the spy manager's
|
||||
craft list; it is the **system's deployed-spy id vector**, written by `ServerSystem::AddSpy` in
|
||||
the same two instructions that set `deat`. L3's own second alternative was the correct one. §1.5.
|
||||
* **Lane AC §5: "the textbook target — an enemy colony with a belt — does not exist in this game."**
|
||||
True of that map and read across the campaign as if it were true of the game. Two corpus saves
|
||||
already had four AI-owned belt systems, and the first map this lane generated had two. §2.
|
||||
* **Lane V2 §3.1's spy-13 row** listed `0x00887c8a` correctly and is **vindicated**, like its
|
||||
trade-13 row before it. Its "never observed firing" note is now discharged for this one site.
|
||||
* **Ghidra's size for `0x008877b0` is 1,869 bytes; the next function starts at `0x00887f30`, i.e.
|
||||
1,920.** The detection roll at `0x00887c8a` is inside both, but the 51-byte shortfall is rule 17
|
||||
again, on the very function this lane exists to read.
|
||||
* **This lane's own pre-registered §1.1, corrected here rather than edited there (rule 12).**
|
||||
`docs/AS-predictions.md` calls `0x0083ce50` "the deploy validator" and says its `0x1000000`
|
||||
refusal bit *is* the `Can't deploy spy: Spy requires an asteroid belt to hide.` message. Read
|
||||
properly afterwards, `0x0083ce50` is a **shared placement validator** with ~21 refusal bits
|
||||
(colonise, mine, dump ore, too many things present, …), reached from `0x00760ce0` and the order
|
||||
path `0x00849460`, and it is **not on `ServerSpyManager::DeploySpy`'s call chain at all** — that
|
||||
chain is `SHIPACTION_DEPLOYSPY 0x0078c930` → vtable slot 7 `0x00887410` → `AddSpy`, and it never
|
||||
calls `0x00743f80`. What survives, and is all §1.2 needs, is that **`0x00743f80` bit 0 gates a
|
||||
belt-shaped refusal there and the detection roll here**; *which* action that refusal belongs to
|
||||
was over-claimed and is not established. The empirical half is unaffected: `Deploy Spy` was
|
||||
offered and accepted at a belt system, and the corpus agreement of `ARes2 > 0` with `TerrFl & 1`
|
||||
is 616 of 616.
|
||||
|
||||
---
|
||||
|
||||
## 9. Artifacts
|
||||
|
||||
| what | where |
|
||||
|---|---|
|
||||
| **the deployed-spy save** — first `deat != 0` and first non-empty `spies2` in the corpus | `verify/results/saves/as-turn15-spydeployed.sav` (76,705 B, `--strict` 0 errors / 0 warnings, sha256 `854a10fa1ea602f0…`) |
|
||||
| the pre-deploy save, one click from a deploy at an AI belt colony | `verify/results/saves/as-turn14-predeploy.sav` (75,908 B, sha256 `53986f8511cbf992…`) |
|
||||
| the instrumented trace (`draw_sites` + `probe_entries` + the boundary ledger) | `verify/traces/as-probes8-turn15-turn16.jsonl.gz` |
|
||||
| shim log for that run (probe and detour install status) | `verify/results/shim/as/as-probes8-run.shim.log` |
|
||||
| addresses this lane mints | `ghidra/addresses.d/as.json` (7 entries; 6 `verified`, 1 `mapped`) |
|
||||
| predictions, committed before the build | `sots-engine` `docs/AS-predictions.md` (`c172c99`, addendum `61875a4`) |
|
||||
| instrument | `sots-engine` `src/shim/shim.cfg.hp8` and `shim.cfg.hoff`, **both unchanged** — this lane wrote no engine code |
|
||||
|
||||
### 9.1 The hashes, for the record
|
||||
|
||||
```
|
||||
input MyGameas1spydep.sav 854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08
|
||||
A/B/M (Autosave EndTurn).sav 98e45d3745b914506fc4c409ad5a5ad0938a2003ab4c845421a99a8065149c3a
|
||||
A (Autosave).sav 262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe
|
||||
B (Autosave).sav e34775a757e6fb3df647d7a589e8b1819b90bea094196a9e3cc819e51ed63257
|
||||
M (Autosave).sav 262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe
|
||||
```
|
||||
|
||||
**This is not a calibration pair for the standalone**, and it must not be entered in
|
||||
`determinism-hashes.txt` as one. A ≠ B, in the AI-research-pick leaf class, so the turn is
|
||||
reproducible only once the client seeds are pinned. What it *is* is a reproducible **input** and a
|
||||
turn whose every non-AI leaf reproduces across three processes and two configurations.
|
||||
|
||||
---
|
||||
|
||||
## Proposed `campaign/board.md` rows
|
||||
|
||||
New row:
|
||||
|
||||
```
|
||||
| THE SPY DETECTION ROLL FIRES - 1 word, gated on an asteroid belt; `spies2` and `deat` closed | verify | verified | high | 100% | 2026-09-09 | **Lane AS, VM144, build `as-c172c99-20260909T0205Z`, `probes=8` (never 11).** `Mars::RNG::Chance` at **0x00887c8a**, in the BODY of `ServerSpyManager::vslot13` (tail phase 23 call 9), fired **1 call / 1 word / no_draw_calls=0 / strategic=true** at `ret_rva 0x00887c8f`; `OnAllCombatDone_Tail` went **363 -> 364**, against 0 on every turn any lane has ever measured. Turn total 19, per-site sum 19, **residual 0**. THE ENTRY PROBE ON `SpyManager::Slot13RngCallee 0x008408e0` READ **0 ON THE SAME TURN** - that zero was never a statement about the subtree, only about which function the call sits in (rule 28 practice 4, demonstrated rather than argued). **CORRECTION: the roll is NOT inline** (the resolution and AC §2.2 both say so) - it is a plain `E8` to the `Chance` entry point in the caller. **THE PREDICATE**, decoded to save fields: `spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1)`; **corpus count 0 of 22, failed conjunct `deat != 0`**. `ServerSystem_BeltUsableFlags 0x00743f80` (16 bytes) returns `TerrFl` with bit 0 cleared when `ARes2 <= 0`; `ARes2>0` and `TerrFl&1` agree on all 616 corpus system records AND on the new map, which also has `TerrFl = 2` and `3`, so bit 0 is the belt specifically. **POLARITY: belt gone => NO DRAW AND THE SPY IS DETECTED ANYWAY** (`je` past the roll to `sdet := turn`) - a zero here is two different results. Cost: `sdo` starts at 0, `SpyCraft_AccumulateDetectionOdds 0x0081f570` ACCUMULATES `0.7*(0.01 + [cA]0.01 + [cB]0.02 + 0.001*cC) * species(1.0/0.75/0.5)` and CLAMPS AT 1.0, so `Chance` costs 1 word until sdo reaches 1 and 0 thereafter. Measured `sdo` 0 -> 0.0084 -> 0.0168 (= 0.7*0.012 exactly, a value no run had produced). **`spies2` IS CLOSED**: `ServerSystem::AddSpy 0x007514c0` sets `spy.deat = system handle` and pushes `spy.sid` into `ServerSystem+0x1cc` in the same two instructions - `spies2` is the per-system DEPLOYED-spy id vector; L3's "not the spy list" was half right, AC's P4 was right. Measured: `count=1 element=1` at Kepler, 0 in the other 27. **NEW DRAW SITE NOBODY HAS**: `0x0078c97f` NextFloat in the SHIPACTION_DEPLOYSPY handler `0x0078c930` (`cbh = NextFloat()*2pi`), strategic generator, in the ProcessTurn bracket - decoded, NOT measured, and its predicate is on the COMMAND STREAM not the save, a column the gate-indexed audit lacks. Oracle: input byte-identical in 3 processes; A vs B differ in **22 leaves, all inside one AI player's research pick** (the known per-process client seed, resolution 2026-09-08) - so this is NOT a calibration pair and must not enter `determinism-hashes.txt`. `probes=8` run came out byte-identical to control A. Saves: `as-turn15-spydeployed.sav`, `as-turn14-predeploy.sav`; findings `findings/subsystems/spy-detection-roll.md` |
|
||||
```
|
||||
|
||||
Edits to existing rows:
|
||||
|
||||
- **Row 62** (guest holders) — `VM144 = FREE (lane AS released 2026-09-09; restored to L3's build + shim.cfg + the three pre-AS autosaves byte-identical, main menu verified by screenshot). SavedGames is now 15 FILES - AS added MyGameas1predeploy (400,347) and MyGameas1spydep (400,376), so ROW POSITIONS HAVE MOVED AGAIN; screenshot the dialog. AS left C:\SOTS\shimdist-as + ui\as{go,hash}.ps1 + ui\preAS-*; click_helper.ps1 untouched.`
|
||||
- **Row 207** (V2's eight sites) — append: `FIRED 2026-09-09: 0x00887c8a (spy 13), lane AS, 1 word, behind a deployed spy at a foreign colony with a usable asteroid belt. V2 vindicated a second time. Five of the eight still unfired: P's three (0x00840929 / 0x008409c7 / 0x00840a3c, gate now `sdet != -1` and turn-sdet>=3, ~20 End Turns from as-turn15-spydeployed.sav), 0x0088dc43 (spy 14, gate `cm in 1..4`, lane AG), 0x0082cdb8 (trade 15). AND THE INVENTORY IS SHORT ONE: 0x0078c97f in the SHIPACTION_DEPLOYSPY handler draws a NextFloat on the strategic generator and is in no sweep, because it hangs off the stack-built ship-action table.`
|
||||
- **Row 373 / the `spies2` rule-6 flag** — `CLOSED 2026-09-09 by lane AS, from the writer and then live. spies2 (ServerSystem+0x1cc) is the per-system vector of DEPLOYED spy ids: ServerSystem::AddSpy 0x007514c0 sets SpyCraft+0x10 (`deat`) to the system handle and push_backs SpyCraft+0x4 (`sid`) into +0x1cc; RemoveSpy 0x0074f550 is the exact mirror. Measured non-empty for the first time in the corpus: count=1, element=1, in the deploy system only. `SysMem` and `mts` are untouched and their flags stay up.`
|
||||
- **Row 186 / `tail-rng-ledger.md`** — append to the existing qualification: `THIRD site outside the firing-indexed table has now fired: the spy detection roll (tail T23c9, lane AS). The pattern is now three for three - hives, the raid roll, the detection roll - and each fired on the first state built for it. The gate-indexed audit is the fix, not a longer table.`
|
||||
61
ghidra/addresses.d/as.json
Normal file
61
ghidra/addresses.d/as.json
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"_note": "Lane AS (2026-09-09). The spy-deployment and detection-roll chain, read from the instruction stream of dumps/sots.exe (disassembled to the NEXT FUNCTION START, rule 17 -- Ghidra's reported ends are not used anywhere below). Every entry marked `verified` was read instruction by instruction and its operand offsets cross-checked against objects/layouts.md for Game::ServerSystem; entries marked `mapped` were reached but not read. Two of these -- ServerSystem_AddSpy and ServerSystem_RemoveSpy -- also settle the campaign's long-standing `spies2` rule-6 flag: they are the only writers of both SpyCraft+0x10 (`deat`) and ServerSystem+0x1cc (`spies2`), and they write them together. Evidence: findings/subsystems/spy-detection-roll.md. NOT minted here because another fragment already has them: ServerSystem_GetOwner 0x007437e0 (lane B5) and ServerSystem_IsIndependent 0x00743fa0 (lane AG), both of which this chain calls.",
|
||||
"entries": [
|
||||
{
|
||||
"name": "ServerSystem_AddSpy",
|
||||
"addr": "0x007514c0",
|
||||
"convention": "__thiscall",
|
||||
"prototype": "void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = this ? this->[+4] : 0; push_back(&this->spies2(+0x1cc), &spy->sid(+0x4))` via lane AI3's 0x0059f1a0. THE ONLY WRITER THAT EVER MAKES `deat` NON-ZERO AT RUNTIME (the other stores to SpyCraft+0x10 are the zero-init in CreateSpyCraft, the deserializer, and three field copies). Exactly one caller, 0x008874d4 inside ServerSpyManager_DeploySpy, and no vtable slot. THIS SETTLES `spies2`: ServerSystem+0x1cc is the per-system vector of DEPLOYED spy ids, written in the same two instructions as `deat`, which is why it is 0 in every save whose only spy is docked to its tender */",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)"
|
||||
},
|
||||
{
|
||||
"name": "ServerSystem_RemoveSpy",
|
||||
"addr": "0x0074f550",
|
||||
"convention": "__thiscall",
|
||||
"prototype": "void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = 0; erase(&this->spies2(+0x1cc), &spy->sid(+0x4))` via 0x0059ec00. The exact mirror of ServerSystem_AddSpy; the two together are the whole life cycle of `deat` and of `spies2` */",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)"
|
||||
},
|
||||
{
|
||||
"name": "ServerSpyManager_DeploySpy",
|
||||
"addr": "0x00887410",
|
||||
"convention": "__thiscall",
|
||||
"prototype": "bool (Game::ServerSpyManager* this, int spyHandle, float cbh) /* Game::ServerSpyManager vftable 0x00a3073c SLOT 7; zero direct call sites -- reached only from the SHIPACTION_DEPLOYSPY handler 0x0078c930 through [[dispatcher+0x15c]+0x1c]. Read at the field writes: spy->Reset() (0x00838070), then sdo(+0x3c) := 0.0f, sdet(+0x40) := -1, cbh(+0x18) := arg, tdep(+0x14) := server->[+0xc] (the current turn), atto(+0xc) := 0 and carrierShip->[+0xa8] := 0 (detach), then ServerSystem_AddSpy(system, spy) at 0x008874d4, then a player message built from system->Name (+0xa8, capacity at +0xbc -- which is what proves the receiver is a ServerSystem). CONSEQUENCE FOR THE TAIL: a spy is deployed with sdet == -1 and sdo == 0, so ServerSpyManager_vslot13's detection branch is entered on the very next OnAllCombatDone_Tail */",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)"
|
||||
},
|
||||
{
|
||||
"name": "ServerSystem_BeltUsableFlags",
|
||||
"addr": "0x00743f80",
|
||||
"convention": "__thiscall",
|
||||
"prototype": "int (Game::ServerSystem* this) /* 16 B, complete: `eax = this->TerrFl(+0x19c); if (this->ARes2(+0x6c) <= 0) eax &= ~1; return eax`. BIT 0 OF THE RETURN IS \"this system still has a usable asteroid belt\" -- TerrFl bit 0 masked by the belt's remaining resources. Corroborated from the save side without a debugger: ARes2 > 0 and TerrFl & 1 agree on all 616 system records of the 22-save corpus and on all 28 of the state this lane built, where TerrFl also takes the values 2 and 3, so bit 1 is a second, independent terrain feature. Called at 0x00887c71 (the detection roll's gate) and at 0x0083d15d inside the shared placement validator 0x0083ce50, where a clear bit 0 sets refusal bit 0x1000000 */",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)"
|
||||
},
|
||||
{
|
||||
"name": "SpyCraft_AccumulateDetectionOdds",
|
||||
"addr": "0x0081f570",
|
||||
"convention": "__stdcall",
|
||||
"prototype": "void (Game::SpyCraft* spy, Game::ServerSystem* sys, Game::ServerPlayer* spyOwner) /* 512 B, `ret 0xc`. Writes spy->sdo (+0x3c) and is called unconditionally at 0x00887c66, immediately before the detection roll's gate. EARLY OUT: if sys->PID == 0 (unowned) it stores 0.0f and returns, so a spy at an unowned system rolls at p = 0 and costs no word. Otherwise it walks the system's fleet vector accumulating three counters and computes, with every literal read as the four bytes in the image (rule 23): p = 0.01f + (cA>0 ? 0.01f : 0) + (cB>0 ? 0.02f : 0) + 0.001*cC, then p *= (spyOwner->[+0x5c]==5 ? 0.75f : spyOwner->[+0x5c]==6 ? 0.5f : 1.0f), then p *= 0.7, then sdo = min(sdo + p, 1.0) -- IT ACCUMULATES onto the old value and is CLAMPED AT 1.0. So a quiet foreign target gives sdo = 0.007 on the first turn and ~0.007/turn after, and a spy left in place long enough reaches p >= 1, at which point RNG_Chance costs 0 words and always succeeds */",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)"
|
||||
},
|
||||
{
|
||||
"name": "ServerSpyManager_DetectionRoll_DrawSite",
|
||||
"addr": "0x00887c8a",
|
||||
"convention": "site",
|
||||
"prototype": "/* THE SPY DETECTION ROLL. A plain E8 rel32 `call Mars_RNG_Chance 0x008e6dd0` in the BODY of ServerSpyManager_vslot13 0x008877b0 -- NOT an inlined draw (the campaign carried `inline` for a day; the draw is a normal entry-point call, it simply sits in the caller rather than in SpyManager_Slot13RngCallee 0x008408e0, which is why an entry probe on that callee reads zero either way). Generator: `mov ecx,[server+0x16c]` at 0x00887c80 -- the STRATEGIC generator. Probability: `fld [spy+0x3c]` -- sdo. Return address for lane Z's draw_sites ledger: 0x00887c8f. Gates, in order: spy.deat != 0, spy.sdet == -1, and ServerSystem_BeltUsableFlags(sys(deat)) & 1. WHEN THAT LAST BIT IS CLEAR THE ROLL IS SKIPPED AND THE SPY IS DETECTED UNCONDITIONALLY (`je 0x887c97`, which stores sdet := turn) -- a zero here is therefore not always a negative */",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)"
|
||||
},
|
||||
{
|
||||
"name": "SpyCraft_ResetMission",
|
||||
"addr": "0x00838070",
|
||||
"convention": "__thiscall",
|
||||
"prototype": "void (Game::SpyCraft* this) /* Called by ServerSpyManager_DeploySpy at 0x0088749e and by ServerSpyManager_vslot14 at 0x0088dc11 when the target system changes hands. NOT READ: only its position in those two chains and the fields its callers write immediately afterwards (sdo := 0, sdet := -1) are established here */",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/spy-detection-roll.md (lane AS 2026-09-09)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ 48db3cc, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ 712d184, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -1281,6 +1281,20 @@ constexpr uint32_t RaidEncounter_Helper848e50 = 0x00448e50;
|
|||
constexpr uint32_t RaidEncounter_Helper848e50_DrawSite = 0x00448fd9;
|
||||
// thiscall Called only from TradeManager_BuildRaidEncounter at 0x00892700; calls TradeManager_ComputeRaidInterceptPoint at 0x0082cf65, which is where the NextFloat at 0x00820c1b is spent on this path. Body not read [mapped]
|
||||
constexpr uint32_t RaidEncounter_Helper82ce00 = 0x0042ce00;
|
||||
// __thiscall void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = this ? this->[+4] : 0; push_back(&this->spies2(+0x1cc), &spy->sid(+0x4))` via lane AI3's 0x0059f1a0. THE ONLY WRITER THAT EVER MAKES `deat` NON-ZERO AT RUNTIME (the other stores to SpyCraft+0x10 are the zero-init in CreateSpyCraft, the deserializer, and three field copies). Exactly one caller, 0x008874d4 inside ServerSpyManager_DeploySpy, and no vtable slot. THIS SETTLES `spies2`: ServerSystem+0x1cc is the per-system vector of DEPLOYED spy ids, written in the same two instructions as `deat`, which is why it is 0 in every save whose only spy is docked to its tender */ [verified]
|
||||
constexpr uint32_t ServerSystem_AddSpy = 0x003514c0;
|
||||
// __thiscall void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = 0; erase(&this->spies2(+0x1cc), &spy->sid(+0x4))` via 0x0059ec00. The exact mirror of ServerSystem_AddSpy; the two together are the whole life cycle of `deat` and of `spies2` */ [verified]
|
||||
constexpr uint32_t ServerSystem_RemoveSpy = 0x0034f550;
|
||||
// __thiscall bool (Game::ServerSpyManager* this, int spyHandle, float cbh) /* Game::ServerSpyManager vftable 0x00a3073c SLOT 7; zero direct call sites -- reached only from the SHIPACTION_DEPLOYSPY handler 0x0078c930 through [[dispatcher+0x15c]+0x1c]. Read at the field writes: spy->Reset() (0x00838070), then sdo(+0x3c) := 0.0f, sdet(+0x40) := -1, cbh(+0x18) := arg, tdep(+0x14) := server->[+0xc] (the current turn), atto(+0xc) := 0 and carrierShip->[+0xa8] := 0 (detach), then ServerSystem_AddSpy(system, spy) at 0x008874d4, then a player message built from system->Name (+0xa8, capacity at +0xbc -- which is what proves the receiver is a ServerSystem). CONSEQUENCE FOR THE TAIL: a spy is deployed with sdet == -1 and sdo == 0, so ServerSpyManager_vslot13's detection branch is entered on the very next OnAllCombatDone_Tail */ [verified]
|
||||
constexpr uint32_t ServerSpyManager_DeploySpy = 0x00487410;
|
||||
// __thiscall int (Game::ServerSystem* this) /* 16 B, complete: `eax = this->TerrFl(+0x19c); if (this->ARes2(+0x6c) <= 0) eax &= ~1; return eax`. BIT 0 OF THE RETURN IS "this system still has a usable asteroid belt" -- TerrFl bit 0 masked by the belt's remaining resources. Corroborated from the save side without a debugger: ARes2 > 0 and TerrFl & 1 agree on all 616 system records of the 22-save corpus and on all 28 of the state this lane built, where TerrFl also takes the values 2 and 3, so bit 1 is a second, independent terrain feature. Called at 0x00887c71 (the detection roll's gate) and at 0x0083d15d inside the shared placement validator 0x0083ce50, where a clear bit 0 sets refusal bit 0x1000000 */ [verified]
|
||||
constexpr uint32_t ServerSystem_BeltUsableFlags = 0x00343f80;
|
||||
// __stdcall void (Game::SpyCraft* spy, Game::ServerSystem* sys, Game::ServerPlayer* spyOwner) /* 512 B, `ret 0xc`. Writes spy->sdo (+0x3c) and is called unconditionally at 0x00887c66, immediately before the detection roll's gate. EARLY OUT: if sys->PID == 0 (unowned) it stores 0.0f and returns, so a spy at an unowned system rolls at p = 0 and costs no word. Otherwise it walks the system's fleet vector accumulating three counters and computes, with every literal read as the four bytes in the image (rule 23): p = 0.01f + (cA>0 ? 0.01f : 0) + (cB>0 ? 0.02f : 0) + 0.001*cC, then p *= (spyOwner->[+0x5c]==5 ? 0.75f : spyOwner->[+0x5c]==6 ? 0.5f : 1.0f), then p *= 0.7, then sdo = min(sdo + p, 1.0) -- IT ACCUMULATES onto the old value and is CLAMPED AT 1.0. So a quiet foreign target gives sdo = 0.007 on the first turn and ~0.007/turn after, and a spy left in place long enough reaches p >= 1, at which point RNG_Chance costs 0 words and always succeeds */ [verified]
|
||||
constexpr uint32_t SpyCraft_AccumulateDetectionOdds = 0x0041f570;
|
||||
// site /* THE SPY DETECTION ROLL. A plain E8 rel32 `call Mars_RNG_Chance 0x008e6dd0` in the BODY of ServerSpyManager_vslot13 0x008877b0 -- NOT an inlined draw (the campaign carried `inline` for a day; the draw is a normal entry-point call, it simply sits in the caller rather than in SpyManager_Slot13RngCallee 0x008408e0, which is why an entry probe on that callee reads zero either way). Generator: `mov ecx,[server+0x16c]` at 0x00887c80 -- the STRATEGIC generator. Probability: `fld [spy+0x3c]` -- sdo. Return address for lane Z's draw_sites ledger: 0x00887c8f. Gates, in order: spy.deat != 0, spy.sdet == -1, and ServerSystem_BeltUsableFlags(sys(deat)) & 1. WHEN THAT LAST BIT IS CLEAR THE ROLL IS SKIPPED AND THE SPY IS DETECTED UNCONDITIONALLY (`je 0x887c97`, which stores sdet := turn) -- a zero here is therefore not always a negative */ [verified]
|
||||
constexpr uint32_t ServerSpyManager_DetectionRoll_DrawSite = 0x00487c8a;
|
||||
// __thiscall void (Game::SpyCraft* this) /* Called by ServerSpyManager_DeploySpy at 0x0088749e and by ServerSpyManager_vslot14 at 0x0088dc11 when the target system changes hands. NOT READ: only its position in those two chains and the fields its callers write immediately afterwards (sdo := 0, sdet := -1) are established here */ [mapped]
|
||||
constexpr uint32_t SpyCraft_ResetMission = 0x00438070;
|
||||
// thiscall void (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) // the body of every `Player.<id>.AIAgent` CD block. 36 wire items, NO conditionals: the only `if` the decompiler shows around `lnat` is an inlined std::vector destructor whose operator delete is marked noreturn, and both paths converge at 0x006c72e8. The agent object is *(this+4) [verified]
|
||||
constexpr uint32_t Game_StrategyAIAgent_Streamable_Write = 0x002c6f00;
|
||||
// thiscall bool (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) [verified]
|
||||
|
|
|
|||
|
|
@ -413,5 +413,23 @@ So, four practices:
|
|||
252 everywhere looked like a fact about the game; it was a fact about our saves. Compare rule 8's
|
||||
eighteen fields that split a roster identically because nothing exercised them.
|
||||
4. **Pair entry probes with the return-address ledger before calling a subtree draw-free.** An entry
|
||||
probe on an inner function reading zero says nothing about an *inline* draw in its caller — which
|
||||
is exactly where the spy half's detection roll lives.
|
||||
probe on an inner function reading zero says nothing about a draw sited in its *caller* — which is
|
||||
exactly where the spy half's detection roll lives.
|
||||
|
||||
**Confirmed live, 2026-09-09.** Lane AS measured both instruments on the same turn: the entry probe
|
||||
on `SpyManager::Slot13RngCallee` read **0**, while the return-address ledger recorded the detection
|
||||
roll firing for **one word** in the caller's body. The zero was never a statement about the subtree —
|
||||
only about which function the call happens to sit in. (The roll is a plain call, not an inlined one;
|
||||
the earlier claim that it was inlined was wrong, and the practice holds either way.)
|
||||
|
||||
5. **A fact about one map is not a fact about the game.** Practice 3's sibling, and it cost a lane a
|
||||
whole workload plan. One lane reported that "an enemy colony with an asteroid belt does not exist in
|
||||
this game", which was true of *its* map. All twenty-two corpus saves have a usable belt, two of them
|
||||
already hold four AI-owned ones, and the next lane's very first generated map had two — **zero
|
||||
regenerations**. Before building a state to satisfy a condition, count the corpus against it: the
|
||||
state you need may already be sitting in the save set.
|
||||
|
||||
6. **Read the polarity before reading the count.** In the same chain, the jump goes *past* the roll:
|
||||
with the belt gone there is **no draw and the spy is detected anyway**. A zero at that site therefore
|
||||
means two opposite things, and a lane that assumed the intuitive polarity would have reported the
|
||||
detection case as "did not fire".
|
||||
|
|
|
|||
BIN
verify/results/saves/as-turn14-predeploy.sav
Normal file
BIN
verify/results/saves/as-turn14-predeploy.sav
Normal file
Binary file not shown.
BIN
verify/results/saves/as-turn15-spydeployed.sav
Normal file
BIN
verify/results/saves/as-turn15-spydeployed.sav
Normal file
Binary file not shown.
114
verify/results/shim/as/as-probes8-run.shim.log
Normal file
114
verify/results/shim/as/as-probes8-run.shim.log
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
23:43:10.838 [tid 7184] ==== sots-engine shim (binkw32 proxy) build as-c172c99-20260909T0205Z ====
|
||||
23:43:10.838 [tid 7184] exe: C:\SOTS\Sword of the Stars.exe
|
||||
23:43:10.838 [tid 7184] exe base=0x006b0000 (link-time image base 0x00400000, ASLR delta +2818048) pid=3060 shim=72b70000
|
||||
23:43:10.838 [tid 7184] addresses: Source: sots-re ghidra/addresses.json @ bdaa26f, generated 2026-09-08 by tools/gen_addresses.py
|
||||
23:43:10.854 [tid 7184] config: hooks=trace
|
||||
23:43:10.854 [tid 7184] config: hook.Shim::SelfTest::Fill=off
|
||||
23:43:10.854 [tid 7184] config: hook.Mars::GlobalConsts::LoadFile=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::WeaponDictionary::Init=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::SectionDictionary::SectionDictionary=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::ServerPlayer::ComputeBudget=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::TechTree::ProcessResearch=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::ServerPlayer::OnTechResearched=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::ServerSystem::ProcessTurn=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::ServerPlayer::ProcessTurn=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::ServerSystem::GroupOutput=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::MoveFleet=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||
23:43:10.854 [tid 7184] config: hook.Game::StrategyHost::Autosave=trace
|
||||
23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::ProcessTurn=trace
|
||||
23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||
23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||
23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::NodeLineDecay=trace
|
||||
23:43:10.854 [tid 7184] config: hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||
23:43:10.854 [tid 7184] config: hook.Game::EncounterDetect::AssignContacts=trace
|
||||
23:43:10.854 [tid 7184] config: hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||
23:43:10.854 [tid 7184] config: fpu.sample_turn=off
|
||||
23:43:10.854 [tid 7184] config: fpu.sample_ticks=off
|
||||
23:43:10.854 [tid 7184] config: trace.inline_max=64
|
||||
23:43:10.854 [tid 7184] config: trace.path=C:\SOTS\shim.trace.jsonl
|
||||
23:43:10.854 [tid 7184] config: trace.flush=always
|
||||
23:43:10.854 [tid 7184] config: probes=8 -> 8 lane-H entry probes
|
||||
23:43:10.947 [tid 7184] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 64, flush always)
|
||||
23:43:10.947 [tid 7184] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=00b50e50
|
||||
23:43:10.947 [tid 7184] hook: MH_Initialize -> MH_OK
|
||||
23:43:10.947 [tid 7184] hook: MH_CreateHook -> MH_OK (trampoline=01930fe0)
|
||||
23:43:10.947 [tid 7184] hook: MH_EnableHook -> MH_OK
|
||||
23:43:10.947 [tid 7184] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
|
||||
23:43:10.947 [tid 7184] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
|
||||
23:43:10.947 [tid 7184] dict: dictionaries hook ready (crt new=74c4232b delete=74c40174)
|
||||
23:43:10.947 [tid 7184] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
|
||||
23:43:10.947 [tid 7184] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] research: ProcessResearch hook ready (Cost=0082da00, node=0x34, rng=0x9cc, fpu_cw=0x027f)
|
||||
23:43:10.963 [tid 7184] hook: Game::TechTree::ProcessResearch rva=0x001876c0 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
|
||||
23:43:10.963 [tid 7184] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] hook: Game::ServerSystem::GroupOutput rva=0x0034b7a0 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] hook: Game::ServerSystem::ComputeTotalOutput rva=0x00350480 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] player_turn: ServerPlayer::ProcessTurn hook armed (ratio helper at 0082e950)
|
||||
23:43:10.963 [tid 7184] hook: Game::ServerPlayer::ProcessTurn rva=0x00491340 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
|
||||
23:43:10.963 [tid 7184] hook: Game::StrategyHost::Autosave rva=0x00495210 -> va=00b45210 MH_CreateHook -> MH_OK (trampoline=01930fc0)
|
||||
23:43:10.979 [tid 7184] hook: Game::StrategyHost::Autosave MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:10.979 [tid 7184] hook: Game::StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=00a8c6c0 MH_CreateHook -> MH_OK (trampoline=01930fa0)
|
||||
23:43:10.994 [tid 7184] hook: Game::StrategyServer::ProcessTurn MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:10.994 [tid 7184] hook: Game::StrategyServer::OnAllCombatDone_Tail rva=0x003d92a0 -> va=00a892a0 MH_CreateHook -> MH_OK (trampoline=01930f80)
|
||||
23:43:11.010 [tid 7184] hook: Game::StrategyServer::OnAllCombatDone_Tail MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.010 [tid 7184] hook: Game::StrategyServer::ApplyEncounterResult rva=0x003d8920 -> va=00a88920 MH_CreateHook -> MH_OK (trampoline=01930f60)
|
||||
23:43:11.026 [tid 7184] hook: Game::StrategyServer::ApplyEncounterResult MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.026 [tid 7184] hook: Game::StrategyServer::NodeLineDecay rva=0x003ae010 -> va=00a5e010 MH_CreateHook -> MH_OK (trampoline=01930f40)
|
||||
23:43:11.041 [tid 7184] hook: Game::StrategyServer::NodeLineDecay MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.041 [tid 7184] hook: Game::StrategyServer::ProcessNodeSpaceTravel rva=0x003a0e20 -> va=00a50e20 MH_CreateHook -> MH_OK (trampoline=01930f20)
|
||||
23:43:11.057 [tid 7184] hook: Game::StrategyServer::ProcessNodeSpaceTravel MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.057 [tid 7184] hook: Game::EncounterDetect::AssignContacts rva=0x003aa240 -> va=00a5a240 MH_CreateHook -> MH_OK (trampoline=01930f00)
|
||||
23:43:11.072 [tid 7184] hook: Game::EncounterDetect::AssignContacts MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.072 [tid 7184] hook: Game::EncounterDetect::ProcessTeamRecord rva=0x003ca640 -> va=00a7a640 MH_CreateHook -> MH_OK (trampoline=01930ee0)
|
||||
23:43:11.088 [tid 7184] hook: Game::EncounterDetect::ProcessTeamRecord MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.088 [tid 7184] hook: Game::StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=00a898e0 MH_CreateHook -> MH_OK (trampoline=01930ec0)
|
||||
23:43:11.104 [tid 7184] hook: Game::StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.104 [tid 7184] hook: Game::SVSOSwarmQueen::OnTurnBegin rva=0x00129930 -> va=007d9930 MH_CreateHook -> MH_OK (trampoline=01930ea0)
|
||||
23:43:11.135 [tid 7184] hook: Game::SVSOSwarmQueen::OnTurnBegin MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.135 [tid 7184] hook: Game::SVSOSwarmQueen::RegisterHives rva=0x00127630 -> va=007d7630 MH_CreateHook -> MH_OK (trampoline=01930e80)
|
||||
23:43:11.151 [tid 7184] hook: Game::SVSOSwarmQueen::RegisterHives MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.151 [tid 7184] hook: Game::SVSOSwarmQueen::TickHives rva=0x00127770 -> va=007d7770 MH_CreateHook -> MH_OK (trampoline=01930e60)
|
||||
23:43:11.166 [tid 7184] hook: Game::SVSOSwarmQueen::TickHives MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.166 [tid 7184] hook: Game::SVSOSlaversRefuel::UpdateDifficultyTier rva=0x00115820 -> va=007c5820 MH_CreateHook -> MH_OK (trampoline=01930e40)
|
||||
23:43:11.182 [tid 7184] hook: Game::SVSOSlaversRefuel::UpdateDifficultyTier MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.182 [tid 7184] hook: Mars::RNG::Seed rva=0x0009fdf0 -> va=0074fdf0 MH_CreateHook -> MH_OK (trampoline=01930e20)
|
||||
23:43:11.197 [tid 7184] hook: Mars::RNG::Seed MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.197 [tid 7184] hook: Game::StrategyApp::RunAI rva=0x004706f0 -> va=00b206f0 MH_CreateHook -> MH_OK (trampoline=01930e00)
|
||||
23:43:11.213 [tid 7184] hook: Game::StrategyApp::RunAI MH_EnableHook -> MH_OK mode=trace
|
||||
23:43:11.229 [tid 7184] drawsite: Mars::RNG::NextFloat rva=0x0007d830 -> va=0072d830 create=MH_OK enable=MH_OK
|
||||
23:43:11.244 [tid 7184] drawsite: Mars::RNG::NextInt rva=0x000271c0 -> va=006d71c0 create=MH_OK enable=MH_OK
|
||||
23:43:11.260 [tid 7184] drawsite: Mars::RNG::Chance rva=0x004e6dd0 -> va=00b96dd0 create=MH_OK enable=MH_OK
|
||||
23:43:11.276 [tid 7184] drawsite: Mars::RNG::NextUInt rva=0x000f7670 -> va=007a7670 create=MH_OK enable=MH_OK
|
||||
23:43:11.291 [tid 7184] drawsite: Mars::RNG::FloatRange rva=0x0007d8a0 -> va=0072d8a0 create=MH_OK enable=MH_OK
|
||||
23:43:11.307 [tid 7184] drawsite: Mars::RNG::IntRangeBell rva=0x004e6d80 -> va=00b96d80 create=MH_OK enable=MH_OK
|
||||
23:43:11.322 [tid 7184] drawsite: Mars::RNG::GaussianRange rva=0x004e6e30 -> va=00b96e30 create=MH_OK enable=MH_OK
|
||||
23:43:11.322 [tid 7184] probe: installing 8 of 12 (probes= in shim.cfg)
|
||||
23:43:11.338 [tid 7184] probe: Game::ServerSpyManager::vslot13 rva=0x004877b0 -> va=00b377b0 create=MH_OK enable=MH_OK
|
||||
23:43:11.354 [tid 7184] probe: Game::ServerSpyManager::vslot14 rva=0x0048db80 -> va=00b3db80 create=MH_OK enable=MH_OK
|
||||
23:43:11.369 [tid 7184] probe: Game::ServerTradeManagerImpl::vslot13 rva=0x0048ef80 -> va=00b3ef80 create=MH_OK enable=MH_OK
|
||||
23:43:11.385 [tid 7184] probe: Game::ServerTradeManagerImpl::vslot15 rva=0x0042cca0 -> va=00adcca0 create=MH_OK enable=MH_OK
|
||||
23:43:11.401 [tid 7184] probe: Game::SpyManager::Slot13RngCallee rva=0x004408e0 -> va=00af08e0 create=MH_OK enable=MH_OK
|
||||
23:43:11.416 [tid 7184] probe: Game::TradeManager::Slot13RngCalleeA rva=0x00420ca0 -> va=00ad0ca0 create=MH_OK enable=MH_OK
|
||||
23:43:11.447 [tid 7184] probe: Game::TradeManager::Slot13RngCalleeB rva=0x0048b440 -> va=00b3b440 create=MH_OK enable=MH_OK
|
||||
23:43:11.463 [tid 7184] probe: Game::ServerTradeManager::CreateRaidEncounter rva=0x004938a0 -> va=00b438a0 create=MH_OK enable=MH_OK
|
||||
23:43:11.463 [tid 7184] watch: disabled (watch=off)
|
||||
23:43:11.463 [tid 7184] aiorders: disabled (aiorders=off)
|
||||
23:43:11.463 [tid 7184] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=off value=0x0000 sample_ticks=off
|
||||
23:43:11.463 [tid 7184] fpu: sample_turn=off (off releases StrategyServer::ProcessTurn for another hook)
|
||||
23:43:11.463 [tid 7184] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=00a33be0 MH_CreateHook -> MH_OK (trampoline=01930c00)
|
||||
23:43:11.479 [tid 7184] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK
|
||||
23:43:11.479 [tid 7184] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=00a898e0 MH_CreateHook -> MH_ERROR_ALREADY_CREATED (trampoline=00000000)
|
||||
23:43:11.479 [tid 7184] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 not installed (sampler off)
|
||||
23:43:11.479 [tid 7184] fpu: DemoApp::OnTick rva=0x0049a640 not installed (sampler off)
|
||||
23:43:11.479 [tid 7184] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0
|
||||
23:43:11.479 [tid 7184] Application::Initialize called (this=01968128)
|
||||
23:48:37.590 [tid 7184] fpu: sample at StrategyClient::EndTurn (this=1e031680): cw=0x127f 53bit-double/nearest [no fpu.force configured]
|
||||
23:48:40.778 [tid 7184] fpu: sample at StrategyClient::EndTurn (this=32a91610): cw=0x127f 53bit-double/nearest [no fpu.force configured]
|
||||
23:48:40.778 [tid 7184] fpu: sample at StrategyClient::EndTurn (this=32a96d70): cw=0x127f 53bit-double/nearest [no fpu.force configured]
|
||||
BIN
verify/traces/as-probes8-turn15-turn16.jsonl.gz
Normal file
BIN
verify/traces/as-probes8-turn15-turn16.jsonl.gz
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue