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.
33 KiB
Civilian population growth — the S11 sub-pass (lane G3, 2026-09-08)
ServerSystem::GrowCivilianPops 0x00754220 and its chain, plus Ship::RepairCost 0x00815180.
Evidence: own objdump -d pass over Sword of the Stars.exe via dumps/b6dis.py, every range
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 or hypothesis is not.
0. The headline
The brief said the growth pass is gated on imperial carrying capacity. It is not — not on this corpus, and the reason is worth stating before anything else:
On both reference pairs the civilian population of every growing colony moves by exactly +20,000,000, and 20,000,000 is
POPTYPE[1] +0x08, an int64 literal in the executable (mov DWORD PTR [ecx+0x8], 0x1312d00at 0x00535d20). It is the per-turn step cap on the whole system's civilian delta, and it is what decides the value. The uncapped delta is 150,000,005 — seven and a half times the cap — and the carrying-capacity headroom is 500,000,000, twenty-five times the cap. Neither the growth curve nor the capacity chain can change the answer without being wrong by more than an order of magnitude.
So the pass can be committed without a tuning table and without a verified carrying capacity, and the report says by how much each of them would have to be wrong before it mattered.
1. The call chain
| addr | name | conv / boundary | note |
|---|---|---|---|
| 0x00754220 | ServerSystem::GrowCivilianPops() |
thiscall, plain ret, real end 0x00754b59 |
loops group type 0,1,2 and does work only for 1 |
| 0x00748100 | ServerSystem::PopGrowthDelta(int group, int species) → int64 |
thiscall ret 8, ends 0x007481b7 |
|
| 0x00537140 | PopGrowthDelta(ServerPlayer*, int group, int species, float suit, float factor, int64 pop) → int64 |
cdecl, add esp,0x1c |
7 dwords of args |
| 0x00536fb0 | PopGrowthFraction(ServerPlayer*, int group, int species, float suit, float factor) → float |
cdecl, ends 0x00537136 | |
| 0x00536eb0 | SuitabilityDistance(ServerPlayer*, int species, float suit) → float |
cdecl, ends 0x00536f8b | re-read: §2.6a |
| 0x0074a4a0 | ServerSystem::MaxPopGeneric(int group, int species, ServerPlayer*, float* suitOverride) → int64 |
thiscall ret 0x10, real end 0x0074a6cd |
0x0074a6d0 is a different function |
| 0x0074a9a0 | ServerSystem::CivilianSettleLimit(int species) → int64 |
thiscall ret 4, ends 0x0074aa2d |
min(idealMaxPop, dcs) — §2.3 |
| 0x00535e00 | PopTypeRow(int t) |
cdecl | table 0x00b104e8, stride 0x30 |
| 0x00535ca0 | InitPopTypeTable() |
— | re-read independently, §3 |
| 0x009abe20 | PopTypeTableStaticInit() |
CRT static initialiser | writes +0x28/+0x2c and nothing else does — §3.1 |
| 0x00536c80 | Population::Count(int group, int species) → int64 |
thiscall ret 8 |
|
| 0x00539070 | Population::SetCount(int group, int species, int64 n) |
thiscall | the write-back |
| 0x0074ec50 / 0x007502d0 | civilian seeding helpers | §2.5, unexercised | |
| 0x00815180 | Ship::RepairCost(bool includeRefit) → int |
thiscall ret 4, real end 0x008151b7 |
§5 |
| 0x007460b0 | SystemRepairDemandForOwnerSystemShipCarriedPopIncome(ServerPlayer*) → int |
sys arrives in EBX |
not a repair function — §5.2 |
2. The formula (instruction-verified unless marked)
2.1 GrowCivilianPops — the shape
The function is a loop over t = 0, 1, 2 (inc esi; cmp esi,3; jl 0x7543e9 at 0x00754b2e — a
back edge outside any decompiler if, and the reason this reads as straight-line code if you
stop at the first ret). Two gates inside the loop head:
row = PopTypeRow(t)
if (Population::TotalOfType(sys->Pop2, t) <= 0) continue // 0x00536c30
if (t != 1) continue // `mov eax,esi; dec eax; jne`
so only civilians grow here; the imperial pass is inlined in ServerSystem::ProcessTurn and
slaves are handled by ProcessSlaves.
2.2 The per-species pass
int64 applied[7]; bool hitLimit[7]; bool declined[7]; // all zeroed
int64 total = 0
for sp in 0..6:
delta = PopGrowthDelta(sys, 1, sp) // 0x00748100
cur = Count(sys->Pop2, 1, sp) + Count(sys->pbon2, 1, sp) // the PENDING BONUS COUNTS
cap = MaxPopGeneric(1, sp, sys->PID, NULL) // the real-suitability capacity
soft = CivilianSettleLimit(sp) // 0x0074a9a0, §2.3
if (soft < cap):
limit = soft
if (cur + delta > soft) hitLimit[sp] = true // raises a morale event later
else:
limit = cap
a = min(delta, limit - cur)
if (sys->haltv[1] && a > 0) { a = 0; hitLimit[sp] = false } // byte at sys+0x79
applied[sp] = a
total += a
Note cur includes pbon2, the pending civilian bonus pool — a colony with a pending pool is
already treated as that large for the purpose of headroom.
2.3 CivilianSettleLimit(sp) — 0x0074a9a0
if (!sys->PID) return 0
idealSuit = StrategyServer::IdealSuit(sys->server, sp) // 0x0080f4b0
A = MaxPopGeneric(1, sp, sys->PID, &idealSuit) // capacity AS IF perfectly suited
B = Population::Count(sys->dcs /*+0x104*/, 1, sp)
return min(A, B)
sys+0x104 is dcs (struct-recovery.md §1). On Gamma Cephei dcs is
{type 1, species 0, count 1,000,000,000} and it is the binding limit there, not either
capacity.
2.4 The system-wide clamp and the proportional rescale — the part that decides the value
maxStep = (int64) row->+0x08 // POPTYPE[1] = 20,000,000; see §3
clamped = min( max(total, -50,000,000), maxStep ) // -50,000,000 is a LITERAL here,
// not POPTYPE[0].maxStep
if (clamped != total):
scale = (double)clamped / (double)total // x87, 80-bit
for sp in 0..6:
if (total > maxStep): if (!(applied[sp] > 0)) continue // scale only gains
else if (total < -50000000): if (!(applied[sp] < 0)) continue // scale only losses
applied[sp] = trunc( (double)applied[sp] * scale )
hitLimit[sp] = false
for sp in 0..6:
n = Count(sys->Pop2, 1, sp)
if (n + applied[sp] < n) declined[sp] = true
Population::SetCount(sys->Pop2, 1, sp, n + applied[sp])
Two things a reimplementation gets wrong by default:
- The clamp is on the system total, not per species, and the redistribution is a truncating
proportional rescale — so the species shares do not have to add back up to
clamped, and the original does not renormalise. With one species present they do add up, and that is the only case the corpus exercises. - The write-back adds
appliedtoPop2alone, while the headroom test usedPop2 + pbon2. So a colony with a pending bonus pool grows less than itsPop2headroom would allow.
2.5 Seeding (unexercised — HYPOTHESIS)
The first loop of the pass (0x00754280..0x007543db, gated on sys->indi == 0) seeds species that
are absent: for each sp with Count(Pop2,1,sp) + Count(pbon2,1,sp) == 0 and a positive settle
limit, it plants min(limit, 1000) civilians — through 0x007502d0 with the two
MORALE_DEFAULT_* slots (0x00aeca78 = 10000, 0x00aeca7c = 0) when the species is the system's
own, and through 0x0074ec50 otherwise, keyed on bit 8 of ServerPlayer+0x348[sp]. No corpus
save has an absent-then-seeded species, so every branch of this loop is a hypothesis. The
workload that would decide it: a newly colonised system on the turn after the colony ship lands.
2.6 PopGrowthDelta and PopGrowthFraction
Re-read from the bytes this lane. Lane B4's model is confirmed on every rule, including the
three that a plausible reimplementation gets wrong: the strict > 0 gate on each of the four
multipliers, the float32 store-back after every one of them, and the delta == 0 && g > 0 -> 1
special case (0x005371b4). One addition B4's note does not carry:
factor = 1.0
if (sys->GFlags /*+0xdc*/ & (1 << owner->PlyrIdx /*+0x28*/)) factor = 1.5 // 0x00a1b000
if (owner->Species == 5) factor = ZuulHordeFactor(sys) // 0x0078f970
GFlags is struct-recovery.md's system flag word; the bit is per player index, and the
factor it selects is 1.5 — a growth bonus, not a penalty. Every corpus system carries
GFlags = 0 for the growing player, so this is unexercised and stays a hypothesis. The Zuul
override at 0x0078f970 is also unexercised for civilians (Zuul have no civilian population).
2.6a SuitabilityDistance — 0x00536eb0, re-read
if (p == 0) { log; return 20.0 } // 0.0 @0x00a1f6ec, 20.0 @0x00a1f6f0
if (p->RebAI /*+0xfc*/) return 0.0 // this is B4's "accommodated"
d = float32( | float32( StrategyServer::IdealSuit(p->server /*+0x8*/, species)
- clamp(suit, 0.0, 20.0) ) | )
return min(d, p->SuitTol /*+0xb4*/)
B4's model is confirmed, with one thing its note does not say: the ideal is the SERVER's
per-species baseline — the save's ISsu array — not the player's own IdealSuit field.
They agree for every player of that species in all 11 saves, which is why the difference is
instruction-verified only. Every owned colony in the corpus sits at Suit == ISsu[species]
exactly, so d = 0, base = 1.0, and the growth exponent is unreachable.
POPULATION_GROWTH_MOD and _EXP are read through the pointer slots 0x00ae2e94 → 0x00ae2e90 and
0x00ae2e9c → 0x00ae2e98. The image defaults, read out of the file, are 1.0 and 2.0; the
shipped data file overrides them to 1.2 and 1.85 (strategic-turn-internals.md §1).
3. InitPopTypeTable re-read — the six-register fxch rotation, independently
Lane N read this table by tracking six values rotated with fxch. Because the whole growth answer
hangs off one of its columns, it was re-derived from scratch here. The initial stack is
st0 = *0x009e5840 = 2.0 st1 = *0x009e2ea0 = 0.5 st2 = *0x009f8d48 = 0.33f
st3 = 1.0 st4 = *0x009e5ac0 = 0.25 st5 = 0.0
and the loop head fxch st(3) (0x00535cce, skipped on the first iteration) rotates it. Tracking
that yields:
| row | +0 | +4 growth | +8/+0xc int64 step cap | +0x10 out | +0x14 income | +0x18 | +0x1c | +0x20 capacity |
|---|---|---|---|---|---|---|---|---|
| 0 imperial | 0 | 1.0 | 50,000,000 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 1 civilian | 1 | 0.25 | 20,000,000 | 0.33f | 0.33f | 1.0 | 0.5 | 2.0 |
| 2 slaves | 2 | 0.0 | 0 | SLAVES_OUTPUT_MOD |
SLAVES_INCOME_MOD |
SLAVES_REPAIR_MOD |
0.0 | 0.0 |
Every value lane N published is reproduced, and lane N's live run had already confirmed four of
them in the running process. The +8 column is confirmed twice over: it is a plain integer
immediate (0x2faf080 / 0x1312d00 / edi) with +0xc always edi = 0, so it is an int64, and
it is not a "max population" — it is the maximum step one turn may take, which is how it is
used at 0x007545b5 and in ApplyImperialGrowth.
3.1 A correction to strategic-turn-internals.md §3.2
That section has cap = min(cap, groupdef[t].max @+0x28 if @+0x2c >= 0). The gate reading is
right and the consequence was never stated: InitPopTypeTable never writes +0x24, +0x28 or
+0x2c. They are written once, by the CRT static initialiser at 0x009abe20, to
row->+0x28 = 0xffffffff ; row->+0x2c = 0x7fffffff // all three rows
i.e. the int64 INT64_MAX. So the group-max clamp is present, always enabled, and always a
no-op. Read as "the table field is zero" — which is what a reader who only opens
InitPopTypeTable will conclude — it would zero every carrying capacity in the game. This is the
second time this campaign that a .bss table's real contents were not where the obvious
initialiser is.
4. PREDICTION — written before the build
Baseline for this lane, measured on ~/sots-engine main = eafbc5f (not the db99971
the brief names; lane T2 merged in between and moved the reference pair well past the brief's
numbers). Measured with tools/standalone_report.py before a line was written:
| pair | baseline | after one turn | closed | regressed |
|---|---|---|---|---|
turn1 -> turn2 |
209 | 131 | 78 | 0 |
turn2 -> turn3 |
108 | 72 | 36 | 0 |
The human's savings came out 289,092 against the oracle's 289,688 — the brief's 596 exactly.
4.1 What the model says the corpus does
Gamma Cephei (Sys[112], human, turn1-state.sav), every number off the wire:
Size 10, Suit 11.106206893920898, Pop 1e9, pbon 1e9, Pop2 {1,0,5e8}, pbon2 empty,
dcs {1,0,1e9}, GFlags 0, haltv[1] false, cm 75; owner IdealSuit 11.106206893920898
(exactly Suit), SuitTol 6.0, PopMod 1.0, harcc false.
- distance =
min(|ideal - clamp(suit,0,20)|, tol)= 0, sobase = 1 - 0/6 = 1.0andpow(1, EXP) = 1.0for every value ofEXP— the exponent cannot matter here. g = float32(1.0 x MOD x 1.0 x 1.0 x 0.25). With the shippedMOD = 1.2that is 0.30000001192092896; with the image defaultMOD = 1.0it is 0.25; with no tuning table loaded at allMOD = 0, the strict> 0gate skips the multiply, and it is also 0.25.delta = trunc(5e8 x g)= 150,000,005 (or 125,000,000 atg = 0.25).limit = min(cap, min(idealCap, dcs)) = min(2e9, min(2e9, 1e9)) = 1e9;headroom = 5e8.applied = min(delta, headroom) = delta;total = delta > 20,000,000 = maxStep.- rescale:
applied = trunc(delta x (20,000,000 / delta))= 20,000,000.
So the answer is 20,000,000 under all three tuning tables and both plausible species factors.
The prediction I am actually committing is the pair of numbers the oracle already holds:
Sys[112]/Pop2/PopG/PopC 500000000 -> 520000000 and Sys[288]/Pop2/PopG/PopC 500000000 -> 520000000 on pair 1, and 520000000 -> 540000000 on both on pair 2.
4.2 The carrying capacity, measured rather than assumed
The imperial capacity at Gamma Cephei is pinned from the corpus, without the data files, by two independent behaviours of the same colony across three consecutive turns:
pbon = 1,000,000,000never drains, andApplyPopBonusreturns early exactly whenPop >= MaxPop. ThereforeMaxPop <= Pop = 1e9.Popnever shrinks, and the imperial apply shrinks by up to 50,000,000 a turn exactly whenpop > cap. ThereforeMaxPop >= Pop = 1e9.
So imperial MaxPop = 1e9 = Size x 1e8 x POPTYPE[0].+0x20 (= 1.0), which forces
speciesGrowthFactor(Human) x crossSpeciesMod x hazard == 1.0 exactly. hazard is 1.0 from the
wire (Suit == IdealSuit) and crossSpeciesMod is not applied (owner species == population
species), so speciesGrowthFactor(Human) = 1.0, measured. The civilian capacity is then
1e9 x POPTYPE[1].+0x20 (= 2.0) = 2e9, and dcs = 1e9 is what binds.
4.3 What I predict for the standalone
- Closed on pair 1: the two
Pop2/PopG/PopCleaves, plusPlayer[16]/Sav. C3's §4.1 predicted our humanSavis short by exactly 597 on pair 1 and 596 on pair 2 for want of this growth; if that decomposition is right, committing growth closesSavoutright on both pairs. I predict 3 closed on pair 1 from S11 alone (2 population + 1Sav) and the same 3 on pair 2, withPvSavon pair 2 a fourth only if S00's snapshot is modelled (it is not — S00 only bumpsModCount), so I predictPvSavdoes not close. - 0 regressed on both pairs. The pass writes exactly one leaf per system
(
Pop2/PopG/PopC) and both of the systems it writes are already diverging. - The AI's
Savdoes not close. Player 32's savings move by its own in-turn orders (lane B6: 11,900 leaves the treasury before phase 0), so growth cannot fix it. BnkEl/BnkPrdo not close without--commit-blocked=T31 --ai-player ..., and T31 needsBANKRUPTCY_PROTECTION_LIMIT_FACTORforBnkPr. I will measure T31 separately and report it on its own line rather than folding it in.- No new RNG words. The pass is draw-free (B4 swept it to depth 1); the ledger must not move.
Ship::RepairCostcloses nothing. No fleet sits at either growing colony (Sys[112]/NumFlts = 0inturn1-state.sav), so the repair demand is 0 whether or not it is modelled. I predict wiring it moves 0 leaves and 0 money on all four corpus pairs, and its value is that it removes the last named unmodelled input from the output turn path rather than that it closes anything.
4.4 Falsification — how this could be wrong, and the symptom of each
| way it could be wrong | symptom |
|---|---|
+0x08 is a capacity column and not a step cap, so the 20,000,000 coincidence is really min(cap, ...) landing there |
our delta would not be 20,000,000 on the second pair, where the colony starts at 520,000,000 — a capacity of 20,000,000 would make it shrink, a step cap makes it +20,000,000 again. Pair 2 is the discriminator and it is already in the corpus. |
| the rescale truncation lands one ulp low | Pop2 comes out 519,999,999 and both population leaves stay open while Sav moves by a few money. This is the one genuinely precision-critical float and it is checked explicitly, §4.5 |
cur does not include pbon2 |
invisible here: pbon2 is empty on every corpus system. Hypothesis. |
the headroom uses Pop2 alone and the write-back uses Pop2 + pbon2 (i.e. I have the two backwards) |
same — invisible, hypothesis |
| the clamp is per species rather than per system | invisible: one species per colony in the whole corpus |
the -50,000,000 floor is POPTYPE[0].+8 rather than a literal |
invisible: no colony shrinks |
C3's 597/596 decomposition is wrong and Sav needs something else too |
the two population leaves close and Sav stays open with a smaller residual, which would name the remainder exactly |
growth is committed but ComputeBudget reads a stale copy |
Sav does not move at all while the population leaves close |
4.5 The float that decides the value (rule: rank precision by consequence)
Ranked by consequence, not by count:
trunc( applied x (clamped / total) )— the rescale.clamped = 20,000,000is exactly representable;total = 150,000,005is not a power of two, sofl(clamped/total)carries a relative error up to2^-53and the product's error,~2.2e-9, is larger than half an ulp of 20,000,000 (1.9e-9). It is therefore not a theorem that the product rounds back to 20,000,000; it has to be checked. Computed both at 53-bit and at x87's 64-bit significand it comes out exactly 20,000,000 on both reference pairs, and a test pins it. One ulp the other way costs one person and moves the human's money.float32(g)— three narrowings, each after a strictly->0-gated multiply. Consequential only through step 3, and step 3 is 7.5x over the cap, so no value ofgin[0.04, 1.0]changes the answer.gis the term everyone will look at first and it is the one that does not matter here.hazardand the capacity chain — a 25x margin. Irrelevant unless wrong by more than an order of magnitude.
5. Ship::RepairCost — 0x00815180 (instruction-verified)
__thiscall, ret 4, 56 bytes, real end 0x008151b7 (0x008151ba..0x008151bf is int3).
Ship::RepairCost(StarShip* sh, bool includeRefit):
d = sh->DesID /*+0x14, the ShipDesign POINTER*/
c = sh->ConCap /*+0x68, int*/
if (includeRefit) c += d->+0xd0
r = d->+0xcc - c
return r > 0 ? r : 0
There is no floating point, no clamp other than the floor at zero, and no fleet or system input:
it is a two-field integer subtraction on the ship and its design. d->+0xd0 is the field lane B6
found StarShip_Create copying into ConCap at birth, so a newly built, undamaged hull has
ConCap == d->+0xd0 and RepairCost(true) = d->+0xcc - 2 x d->+0xd0; RepairCost(false) = d->+0xcc - d->+0xd0. Both are floored at zero. The identity of +0xcc and +0xd0 on
ShipDesign is not settled by this lane and is the open item (§5.3).
5.1 Where it is consumed
ServerSystem::RepairShipsInOrbit 0x00751590 (lane C3 §2.4) calls it with includeRefit = 1 both
to size the demand and, in the round robin, to size each ship's take. ComputeOutputFromRates
passes estimateOnly = 0, so calling the original to harvest a repair number repairs ships —
the unresolved B1 replace double-run defect. Nothing in the engine calls it.
5.2 out[6] is not a repair number at all — a correction to output-turn-path.md
out[6] was published as SystemRepairDemandForOwner 0x007460b0. Read to its callee, it is
not a repair function. It takes the system in EBX, iterates the fleets at the system
through the system's own vtable slots 2 and 3, skips a fleet whose owner is not the argument or
whose +0x78 byte is clear, and sums 0x0081f8c0 over each ship — and 0x0081f8c0
- returns 0 unless
design->+0xb8 & 0x04000000, the carried-population bit lane B6 named, and - then computes
GroupIncomeover the ship's ownPopulationatship+0x9c.
So out[6] is the income of population carried in slaver and colony hulls in orbit — which is
a slot sots-engine's BudgetInputs already carries as shipCarriedPopIncome, under a name
nobody had connected to it. It is reporting-only and reaches no save leaf, so nothing downstream
moves; the name does. Corrected in place in output-turn-path.md §1, §2.1 and a new §5.
addresses.d/lane-c3.json's three stubs (Ship_RepairCost, Ship_ApplyRepair,
SystemRepairDemandForOwner, all marked "body not read") are superseded by lane-g3.json's
read entries, which carry a _G3 suffix so the duplicate-name check stays a hard error rather
than a silent last-wins. The integrator should drop C3's three when folding.
5.3 What is not settled, and the workload
ShipDesign+0xccand+0xd0are read but not named.Ship::ApplyRepair0x008151c0 settles half of it: it doesConCap += max(points, 0)and then clampsConCapinto[0, +0xcc], soConCapis the construction invested in the hull so far — not a per-turn capacity, despite the save-format name — and+0xccis its ceiling.+0xd0is an allowance subtracted only when the caller asks for it. Naming the two needs the design catalogue's cached-stat block, which is lane B6's territory. Note also thatApplyRepairsilently does nothing unless the ship's cached role word (+0x18, not on the wire) carries bit0x400000, while the pass's candidate filter tests a different bit — so a ship can be charged points that never reach it. Read, not explained.- No corpus save produces a non-zero value — and that is a measurement, not an absence.
Rule 20: a zero cost cannot be told from "never entered" by a count. Here the entry is
observable from the wire. The human's Gamma Cephei has
NumFlts = 0on both reference pairs, so the demand is structurally zero there. The independent colony's Koa'Vo carries a fleet of ten ships in orbit on both pairs, and that player'sSavcloses exactly with the demand taken as zero — which it could not do if any of those hulls had a positive cost. SoShip::RepairCostis entered on this corpus and returns 0, which is a much stronger negative than reachability. What is still unexercised is a hull withConCap < +0xcc. The workload that would decide that: take a fleet into combat, lose structure, park it over a colony with an empty build queue, end a turn.
6. MEASURED
Engine work on sots-engine wip/growth off main eafbc5f. Gates run as separate
commands: tools/clean_room_check.sh OK; host ctest 49/49; and — because
src/game/sim/* is compiled into the shim — the CT111 cross-build was run by this lane
(/srv/re-lab/build/sots-engine-g3, cmake --preset shim + --build, exit 0,
binkw32.dll and sots_turn.exe linked). The host build and ctest were also re-run on CT111
(49/49) and tools/standalone_report.py produced identical numbers there and on the WSL
host, which is a free check that nothing here depends on the toolchain.
6.1 Closed and regressed, never netted
| pair | baseline | before this lane | after | closed | regressed |
|---|---|---|---|---|---|
turn1 -> turn2 |
209 | 131 (closed 78) | 128 | 81 | 0 |
turn2 -> turn3 |
108 | 72 (closed 36) | 69 | 39 | 0 |
So +3 closed on each pair, 0 regressed on each pair, by default with no flags. The three are the same on both pairs:
Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC— the human's colony;Sys[288 "Ke'Dolarra"]/Pop2/PopG/PopC— the AI's;Player[16 "re"]/Sav— the human's savings.
With --commit-blocked=T31 --ai-player 1 (reported separately, as a blocked phase should be):
pair 1 closed 83, regressed 0; pair 2 closed 41, regressed 0. The extra two per pair are
Player[16]/BnkEl and Player[32]/BnkEl. T31 used to close nothing with those same flags,
for exactly the reason its own catalogue note gave — the limits move with the civilian population
and that growth was not committed. BnkPr still needs BANKRUPTCY_PROTECTION_LIMIT_FACTOR.
6.2 Every prediction in §4.3, checked
| predicted | measured | verdict |
|---|---|---|
the two Pop2/PopG/PopC leaves close on both pairs |
closed on both | HELD |
Player[16]/Sav closes on both pairs |
closed on pair 2 immediately; on pair 1 it came out 289,689 against 289,688 — one high | FALSIFIED, and it found a defect: §6.3 |
| 3 closed per pair from S11 | 3 per pair — after §6.3 | HELD |
| 0 regressed on both pairs | 0 and 0 | HELD |
PvSav does not close |
it does not | HELD |
the AI's Sav does not close |
it does not (its own in-turn orders move it) | HELD |
BnkEl/BnkPr need --commit-blocked=T31 |
BnkEl closes with it, BnkPr does not |
HELD |
| no new RNG words | the ledger did not move | HELD |
Ship::RepairCost closes nothing |
0 leaves, 0 money, all four pairs | HELD |
the +0x08 column is a step cap, not a ceiling |
pair 2 takes the same +20,000,000 step from a higher starting population, which a ceiling of 20,000,000 could not do | HELD — and this was the falsification test §4.4 named |
6.3 The falsified prediction, and what it found
Sav was one money high on pair 1 and exact on pair 2. That is not a growth error: growth closed
596 of the 596-money gap. The remaining one is ComputeBudget's savings interest.
Both interest rates in ComputeBudget 0x00863030 are widened float literals, not the exact
decimals:
0x008631ce fild [savings] ; fmul QWORD PTR ds:0x009e31c0 ; call _ftol2 ; 0.009999999776482582 = (double)0.01f
0x008631b0 fild [debt] ; fmul QWORD PTR ds:0x009ed188 ; call _ftol2 ; 0.15000000596046448 = (double)0.15f
sots-engine's game/sim/economy.cpp used exact 0.01 and 0.15, so a treasury of exactly
50,000 earned 500 where the game pays trunc(50000 x 0.009999999776482582) = 499. Pair 2's
treasury is 289,688, where the two literals agree, which is why only one of the two pairs showed
it. Corrected, with the constants named in economy.h and sixteen hand-computed expectations
in tests/game_sim/test_economy.cpp moved by exactly one — they were derived from the exact
decimal, not measured, and are now derived from the literal.
Two things follow that matter beyond this lane:
- A live-verified module was wrong.
ComputeBudgetcompared 4,437 calls with 0 divergences. It did not catch this because — rule 15, and that run's own report says so — it presented only 20 distinct states and none of them sat on a rounding boundary. This is the second campaign example of rule 3: static reading found what behavioural comparison could not. kBankruptcyInterestDivisor, which lane E1 already carries as-0.15000000596046448, is the same constant at the same address, negated. The campaign had half of this fact for a day.
6.4 What this run did NOT cover — read this before quoting the zero
- One species per colony, everywhere. The proportional rescale of §2.4 is exercised only in its degenerate one-species form, where it is exact. Its truncation and its lack of renormalisation are read from the bytes and pinned by a unit test, not measured.
- Nothing shrinks. The
-50,000,000decline floor, the negative-side rescale and thedeclined[]flag are all unexercised. pbon2is empty on every corpus system, so the asymmetry between the headroom (which counts the pending bonus pool) and the write-back (which does not) is invisible. If I have those two sides backwards, no corpus save can tell.haltv[1]is false everywhere — the blockade gate never fires.GFlagsis 0 for the growing player everywhere — the 1.5x factor never fires.- No species is ever seeded (§2.5): the whole first loop of the pass is a hypothesis.
- The morale-event half is not modelled. One species per turn (Koa'Vo's Tarkas) does hit its settle limit and the original raises a growth/decline morale event for it; we raise none. No corpus morale leaf moves, so this costs nothing today — but it is a known omission, not an absence, and it belongs to the events lane.
- Imperial growth is still not committed. It is a no-op on this corpus (the homeworld sits exactly at its cap) and committing it would need a capacity the corpus can bound from below but not from above — see §4.2. Committing it would risk a regression to buy nothing.
- The per-species civilian capacity factor is a data-file value taken as 1.0. The run reports
the threshold: it would have to fall below 0.260 before any committed value changed, against
a measured lower bound of 0.27 from the growth the oracle shows. The commit is additionally
gated on the pass agreeing with itself when the modelled capacity is replaced by the system's
own wire-known
dcslimit.
6.5 The margin, reported by the run itself
civilian growth: 3 owned system(s) with civilians, 2 grew, 2 leaf write(s)
civilian growth: 2 system(s) had the value decided by the 20,000,000 per-turn step cap
civilian growth: smallest capacity headroom on any growing species: 500000000, against a step
cap of 20000000 -- a 25.0x margin
civilian growth: the per-species civilian capacity factor would have to fall below 0.260 before
any committed value changed
civilian growth: NO tuning table ... the curve's base is 1.0 and the exponent cannot matter
ship-repair demand taken as 0: 1 owned colony(ies) carry a fleet in orbit at all, 10 ship(s)
between them
7. Engine changes
src/game/sim/colony.{h,cpp}—GrowCivilianPopulations,ShipRepairCost, and the population-type table's own columns as named constants (kCivilianGrowthStepCapand friends).src/game/sim/economy.{h,cpp}— the two interest literals (§6.3).src/game/sim/species.h—growthFactor, with the corpus measurement for Human in the comment.src/app/growth_phase.{h,cpp}— the wiring, the twice-run capacity-insensitivity gate and the margin reporting. Reported on its own line under S11 rather than folded into it.src/app/turn.cpp— S11 runs it; S13 reports the repair-demand candidate set so the zero is evidenced rather than silent.tests/game_sim/test_colony.cpp— six growth cases including both reference pairs, the step-cap-vs-ceiling discriminator, the zero-headroom colony, the shrink floor and the two-species rescale; plusShipRepairCost.tests/game_sim/test_economy.cpp— the sixteen corrected expectations.
8. Open, and named
ShipDesign+0xccand+0xd0— read but not identified.Ship::ApplyRepairnames+0xccas the ceiling on the hull's invested construction;+0xd0is an allowance subtracted only when the caller asks, and lane B6 has it copied into a new hull'sConCapat birth, which would makeRepairCost(true)at birth+0xcc - 2 x +0xd0. One of those two readings is probably off by a field. The design cached-stat block is lane B6's territory.Ship::ApplyRepair's+0x400000gate. The pass's candidate filter tests one bit and the apply tests another, so a ship can be charged points that never reach it. Read, not explained.- The
dcsPopulation — who writes it, and when. It is the binding civilian limit on every corpus colony and it does not move across three turns, so nothing in the corpus constrains its writer. Until that is known, "the settle limit isdcs" is a fact about the read and a hypothesis about the game. ServerPlayer+0x348[species], the per-species xenotech flag word. Bit 3 gates civilians existing at all, bit 7 gates the hazard modifier and bit 8 selects a cross-species capacity constant. It is not on the wire and is derived from the tech tree; the engine currently assumes a species with a population present passed the gate.- The morale events the settle-limit and decline flags raise (ids 0x10 / 0x11) — structure
read, deltas already tabulated in
strategic-turn-internals.md§3.2, wiring not done. - Imperial growth, which needs a capacity bounded from above. The workload that would give one: a colony below its cap that grows for two consecutive turns, which no corpus save has.