# The complete inlined-MT-draw inventory, and the two `ProcessTurn` functions read in full Lane I, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs. **Method.** Every function in the image was decoded from its real instruction boundaries with `tools/x86disp.py`'s length decoder, **to the next function start, never to Ghidra's reported size** (rule 17). Every claim about a function body below was read from `objdump -b binary -m i386 -M intel` over `dumps/sots.exe`, not from the decompiler (rule 4). No claim here comes from a delegated sweep — lane J's brief records that a delegated sweep repeated the truncation mistake independently, so this lane ran none. Closes the top-ranked gap of `findings/control-flow/combat-resolver.md` §0.1 (lane J): *"`FUN_004f7670` and `FUN_007aa240` … the cheapest RNG facts left."* It **corrects** lane J's §0.1 in three places and **confirms** its central claim. Reconciles against lane Z's measured ledger (`findings/control-flow/tail-rng-ledger.md`). --- ## 0. Lead Lane J found fourteen game functions with inlined MT draws and put two of them inside `StrategyServer::ProcessTurn`'s closure at depth 4. Re-run at instruction boundaries and verified site by site: * **Sixteen** functions in the image contain the tempering immediates at a real instruction boundary, **67 immediate occurrences in total**, and every one of the 67 falls inside a decoded instruction — a brute byte scan finds nothing the boundary decode misses, so **recall is complete**. * **One of the sixteen is a false positive.** `FUN_008cca30` has no temper chain; the four bytes at 0x008cca90 are the rel32 displacement of `call 0x008caa20`. Strike it from lane J's list. * **Four of the sixteen are RNG entry points, not game code** — the tempering there *is* the primitive. Two were already known (`RNG_NextFloat`, `RNG_NextInt`); **two were not**: `FUN_004f7670` = `Mars::RNG::NextUInt` and `FUN_008e6e30` = a truncated-normal integer range. * That leaves **eleven game functions with genuinely inlined draws, 28 sites between them**, not fourteen. * **Exactly one of the eleven is reachable from `StrategyServer::ProcessTurn`** — `FUN_007aa240`, at depth 4 — and **one more from `OnAllCombatDone_Tail`** (`FUN_007a7f30`, lane J's R2, at depth 3). The other nine belong to map setup, the lobby screen, the strategy network client/server and two scripted encounters, and are reachable from **no** turn driver by a direct call. Lane J's operational conclusion survives intact and its taxonomy does not: there really are two unmodelled draw sources at depth 4 under `ProcessTurn`, but only one of them is an inlined game draw. The other is a **fourth draw primitive nobody had in their primitive set** — which lane Z had already found from the other direction and which this lane confirms from the instruction stream. **And a fifth, sixth and seventh entry point exist.** Three more functions take the generator and return a draw, none of them in any earlier lane's list: `FUN_0047d8a0` (float range, 1 word), `FUN_008e6d80` (triangular integer range, ≥2 words) and `FUN_008e6e30` (truncated normal, **2 words per attempt, unbounded**). One of them, `FUN_0047d8a0`, is inside `ProcessTurn`'s closure at depth 3 with two call sites. **Correction to my own first reading, recorded per rule 11.** The masks `0xff3a58ad` / `0xffffdf8c` are *not* a Mars variant of MT19937, as I wrote into Ghidra before checking. They are the textbook tempering with the mask applied **before** the shift instead of after: `(y & 0xff3a58ad) << 7 == (y << 7) & 0x9d2c5680` and `(y & 0xffffdf8c) << 15 == (y << 15) & 0xefc60000`, because the mask bits that would shift past bit 31 are don't-cares (`0x9d2c5680 >> 7 == 0x013a58ad == 0xff3a58ad & 0x01ffffff`; `0xefc60000 >> 15 == 0x0001df8c == 0xffffdf8c & 0x0001ffff`). Checked over 200,000 random words. `sots-engine`'s `mars::rng` is **not** wrong. But a scan for the textbook constants finds **nothing** in this image, which is precisely why rule 16's scan has to use these two values. --- ## 1. The inventory Scan definition, so it can be repeated: decode every one of the 41,089 functions from its start to the **next function start**; at each real instruction boundary, flag `and r32, 0xff3a58ad` (opcode `0x25` or `0x81 /4`, mod=3) and require, within eight instructions forward, `and r32, 0xffffdf8c`, and within six instructions back, `shr r32, 0xb`. A site is classified `float` if an `fmul QWORD ds:0x009e61b0` (the `1/(2^32-1)` multiplier) follows within 26 instructions, otherwise `uint`. Recall audit: a naive byte scan over the executable sections finds **67** occurrences of the two immediates; **all 67** lie inside an instruction the boundary decode produced, and **0** are orphans. 70 functions desync during decode, every one of them inside the `int3` padding after the body, so no desync hides a site. | # | function | body size | sites | kind | in the closure of | depth | subsystem it belongs to | |---|---|---|---|---|---|---|---| | — | `RNG_NextInt` 0x004271c0 | 139 | 1 | uint | S::ProcessTurn / Tail | 3 / 4 | **entry point** | | — | `RNG_NextFloat` 0x0047d830 | 109 | 1 | float | S::ProcessTurn / Tail | 3 / 3 | **entry point** | | — | **`Mars_RNG_NextUInt` 0x004f7670** | **84** | 1 | uint | **S::ProcessTurn** | **4** | **entry point — §2.1** | | — | **`Mars_RNG_GaussianRange` 0x008e6e30** | 410 | 2 | uint | — | — | **entry point — §4.4** | | 1 | `FUN_004b1f20` | 1349 | 4 | float | — | — | combat network client (vslot 3 of `Game::CombatNetworkClient`) | | 2 | `FUN_00507ac0` | 3073 | **12** | float | — | — | `Game::CrowRuinsEncounter` vslot 11 | | 3 | `FUN_005232a0` | 562 | 1 | uint | — | — | `Game::SwarmEncounter` vslot 11 | | 4 | `FUN_006ec720` | 483 | 1 | float | — | — | `Game::StrategyNetworkServer` vslots 3/4/7 | | 5 | `FUN_006f65f0` | 1607 | 1 | float | — | — | same | | 6 | `FUN_006f7890` | 1649 | 1 | float | — | — | same | | 7 | `FUN_0079f7d0` | 656 | 1 | uint | — | — | `Game::StrategyNetworkClient` vslot 3 / `OnMessage` | | 8 | **`FUN_007a7f30`** `CombatResolve_SalvageBackEng` | 2670 | 1 | float | **OnAllCombatDone_Tail**, `CombatResolver_Run` | **3 / 1** | lane J's R2 | | 9 | **`FUN_007aa240`** `EncounterDetect_AssignContacts` | 944→**953** | 1 | float | **StrategyServer::ProcessTurn** | **4** | **§2.2 — the one that matters** | | 10 | `FUN_007c2fa0` | 4331 | 4 | uint | — | — | `Game::StrategyLobbyScreen` vslot 3 | | 11 | `FUN_007c4140` | 2561 | 1 | float | — | — | `Game::StrategyNetworkClient` vslot 3 | | ✗ | ~~`FUN_008cca30`~~ | 175 | **0** | — | — | — | **FALSE POSITIVE**, see below | 28 sites in the eleven game functions; 5 more inside the four entry points; 33 `and`-pairs in total, which with the one stray displacement accounts for all 67 immediate occurrences. **Reachability caveat, stated as loudly as the result.** The closure is a **direct-call** closure (`E8 rel32` and tail `E9 rel32` only). Nine of the eleven have their topmost caller in a **vtable slot with no direct caller at all** — `Game::StrategyNetworkServer::Update`, `StrategyNetworkClient` slot 3, `StrategyLobbyScreen` slot 3, `CombatNetworkClient` slot 3, and the two encounter classes' slot 11. Those are dispatched virtually. "Not reachable from `ProcessTurn`" therefore means *not reachable by a direct call*, which is what a call-graph sweep can prove and no more. The message-handler and encounter subtrees can and do run at other points of the frame; what is settled is that **`ProcessTurn` does not call into them**, and lane Z's independent measurement — the generator does not move at all between the post-turn autosave and the next `ProcessTurn` — is the stronger evidence for the same conclusion. ### The false positive, in full ``` 008cca8a 89 45 f8 mov DWORD PTR [ebp-0x8],eax 008cca8d 8b ce mov ecx,esi 008cca8f e8 8c df ff ff call 0x8caa20 <-- "8c df ff ff" is the rel32 ``` No `0xff3a58ad` anywhere in the function, no `shr r32,0xb`, no `shl 7` / `shl 0xf`. Lane J's list has fourteen entries; this is one of them, and it should be thirteen minus the two primitives = eleven. Recorded at `ghidra/addresses.d/lane-i.json` under `InlinedDrawScan_FalsePositive_008cca30` so nobody re-derives it. **Requiring both masks plus the preceding `shr 0xb` is what separates 33 real chains from 34 candidate hits**, and one hit in 67 is a 1.5% false-positive rate on a scan whose whole value is that it sees what the call graph cannot. --- ## 2. The two functions lane J handed over, read completely ### 2.1 `FUN_004f7670` — 84 bytes, and it is not a game function Whole body, every instruction: ``` 004f7670 83 b9 c8 09 00 00 00 cmp DWORD PTR [ecx+0x9c8],0x0 ; left == 0 ? 004f7677 56 push esi 004f7678 8d 71 04 lea esi,[ecx+0x4] ; esi = &mt[0] 004f767b 75 07 jne 0x4f7684 004f767d 8b ce mov ecx,esi 004f767f e8 7c f7 f2 ff call 0x426e00 ; RNG_Twist(&mt) -- LAZY 004f7684 8b 86 c0 09 00 00 mov eax,DWORD PTR [esi+0x9c0] ; next (= RNG+0x9c4) 004f768a ff 8e c4 09 00 00 dec DWORD PTR [esi+0x9c4] ; --left (= RNG+0x9c8) 004f7690 8b 08 mov ecx,DWORD PTR [eax] ; y = *next 004f7692 83 c0 04 add eax,0x4 004f7695 89 86 c0 09 00 00 mov DWORD PTR [esi+0x9c0],eax ; next++ 004f769b 8b c1 mov eax,ecx 004f769d c1 e8 0b shr eax,0xb 004f76a0 33 c8 xor ecx,eax ; y ^= y >> 11 004f76a2 8b d1 mov edx,ecx 004f76a4 81 e2 ad 58 3a ff and edx,0xff3a58ad 004f76aa c1 e2 07 shl edx,0x7 004f76ad 33 ca xor ecx,edx ; y ^= (y & M1) << 7 004f76af 8b c1 mov eax,ecx 004f76b1 25 8c df ff ff and eax,0xffffdf8c 004f76b6 c1 e0 0f shl eax,0xf 004f76b9 33 c8 xor ecx,eax ; y ^= (y & M2) << 15 004f76bb 8b c1 mov eax,ecx 004f76bd c1 e8 12 shr eax,0x12 004f76c0 33 c1 xor eax,ecx ; y ^= y >> 18 004f76c2 5e pop esi 004f76c3 c3 ret 004f76c4..004f76cf int3 ×12 ``` **What it draws:** one MT word. **How many:** exactly one. **Under what condition:** none — there is no early-out, no rejection loop, no branch at all except the lazy twist, which consumes nothing. **How it scales with game state:** it does not. `words(call) = 1`, always. Ghidra's size of 84 is **correct here** (body 0x004f7670–0x004f76c3, then twelve `int3` to the next function start 0x004f76d0) — which is worth saying out loud, because rule 17 is about never *trusting* the size, not about the size always being wrong. Two facts that matter more than the count: 1. **ECX is the `Mars::RNG` object, not `&mt`.** It does `lea esi,[ecx+4]` itself and hands `esi` to `RNG_Twist`. `RNG_NextFloat` 0x0047d830 and `RNG_NextInt` 0x004271c0 are entered with ECX already equal to `&mt`, i.e. **object + 4**. Two conventions for the same generator, and `ProbabilisticJump` uses **both, 0x6b bytes apart** (§3.1). Rule 1's "two bases 4 bytes apart" is live here in a second place. 2. **This is a draw primitive, and it was in nobody's primitive set until lane Z's ledger.** Lane J listed it among "fourteen *game* functions with inlined draws". It is not a game function; the tempering in it is its own. Confirmed independently here from the instruction stream — lane Z reached the same conclusion from the caller side, which is two instruments agreeing. Named `Mars_RNG_NextUInt` and written back to Ghidra with a plate comment. ### 2.2 `FUN_007aa240` — `EncounterDetect_AssignContacts`, the one that matters `__thiscall`, `ret 0xc`. Three stack arguments, resolved at the single call site 0x007ca73a (§2.3): | slot | contents | |---|---| | `this` (ECX, spilled to `[ebp-0x14]`) | `{ +0x00 StrategyServer* S; +0x04 TechDef* (id 0x2729); +0x08 TechDef* (id 0x2728) }` | | `[ebp+0x08]` | `out` — `std::vector>`, **16-byte elements**, one bucket per detector | | `[ebp+0x0c]` | `detectors` — `std::vector` | | `[ebp+0x10]` | `contacts` — `std::vector` | Two locals do the bookkeeping and both are load-bearing for the draw count: * `[ebp-0x38]` — `std::vector`, `|contacts|` words, filled with 0 by `0x0050e6a0(0, |contacts|, &0)` at 0x007aa296. Bit `d` of word `c` is *"pair (contact c, detector d) has been tried"*. * `[ebp-0x4c]` — `std::vector`, `|contacts|` bits, `resize(|contacts|, false)` at 0x007aa2c8. Bit `c` is *"contact c has been assigned"*. The `shr eax,5` / `and ecx,0x1f` / `shl esi,cl` addressing at 0x007aa2ea–0x007aa320 is what identifies it as `vector`, not a branch. Control flow, from the instruction stream: ``` 7aa2d0 PASS TOP ------------------------------------- <- back edge from 7aa587 7aa2da mov BYTE [ebp-0xd],1 ; "no progress this pass" 7aa2de if (|contacts| == 0) goto 7aa58d ; whole-function exit 7aa2e6 OUTER TOP: ebx = contact index ----------------- 7aa320 if (assigned[ebx]) goto 7aa56d ; next contact, NO DRAW 7aa335 if (|detectors| == 0) goto 7aa56d ; next contact, NO DRAW 7aa33b INNER TOP: edi = detector index ---------------- 7aa344 if (tried[ebx] & (1<+8 && det && HasTech(det->+0xf4, this->+8)) thresh = 0.25f 7aa37c else if (this->+4 && det && HasTech(det->+0xf4, this->+4)) thresh = 0.25f 7aa3a3 scale = 1.0 - 0.0 ; ds:0x009e1e68 is 0.0 7aa3b0 esi = S->rng ; S->+0x16c 7aa3b6 ***** INLINED NextFloat -- ONE WORD ***** 7aa435 fcompp / test ah,5 / jp 7aa45a ; ACCEPT iff thresh >= r 7aa43e (reject) -> 7aa444 7aa444 ++edi; if (edi < |detectors|) goto 7aa33b else goto 7aa56d 7aa45a ACCEPT: assigned[ebx] = 1; out[edi].push_back(contacts[ebx]); -> 7aa56d 7aa56d ++ebx; edi = 0; if (ebx < |contacts|) goto 7aa2e6 7aa583 if ([ebp-0xd] == 0) goto 7aa2d0 ; repeat the pass 7aa58d EXIT: destroy the two locals, unwind, ret 0xc ``` **The draw.** At 0x007aa3b6 the compiler emitted `RNG_NextFloat` inline, byte for byte — the lazy-twist pre-check, the `next++/left--` pair, the four temper steps, the `fild` / `+2^32` / `× 1/(2^32-1)`: ``` 007aa3a8 mov edx,[ecx] ; ecx = this -> edx = StrategyServer S 007aa3b0 mov esi,[edx+0x16c] ; the strategic generator -- the SAME object 007aa3b6 cmp DWORD PTR [esi+0x9c8],0x0 007aa3c0 jne 0x7aa3ca 007aa3c2 lea ecx,[esi+0x4] 007aa3c5 call 0x426e00 ; RNG_Twist -- THE LAZY TWIST INSIDE NextFloat 007aa3ca mov ecx,[esi+0x9c4] / dec [esi+0x9c8] / mov eax,[ecx] / add ecx,4 ... 007aa3ea and edx,0xff3a58ad ... 007aa3f7 and ecx,0xffffdf8c ... 007aa40c fild DWORD PTR [ebp-0x20] 007aa40f jns 0x7aa417 007aa411 fadd QWORD PTR ds:0x9e61b8 ; +2^32 007aa417 fmul QWORD PTR ds:0x9e61b0 ; × 1/(2^32-1) ``` The only call-graph edge this leaves is `FUN_007aa240 → RNG_Twist`, which reads as a bare twist and is not one. **This is the site rule 16 exists for**, and the second confirmed instance of the pattern after lane J's R2. **The accept test, derived from the ISA rather than the mnemonic (rule 10).** `fld [ebp-0x20]` (r), `fld [ebp-0x18]` (thresh), `fcompp` compares **st0 = thresh against st1 = r**. `test ah,5` isolates C0 and C2: `ah&5` is 0 for *thresh > r*, 0 for *thresh == r*, 1 for *thresh < r*, 5 for unordered. PF is even for 0 and 5, so `jp 0x7aa45a` — the **accept** branch — is taken iff **`thresh >= r`** or unordered. **Equality accepts.** A reimplementation that writes `thresh > r` diverges on the exact-tie word and on any NaN. **The constants**, read from `.rdata`: `ds:0x009e1e68` is `0.0` (double) and `ds:0x00a23a6c` is `0.25f`. So `scale = 1.0 - 0.0 = 1.0` and `r = (float)(0.0 + 1.0 × unit) = float(unit)` — the `fsub`/`fadd` pair is the MSVC float-literal-zero idiom, not an offset. And the threshold is a flat **1-in-4**. **Word cost.** One word per *(contact, detector)* **trial**, drawn **before** the accept test and **regardless of the threshold**. Therefore: ``` words(call) = SUM over contacts c of min( T_c , |detectors| ) T_c = trials until the first detector accepts c ``` * If every detector holds tech 0x2728 or 0x2729, each trial accepts with p = 1/4 and `E[words] = |contacts| × 4 × (1 − 0.75^|detectors|)` — 1 word for one detector, 1.75 for two, 2.3125 for three, 2.734 for four, → 4 asymptotically. * If **no** detector holds either tech, `thresh = 0.0f` and the test `0.0f >= r` can only pass on the word `0x00000000`. Every trial still burns a word, so the cost is **exactly `|contacts| × |detectors|`** and no contact is ever assigned. **A detection roll that can never succeed still moves the generator.** That asymmetry is the single most important thing about this function for a standalone. **The outer loop is not a redraw loop, and rule 17 is why I checked.** `0x007aa583 cmp BYTE [ebp-0xd],0 / je 0x007aa2d0` is a repeat-until-no-progress back-edge that a truncated dump would hide. It cannot redraw a pair: `tried[]` is filled once *before* the loop (0x007aa296, i.e. above the back-edge target 0x007aa2d0) and **never cleared**, so the second pass finds every pair tried, evaluates nothing, clears nothing, and the flag survives at 1. **At most two passes, and the whole-call cost is bounded by `|contacts| × |detectors|`.** Total words never exceed that no matter how the passes interleave. **Rule 17, positively.** Ghidra reports **944** bytes, i.e. an end of 0x007aa5f0 — which lands **inside** the five-byte `push 0x9e1f90` at 0x007aa5ee. The real body ends at **0x007aa5f9**, after the `call ds:0x9dd150` (`std::vector` length_error throw) at 0x007aa5f3; padding runs to the next function start 0x007aa600. **Real size 953.** Here the nine hidden bytes are only a throw stub and change no draw count — but they are outside the range, the range is wrong, and the same defect one function over changed lane J's answer. ### 2.3 How it is reached, and what gates it ``` StrategyServer::ProcessTurn 0x007dc6c0 @0x007dcc19 -> StrategyServer::DetectEncounters 0x007d7f70 (lane T: the LAST phase of the turn) @0x007d8470 -> EncounterDetect_Run 0x007cb080 @0x007cb0f9 -> EncounterDetect_ProcessTeamRecord 0x007ca640 (once per 0x74-byte team record) @0x007ca73a -> EncounterDetect_AssignContacts 0x007aa240 ``` `EncounterDetect_Run` builds the 12-byte context — `{S, TechDef(0x2729), TechDef(0x2728)}`, the two techs looked up by id through `0x0057d610(g_0x00b2d540->+0x110, id)` — and then walks the team-record vector with the `0x8d3dcb09 / sar 6` divide-by-0x74 idiom. `EncounterDetect_ProcessTeamRecord` is `ret 4` and gates the draw three times, each of which is a **whole-call zero**: 1. `0x007892d0(rec)` at 0x007ca671 — false unless **some** entry of `rec->(+0x28 .. +0x2c)` (stride 0x44) has `entry[0]->+0xfc != 0`. False → the function returns without building anything. 2. `detectors = 0x007949b0(rec)` — the entries whose object has `+0xfc == 0` **and** `+0xfb == 0`. Empty → return. 3. `contacts = 0x00791460(rec)` — the entries whose object has `+0xfc != 0`. Empty → return. `0x0057d7e0`, the predicate that chooses 0.25f over 0.0f, is 40 bytes and reads in full as `bool TechTree::HasTechComplete(TechTree* this, TechDef* def) { if (!def) return false; node = this->+0x10[def->+0x00]; return node && node->+0x14 == 4; }` — state 4 is *researched*. `detector->+0xf4` is therefore a `TechTree*`. So the mechanism is: **a team record's `+0xfc`-flagged members are contacts to be spotted; its unflagged members are the detectors; each detector with one of two specific techs spots a given contact with probability 1/4 per trial; the first to succeed claims it — and every trial costs a word whether the detector could ever have succeeded or not.** The semantic reading (which flag means what) is **inferred**; everything about the loop shape, the gates and the word cost is instruction-verified. --- ## 3. The other newly-visible draw under `ProcessTurn` ### 3.1 `ProbabilisticJump` 0x007b6700 draws **two** words, not one `addresses.json` already carries this function with the note *"v = float32(NextFloat() × player->CstE); arrives iff !(v > player->CstT…)"*. That is right and incomplete. The **failure** branch draws again: ``` 007b677c call 0x47d830 ; RNG_NextFloat, ECX = rng+4 <-- WORD 1, unconditional 007b6781 fmul ... fadd 0.0 ... ; v = 0.0 + (X - 0.0) * unit = float(unit * X) 007b67a8 fld DWORD [ecx+0x158] 007b67ae fcompp / fnstsw / test ah,1 / jne 0x7b67d5 007b67d5 ... ; the branch taken when [+0x158] < v 007b67db mov ecx,[edx+0x16c] ; ECX = the RNG OBJECT this time, not rng+4 007b67e7 call 0x4f7670 ; Mars_RNG_NextUInt <-- WORD 2, conditional 007b67ec mov [ebp-0x194],eax 007b6800 call 0x8a6fa0 ; word -> pointer to three floats (a scatter direction) ``` On the arriving branch (0x007b67b7) it copies the destination's `+0x18/+0x1c/+0x20` and never draws again. So: ``` words(ProbabilisticJump) = 1 + [ dest->+0x58->+0x158 < float(unit * dest->+0x58->+0x154) ] ``` and the call is reached once per fleet whose current waypoint is type 5, via `ProcessFleetMovement 0x007da9a0 → MoveFleet 0x007d9ee0 (@0x007da0c2)`. `MoveFleet` is also **self-recursive** (0x007d9ee0 calls itself), which a per-call ledger must not double-count. Note the convention flip inside one function: the `NextFloat` at 0x007b677c is entered with `ECX = rng+4`, the `NextUInt` at 0x007b67e7 with `ECX = rng`. 0x6b bytes apart, in one straight-line function. --- ## 4. The RNG entry-point set, corrected and completed Seven functions in the image take a `Mars::RNG` and return a draw. Three were in `ghidra/addresses.json`; lane Z added a fourth from the caller side; this lane confirms that one from the instruction stream and adds three more. | VA | name | receives the generator as | words per call | |---|---|---|---| | 0x0047d830 | `RNG_NextFloat` | `&mt` = **object + 4** | exactly 1 | | 0x004271c0 | `RNG_NextInt` | `&mt`, bound **by pointer** | 1 + rejections | | 0x008e6dd0 | `RNG_Chance` | **the object** (`add ecx,4` inside) | **0 or 1** — `p<=0` and `p>=1` return with no draw | | 0x004f7670 | **`Mars_RNG_NextUInt`** | **the object** | exactly 1, unconditional | | 0x0047d8a0 | **`Mars_RNG_FloatRange`** | **the object** | exactly 1 | | 0x008e6d80 | **`Mars_RNG_IntRangeBell`** | **the object, as a stack arg** | **≥ 2** | | 0x008e6e30 | **`Mars_RNG_GaussianRange`** | **the object, as a stack arg** | **2 per attempt, unbounded** | ### 4.2 `Mars_RNG_FloatRange` 0x0047d8a0 — 41 bytes, `ret 8`, in `ProcessTurn`'s closure at depth 3 ``` 0047d8a3 add ecx,0x4 0047d8a6 call 0x47d830 ; NextFloat -> st0 = unit 0047d8ab fld [ebp+0xc] (hi) / fld [ebp+0x8] (lo) / fld st(0) / fsubp st(2),st ; st1 = hi-lo 0047d8b5 fxch st(2) / fmulp st(1),st ; (hi-lo) * unit 0047d8b9 fstp DWORD [ebp+0xc] ; ***** ROUNDED TO FLOAT ***** 0047d8bc fadd DWORD [ebp+0xc] ; lo + that 0047d8bf fstp DWORD [ebp+0xc] ; ***** ROUNDED AGAIN ***** 0047d8c2 fld DWORD [ebp+0xc] ; ret 8 ``` `lo + (float)((hi − lo) × unit)`, narrowed **twice**. Evaluating the expression in double and narrowing once disagrees on a measurable fraction of words — modelled and pinned in `sots-engine` (`tests/mars_stream/test_rng.cpp` asserts the two models are *distinguishable*, so the shortcut cannot creep back in unnoticed). Reached from `ProcessTurn` at depth 3 via `ServerPlayer::ProcessTurn → 0x00889dc0`, call sites 0x0088a1bd and 0x0088a20f. ### 4.3 `Mars_RNG_IntRangeBell` 0x008e6d80 — 70 bytes, cdecl, plain `ret` `h = hi − lo`; `half = h/2` truncated **toward zero** (the `cdq / sub eax,edx / sar eax,1` idiom, not an arithmetic shift alone); returns `lo + NextInt(half) + NextInt(h − half)`, the halves drawn **in that order**, both entered with `ECX = rng + 4` and both taking the bound by pointer. **Triangular, not uniform**, and **at least two words** — each `NextInt` carries its own rejection loop. The bounds reach the draw as `uint32`, so an inverted range yields a huge first bound rather than an empty one. Modelled in `sots-engine` as `MT19937::int_range_bell`. ### 4.4 `Mars_RNG_GaussianRange` 0x008e6e30 — the only unbounded entry point 410 bytes, cdecl, plain `ret`, four stack args `(rng, lo, hi, mode)`. **Both** of its draws are **inlined** (temper chains at 0x008e6eb8 and 0x008e6f34), so a call-graph sweep sees only two bare `RNG_Twist` edges. Box–Muller with rejection: ``` half = (hi - lo) / 2 ; via fild/fmul 0.5 mean = 2.1 * ((mode - lo)/half - 1) loop: y1 = ; WORD 1 z = sqrt( -2 * ln( 1 - (y1 + 0.5) * 2^-32 ) ) ; 0x0092537c = log, 0x00924f52 = sqrt y2 = ; WORD 2 z = z * cos( 2*pi * y2 * 2^-32 ) + mean ; 0x00924f46 = cos if (z > 2.1 || z < -2.1) goto loop ; 0x008e6f8d / 0x008e6fa0 -> 0x008e6e7f return lo + ftol( ((z + 2.1) / 4.2) * (hi - lo) ) ; 0x00925220 = ftol ``` **Two words per attempt, attempts unbounded.** And note the divisor: this path scales by **`2^-32`** (`ds:0x00a3b6f0`) with a `+0.5` offset on the word, **not** the `1/(2^32−1)` at `ds:0x009e61b0` that `NextFloat` uses. Two different divisors in one image; rule 3 records that the campaign has already been burned once by getting a divisor wrong in a way behavioural comparison could not see. Three callers (0x00786200, 0x00786230, 0x00798040); reachable from no turn driver by a direct call. --- ## 5. Reconciliation against lane Z's ledger — with the residual stated plainly Lane Z measured **18 / 19 / 20 / 18 words** for turns 3–6 of `ref-turn2`, *all* inside `StrategyServer::ProcessTurn`, residual outside the two turn drivers **exactly zero**, and confirmed turn 6's 18 independently from the two autosave files. `docs/mars-rng.md` records the same order of magnitude from a third instrument: `left` across three corpus saves runs 454 → 432 → 413, i.e. ~20 per turn. Whatever those 18–20 words are, they come out of the sites below. The **complete** draw-site inventory of the `ProcessTurn` direct-call closure (1,426 functions) is **22 sites**: 21 calls to an entry point, plus the one inlined site. | depth | caller | entry point | site | what it is | |---|---|---|---|---| | 2 | `TechTree_ProcessResearch` 0x005876c0 | NextFloat ×2 | 0x00587888, 0x005878bb | per player | | 2 | `ServerSystem_ProcessRebellion` 0x007583b0 | Chance | 0x00758966 | per system | | 2 | 0x00889dc0 | NextFloat, NextInt ×3 | 0x0088a08f, 0x00889e40/f86/fd6 | per player, all behind top-of-function gates | | 2 | `ServerPlayer_RollResearchEvent` 0x0088df20 | NextFloat | 0x0088df4f | per player, **unconditional** (lane T) | | 3 | **`Mars_RNG_FloatRange` 0x0047d8a0** | NextFloat | 0x0047d8a6 | ← 0x00889dc0 @0x0088a1bd, @0x0088a20f | | 3 | 0x00747f50 | NextInt ×2 | 0x00747fa8, 0x00747ffd | ← `MoveFleet` | | 3 | 0x0074fbe0 / 0x00753c60 / 0x00756350 | Chance ×3 | — | ← `ProcessRebellion` | | 3 | 0x00792750 | NextInt | 0x007929a4 | ← `DetectEncounters` | | 3 | **`ProbabilisticJump` 0x007b6700** | NextFloat **+ NextUInt** | 0x007b677c, **0x007b67e7** | ← `MoveFleet`; §3.1 | | 3 | `RNG_Chance` 0x008e6dd0 | NextFloat | 0x008e6e04 | the inside of `Chance` | | 4 | 0x00503200 | NextFloat | 0x0050329d | ← `DetectEncounters` | | 4 | 0x008134e0 | Chance | 0x0081351c | ← the end-of-turn tail | | 4 | 0x00889bb0 | NextInt | 0x00889c33 | ← 0x00889dc0 | | **4** | **`EncounterDetect_AssignContacts` 0x007aa240** | **inlined NextFloat** | **0x007aa3b6** | **§2.2 — invisible to every call-graph sweep** | **The arithmetic, honestly.** My inventory adds **two** previously-uncounted draw sources to this list — the inlined site at 0x007aa3b6 and the `NextUInt` at 0x007b67e7 — plus one previously-unnamed entry point (`FloatRange`) at two existing call sites. It does **not** account for 18–20 words. It cannot: both new sources are gated, and neither has been measured. * `EncounterDetect_AssignContacts`: `|contacts| × |detectors|` at worst, `|contacts| × 4(1−0.75^|detectors|)` in expectation when the detectors are teched, **0** when the team record has no `+0xfc` member. Lane Z observed the encounter vector go 0 → 1 inside `DetectEncounters` on every one of four turns, so this path was *entered* every turn; whether the `+0xfc` gate passed is not knowable from statics. **Live candidate, unmeasured incidence, contribution 0..(|contacts|×|detectors|).** * `ProbabilisticJump`'s second word: **0** on any turn with no type-5 waypoint whose arrival test fails. `ref-turn2` is a two-player game in contact; nothing says a node jump occurred. * Everything else in the table was already visible to a call-graph sweep and none of it has a measured per-turn count either. **So the residual is: of lane Z's 18–20 words per turn, this lane explains between 0 and a state-dependent handful, and 18–20 minus that remains unattributed.** Stating it the way lane P stated its "short by exactly 1": **the inlined-draw inventory closes the *search space* — after this document there are 22 sites and no twenty-third — but it closes none of the *count*.** What is now provable is the negative that matters: > **There is no unaccounted-for draw mechanism left in `StrategyServer::ProcessTurn`.** The 18–20 words > are distributed among exactly these 22 sites (subject to the indirect-call caveat in §1), and the > largest previously-unknown one is a per-(contact, detector) roll that costs a word even when it cannot > succeed. That is a smaller claim than "here are your 18 words" and a bigger one than lane J's prediction, which was that the two depth-4 functions would *explain* the gap. **Lane J's prediction is not confirmed.** `FUN_004f7670` is a primitive that costs one conditional word on a fleet-movement path, and `FUN_007aa240` costs a state-dependent number that may well be zero on lane Z's workload. If lane Z's next run brackets `DetectEncounters` the way it bracketed the tail, one measurement settles it. ### 5.1 What would falsify each claim here | claim | how it would be shown wrong | |---|---| | 0x004f7670 is exactly one word, always | a ledger seeing `ProbabilisticJump` cost anything other than 1 or 2 words | | 0x007aa240 draws once per *trial*, not per *assignment* | a bracket of `DetectEncounters` whose word count equals the number of contacts assigned rather than the number of pairs tested | | the accept test is `thresh >= r` (equality accepts) | a tie word producing no assignment — needs a forced state, not a normal run | | a detector without either tech still burns a word | a team record with untech'd detectors costing 0 words | | the outer loop cannot redraw a pair | any `DetectEncounters` bracket exceeding `\|contacts\| × \|detectors\|` | | only one of eleven inlined sites is in the turn | a nonzero residual reappearing *inside* `ProcessTurn` after all 22 sites are hooked | --- ## 6. What this lane did not settle * **Indirect calls are not modelled.** §1's reachability is direct-call only, and nine of the eleven inlined-draw functions hang off vtable slots with no direct caller. Their subtrees are not in `ProcessTurn`, but "not in the closure" is proved only for direct edges. * **The semantics of `+0xfb` / `+0xfc`** on the team-record members — which is the contact and which is the detector in game terms — is inferred from the filters, not from a string, an RTTI name or a behavioural observation. The *arithmetic* does not depend on it; the *story* does. * **Tech ids 0x2728 / 0x2729** (10024 / 10025) were not resolved to names against the tech data. * **No draw here has been observed running.** Every count in this document is static. Lane Z's instrument exists and brackets four functions; the useful next step is a fifth bracket on `EncounterDetect_ProcessTeamRecord` with `|contacts|` and `|detectors|` in the argument record — which turns §2.2's formula into a testable prediction in one turn. * **`Mars_RNG_GaussianRange` is not modelled** in `ours` and should not be until something reaches it: its stream position depends on `log`, `sqrt` and `cos` matching the original CRT bit for bit. * **The eleven game functions' 28 sites** were classified (float vs uint) and counted but only two of the eleven were read. The other nine are map setup, lobby, network and encounter code, and none of them is on the standalone's critical path — but their per-call costs are unknown. --- ## 7. Corrections to earlier findings Per rule 11, plainly, in place: 1. **`combat-resolver.md` §0.1 (lane J): "fourteen game functions with inlined MT draws".** The list of fourteen contains one **false positive** (`FUN_008cca30`, a call displacement) and two **RNG primitives** (`FUN_004f7670`, `FUN_008e6e30`). The correct figure is **eleven game functions, 28 sites**. The sixteen-function scan total and the "two in `ProcessTurn`'s closure at depth 4" both stand. 2. **`combat-resolver.md` §0.1: `FUN_004f7670` and `FUN_007aa240` are "the leading candidate mechanism" for lane Z's per-turn gap.** Read in full, `FUN_004f7670` is a one-word primitive on a conditional fleet-movement branch and `FUN_007aa240` is a gated per-pair roll. Neither is a plausible source of 18–20 words on a quiet turn. The gap is still unattributed; see §5. 3. **My own Ghidra plate comment at 0x004f76a4, written earlier today**, claimed the tempering masks are a Mars variant of MT19937. They are the standard masks re-expressed; corrected in place, and `mars::rng` in `sots-engine` was never wrong. 4. **`addresses.json`'s `ProbabilisticJump` prototype** describes one `NextFloat`. It draws a second word on the non-arrival branch. Recorded at `ProbabilisticJump_NextUIntDraw` rather than edited into the shared file, per the fragment convention. 5. **`RNG_size` / the primitive set in `addresses.json`** lists three draw entry points. There are **seven**; §4. --- ## 8. Artefacts * `ghidra/addresses.d/lane-i.json` — 16 entries: 4 entry points, 4 encounter-detection functions and sites, 3 named sites inside 0x007aa240 (draw, accept test, outer back-edge), the real-end marker, the `ProbabilisticJump` second draw, the false positive, and the two tempering masks as constants. Generated to a scratch path only; the tracked header is untouched while lane Z is in flight. * Ghidra: 8 prototypes, 9 plate comments and 8 pre-comments written back, all verified applied. * `sots-engine` `wip/inlined` (`dc43f93`): `MT19937::float_range` / `range_from` / `int_range_bell` with tests, and the entry-point table in `docs/mars-rng.md`. `tools/clean_room_check.sh` → OK and host `ctest` → 36/36, run as separate commands. No `src/shim` change, so no cross-build is implicated.