lane G3: civilian growth read and measured; out[6] is not a repair number

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.
This commit is contained in:
alex 2026-09-08 15:53:08 -04:00
parent 3e5117bc05
commit d4f9404960
3 changed files with 728 additions and 2 deletions

View file

@ -42,7 +42,7 @@ This supersedes two labels in earlier notes:
| 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 | `SystemRepairDemandForOwner(ServerPlayer*)` → int | **reads `ebx` as `sys`** | fills `out[6]` |
| 0x007460b0 | ~~`SystemRepairDemandForOwner`~~ **`SystemShipCarriedPopIncome(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
@ -63,7 +63,8 @@ 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] = SystemRepairDemandForOwner(sys->PID) // 0x007460b0, `sys` arrives in EBX
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
@ -503,3 +504,31 @@ what stands between `BnkEl`/`BnkPr` and the oracle (four more leaves), and it is
`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 `GroupIncome` over the ship's own `Population` at `ship+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.

View file

@ -0,0 +1,557 @@
# 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.

View file

@ -0,0 +1,140 @@
{
"entries": [
{
"name": "ServerSystem_GrowCivilianPops_G3",
"addr": "0x00754220",
"convention": "thiscall",
"prototype": "void (ServerSystem* sys) // plain `ret`, REAL END 0x00754b59 -- the body is a loop over group types 0,1,2 whose back edge is at 0x00754b2e (`inc esi; cmp esi,3; jl 0x7543e9`) and lies outside every decompiler `if`, so the function reads as straight-line code if you stop at the first `ret`. Returns immediately when the system has no owner. Per group type: skip when Population::TotalOfType(Pop2, t) <= 0, then skip unless t == 1 -- so only CIVILIANS grow here. Per species: delta = PopGrowthDelta(1, sp); cur = Count(Pop2,1,sp) + Count(pbon2,1,sp); cap = MaxPopGeneric(1, sp, PID, NULL); soft = CivilianSettleLimit(sp); limit = soft < cap ? soft : cap (and soft < cap with cur+delta > soft raises a per-species settle flag); applied = min(delta, limit - cur); haltv[1] with applied > 0 zeroes both. The SYSTEM TOTAL is then clamped into [-50,000,000 (a literal here), POPTYPE[1]+0x08 = 20,000,000] and, when the clamp bit, every entry of the clamp's own sign is rescaled by trunc(applied x (clamped / total)) with NO renormalisation. Write-back is Population::SetCount(Pop2, 1, sp, Count(Pop2,1,sp) + applied) -- note the headroom counted pbon2 and the write does not. THE 20,000,000 CLAMP IS WHAT DECIDES THE VALUE on both reference pairs: the uncapped delta is 7.5x it and the capacity headroom 25x it",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "ServerSystem_CivilianSettleLimit",
"addr": "0x0074a9a0",
"convention": "thiscall",
"prototype": "int64 (ServerSystem* sys, int species) // `ret 4`, ends 0x0074aa2d. Returns 0 without an owner. Otherwise min( MaxPopGeneric(1, sp, PID, &StrategyServer::IdealSuit(server, sp)) , Population::Count(sys->dcs /*+0x104*/, 1, sp) ). The first term is the capacity the colony would have if the planet were perfectly suited; the second is the system's own `dcs` Population, which is on the wire. On Gamma Cephei `dcs` = 1e9 and it is the binding limit, not either capacity",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "ServerSystem_MaxPopGeneric_G3",
"addr": "0x0074a4a0",
"convention": "thiscall",
"prototype": "int64 (ServerSystem* sys, int groupType, int species, ServerPlayer* p, float* suitOverride) // `ret 0x10`, REAL END 0x0074a6cd -- 0x0074a6d0 is a DIFFERENT function (lane N and E1 both cite 0x0074a6d0 for the capacity-surplus pair; that is the next one along, not this). Returns 0 when species == 4; when the OWNER species' SpeciesDef+0x168[groupType] is not > 0 (which is why Zuul have no civilians); when groupType != 0 and the owner is RebAI; when groupType == 1 and bit 3 of ServerPlayer+0x348[species] is clear; and when groupType == 2 and 0x0082bdf0(p, species) is false. Otherwise ftoi64( (int64)Size x 1e8 x [ hazard x (POPTYPE[t]+0x20 x SpeciesDef(sp)+0x168[t] x (crossSpecies ? SpeciesDef(sp)+0x174[t] : 1)) ] ) via 0x00535eb0, plus the arcology flat bonus 0x0080dd30(p, t), then clamped to the int64 at POPTYPE[t]+0x28 -- see PopTypeTableStaticInit, that clamp is always a no-op -- and finally, for groupType 0 ONLY, scaled by INDSYS_IMPERIAL_POPULATION_MOD when the owner species is 4",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "SpeciesDef_GroupCapacityFactor",
"addr": "0x0053bb00",
"convention": "thiscall",
"prototype": "float (SpeciesDef* d, int groupType) // `ret 4`, 15 bytes. Returns d->float[0x168 + groupType*4]. The per-species, per-population-group capacity factor -- a DATA FILE value. Its imperial entry for Human is pinned at exactly 1.0 by the corpus (Gamma Cephei's pbon never drains and its Pop never shrinks, which bracket the imperial capacity at Size x 1e8); the civilian entry is bounded below at 0.27 by the observed growth and is otherwise unmeasured",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "SpeciesDef_GroupCrossSpeciesMod",
"addr": "0x0053bb20",
"convention": "thiscall",
"prototype": "float (SpeciesDef* d, int groupType) // `ret 4`, 15 bytes. Returns d->float[0x174 + groupType*4]; applied by MaxPopGeneric only when the system owner's species differs from the population species and is not 4",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "SpeciesDef_HasPopulationGroup",
"addr": "0x0053bb40",
"convention": "thiscall",
"prototype": "bool (SpeciesDef* d, int groupType) // `ret 4`. Returns `0.0 < d->float[0x168 + groupType*4]` -- the SAME array SpeciesDef_GroupCapacityFactor reads, so 'this species has this population group' and 'its capacity factor is positive' are one fact. MaxPopGeneric asks it about the system OWNER's species, not the population's",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "PopGrowthSuitabilityDistance",
"addr": "0x00536eb0",
"convention": "cdecl",
"prototype": "float (ServerPlayer* p, int species, float suit) // ends 0x00536f8b. Returns 20.0 and logs when p is null; 0.0 when p->RebAI (+0xfc) -- the 'accommodated' case; otherwise min( float32(|float32(StrategyServer::IdealSuit(p->server /*+0x8*/, species) - clamp(suit, 0, 20))|) , p->SuitTol /*+0xb4*/ ). The ideal is the SERVER's per-species baseline (the save's ISsu array), NOT the player's own IdealSuit field",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "PopGrowthDeltaHelper",
"addr": "0x00537140",
"convention": "cdecl",
"prototype": "int64 (ServerPlayer* p, int groupType, int species, float suit, float factor, int64 pop) // `add esp,0x1c`, 7 dwords of arguments, ends 0x005371ca. Returns 0 unless pop > 0; g = PopGrowthFraction(p, groupType, species, suit, factor); returns 0 when g == 0 (an fucomp against zero, so a NaN g also proceeds); d = ftoi64(g x pop); returns d, or exactly 1 when d == 0 and g is strictly positive, or 0 when d == 0 and g is negative",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "PopTypeTableStaticInit",
"addr": "0x009abe20",
"convention": "cdecl",
"prototype": "void () // the CRT static initialiser for the population-type table at 0x00b104e8, and THE ONLY WRITER of the group-ceiling field. It sets every row's +0x28 to 0xffffffff and +0x2c to 0x7fffffff -- the int64 INT64_MAX -- along with +0x00/+0x04/+0x08/+0x0c to -1/1.0/-1/-1 and the float columns to 1.0. InitPopTypeTable 0x00535ca0 runs later and overwrites +0x00 through +0x20 but NEVER touches +0x24/+0x28/+0x2c. So MaxPopGeneric's group-ceiling clamp is present, always enabled, and always a no-op; a reader who opens only InitPopTypeTable sees zero there and would cap every carrying capacity in the game at nothing",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "Ship_RepairCost_G3",
"addr": "0x00815180",
"convention": "thiscall",
"prototype": "int (StarShip* sh, bool useAllowance) // `ret 4`, REAL END 0x008151b7 (0x008151ba..0x008151bf is int3). max(0, design->+0xcc - (sh->ConCap /*+0x68*/ + (useAllowance ? design->+0xd0 : 0))). No floating point, no clamp but the floor at zero. Its only caller, RepairShipsInOrbit, always passes 1. UNEXERCISED on the corpus, and that is a measurement rather than an absence: the independent colony's fleet sits over Koa'Vo on both reference pairs and that player's Sav closes exactly with the demand taken as zero",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "Ship_ApplyRepair_G3",
"addr": "0x008151c0",
"convention": "thiscall",
"prototype": "void (StarShip* sh, int points) // `ret 4`, ends 0x00815221. Does NOTHING unless the ship's cached role word (+0x18, not on the wire) carries bit 0x400000 -- a different bit from the one RepairShipsInOrbit's candidate filter tests, so a ship can be charged points that never reach it. Otherwise sh->ConCap += max(points, 0) and then ConCap = min(max(ConCap, 0), design->+0xcc). THIS IS WHAT NAMES THE TWO FIELDS: ConCap is the construction invested in the hull so far -- not a per-turn capacity, despite the save-format name -- and design+0xcc is its ceiling",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "ServerSystem_ShipCarriedPopIncome",
"addr": "0x007460b0",
"convention": "cdecl-through-register",
"prototype": "int (ServerPlayer* owner) /* the system arrives in EBX */ // ends 0x0074615a. CORRECTION to output-turn-path.md, which names this SystemRepairDemandForOwner: it is NOT a repair function. It walks 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 gates on the design's CARRIED-POPULATION bit (design->+0xb8 & 0x04000000) and computes GroupIncome over the ship's Population at ship+0x9c. So ComputeOutput's out[6] is the income of population carried in slaver/colony hulls in orbit, which is a slot the engine's BudgetInputs already has, and it feeds no save leaf",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "ShipCarriedPopIncomePerShip",
"addr": "0x0081f8c0",
"convention": "thiscall",
"prototype": "int (StarShip* sh) // returns 0 unless design->+0xb8 & 0x04000000 (the carried-population bit lane B6 named); otherwise sums GroupIncome over the ship's `hsp` Population at ship+0x9c for group types 0 and 1. Body read to the second GroupIncome call only",
"status": "unverified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "g_SavingsInterestRate",
"addr": "0x009e31c0",
"convention": "data",
"prototype": "double 0.009999999776482582 = (double)0.01f -- the savings-interest multiplier ComputeBudget uses at 0x008631ce, then truncated by _ftol2. It is a WIDENED FLOAT, not the exact decimal, and the difference is not cosmetic: a treasury of exactly 50,000 earns 499, not 500. That one money was the whole residual on the human's Sav after civilian growth landed",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "g_DebtInterestRate",
"addr": "0x009ed188",
"convention": "data",
"prototype": "double 0.15000000596046448 = (double)0.15f -- the debt-interest multiplier ComputeBudget uses at 0x008631b0. The same constant lane E1 already carries, negated, as the bankruptcy-elimination divisor",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "g_FlaggedSystemGrowthFactor",
"addr": "0x00a1b000",
"convention": "data",
"prototype": "float 1.5 -- the per-call growth factor PopGrowthDelta 0x00748100 substitutes for 1.0 when the system's GFlags (+0xdc) carries the owner's PlyrIdx bit. It is a BONUS, not a penalty. UNEXERCISED: GFlags is 0 for the growing player on every corpus system",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
},
{
"name": "g_PopTypeCivilianGrowthMod",
"addr": "0x009e5ac0",
"convention": "data",
"prototype": "float 0.25 -- POPTYPE[1]+0x04, the civilian growth column, read out of InitPopTypeTable's six-register fxch rotation. Independently re-derived this lane; lane N's whole table is reproduced (imperial +0x04 = 1.0 from 0x009e2ea0's neighbours, civilian +0x10/+0x14 = 0.33f from 0x009f8d48, civilian +0x1c = 0.5, civilian +0x20 = 2.0 from 0x009e5840)",
"status": "verified",
"source": "findings/subsystems/population-growth.md (lane G3 2026-09-08)"
}
]
}