# The output → money term (lane E1, 2026-09-08) `ServerSystem::ComputeMaxIncome` and its `TradePointsToMoney` tail — the second chain `ComputeBudget` needs, and the one `output-term.md` §6 named as the reason the verified output total did **not** unblock `P01 P02 P03 P05 P06`. Evidence: own `objdump -d` pass over `Sword of the Stars.exe`, every range disassembled **to the next function start** and the real boundary found (rule 17). Field names per `findings/objects/struct-recovery.md`. Everything below marked *instruction-verified* is read off the instruction stream; everything marked *inferred* is not. --- ## 1. The call chain | addr | name | conv / boundary | note | |---|---|---|---| | 0x007521c0 | `ServerSystem::ComputeMaxIncome()` → int | thiscall, ends 0x0075223d | Ghidra size 126 = correct | | 0x00751bb0 | `ServerSystem::ComputeOutputFromRates(int out[12], OutputRates*)` | thiscall `ret 8`, ends 0x00751fa0 | **repairs ships** — not compare-safe | | 0x007505b0 | `ServerSystem::TradePointsToMoney(double trade)` → double | thiscall `ret 8`, ends 0x007506c8 | Ghidra size 281 = correct | | 0x0074d760 | `ServerSystem::PopIncome(int groupType)` → double | thiscall `ret 4`, ends 0x0074d8e1 | | | 0x0074b700 | `ServerSystem::SlaveIncome()` → double | thiscall, ends 0x0074b793 | | | 0x00535e80 | `GroupIncome(int groupType, int64 count)` → int | **cdecl**, tail-jumps `_ftol2` | the per-capita income term | | 0x00535e00 | `PopTypeRow(int t)` | cdecl | same 3-row table as the output term | | 0x007484d0 | `ServerSystem::CalcSuitMod(int species)` → double | thiscall `ret 4`, ends 0x00748560 | the suitability money **cost** | | 0x0080f470 | `StrategyServer::IncomeDifficultyMod(ServerPlayer*)` → float | thiscall `ret 4`, ends 0x0080f49f | **the missing ×1.1** | | 0x0059b490 | `DifficultyMods::Select(ServerPlayer*)` → float* | thiscall `ret 4`, ends 0x0059b4b3 | picks the AI or the non-AI triple | | 0x005a3990 | `LoadDifficultyRow(int level, DiffRec* out)` | cdecl | fills the 0x1c record | | 0x005a3870 | `BuildDifficultyTable(vector* out)` | thiscall | **the table is built in code** — §3 | | 0x0080dd10 | `ServerPlayer::GetIncMod()` → float | thiscall | `player+0x30c` | | 0x0080dd20 | `ServerPlayer::GetSpeciesCostFactor()` → float | thiscall | `SpeciesDef(Species)->+0x24` | | 0x0080f4b0 | `StrategyServer::IdealSuit(int species)` → float | thiscall `ret 4` | `server+0xf8 + species*4` | | 0x00746890 | `ServerSystem::TerraformPointsNeeded()` → double | thiscall | always ≥ 0 — §2.4 | | 0x009dd1e4 | `ceil` (MSVCR100) | import | resolved from the IAT | | 0x00925086 | `_CIfmod` (MSVCR100) | import thunk | | --- ## 2. The formula ### 2.1 `ComputeMaxIncome()` — 0x007521c0 (instruction-verified) ``` rates = { SRt = 1.0, SRsc = SRtf = SRi = SRoh = SRs = 0.0, SRnr = 0 } NormaliseOutputRates(&rates, this, 0) // 0x00747390, cdecl int out[12] = {0} ComputeOutputFromRates(out, &rates) // 0x00751bb0 return (out[3] > 0) ? out[3] : 0 // a `jg`, so a negative colony contributes 0 ``` `NormaliseOutputRates` clamps only trade to `[0,1]` and rescales the other three to `1 − trade`; with trade already 1.0 the other three stay 0 and `SRoh` is untouched. So the max-income rate vector really is "all output to the trade channel, no over-harvest". ### 2.2 `out[3]` inside `ComputeOutputFromRates` — the money channel (instruction-verified) The last four instructions of 0x00751bb0 are ``` out[3] = _ftol2( TradePointsToMoney( [ebp-0x28] + [ebp-0x40] + [ebp-0x18] ) ) ``` with * `[ebp-0x40]` = `round(round(ComputeTotalOutput(SRoh)) × SRt)` — the **trade points**; * `[ebp-0x28]` = the trade share of the leftover **ship-construction** points, non-zero only when the build queue and the repair pass left some over; * `[ebp-0x18]` = leftover **terraforming** points, itself fed by leftover **industry** points. **Correction (lane C3).** This section originally called `[ebp-0x28]` "leftover **science** points". There is no science channel in this function: `OutputRates+4` is the ship-construction slider `SRsc`, 0x00746830 turns it into construction points, and the leftover is what the build queue (0x008251e0) and the ship-repair pass (0x00751590) did not spend. Research is bought with money at the empire level, not produced per system. `strategic-turn-internals.md` §3.3 had this right. See `output-turn-path.md`. `round` is 0x008e5660 (`fistp`/`fild`, ties to even); the outer conversion is the truncating `_ftol2`. Under the max-income rate vector all three of science, terraform and industry points are `round(T × 0) = 0`, and §2.4 shows both cascade sources are then zero, so > **`out[3] = trunc( TradePointsToMoney( round_half_even( ComputeTotalOutput(0) ) ) )`.** ### 2.3 `TradePointsToMoney(trade)` — 0x007505b0 (instruction-verified) ``` t = PopIncome(0) // 0x0074d760(this, 0) t = ((trade − fmod(trade, 5.0)) × 5.0 + 0.0) + t // 5.0 @0x009e2398, 0.0 @0x009e1e68 t = PopIncome(1) + t t = SlaveIncome() + t // 0x0074b700 owner = sys->PID // +0x100 t = float32( owner ? SpeciesDef(owner->Species)->f18 : 1.0 ) × t t = IncomeDifficultyMod(sys->server, owner) // 0x0080f470, a float32 × ( float32(owner ? owner->IncMod /*+0x30c*/ : 1.0) × t ) sp = owner ? (sys->indi ? sys->indi->[4] : owner->Species) : -1 // +0x1c8 cost = float32( owner ? SpeciesDef(owner->Species)->f24 : 1.0 ) × ( CalcSuitMod(sp) × 10000.0 × 1.5 ) // 10000.0 @0x009e9398, 1.5 @0x009e90b8 return t − cost // a DOUBLE; the caller truncates ``` Three corrections to `formula-gaps.md` Q3, which had this block as MEDIUM confidence: 1. **The function returns a double, not an int.** The `ftol` is in `ComputeOutputFromRates`, not here. 2. **`t` is `(trade − fmod(trade,5)) × 5`, not `× 5` of the block count** — i.e. whole 5-point blocks are worth **25** money each, not 5. The literal at 0x009e2398 is used twice, once as the modulus and once as the multiplier. 3. **Every per-player multiplier is narrowed to float32 before use** (`fstp DWORD [ebp+0xc]` / `fld DWORD [ebp+0xc]`, three times), and the additive chain associates `Slaves + (Pop1 + ((blocks + 0.0) + Pop0))`. x87 is not associative; this matters at the ulp. `sys->server` is `ServerSystem+0x10`, the **raw** StrategyServer base (B4 trap 1). ### 2.4 The two cascade terms are zero under max-income (instruction-verified) * Industry: `infraNeed = ceil((1.0 − sys->Infra) / 3.3e-5)`. `Infra` is clamped to `≤ 1.0` by `ApplyInfraBonus`, so `infraNeed ≥ 0`, so `leftoverIndustry = 0 − min(0, infraNeed) = 0`. * Terraform: `TerraformPointsNeeded` (0x00746890) ends `|Δsuit| / |rate × sign / 20000|` — the `fabs` at 0x00746906 is **after** the sign multiply, so the sign cancels and the result is always `≥ 0`. `leftoverTerraform = 0 − min(0, ceil(that)) = 0`. (This corrects the implication in `output-term.md`'s "unspent terraforming points cascade into the money channel": they do cascade, but only when the terraform channel is actually funded.) * Construction: `out[7] = 0x00746830(sys, 0.0)`; with zero construction points nothing is left over and `[ebp-0x28]` keeps the `fldz` written at 0x00751d5f. **Lane C3 upgraded this from inferred to instruction-verified**: 0x00746830 with an argument of 0 returns `ftol(k x (b x 0) + 0) = 0` on both its branches, so the guard really does fail. ### 2.5 `PopIncome(t)` — 0x0074d760 (instruction-verified) ``` sum = 0.0 for sp in 0..6: n = GroupPopulation(t, sp) // 0x00747ba0, int64 surplus = 0 if (owner && sys->indi == 0 && t == 1 && sp == owner->Species): MaxPop(sys, owner, sp, &A, 0, 0) // 0x0074a6d0, out slot 4 MaxPop(sys, owner, sp, 0, 0, &B) // out slot 6 surplus = (B > A) ? (B − A) : 0 total = surplus + n if (total > 0): mo = (t == 1) ? MoraleOutputMod(sp) : 1.0 // 0x00746910, the SAME helper as output ad = sys->addiction[sp] ? ADDICTION_INCOME_MOD : 1.0 // int[7] @+0x1e4; slot 0x00aeca48 r = GroupIncome(t, total) // an int sum += (double) trunc( (double)r × mo × ad ) return sum SlaveIncome(): // t = 2, no morale, no surplus same loop with n = SlaveCount(sp) (0x0074b610) and no morale factor GroupIncome(t, count): // 0x00535e80, cdecl return _ftol2( float32(POPTYPE[t].income) × ((double)count / 14000.0) ) ``` **So the per-capita income rate is `typeIncomeMod / 14000` — no 1.8 and no 500000.** For imperial population that is `1/14000` money per head; the output law over the same head is `1.8/500000` points. Two different laws off two adjacent columns of the same three-row table. Note the **double truncation**: `GroupIncome` truncates, and then the morale/addiction product is truncated again, per species, before it is summed. Summing first and truncating once is wrong on any colony with more than one species or a non-unit morale/addiction factor. ### 2.6 `CalcSuitMod(species)` — 0x007484d0 (instruction-verified) ``` if (sys->vnh /*+0xc6*/) return 0.0 if (sys->PID == 0) return 20.0 // 0x009e2c08 − 0.0, and it logs a warning if (owner->RebAI /*+0xfc*/) return 0.0 d = | IdealSuit(species) − sys->Suit /*+0x64*/ | return (d <= owner->SuitTol /*+0xb4*/) ? d : owner->SuitTol ``` `IdealSuit` is `server->float[0xf8 + species*4]`, the **species baseline**, not the player's own `IdealSuit` field. In all 11 corpus saves the two agree for every player of that species, which is what makes the oracle self-contained (§5). So the money cost of a badly-suited colony is `speciesCostFactor × min(|Δsuit|, SuitTol) × 15000`, and `SuitTol` therefore caps the cost as well as extending the habitable range. --- ## 3. The difficulty multiplier — where it comes from (instruction-verified) `formula-gaps.md` Q3 named `DifficultyMods(owner)->+4` and left it there. It resolves to a **three-row table built in code from `.rdata` float literals**, exactly as lane N found for the pop-type table. There is no data-file dependency and no `GlobalConst` key. ### 3.1 Selection ``` StrategyServer::IncomeDifficultyMod(ServerPlayer* p): // 0x0080f470 m = float32(server->IncMod) // raw base +0xbc == save tag `IncMod` if (p) m = float32( float32(Select(p->diffMods /*+0x36c*/, p)[1]) × m ) return m // float32 DifficultyMods::Select(rec, p): // 0x0059b490 return (p && p->isAI /*+0xf9*/ && !p->NPC /*+0xfb*/) ? &rec->f[0] : &rec->f[3] ``` `ServerPlayer+0x36c` is an **unnamed, unsaved pointer** sitting between `aidf` (+0x368) and `civr` (+0x370) — it is not in `struct-recovery.md` §2 because the serializer never touches it. `ServerPlayer::Read` (0x008804d0) sets it at 0x00880fa3: ``` if (0 <= aidf && aidf < 3) { LoadDifficultyRow(aidf, p->diffMods); p->aidf = aidf; } ``` `p->isAI` at `+0xf9` is **not on the wire**. It is copied from the setup/network player record at `+0xd` by 0x0077b620, alongside `PvMA`(+0x18c), `+0xfa` and `Elim`(+0xf8). 0x0080d7a0 reads it as `NPC || RebAI ? 0 : (isAI ? 2 : 1)`, which is what fixes its polarity. This is a **game-setup input the save does not carry**; §5 states how the engine takes it. ### 3.2 The record and the table `LoadDifficultyRow(level, out)` (0x005a3990) writes the default `{1, 1,1,1, 1,1,1}` first, then linear-searches the table for `id == level` and copies its six floats. So an out-of-range level yields all-ones, not a crash. ``` struct DifficultyMods { // 0x1c bytes, stride confirmed by the 0x92492493 magic divide int id; // +0x00 float aiMaintDivisor; // +0x04 ComputeBudget: Maint /= ftol(this) float aiIncomeMod; // +0x08 TradePointsToMoney, and trade-route income float aiResearchMod; // +0x0c research points bought with money float plMaintDivisor; // +0x10 float plIncomeMod; // +0x14 float plResearchMod; // +0x18 }; ``` The three consumers, all reading through `Select`'s returned triple pointer: | triple offset | reader | what it scales | |---|---|---| | `+0` | `ComputeBudget` 0x0086338b | `Maint = Maint / ftol(m)` -- a fleet-upkeep **divisor** | | `+4` | `IncomeDifficultyMod` 0x0080f470, and the trade manager at 0x00833938 | a system's money income, and a trade route's | | `+8` | `ResearchPointsFromMoney` 0x0080e229, inlined again in `ComputeBudget` at 0x00863618 | research points bought with money | `BuildDifficultyTable` (0x005a3870) push_backs three rows: | id | AI: maint / | AI: **income x** | AI: research x | | else: maint / | income x | research x | |---|---|---|---|---|---|---|---| | 0 | 1.0 | 1.0 | 1.0 | | 1.5 @0x00a1b000 | 1.5 | 1.5 | | 1 | 3.0 @0x00a0451c | **1.1 @0x009f957c** | 1.5 @0x00a1b000 | | 1.0 | 1.0 | 1.0 | | 2 | 1e6 @0x009ebd7c | **1.7 @0x009f9580** | 2.0 @0x00a04518 | | 1.0 | 1.0 | 1.0 | Read as: on **easy** (id 0) the *player* gets the break -- 1.5x income, 1.5x research and 2/3 maintenance -- and the AI is unmodified; on **normal** (id 1) and **hard** (id 2) the *AI* gets the break, and on hard its fleet maintenance is divided by a million, i.e. free. **Every player record in all 11 corpus saves carries `aidf = 1`**, so the live multiplier for an AI-owned system in this corpus is exactly `1.1f = 1.100000023841858` — which is the ×1.1 the `BnkEl` oracle measured and `formula-gaps.md` Q3 could not name. The other two columns are named here because they come off the same record and the same selector, and both already have a home in `game::sim::BudgetInputs` (`maintenanceDivisor`, `researchDifficultyMult`) that had no source until now. --- ## 4. PREDICTION — written before the run The oracle is `tools/max_income_oracle.py`, which inverts the stored `BnkEl` to `Σ max(ComputeMaxIncome(s), 0)` over each player's owned systems: 25 player-records over 11 saves. Lane N's predictor scores **6/25** — every human-owned or independent record, no AI-owned one. ### 4.1 What I expect 1. Adding **only** the difficulty multiplier (`1.1` on AI-owned systems) does **not** reach 25/25. The single-system AI empires close; the multi-system ones do not, because they miss the suitability cost (§2.6), which is zero on a homeworld and non-zero on every other colony. 2. Adding the suitability cost, the species income/cost factors, the per-species truncation of `PopIncome`, and the civilian species loop closes the rest: **25/25**. 3. `SpeciesDef +0x18` (income factor) and `+0x24` (cost factor) are data-file values. Q3 quotes Zuul 1.1 / Morrigi 0.8 for the first and Zuul 0.7 for the second. If those are right the Zuul saves close with them and miss without them. 4. `rbfl` (rebelling) zeroes `ComputeTotalOutput` but **not** the money chain — the population income and the suitability cost still apply. Lane N's predictor returns 0 for such a system; that is wrong, and no corpus system is rebelling, so it is untested either way. ### 4.2 Falsification — how this could be wrong, and the symptom of each | way it could be wrong | symptom | |---|---| | `p->[0xf9]` is not "is AI" but something else | the ×1.1 lands on the wrong records; the human records that match today would break | | the blocks term is `×5` not `(t − t mod 5) × 5` | every record off by a multiple of the block count | | `POPTYPE[1].income` is not `0.33f` | civilian-bearing colonies off by a clean ratio; Zuul (no civilians) unaffected | | the per-species truncation is really one truncation of the sum | off by at most 6 per colony, and only where two species or a non-unit factor coexist | | `IdealSuit` is the player's field, not the server's species array | invisible in this corpus, where they are equal — so this stays **unverified** | | `SpeciesDef +0x24` for species 4 (the NPC species) is not 1.0 | only the NPC-owned records miss | | the science-cascade term is not zero | every record short by the same non-zero amount | --- ## 5. Result **The oracle goes from 6/25 to 25/25.** No VM time was spent: `BnkEl` is an inversion of the number under test, so the corpus states the answer for every player-record it holds. ``` tools/max_income_oracle.py --json oracle.json tools/max_income_predict.py verify/results/saves/*.sav --oracle oracle.json -> 25 match, 0 differ, 0 with no oracle record ``` ### 5.1 Every prediction in §4.1, checked | predicted | outcome | verdict | |---|---|---| | the difficulty multiplier alone does not reach 25/25 | 13/25 with it; the twelve Zuul records still missed | HELD | | the remaining terms close the rest | 25/25 | HELD, but **for a different reason than predicted** -- see §5.2 | | the Zuul `SpeciesDef +0x18`/`+0x24` values matter | 1.1 / 0.7 give 25/25 | HELD (weakly: the cost factor is multiplied by a zero cost on every corpus colony, so **only the income factor is actually tested**) | | `rbfl` does not zero the money chain | no corpus system is rebelling | UNEXERCISED, and now labelled as such in the code | | `IdealSuit` is the server's species array, not the player's field | it is the server's, and it is **on the wire** -- see §5.3 | HELD, and no longer unfalsifiable | ### 5.2 The prediction that was wrong §4.1 said the twelve Zuul misses were the suitability cost. They were not: every Zuul colony in the corpus sits exactly at its species' ideal suitability, so `CalcSuitMod` is 0 and the cost term is 0 on all of them. The suitability cost is **completely unexercised by this corpus** and stays a hypothesis, exactly like the morale and station branches of the output term. What the Zuul records were actually missing was in the *output* half, not the income half: `SpeciesDef +0x4c` (base resource demand) and `+0x50` (resource output factor) are **per species** and are read off the system **owner's** species. Lane N measured them live -- 0/10 for Human and Tarkas, 10/40 for Zuul -- but `max_income_predict.py` carried them as one global pair defaulting to the Human values. A Zuul colony's harvest term is `min(resAvail, 10) x 40 = 400` output points that a 0/10 pair scores as **zero**; 400 points is 80 whole five-point blocks worth 5 money each after the x5, i.e. exactly 2000 money per colony, and the observed shortfalls were 4400 (two colonies x 2000 x the 1.1 species income factor) and 5566 (the same, with a 1.15 `OutMod` and the AI's 1.1). Both deltas fall out to the unit. The lesson is the campaign's own rule 8 in a new dress: a per-species table taken as a scalar agrees with the oracle on the species it was measured from and disagrees on every other, and there is no symptom until a second species appears in the corpus. ### 5.3 A new wire fact: `ISsp`/`ISsu` is `server->IdealSuit[]` The Sim block's `ISsp`/`ISsu` pairs -- seven of them, in species-index order -- are exactly the float[7] that `StrategyServer::IdealSuit` (0x0080f4b0) indexes at `raw base + 0xf8`. The array is randomised per game by the map generator (`turn1-state` has Human 11.106, `human-turn2` has Human 10.220, `zuul-turn15` has Human 7.502), and every `ServerPlayer`'s own `IdealSuit` field carries the same value for its own species in all 11 saves. So the suitability cost's ideal is fully held on the wire and needs no data file. `struct-recovery.md` §5 lists the two tags without saying what they are; this names them. ### 5.4 Falsification actually run | run | score | what it shows | |---|---|---| | `--ai-rule non-npc-not-first` (the model) | **25/25** | | | `--ai-rule none` (nobody is AI) | 14/25 | the eleven AI-owned records break -- the x1.1 is load-bearing | | `--ai-rule all-non-npc` (both real players are AI) | 14/25 | the eleven human-owned records break, each by exactly x1.1 -- so the multiplier has to land on **precisely** the AI set, not merely somewhere | | `--base-demand 0 --res-output 10` on a Zuul save | 0/2 | the per-species resource pair is load-bearing, by the 4400 computed in §5.2 | ### 5.5 What this run did NOT cover -- read this before quoting the 25/25 * **The suitability cost is untested.** Every colony in the corpus is at its species' ideal, so `CalcSuitMod` returns 0 everywhere and `SpeciesDef +0x24` is multiplied by zero. The `<=` boundary, the `SuitTol` cap, the `vnh` early-out and the unowned-system 20.0 are all unexercised. * **The slave income term is untested**: no colony carries slaves. * **The addiction income modifier is untested**: `nadct` is 0 on every system. * **The morale multiplier is untested**: every colony sits at 75, strictly between the thresholds. * **The civilian capacity surplus is untested**: no colony is at its cap. * **Only difficulty level 1 is exercised.** Levels 0 and 2, and the maintenance and research columns of all three rows, are read from the initialiser and never run. * **`aidf` is 1 on all 25 records**, so the level lookup itself is a constant here. * **The AI flag is not on the wire**, so 25/25 pins *an* assignment of it (player 0 human, player 1 AI, NPCs neither), not the flag's provenance. §5.4's second falsification run is what makes that assignment non-trivial rather than a free parameter. * Two of the 25 records (the human-turn2/turn3 pair) come from a game whose Sim-level `IncMod` is 1.48 rather than 1.0, which is the only exercise the server income modifier gets. --- ## 6. What this unblocks, and what it does not ### 6.1 T31 `UpdateBankruptcyLimits` -- unblocked in substance `UpdateBankruptcyLimits` sums exactly `max(ComputeMaxIncome(s), 0)` over owned, non-abandoned systems, which is the number the oracle validates. It is now wired into `sots_turn` and self-checks every run against the `BnkEl` the input save already carries -- 8 of 8 players on `turn1-state`. It is still listed **blocked**, for two reasons that are not the formula: 1. `ServerPlayer+0xf9` (is this player AI?) is a game-setup input the save does not carry; `--ai-player N` supplies it, and without it an AI empire's limit comes out 1/1.1 low. 2. `BnkPr` needs `BANKRUPTCY_PROTECTION_LIMIT_FACTOR` from the data files, so it is only offered with a tuning table loaded. Committing a value computed from an unloaded (zero) constant regressed one leaf in the first measurement and was removed. Committing it closes **nothing** on the reference pair, and that is a fact about the rest of the engine rather than about this chain: the limits move between `turn1` and `turn2` because the **civilian population grows**, and that growth is not committed, so our limit equals the input save's. Measured with `--commit-blocked=T31 --ai-player 1`: **0 closed, 0 regressed** (209 -> 204 and 108 -> 103, unchanged from the baseline). The same numbers hold with every other blocked phase committed alongside it. ### 6.2 P01 `ComputeBudget` -- NOT unblocked by THIS lane (closed by lane C3) > **Update, lane C3 (same day).** Everything this section says is right, and the turn path is > now read and modelled: see `output-turn-path.md`. The one thing it did not anticipate is that > the leftover construction points come back to the **trade** channel, so on a corpus where > every build queue is empty the two paths give the same money to within one trade point -- > which makes the `BnkEl` oracle an indirect check on the turn path after all. P01/P02 are > committed and closed one leaf on each reference pair. `ComputeBudget` has two modes and they take their per-system money from **different functions** (0x008631fd, the `[ebp+0xc]` test): ``` if (projected) money = ComputeMaxIncome(s) // 0x007521c0 -- what this lane closed else { OutputRates r; ComputeOutput(s, &r); money = r.out[3] } // 0x00751fb0 ``` `ComputeOutput` (0x00751fb0) passes the system's **own** `Rts` sliders, not a max-mods vector. On that path the science, construction and terraform channels are funded, which means: * the repair pass inside `ComputeOutputFromRates` runs (it is **not** side-effect free -- it repairs ships in orbit), so leftover construction points can be non-zero; * the unspent-industry and unspent-terraforming cascades into the money channel are live, and §2.4's proof that both are zero **does not apply**; * the build queue's consumption of construction points is in the same call. So the turn's real per-system money is a strictly larger problem than the one the `BnkEl` oracle can falsify, and nothing in this corpus states its answer. P01/P02/P03/P05/P06 stay blocked, and the next lane on them should target `ComputeOutputFromRates`'s full channel split rather than this chain. The roadmap's item 1 claimed this chain unblocks them; it does not. ### 6.3 The two research RNG words Both are downstream of the budget's research allocation: 1. `ProcessResearch`'s `RNG::Chance(odds)` on the funded node, where `odds` is built from the research **points** the allocation bought (0 or 1 word -- `Chance` draws nothing for `p <= 0` or `p >= 1`); 2. the draw inside `ServerPlayer::OnTechResearched`'s effect callback, which fires only when a tech actually completes and therefore only when the allocation was large enough (0 or 1 word, measured live in the B3 compare run). Those points come from `ComputeBudget`'s research money, which is on the P01 path of §6.2 -- not this one. **The generator model still cannot include them.** What has changed is the reason: it is no longer "the money output of a system is unmodelled" but "the *projected-rate* money output is", which is a narrower and differently-shaped gap, and it is now the only thing between the ledger and those two words.