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

14 KiB
Raw Blame History

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.