# 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], 0x1312d00` at 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 | ~~`SystemRepairDemandForOwner`~~ `SystemShipCarriedPopIncome(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: 1. **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. 2. **The write-back adds `applied` to `Pop2` alone**, while the headroom test used `Pop2 + pbon2`. So a colony with a pending bonus pool grows *less* than its `Pop2` headroom 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`. 1. distance = `min(|ideal - clamp(suit,0,20)|, tol)` = **0**, so `base = 1 - 0/6 = 1.0` and `pow(1, EXP) = 1.0` **for every value of `EXP`** — the exponent cannot matter here. 2. `g = float32(1.0 x MOD x 1.0 x 1.0 x 0.25)`. With the shipped `MOD = 1.2` that is **0.30000001192092896**; with the image default `MOD = 1.0` it is 0.25; with **no tuning table loaded at all** `MOD = 0`, the strict `> 0` gate skips the multiply, and it is also 0.25. 3. `delta = trunc(5e8 x g)` = **150,000,005** (or 125,000,000 at `g = 0.25`). 4. `limit = min(cap, min(idealCap, dcs)) = min(2e9, min(2e9, 1e9)) = 1e9`; `headroom = 5e8`. 5. `applied = min(delta, headroom) = delta`; `total = delta > 20,000,000 = maxStep`. 6. 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,000` **never drains**, and `ApplyPopBonus` returns early exactly when `Pop >= MaxPop`. Therefore `MaxPop <= Pop = 1e9`. * `Pop` **never shrinks**, and the imperial apply shrinks by up to 50,000,000 a turn exactly when `pop > cap`. Therefore `MaxPop >= 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 1. **Closed on pair 1: the two `Pop2/PopG/PopC` leaves, plus `Player[16]/Sav`.** C3's §4.1 predicted our human `Sav` is short by exactly 597 on pair 1 and 596 on pair 2 for want of this growth; if that decomposition is right, committing growth closes `Sav` outright on both pairs. I predict **3 closed on pair 1** from S11 alone (2 population + 1 `Sav`) and the same 3 on pair 2, with `PvSav` on pair 2 a fourth **only if** S00's snapshot is modelled (it is not — S00 only bumps `ModCount`), so I predict `PvSav` does **not** close. 2. **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. 3. **The AI's `Sav` does 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. 4. **`BnkEl`/`BnkPr` do not close without `--commit-blocked=T31 --ai-player ...`**, and T31 needs `BANKRUPTCY_PROTECTION_LIMIT_FACTOR` for `BnkPr`. I will measure T31 separately and report it on its own line rather than folding it in. 5. **No new RNG words.** The pass is draw-free (B4 swept it to depth 1); the ledger must not move. 6. **`Ship::RepairCost` closes nothing.** No fleet sits at either growing colony (`Sys[112]/NumFlts = 0` in `turn1-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: 1. **`trunc( applied x (clamped / total) )`** — the rescale. `clamped = 20,000,000` is exactly representable; `total = 150,000,005` is not a power of two, so `fl(clamped/total)` carries a relative error up to `2^-53` and 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. 2. `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 of `g` in `[0.04, 1.0]` changes the answer. `g` is the term everyone will look at first and it is the one that does not matter here. 3. `hazard` and 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 `GroupIncome` over the ship's own `Population` at `ship+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+0xcc` and `+0xd0` are read but not named. `Ship::ApplyRepair` 0x008151c0 settles half of it: it does `ConCap += max(points, 0)` and then clamps `ConCap` into `[0, +0xcc]`, so **`ConCap` is the construction invested in the hull so far — not a per-turn capacity, despite the save-format name — and `+0xcc` is its ceiling.** `+0xd0` is 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 that `ApplyRepair` silently does nothing unless the ship's cached role word (`+0x18`, not on the wire) carries bit `0x400000`, 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 = 0` on 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's `Sav` closes **exactly** with the demand taken as zero — which it could not do if any of those hulls had a positive cost. So `Ship::RepairCost` is *entered* on this corpus and returns 0, which is a much stronger negative than reachability. What is still unexercised is a hull with `ConCap < +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.** `ComputeBudget` compared **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,000` decline floor, the negative-side rescale and the `declined[]` flag are all unexercised. * **`pbon2` is 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. * **`GFlags` is 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 `dcs` limit. ### 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 (`kCivilianGrowthStepCap` and 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; plus `ShipRepairCost`. * `tests/game_sim/test_economy.cpp` — the sixteen corrected expectations. ## 8. Open, and named 1. **`ShipDesign+0xcc` and `+0xd0`** — read but not identified. `Ship::ApplyRepair` names `+0xcc` as the ceiling on the hull's invested construction; `+0xd0` is an allowance subtracted only when the caller asks, and lane B6 has it copied into a new hull's `ConCap` at birth, which would make `RepairCost(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. 2. **`Ship::ApplyRepair`'s `+0x400000` gate.** 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. 3. **The `dcs` Population — 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 is `dcs`" is a fact about the read and a hypothesis about the game. 4. **`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. 5. **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. 6. **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.