From 712d184599539c073734448d1c3a7b8e8d05315e Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 8 Sep 2026 23:12:41 -0400 Subject: [PATCH] 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. --- .../control-flow/raid-target-pick-verdict.md | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 findings/control-flow/raid-target-pick-verdict.md diff --git a/findings/control-flow/raid-target-pick-verdict.md b/findings/control-flow/raid-target-pick-verdict.md new file mode 100644 index 0000000..076da74 --- /dev/null +++ b/findings/control-flow/raid-target-pick-verdict.md @@ -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::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` 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.*