Reads the whole colony output chain off the instruction stream (every range disassembled to the next function start) and compares two of its functions against the running game. The population -> output law is linear and is carried by the executable: output points per head are typeOutputModifier x 1.8 / 500000, and the three-row population-type table is built in code rather than loaded, so the imperial (1.0) and civilian (0.33f) modifiers are facts about the binary. A system's total output is a SUM of three terms, not one multiplicative chain. The station bonus scales only the imperial term and morale only the civilian one, so OutputModifiers no longer carries either; they belong to GroupOutputInputs. The function previously described as the base-output term is the over-harvest RESOURCE demand, and it is corrected in place. Live on VM140, both hooks in compare mode over two species and two workloads: GroupOutput 13,105 calls / 0 divergences; ComputeTotalOutput 11,252 calls / 1 divergence of one ulp, in a value its caller rounds to an integer. Both functions declare a whole-object Guard: 0 undeclared writes in 24,357 calls, which is what makes the side-effect-free claim a measurement. sim::Narrow forces the double rounding a 32-bit x87 build otherwise skips; without it every civilian row came out one ulp low. Also fixes ComputeBankruptcyLimits' elimination divisor, which was the decimal -0.15 rather than the image's widened float -0.15000000596046448. The two disagree for every maximum income divisible by 3 and for essentially every empire above ~3,000,000. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
5.4 KiB
N — the population → base-output term
The formula that a colony's whole economy hangs off, and the one src/app names as its
largest unmodelled input. It is now read from the instruction stream and compared against the
running game.
What it is
Output points are linear in population:
outputPerHead = populationTypeOutputModifier x 1.8 / 500000
1.8 and 500000 are literals in the executable, and so are the imperial (1.0) and civilian
(0.33f) type modifiers — the three-row population-type table is built in code, not loaded
from the data files. Only the slave row's modifier comes from the data. An imperial population
with no station therefore contributes exactly 3.6e-6 output points per head.
A system's total output is a sum of three independent terms, not one multiplicative chain:
base = overHarvestDemand x speciesResourceOutputFactor
+ (transitResources + availableResources) x stripMineFraction x 0.9
+ imperialOutput + civilianOutput + slaveOutput
total = addictionModifier x base
x playerOutMod x systemOutMod x setupOutputMult x rebOutMod x scOutMod
The station bonus multiplies only the imperial term; the morale modifier multiplies only
the civilian term. The previous model in this module applied both to the whole thing, which is
why OutputModifiers no longer carries morale or stations — they belong to
GroupOutputInputs, one population row at a time.
API
src/game/sim/colony.h:
| function | what |
|---|---|
PopTypeOf(group, tuning) |
the population-type row: output/income modifiers and the row's population cap (imperial 50,000,000, civilian 20,000,000) |
GroupOutput(GroupOutputInputs, tuning) |
one (group, species) row's contribution — the per-capita law |
MoraleOutputMultiplier(morale, tuning) |
the civilian morale factor, with both of the original's guards |
StripMineFraction(StripMineInputs) |
resource extraction efficiency |
OverHarvestDemand(OverHarvestInputs) |
the strip-mining resource demand, which is also a summand of output |
SystemBaseOutput(BaseOutputInputs, tuning) |
the three terms summed in the original's association |
TotalSystemOutputRaw / TotalSystemOutput |
the multiplier tail, unrounded and rounded half-to-even |
Verified against the running game
Two hooks in src/shim/hooks/system_output.{h,cpp}, both compare mode, config
src/shim/shim.cfg.output:
Game::ServerSystem::GroupOutput— the per-capita law itself. 13,105 calls, 0 divergences across two species and two workloads.Game::ServerSystem::ComputeTotalOutput— the whole sum and multiplier tail. 11,252 calls, 1 divergence, and that one is a single ulp on one system, in a value the caller rounds to an integer before using — so it cannot move any number the game stores. It is recorded, not fixed.
Both functions were checked for stores to the game state before being chosen as compare
targets, and each declares a Guard region over the whole system object: 0 undeclared writes in
24,357 calls turns that check from a claim into a measurement. The neighbouring
ComputeOutputFromRates is deliberately not hooked — it repairs damaged ships in orbit.
Coverage — read this before quoting the zero
24,357 calls is 13 distinct system states. Unexercised, and therefore hypotheses:
- the over-harvest branch (
SRohis 0 on every call in the corpus), including itsmax(v, 1)floor; - the station factor (no system in the corpus has a station);
- the slave term (no system in the corpus holds slaves);
- both morale branches (every colony sits at 75, strictly between the two thresholds);
- the addiction multiplier and the civilian capacity surplus.
sim::Narrow
A 32-bit x87 build may leave an intermediate in a register at the register's own precision; the
original's x87 runs with its precision-control field at 53 bits and rounds every multiply to
double. The first live run made that visible as a one-ulp low result on every civilian row —
4,957 divergences that were all the same defect, and none of them the model's. sim::Narrow
forces the round the original performs anyway, and is a no-op on any SSE2 target.
Correction shipped alongside
ComputeBankruptcyLimits used the decimal -0.15 as the elimination-limit divisor. The image
holds -0.15000000596046448, a widened float literal. The two disagree for every maximum
income divisible by 3 — from maxIncome = 3 upward — and for essentially every empire above
about 3,000,000, which is six of the twenty-five bankruptcy records recoverable from the save
corpus. Fixed, with kBankruptcyInterestDivisor named in the header and a test at the value
where it first bites.
What this does and does not unblock
It does not unblock P01. ComputeBudget's missing input is a system's money, which runs
this verified total through a second chain with its own population law (incomeModifier / 14000,
no 1.8) and its own multipliers. Checked against the bankruptcy-limit oracle over the whole
save corpus, that composition reproduces 6 of 25 player-records exactly — every record whose
owner is the human player or an independent colony — and the single-system misses are short by a
clean factor of 1.1, the AI trade/income difficulty multiplier, which is not on the wire.
So the blocker has moved from the output term to the income tail. src/app is unchanged:
5 leaves closed, 0 regressed, on both save pairs.