findings/subsystems/population-growth.md -- ServerSystem::GrowCivilianPops 0x00754220 read byte for byte, its whole chain, and Ship::RepairCost 0x00815180. The headline is a correction to the brief's premise. The pass is NOT gated on imperial carrying capacity. The whole system's civilian delta is clamped to 20,000,000 -- POPTYPE[1]+0x08, an int64 literal in the executable -- and on both reference pairs that clamp decides the value: the uncapped delta is 7.5x it and the capacity headroom 25x it. The imperial capacity is pinned at exactly Size x 1e8 from the corpus alone, by two independent behaviours of one colony across three turns, with no data files. Two boundary corrections, both of the rule-17 shape. GrowCivilianPops is a loop over group types whose back edge lies outside every decompiler `if`, so it reads as straight-line code if you stop at the first `ret`; its real end is 0x00754b59. MaxPopGeneric ends at 0x0074a6cd, so the 0x0074a6d0 that lanes N and E1 both cite for the capacity-surplus pair is a different function. A table correction with teeth: InitPopTypeTable never writes the group ceiling at +0x28/+0x2c. The CRT static initialiser at 0x009abe20 does, to INT64_MAX, and nothing else touches it -- so the clamp that reads it is always a no-op. A reader who opens only the obvious initialiser sees zero there and would cap every carrying capacity in the game at nothing. Lane N's whole table is otherwise reproduced independently from the six-register fxch rotation. output-turn-path.md: out[6] is NOT the ship-repair demand. 0x007460b0 sums 0x0081f8c0, which gates on the design's carried-population bit and computes GroupIncome over the ship's own Population -- so it is the income of population carried in slaver and colony hulls, a slot the engine already had under a name nobody had connected to it. Corrected in place, with lane-c3.json's three unread stubs superseded by lane-g3.json's read entries. Ship::RepairCost is unexercised on the corpus and that is a measurement, not an absence (rule 20): the independent colony keeps a ten-ship fleet in orbit over Koa'Vo on both reference pairs and its Sav closes exactly with the demand taken as zero, which it could not do if any hull had a cost. Engine side: sots-engine wip/growth ed6602e -- reference pair 78 -> 81 closed, 0 regressed; pair 2 36 -> 39 closed, 0 regressed.
31 KiB
ComputeOutput on the turn path (lane C3, 2026-09-08)
ServerSystem::ComputeOutput 0x00751fb0 and the whole of ComputeOutputFromRates
0x00751bb0 — the money ComputeBudget actually sums during a turn, as opposed to the
projected-mode ComputeMaxIncome that lane E1 closed at 25/25 on the BnkEl oracle.
Evidence: own objdump -d pass over Sword of the Stars.exe. Both ranges disassembled to
the next function start and the real boundary found (rule 17). Field names per
findings/objects/struct-recovery.md. Everything marked instruction-verified is read off
the instruction stream; everything marked inferred is not.
This supersedes two labels in earlier notes:
income-term.md§2.2 calls[ebp-0x28]"leftover science points". There is no science channel in this function.OutputRates+4is the ship-construction sliderSRsc, the helper at 0x00746830 turns it into construction points, and the "leftover" is what the build queue and the ship-repair pass did not spend.strategic-turn-internals.md§3.3 had this right; the income lane's paraphrase did not.output-term.md§1 givesComputeOutputFromRates's end as 0x00751fa8. Theret 8is at 0x00751fa0; 0x00751fa3..0x00751faf isint3padding. E1's 0x00751fa0 is correct.
1. The call chain
| addr | name | conv / boundary | note |
|---|---|---|---|
| 0x00751fb0 | ServerSystem::ComputeOutput(int out[12]) |
thiscall ret 4, ends 0x00752070 |
Ghidra size 192 = correct |
| 0x00751bb0 | ServerSystem::ComputeOutputFromRates(int out[12], OutputRates*) |
thiscall ret 8, ends 0x00751fa0 |
repairs ships in orbit |
| 0x00747390 | NormaliseOutputRates(float rates[4], ServerSystem*, float* pinned) |
cdecl | pinned = null → trade |
| 0x00750480 | ServerSystem::ComputeTotalOutput(double SRoh) |
thiscall ret 8 |
lane N; side-effect free |
| 0x00746830 | ServerSystem::ConstructionPoints(double consShare) → int |
thiscall ret 8, ends 0x00746883 |
shipyard-station bonus |
| 0x00815c10 | ServerSystem::StationCount(ServerPlayer*, int kind) |
cdecl-through-thiscall | kind 1 here (shipyards) |
| 0x008251e0 | BuildQueue::TotalConstructionLeft() → int |
thiscall ret 0, 23 bytes |
Σ conleft over the order list |
| 0x00751590 | ServerSystem::RepairShipsInOrbit(int points, bool estimateOnly) → int |
thiscall ret 8, ends 0x007517b5 |
the side effect; returns points LEFT |
| 0x00745d60 | ServerSystem::IdealSuitability() → double |
thiscall ret 0, ends 0x00745dc0 |
§2.6 |
| 0x00746890 | ServerSystem::TerraformPointsNeeded() → double |
thiscall ret 0, ends 0x0074690d |
§2.5 |
| 0x0074c6f0 | ServerSystem::CivilianConsumption() → int |
reads esi as this |
resource ledger only |
| 0x007483b0 | ServerSystem::OverHarvestDemand(double SRoh) |
thiscall ret 8 |
lane N |
| 0x007505b0 | ServerSystem::TradePointsToMoney(double trade) → double |
thiscall ret 8 |
lane E1 |
| 0x008e5660 | round(double) |
cdecl | fistp/fild, ties to even |
| 0x00925220 | _ftol2 |
— | truncating |
| 0x009dd1e4 | ceil (MSVCR100) |
IAT | |
| 0x0083a5b0 | TradeManager::SystemRouteIncome(mgr, sys, ServerPlayer*, int* a, int* b) |
ret 0x10 |
fills out[4],out[5] |
| 0x007460b0 | SystemRepairDemandForOwnerSystemShipCarriedPopIncome(ServerPlayer*) → int |
reads ebx as sys |
fills out[6] — not a repair function, see §5 |
Two non-standard conventions worth flagging (both would be silently wrong if taken as
__thiscall): 0x0074c6f0 takes its this in ESI and 0x007460b0 takes the system in
EBX. Both are compiler-local helpers that inherit a register the caller happens to hold.
2. The formula
2.1 ComputeOutput(out) — 0x00751fb0 (instruction-verified)
if (out == 0) return
memset(out, 0, 48) // 12 dwords; slots 10 and 11 are FLOATS
if (sys->PID /*+0x100*/ == 0) return
ComputeOutputFromRates(out, &sys->Rts) // sys+0x88 -- the system's OWN sliders, unnormalised
if (sys->[0x230] != 0) return // a byte; NOT on the wire
mgr = server->vtbl[2]() // server = sys->+0x10, the RAW StrategyServer base
TradeManager::SystemRouteIncome(mgr, sys, sys->PID, &a, &b)
out[4] = a ; out[5] = b
out[6] = SystemShipCarriedPopIncome(sys->PID) // 0x007460b0, `sys` arrives in EBX
// NOT repair demand -- G3 correction, §5
out[3] — the money — is written entirely inside ComputeOutputFromRates. Nothing after
that call touches it, so the trade-route and repair-demand slots are reporting only.
ComputeBudget 0x00863030 confirms this at 0x008631fd:
for s in player->systems:
if (s->Abdn /*+0xc4*/) continue
if (projected) money = ComputeMaxIncome(s) // 0x007521c0
else { int o[12]; ComputeOutput(s, o); money = o[3]; } // [ebp-0xe0 + 0xc]
if (money > 0) income += money // budget+0x04
if (money < 0) expense += -money // budget+0x1c
2.2 The OutputRates memory layout (instruction-verified)
ServerSystem+0x88, 0x1c bytes, seven dwords. The memory order is not the wire order:
| offset | field | who reads it |
|---|---|---|
| +0x00 | SRt trade |
trade points, and the leftover weight |
| +0x04 | SRsc ship construction |
construction points |
| +0x08 | SRtf terraform |
zeroed by normalisation when Suit == IdealSuitability() |
| +0x0c | SRi infrastructure |
zeroed by normalisation when f32(Infra + ibon) >= 1 |
| +0x10 | SRoh over-harvest |
ComputeTotalOutput / OverHarvestDemand |
| +0x14 | SRs |
not read here (slave death rate) |
| +0x18 | SRnr (int) |
not read here |
The suppressions are read off NormaliseOutputRates 0x00747390: [edi+0x8] := 0 under the
Suit == IdealSuitability() branch and [edi+0xc] := 0 under 1.0 <= f32(ibon + Infra).
The wire order (SRs, SRt, SRsc, SRtf, SRi, SRoh, SRnr) is a serialiser choice.
2.3 ComputeOutputFromRates(out, rates) — 0x00751bb0 (instruction-verified)
memset(out, 0, 48)
if (sys->PID == 0) return
r = *rates // 7 dwords copied to the stack: the caller's
NormaliseOutputRates(&r, sys, 0) // struct is never modified
T = round( ComputeTotalOutput(r.SRoh) ) // ties to even, kept as a double
tradePts = round( T * r.SRt )
consPts = round( r.SRsc * T )
terraPts = round( r.SRtf * T )
infraPts = round( r.SRi * T )
out[0] = ftol(T)
// --- resource ledger (independent of everything below) -------------------------
need = ftol(OverHarvestDemand(r.SRoh)) + CivilianConsumption()
resAvail = sys->Res + (PID->[0x138] ? sys->MRes + sys->ARes2 : 0)
out[2] = min(resAvail, max(need, 0))
out[1] = min(out[2], ftol(OverHarvestDemand(r.SRoh)))
// --- construction ---------------------------------------------------------------
queueNeed = sys->BQ ? BuildQueue::TotalConstructionLeft(sys->BQ) : 0
out[7] = ConstructionPoints(consPts) // ftol(c + stations*BONUS*c)
out[8] = min(queueNeed, out[7])
rem = max(out[7] - out[8], 0)
out[9] = 0
if (rem > 0):
left = RepairShipsInOrbit(rem, /*estimateOnly=*/false)
out[9] = rem - left
rem = left
// --- the leftover redistribution -------------------------------------------------
Ltrade = Lterra = Linfra = 0.0
if (rem > 0):
P = (double) rem
if (r.SRsc == 1.0): // an exact ==
wT = 1.0
wF = (sys->Suit != IdealSuitability()) ? 1.0 : 0.0
wI = (sys->Infra != 1.0) ? 1.0 : 0.0
else:
wT = r.SRt ; wF = r.SRtf ; wI = r.SRi
S = wI + (wF + wT) // that association
Ltrade = round( wT * P / S )
Lterra = round( wF * P / S )
Linfra = round( wI * P / S )
// --- infrastructure --------------------------------------------------------------
infraNeed = ceil( (1.0 - sys->Infra) / 3.3e-5 ) // 0x00a1f930
poolI = Linfra + infraPts
spendI = min(poolI, infraNeed)
leftI = max(poolI - spendI, 0)
out[10] = (float) max( spendI / 500.0 * 0.01 * 1.65, 0 ) // three 80-bit steps
// --- terraforming ----------------------------------------------------------------
terraNeed = ceil( TerraformPointsNeeded() )
poolF = (Lterra + terraPts) + leftI // the infra cascade lands HERE
spendF = min(poolF, terraNeed)
leftF = max(poolF - spendF, 0)
sign = (IdealSuitability() < sys->Suit) ? -1.0 : 1.0
out[11] = (float) ( sign * (spendF * 1.5 * 1.2f * PID->TerraMod) / 20000.0 )
// --- money -----------------------------------------------------------------------
out[3] = ftol( TradePointsToMoney( (Ltrade + tradePts) + leftF ) )
Two things this makes explicit that no earlier note stated:
- The infrastructure leftover cascades into the terraform pool, and the terraform
leftover cascades into money. There is no direct industry→money edge; it is two hops.
income-term.md§2.4's "both cascade sources are zero" is right for the max-income rate vector (whereinfraPts = terraPts = 0and both needs are>= 0) but says nothing about the turn path. - The construction leftover's trade share is added to the money channel. On a colony
whose build queue is empty, every construction point the system produced comes back as
trade points. A system with
SRt = 0, SRsc = 1and an empty queue therefore earns exactly as much money as one withSRt = 1. That is the single most consequential fact in this function and it is why the corpus's turn-path money is so close to its projected money (§4).
2.4 RepairShipsInOrbit(points, estimateOnly) — 0x00751590 (instruction-verified)
if (points == 0) return 0
need = 0 ; cand = vector<StarShip*>()
for i in 0 .. sys->vtbl[2]()-1: // fleets at the system
f = sys->vtbl[4](i)
if (f->[0x58] != sys->PID) continue
if (!Fleet::TestFlags(f, 0, 8)) continue
for sh in f->ships (+0xa4 .. +0xa8):
if (!Ship::TestFlags(sh, 0, 8)) continue
c = Ship::RepairCost(sh, 1) // 0x00815180
if (c <= 0) continue
need += c
if (!estimateOnly) cand.push_back(sh)
if (estimateOnly) return max(points - need, 0)
// round robin
progress = true
while (progress && points > 0):
share = max(points / cand.size(), 1) // integer divide
progress = false
for sh in cand:
if (points <= 0) return points
take = min(Ship::RepairCost(sh, 1), share)
if (take > 0) { Ship::ApplyRepair(sh, take); progress = true; points -= take }
return points
The round robin is equivalent to points - min(points, need) and this is a proof, not an
observation: share >= 1 always, so every ship with a positive remaining cost takes at least
one point per pass; the loop can only stop early via progress == false, which requires every
remaining cost to be zero. So the pass ends with points == 0 (need was the binding
constraint) or with every cost zero (points was). The engine models it as the min and pins
the equivalence in a test rather than reproducing the round robin.
ComputeOutputFromRates passes estimateOnly = 0, so this is the B1 replace double-run
hazard: calling the original to harvest a value repairs ships. The engine never does.
2.5 TerraformPointsNeeded() — 0x00746890 (instruction-verified)
if (PID == 0) return 0.0
sign = (IdealSuitability() < sys->Suit) ? -1.0 : 1.0 // 0x009e3348 = -1.0
rate = f32(PID->TerraMod /*+0x134*/) * 1.8000000715255737 // 0x00a1f928 = (double)1.8f
d = fabs( f32(IdealSuitability() - sys->Suit) )
return d / fabs( rate * sign / 20000.0 )
The fabs is after the sign multiply, so the sign cancels and the result is always >= 0
(E1's §2.4 reading confirmed). Note 1.8f here is the same number as 1.5 * 1.2f in
out[11] — 1.5 * 1.2000000476837158 == 1.8000000715255737 exactly — so the point count and
the point value use one rate, folded differently.
TerraMod == 0 makes this +inf, ceil(+inf) = +inf, and the terraform channel then absorbs
its whole pool with nothing cascading to money. No corpus player has TerraMod == 0 (Human
and Tarkas carry 1.0, species 4 carries 3.7), so that branch is unexercised.
2.6 IdealSuitability() — 0x00745d60 (instruction-verified)
if (sys->PID == 0) return (double) sys->Suit // an unowned system is "at its ideal"
v = PID->IdealSuit /*+0xb0*/
if (sys->indi /*+0x1c8*/) v = StrategyServer::IdealSuit(sys->server, sys->indi->indsp)
if (sys->dsu /*+0x118*/ != SENTINEL) v = sys->dsu // SENTINEL is the float at 0x00aeca6c
return v
This is not the same source CalcSuitMod uses in the money cost (E1 §2.6 reads the
server's per-species array there). Here the owner's own IdealSuit field is the default and
the server array is only consulted for an independent colony. In the 11-save corpus the two
agree, so the difference is instruction-verified only.
dsu is 3.4028235e38 (FLT_MAX) on every system of every corpus save, so the override
never fires and the sentinel's identity is inferred from that (the value at 0x00aeca6c is a
data-file-loaded GlobalConst slot, not a .rdata literal, so it was not read directly).
2.7 ConstructionPoints(cons) — 0x00746830 (instruction-verified)
if (PID == 0) return ftol(cons)
k = StationCount(sys, PID, 1) // 1 = shipyard
b = (STATION_BONUS_SHIPCON > 0) ? STATION_BONUS_SHIPCON : 0
return ftol( k * (b * cons) + cons )
Note the association ((b * cons) * k) + cons, and that the bonus is ignored unless strictly
positive — the same "unloaded tuning table reads as 1.0, not 0.0" guard the output term has.
3. What this needs that the max-income path did not
| input | source | held? |
|---|---|---|
Rts (5 floats) |
wire, Sys/Rts |
yes |
IdealSuitability() |
Player/IdealSuit, Sys/dsu, Sim/ISsu[] |
yes |
Infra, ibon |
wire | yes |
| shipyard station count | Sys/NumSnF |
yes (0 on every corpus system) |
build-queue demand Σconleft |
wire, Sys/BQ/ords |
yes |
| ship repair demand in orbit | needs Ship::RepairCost 0x00815180 over the fleets at the system |
NO |
TerraMod |
wire, Player/TerraMod |
yes |
| the output total | lane N | yes |
One input of nine is missing, and it is the repair demand. It is zero whenever no ship in orbit is damaged. On the two reference pairs the human's and the AI's colonies carry no fleet at all in the input state except Ke'Dolarra (turn 2) and Koa'Vo, so the exposure is small but not provably nil; the engine takes it as an explicit input defaulting to 0 and reports it.
4. PREDICTION — written before the build
The corpus turns out to make this function much simpler than it looks, and the predictions below are the consequences.
Measured facts about the corpus, from the wire (Sys/Rts, Sys/dsu, Sys/BQ, Sys/NumSnF,
Player/IdealSuit, Player/TerraMod), before any model was written:
- every owned colony in all 11 saves has
Suit == IdealSuitexactly andInfra == 1.0exactly, soNormaliseOutputRateszeroes bothSRtfandSRion every one of them; - consequently
infraNeed = ceil(0/3.3e-5) = 0,terraNeed = ceil(0/rate) = 0, andinfraPts = terraPts = 0; - no system carries a station, so the shipyard bonus is 1;
dsuisFLT_MAXeverywhere, so the ideal-suitability override never fires;- the sliders that occur are
{t .5, sc .5},{t 0, sc 1},{t .04, sc .96}and{.25,.25,.25,.25}; after suppression they normalise to{t, 1-t, 0, 0}.
4.1 What I expect
-
The leftover redistribution collapses to "all of it to trade" on every corpus colony. In the
SRsc == 1branch the weights are(1, 0, 0)because suit is at the ideal and infra is exactly 1; in the other branch they are(SRt, 0, 0). Either wayLtrade = round(P) = PandLterra = Linfra = 0. -
Therefore
out[3] = ftol(TradePointsToMoney(round(T*SRt) + ftol(round(T*SRsc)))), and with an empty build queue and no repairs the argument is2*round(T/2)for a 50/50 colony andTfor a pure-construction one — i.e. within ±1 ofComputeMaxIncome'sT. -
The turn path's money therefore equals the projected path's money to ±1 trade point on this corpus. If that is right, the
BnkEloracle — which constrains only the projected path — becomes an indirect check on the turn path here, and only here. -
Numerically, using the
BnkElthe oracle saves already carry (inverted throughftol(maxIncome / -0.15000000596046448)):player pair oracle's Savmovedecomposition 576 Independent Colony 1→2 0 → 9887199871 − 1000 maint + 0 interest576 2→3 98871 → 19873099871 + 988 interest − 1000 maint16 human 1→2 50000 → 289688239188 + 500 interest, and239189is the max income of the post-growth turn-2 colony16 human 2→3 289688 → 532369239785 + 2896 interest, again post-growthSo
ComputeBudgetruns on the colony state afterS11 SystemTurn— the strategic driver runs the system turn at index 11 and the player driver at index 13 — and the human's system income is the post-growth number. -
What will therefore close, and what will not:
- Player 576's income does not move with growth (Koa'Vo has no civilian population and
its
BnkElis identical in all three saves), so itsSavshould come out exactly right on both pairs. +1 leaf per pair. - Player 16's income does move with growth, and our
S11does not commit civilian growth (it needs the carrying capacity). Our number will be short by 597 on pair 1 and 596 on pair 2, soSavstays wrong — but it was already wrong, so 0 regressed. - Player 32 (the AI) picks a research target and changes
ResRateduring the turn; that is Rung B. Not closed. - The 24
turnstatsleaves need the budget and ship construction (lane E2's finding). Not closed by this lane alone. PvSavisS00's snapshot, notP02's write, and player 32'sPvSavis not its pre-turnSav, so it is left alone.
Predicted total: pair 1 closed 1 regressed 0; pair 2 closed 1 regressed 0.
- Player 576's income does not move with growth (Koa'Vo has no civilian population and
its
-
The two research RNG words stay out of the generator model, and for a new reason: the allocation is now computable, but no player in either input state carries a research target (
ResTNmis empty on 16 and 32 inturn1-state), soProcessResearchhas nothing to fund. The blocker moves from "the money is unmodelled" to "the AI's target selection is unmodelled".
4.2 Falsification — how this could be wrong, and the symptom of each
| way it could be wrong | symptom |
|---|---|
OutputRates+4 is not construction but something else, so SRsc == 1 is not what the leftover branch tests |
player 576 (whose rates are .25/.25/.25/.25, the only four-way vector) would miss while the {0,1} colonies matched, or vice versa |
| the leftover weights are the raw sliders, not the normalised ones | 576 misses by a clean ratio (.25 vs .75 denominators); the {t,1-t} colonies are unaffected because the ratio is the same |
Ltrade is not added to the money channel (i.e. I misread the slot reuse of [ebp-0x28]) |
every colony's money collapses to round(T*SRt), so the {t 0, sc 1} colonies earn zero and player 32's and the turn-2/3 colonies' income falls by 100 % — a very loud failure |
the budget runs before S11, and the human's +597 has another source |
player 576, whose income does not move with growth, would still match, so this is NOT separable on 576 — it is separable only on 16, where a pre-growth budget predicts 289092 and a post-growth one 289688 |
| the repair pass consumes points on a corpus colony | player 576 (Koa'Vo has one fleet in every save) misses low by the repair spend; 16's homeworld has no fleet in turn1-state, so a 576-only miss names this |
the round robin is not equivalent to the min |
only visible where points < need; unexercised here |
ConstructionPoints truncates where I read a round |
off-by-one on colonies whose T*SRsc is not integral — but T is already an integer and the sliders are exact binary fractions on most colonies, so this is nearly invisible; {.04, .96} (Downbelow) is the one colony that would show it, and it is not on either reference pair |
sys->[0x230] gates something that feeds money |
it cannot: out[3] is written before that test and never again |
A run that closes player 576 and nothing else is a weak result and will be reported as one: it is one leaf per pair, and it is the leaf where the missing upstream input happens not to matter.
5. The result
Engine: sim::ComputeSystemOutput (src/game/sim/colony.{h,cpp}) plus
sim::IdealSuitability and sim::RepairShipsInOrbit, wired into P01/P02 through
BuildBudgetFeeds in src/app/turn.cpp. Host ctest 46/46, clean-room OK, both run as
separate commands.
reference pair turn1-state -> turn2-state 209 -> 157 closed 52 regressed 0 (was 51 / 0)
second pair turn2-state -> turn3-state 108 -> 86 closed 22 regressed 0 (was 21 / 0)
Closed 1 per pair, regressed 0 per pair. The leaf is
/Sim/players/Player[576 "Independent Colony"]/Sav on both.
5.1 Every prediction in §4.1, checked
| predicted | outcome | verdict |
|---|---|---|
| the leftover redistribution collapses to "all to trade" on every corpus colony | it does; leftoverToTrade == leftover on all of them |
HELD |
| the turn path equals the projected path to within the trade-point rounding when the queue is empty | exactly equal on 5 of the 11 saves — every save whose build queues are all empty | HELD |
player 576's Sav comes out exactly right on both pairs |
0 -> 98871 and 98871 -> 198730, both exact |
HELD |
player 16's Sav is short by the uncommitted civilian growth |
289092 against the oracle's 289688 |
HELD (the amount was predicted as 597 and is 596 — see §5.2) |
| player 32 is not closed | 297492 against 92651 |
HELD |
the turnstats leaves are not closed |
not closed | HELD |
| pair 1 closed 1 regressed 0; pair 2 closed 1 regressed 0 | exactly that | HELD |
| the two research RNG words stay out of the generator model | they do, for the predicted reason | HELD (§5.5) |
Nothing in §4.2's falsification table fired. In particular the loudest one — "the construction
leftover does not reach money" — would have collapsed every {SRt 0, SRsc 1} colony's income
to zero, and those colonies price correctly.
5.2 The prediction that was off, and why
§4.1 said player 16 would be short by 597 on pair 1. It is short by 596. The
arithmetic: our pre-growth output gives a max income of 238,592 and the post-growth one gives
239,189, a gap of 597 — but the oracle's own turn-path number is 239,188, one below its
projected 239,189, because 2 x round(T/2) lost a point on the post-growth total and did not
on the pre-growth one. So the two errors partly cancel, and the residual is the growth gap
minus one trade point. The mechanism was predicted; the sign of its interaction was not.
5.3 The strongest check available without a VM
The engine now runs every owned system through both paths on the same colony state and
reports the difference, every run. The projected sum is the number the save's own BnkEl
states and that lane E1 scored 25/25 against, so a large delta is the new model failing and it
is visible on load.
| save | landed players agreeing exactly | worst |delta| | explanation |
|---|---|---|---|
turn1-state, turn2-state, turn3-state |
3 of 3 | 0 | every build queue empty |
human-turn2-orders |
2 of 2 | 0 | every build queue empty |
zuul-turn15-orders |
2 of 2 | 0 | every build queue empty |
human-turn3-noderoute |
1 of 2 | 37 | queues empty; one trade point crossed a five-point block: 5 money x 5 x IncMod 1.48 = 37 |
zuul-turn23-fleet23 |
1 of 2 | 2035 | Gallandro's queue holds 370 points: 370 x 5 x 1.1 = 2035 |
zuul-turn17-orders2, -rollpending |
0 of 2 | 21010 | W'kanda's queue holds 3818: 3820 x 5 x 1.1 = 21010 (the block count moves by 3820, not 3818, because of the two mod 5 offsets) |
zuul-turn16-noderoute |
0 of 2 | 36438 | W'kanda's queue holds 6626: 6625 x 5 x 1.1 = 36437.5, and two independent truncations put it at 36438 |
zuul-turn5-species5 |
1 of 2 | 39160 | W'kanda's queue holds 7120: 7120 x 5 x 1.1 = 39160 |
Every delta decomposes, to the unit, into the build queue's construction points priced
through the money chain — three of the four exactly, and the fourth within the one unit that
two independent truncations of a .5 allow. That is the closest thing to an oracle this path
has, and it is a much stronger statement than "the two agree": it says the two paths differ
by exactly the thing the disassembly says they differ by.
5.4 What this run did NOT cover — read this before quoting the numbers
- One leaf per pair is a weak result and it is the one leaf where the missing upstream input does not bite. Player 576 owns one colony with no civilian population and an empty queue; it is the easy case.
- The ship-repair demand is taken as 0 everywhere.
Ship::RepairCost(0x00815180) is unread, so no save can supply it. Every colony with a damaged fleet in orbit therefore reads high, and the corpus cannot tell us whether any does. - No corpus colony is off its ideal suitability, and none is below full infrastructure. So
the terraform channel, the infrastructure channel,
infraNeed,terraNeed, the infra→terraform cascade, the terraform→money cascade and the non-trivial half of theSRsc == 1leftover branch are all unexercised by the corpus. They are covered by host tests written from the instruction stream, which is a much weaker claim. - No corpus system has a station, so the shipyard bonus in
ConstructionPoints— including the "strictly positive" guard and the association this lane corrected — is unexercised. SRohis 0 on every save, so the over-harvest term is unexercised here too (lane N's finding, unchanged).dsuis FLT_MAX everywhere, so the ideal-suitability override is unexercised and the sentinel's identity is inferred from that fact rather than read.- The build queue is exercised only on Zuul saves that are not on either reference pair. The two pairs the metric is measured on have empty queues throughout, so the queue edge is checked by the §5.3 self-check and not by the leaf count.
- The
[0x230]gate,out[4],out[5]andout[6]are read but not modelled; nothing downstream ofout[3]needs them. - Neither
NormaliseOutputRates' tail (past 0x007474d4) norCivilianConsumption's ratio was read this lane. The first is B4's; the second feeds only the resource ledger.
5.5 The two budget-downstream RNG words
They still cannot enter the generator model, and the reason has changed again.
income-term.md §6.3 named them: ProcessResearch's RNG::Chance(odds) on the funded node,
and the draw inside OnTechResearched's effect callback. Both are downstream of the research
allocation, which is ComputeBudget's research money — and that money is now computable,
because its only missing input was the per-system money this lane closed.
What blocks them now is one step further out: the only corpus player that reaches
ProcessResearch with a research target is the AI, and its research rate (ResRate
0.25 → 0.8) and its target (ResTNm '' → 'IND_Waldo') are its own orders, generated during
the same turn. Feeding our budget into ProcessResearch would allocate against the wrong
rate. On turn1-state no player has a target at all, so the phase has nothing to fund and
both words cost zero on the reference pair.
That last sentence is a claim about the workload, not about the phase, and rule 20 applies
to it: a word count cannot separate "ProcessResearch was not entered" from "it was entered
and drew nothing". An entry probe on ProcessResearch would settle it in minutes and is the
cheap next step. Until then the two words are best described as unblocked in formula, blocked
in workload, and the honest ledger entry is that the generator model is unchanged at 16 words
modelled.
5.6 What is now blocked on what
| phase | was blocked on | is blocked on |
|---|---|---|
P01 ComputeBudget |
the per-system money output | nothing here; S11's uncommitted civilian growth upstream, and the unread ship-repair demand |
P02 ApplyNetToSavings |
P01 | committed; exact where the upstream is |
P03 RecordBudgetDerivedFields |
the budget | two player words that are still not identified on the wire — unchanged, this lane does not help it |
P05 ProcessResearch |
the allocation | the AI's own research rate and target (Rung B) |
P06 ResearchRefund |
P01 and P05 | P05 |
| T36 turn record | the budget and ship construction | ship construction (lane B6) plus the same growth gap; the money half is now available |
The single highest-value next item this lane can name is committing S11's civilian
growth: it is what stands between player 16's Sav and the oracle on both pairs, it is also
what stands between BnkEl/BnkPr and the oracle (four more leaves), and it is the same
Pop2/PopG/PopC and PvPop2/PopG/PopC leaves that are already on the remaining list. One
input — the imperial carrying capacity — gates all of it.
5. Correction (lane G3, 2026-09-08): out[6] is not a repair number
0x007460b0 was named SystemRepairDemandForOwner here on the strength of its position next
to the repair pass. Read to its callee it is not a repair function at all. The per-ship
summand it calls is 0x0081f8c0, which
- gates on the design's carried-population bit (
design->+0xb8 & 0x04000000, the bit lane B6 named), and - computes
GroupIncomeover the ship's ownPopulationatship+0x9c.
So out[6] is the income of population carried in slaver/colony hulls in orbit — a slot
sots-engine's BudgetInputs already carries as shipCarriedPopIncome. It is reporting-only
and reaches no save leaf, so nothing downstream changes; the name does.
The real per-ship repair term is Ship::RepairCost 0x00815180, which §2.4 correctly attributes
to RepairShipsInOrbit. Its body, and the one that names its two design fields
(Ship::ApplyRepair 0x008151c0), are read in
findings/subsystems/population-growth.md §5. addresses.d/lane-c3.json's three stubs
(Ship_RepairCost, Ship_ApplyRepair, SystemRepairDemandForOwner) are superseded by
lane-g3.json's read entries; the integrator should drop C3's when folding, rather than keep
both.
§3's "one input of nine is missing" still stands, and the missing one is still the repair
demand — but its exposure is now measured rather than argued: the independent colony keeps a
fleet in orbit over Koa'Vo on both reference pairs and its Sav closes exactly with the
demand taken as zero, which cannot happen if any of those hulls carried a positive cost.