sots-re/findings/control-flow/raid-target-pick-verdict.md
alex 712d184599 AR: predictions for the raid-target-pick verdict, committed before the build
Includes the static decode of FUN_00841700 -- the writer of TradeManager+0x0c that
lane AG's section 7 lists as not found -- stated as a falsifiable prediction (P0)
with the symptom of each way it could be wrong. Also corrects the route count in
ad-turn27-two-raiders.sav: five rt records, trfr 18/16/14/20/14, not three.
2026-09-08 23:12:41 -04:00

198 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.*