findings/subsystems/output-term.md is the whole reading: the call chain with real function boundaries, the formula, the closed list of nine values the data files supply (down from an unbounded fear), the advance prediction with its falsification table, and the live result. Corrects strategic-turn-internals.md 3.3 in place. That block had the SHAPE wrong, not just the detail: a system's output is a sum of three terms, and the function it named as the population base-output term is the over-harvest resource demand. Live on VM140, two builds, two species, both hooks in compare mode: GroupOutput 13,105 calls / 0 divergences, ComputeTotalOutput 11,252 / 1 (one ulp), 0 undeclared writes in 24,357 guarded calls. Thirteen distinct system states, and every unexercised branch is listed rather than counted as covered. tools/max_income_predict.py is the other half: it computes lane Y's bankruptcy-limit oracle from colony state and reports 6 of 25 player-records matching exactly, with the misses all AI-owned and the single-system ones short by exactly the 1.1 difficulty income multiplier.
414 lines
23 KiB
Markdown
414 lines
23 KiB
Markdown
# The population → base-output term (lane N, 2026-09-08)
|
||
|
||
The formula that blocks `P01 P02 P03 P05 P06` in `src/app`. Read from the instruction stream,
|
||
then hooked live on VM140.
|
||
|
||
Evidence: own `objdump -d` pass over `Sword of the Stars.exe`, every range disassembled **to the
|
||
next function start** and the real boundary found (rule 17). Field names per
|
||
`findings/objects/struct-recovery.md`.
|
||
|
||
---
|
||
|
||
## 0. Correction to `strategic-turn-internals.md` §3.3
|
||
|
||
That section says:
|
||
|
||
> ```
|
||
> total = round( BaseOutput × mods ) // 0x00750480:
|
||
> BaseOutput from pop (0x007483b0: (pbon+Pop)/… resource-availability factor,
|
||
> capped by species +0x4c)
|
||
> ```
|
||
|
||
**0x007483b0 is not the population→output term.** It is the *strip-mining / over-harvest resource
|
||
demand*, its `P` is `Res` (+0x68) and not `Pop`, and species `+0x4c` is **added**, not a cap — the
|
||
cap is the available resource stock. `(pbon+Pop)` enters it only as a `clamp01((pbon+Pop)×1e-5)`
|
||
scale factor, and only when the over-harvest rate `SRoh > 0`. The line is corrected in place below
|
||
and in `strategic-turn-internals.md`.
|
||
|
||
The real population→output term is `PopOutput(0) + PopOutput(1) + SlaveOutput()` inside
|
||
0x00750480, and it is far simpler than the note implied: **output points are linear in population**,
|
||
`pop × typeMod × 1.8 / 500000`.
|
||
|
||
---
|
||
|
||
## 1. The call chain
|
||
|
||
| addr | name | conv / boundary | note |
|
||
|---|---|---|---|
|
||
| 0x00751bb0 | `ServerSystem::ComputeOutputFromRates(int out[12], OutputRates* rates)` | thiscall, ends 0x00751fa8 | **repairs ships in orbit** — not compare-safe |
|
||
| 0x00750480 | `ServerSystem::ComputeTotalOutput(double SRoh)` | thiscall `ret 8`, ends 0x007505a5 | **side-effect free** — the compare target |
|
||
| 0x00747d30 | `ServerSystem::StripMineFraction()` | thiscall, returns float, ends 0x00747ddd | |
|
||
| 0x007483b0 | `ServerSystem::OverHarvestDemand(double SRoh)` | thiscall `ret 8`, ends 0x007484c4 (Ghidra size 276 = correct here) | |
|
||
| 0x0074d8f0 | `ServerSystem::PopOutput(int groupType)` | thiscall `ret 4`, ends 0x0074da01 | |
|
||
| 0x0074b880 | `ServerSystem::SlaveOutput()` | thiscall, ends 0x0074b901 | |
|
||
| 0x0074b7a0 | `ServerSystem::GroupOutput(int groupType, int species, double count)` | thiscall `ret 0x10`, ends 0x0074b871 | **the per-capita term** |
|
||
| 0x00747ba0 | `ServerSystem::GroupPopulation(int groupType, int species)` → int64 | thiscall `ret 8` | |
|
||
| 0x00746910 | `ServerSystem::MoraleOutputMod(int species)` → double | thiscall `ret 4` | |
|
||
| 0x00535e00 | `PopTypeRow(int t)` | cdecl | table at .bss 0x00b104e8, stride 0x30, 3 rows |
|
||
| 0x00535ca0 | `InitPopTypeTable()` | — | **the table is built in code**, see §3 |
|
||
| 0x00545cc0 | `SpeciesDef(int sp)` | cdecl | table at .bss 0x00b10a00, stride 0x184, 7 rows |
|
||
| 0x008e5680 | `signed_cbrt(double)` | cdecl | `x>=0 ? pow(x,1/3) : -pow(-x,1/3)`; exponent is the double 1/3 at 0x00a3a7c8 |
|
||
|
||
`ComputeOutputFromRates` is reached from `ComputeOutput` 0x00751fb0 (projected) and
|
||
`ComputeMaxIncome` 0x007521c0 (max), which are what `ComputeBudget` 0x00863030 and
|
||
`UpdateBankruptcyLimits` 0x00818600 call.
|
||
|
||
---
|
||
|
||
## 2. The formula
|
||
|
||
### 2.1 `ComputeTotalOutput(SRoh)` — 0x00750480
|
||
|
||
`SRoh` is the **fifth float of the normalised `OutputRates`** (`{SRt, SRsc, SRtf, SRi, SRoh, SRs,
|
||
int SRnr}`, 0x1c), read at `[ebp-0x4c]` after `NormaliseOutputRates`. It is the over-harvest rate.
|
||
|
||
```
|
||
if (sys->PID == 0) return 0.0 // +0x100
|
||
if (sys->rbfl != 0) return 0.0 // +0x1dc, any rebelling species
|
||
|
||
resAvail = sys->Res // +0x68
|
||
if (PID && PID->[0x138]) resAvail += sys->MRes + sys->ARes2 // +0x70 + +0x6c (the AMine flag)
|
||
|
||
resTerm = (double)(sys->TRes + resAvail) // +0x74
|
||
* (double)StripMineFraction() // float, narrowed
|
||
* 0.9 // .rdata 0x00a05d48
|
||
|
||
baseTerm = OverHarvestDemand(SRoh) * (double)(float)SpeciesDef(PID->Species)->f50 // +0x50
|
||
|
||
popTerm = PopOutput(0) + 0.0 // .rdata 0x009e1e68 == 0.0
|
||
+ PopOutput(1)
|
||
+ SlaveOutput()
|
||
|
||
sum = baseTerm + resTerm + popTerm // in that association:
|
||
// ((y + x1 + (x0 + 0.0)) + (baseTerm + resTerm))
|
||
|
||
mod = (AddictionPhase(sys, sp) >= 3) ? ADDICTION_OUTPUT_MOD : 1.0
|
||
sp = PID ? (sys->indi ? sys->indi->[4] : PID->Species) : -1
|
||
|
||
sum *= PID->[0x124] // p.OutMod
|
||
sum *= sys->OutMod // +0x7c
|
||
sum *= PID->[0x224] // the game-setup handicap output multiplier
|
||
sum *= PID->[0x128] // p.RebOutMod
|
||
sum *= PID->[0x12c] // p.ScOutMod
|
||
return mod * sum
|
||
```
|
||
|
||
Every multiply is 80-bit x87 with no intermediate store, so the whole tail is one long-double
|
||
chain; only the two `(double)(float)` narrowings above are real roundings.
|
||
|
||
### 2.2 The population term — `PopOutput` 0x0074d8f0 and `GroupOutput` 0x0074b7a0
|
||
|
||
```
|
||
PopOutput(t): // t = 0 imperial, 1 civilian
|
||
sum = 0
|
||
for sp in 0..6:
|
||
n = GroupPopulation(t, sp) // int64
|
||
surplus = 0
|
||
if (PID && sys->indi == 0 && t == 1 && sp == PID->Species):
|
||
MaxPop(sys, PID, sp, &A, 0, 0) // 0x0074a6d0, out slot 4
|
||
MaxPop(sys, PID, sp, 0, 0, &B) // out slot 6
|
||
surplus = (B > A) ? (B - A) : 0
|
||
total = surplus + n
|
||
if (total > 0) sum += GroupOutput(t, sp, (double)total)
|
||
return sum
|
||
|
||
SlaveOutput(): // t = 2, no station factor, no morale
|
||
sum = 0
|
||
for sp in 0..6:
|
||
s = SlaveCount(sp) // 0x0074b610, int64
|
||
if (s > 0) sum += max(0.0, POPTYPE[2].out * 1.8 * ((double)s / 500000.0))
|
||
return sum
|
||
|
||
GroupOutput(t, sp, count):
|
||
if (!(count > 0.0)) return 0.0
|
||
q = count / 500000.0 // .rdata 0x00a1fb00 = 500000.0
|
||
sf = 1.0
|
||
if (PID != 0 && t == 0):
|
||
k = StationCount(sys, PID, 0) // 0x00815c10 -> 0x00815b10(.., 1)
|
||
b = (STATION_BONUS_IMPERIAL_OUTPUT > 0.0) ? STATION_BONUS_IMPERIAL_OUTPUT : 0.0
|
||
sf = 1.0 + (double)k * b
|
||
mo = (t == 1) ? MoraleOutputMod(sp) : 1.0
|
||
v = (double)POPTYPE[t].out * (sf * 1.8) * mo * q // 1.8 = .rdata 0x00a1faf8
|
||
return max(v, 0.0)
|
||
```
|
||
|
||
**`GroupPopulation(t, sp)`** (0x00747ba0):
|
||
`t == 0` → `(sp == (sys->indi ? sys->indi->[4] : PID->Species)) ? (sys->pbon + sys->Pop) : 0`,
|
||
sign-extended to int64. Otherwise → `Population::Count(sys->pbon2 /*+0x1b4*/, t, sp) +
|
||
Population::Count(sys->Pop2 /*+0x1a0*/, t, sp)`.
|
||
|
||
**`MoraleOutputMod(sp)`** (0x00746910): `1.0` if no owner, if `sys->indi != 0`, or if
|
||
`sys->cm.value[sp] == 0` (the `Morale` int[7] at +0x120). Else, with `m = cm.value[sp]`:
|
||
`m >= MORALE_INCREASE_OUTPUT` → `MORALE_INCREASE_OUTPUT_MOD` if that is `> 0`, else `1.0`;
|
||
`m <= MORALE_DECREASE_OUTPUT` → `MORALE_DECREASE_OUTPUT_MOD` if that is `> 0`, else `1.0`;
|
||
otherwise `1.0`.
|
||
|
||
**So the per-capita output rate is `typeMod × 1.8 / 500000`** — for an imperial population with no
|
||
stations that is exactly `3.6e-6` output points per head.
|
||
|
||
### 2.3 The two resource terms
|
||
|
||
`StripMineFraction()` 0x00747d30, returns float:
|
||
```
|
||
Fi = (double)float32(sys->ibon + sys->Infra) // +0x198 + +0x190, narrowed
|
||
V = signed_cbrt((sys->pbon + sys->Pop) / 100.0) * 0.01 // 100.0 @0x009e20e8, 0.01 @0x00a1a448
|
||
R = clamp01(V)
|
||
if (0.0001 + R >= 1.0) R = Fi // 0.0001 @0x00a1fa18
|
||
return float32( (R < Fi) ? R : Fi )
|
||
```
|
||
|
||
`OverHarvestDemand(SRoh)` 0x007483b0, `ret 8`, returns double — the corrected §3.3 row:
|
||
```
|
||
resAvail = sys->Res; if (PID && PID->[0x138]) resAvail += sys->MRes + sys->ARes2
|
||
P = (double)resAvail
|
||
B = 0.0
|
||
if (SRoh > 0.0):
|
||
S = sys->pbon + sys->Pop // int add, may wrap
|
||
R = clamp01((double)S * 1e-5) // 1e-5 @0x009faa28
|
||
B = max(SRoh * P * R, 1.0) // floor of 1.0, not 0.0
|
||
D = PID ? (double)(int)SpeciesDef(PID->Species)->f4c : 0.0 // +0x4c, an int
|
||
return min(P, max(D + B, 0.0))
|
||
```
|
||
`ComputeOutputFromRates` calls it a **second** time for the resource ledger:
|
||
`need = ftol(OverHarvestDemand(SRoh)) + CivilianConsumption()` (0x0074c6f0);
|
||
`out[2] = min(resAvail, max(need, 0))`; `out[1] = min(out[2], ftol(OverHarvestDemand(SRoh)))`
|
||
— which confirms B4's correction that `out[1]` is `min(out[2], stripMineDemand)` and names the
|
||
demand.
|
||
|
||
---
|
||
|
||
## 3. Where each input comes from — and this is the useful part
|
||
|
||
`InitPopTypeTable` 0x00535ca0 builds the 3-row table **in code**, from x87 literals, with only
|
||
three fields taken from GlobalConst slots. Read out of the initialiser (the function keeps six
|
||
values on the x87 stack and rotates them with `fxch`, so the rows are not adjacent in the
|
||
listing):
|
||
|
||
| row | +0 | +4 | +8 (max pop) | +0xc | **+0x10 `out`** | +0x14 `income` | +0x18 | +0x1c | +0x20 |
|
||
|---|---|---|---|---|---|---|---|---|---|
|
||
| 0 imperial | 0 | 1.0 | **50 000 000** | 0 | **1.0** | 1.0 | 1.0 | 1.0 | 1.0 |
|
||
| 1 civilian | 1 | 0.25 | **20 000 000** | 0 | **0.33** | 0.33 | 1.0 | 0.5 | 2.0 |
|
||
| 2 slaves | 2 | 0.0 | 0 | 0 | **`SLAVES_OUTPUT_MOD`** | `SLAVES_INCOME_MOD` | `SLAVES_REPAIR_MOD` | 0.0 | — |
|
||
|
||
(the 50 000 000 confirms B4's "the 50,000,000 cap"; the GlobalConst slot pointers are
|
||
0x00ae2e88 → 0x00b0e9b0 out, 0x00ae2e84 → 0x00b0e9ac income, 0x00ae2e8c → 0x00b0e9b4 repair.)
|
||
|
||
So the whole population→output term is **hard-coded in the executable** for imperial and civilian
|
||
population. The data-file dependency of §2 reduces to this closed list:
|
||
|
||
| # | value | where | needed when |
|
||
|---|---|---|---|
|
||
| 1 | `STATION_BONUS_IMPERIAL_OUTPUT` | slot 0x00af08f4 → 0x00af08f0, **in the file image as `0.1f`** | any imperial pop with ≥1 station |
|
||
| 2 | `MORALE_INCREASE_OUTPUT` (int) | slot 0x00aec784 | civilian pop with non-zero morale |
|
||
| 3 | `MORALE_INCREASE_OUTPUT_MOD` (float) | slot 0x00aec78c | ditto |
|
||
| 4 | `MORALE_DECREASE_OUTPUT` (int) | slot 0x00aec794 | ditto |
|
||
| 5 | `MORALE_DECREASE_OUTPUT_MOD` (float) | slot 0x00aec79c | ditto |
|
||
| 6 | `SLAVES_OUTPUT_MOD` (float) | 0x00b0e9b0 | any slaves |
|
||
| 7 | `ADDICTION_OUTPUT_MOD` (float) | slot 0x00aeca50 → 0x00aeca4c | addiction phase ≥ 3 |
|
||
| 8 | `SpeciesDef[sp]->f4c` (int) | 0x00b10a00 + sp·0x184 + 0x4c | always (the base resource demand) |
|
||
| 9 | `SpeciesDef[sp]->f50` (float) | +0x50 | always |
|
||
|
||
Everything else — `1.8`, `500000`, `0.9`, `1.0`, `0.33`, `1e-5`, `0.01`, `100.0`, `1/3`, `0.0001`
|
||
— is a `.rdata` literal, i.e. a fact about the algorithm, not about the shipped data.
|
||
|
||
**Nine numbers.** That is the whole tuning-table surface of this term.
|
||
|
||
---
|
||
|
||
## 4. PREDICTION — written before the run
|
||
|
||
Build `output-<sha>`, hook `Game::ServerSystem::ComputeTotalOutput` (0x00750480) in **compare**
|
||
mode. Chosen over `ComputeOutputFromRates` deliberately: that one repairs ships in orbit and is
|
||
not compare-safe. `ComputeTotalOutput` and every one of its callees were checked for stores to
|
||
the game state and are read-only (0x0074a6d0's only writes are through its int64 out-parameters).
|
||
|
||
### 4.1 What I expect
|
||
|
||
1. **Call count.** `ComputeOutput`/`ComputeMaxIncome` are called from the UI as well as the turn,
|
||
so the count will be dominated by UI polling, exactly as `ComputeBudget`'s 4,437 were. I expect
|
||
**hundreds to thousands of calls, and only a few tens of distinct system states** — and I will
|
||
report distinct states, not the call count (rule 15).
|
||
2. **Zero divergences on the returned double**, compared as exact IEEE-754 bits.
|
||
3. `SRoh == 0.0` on **every** call from all 11 saves (all 28 systems in `turn1-state.sav` carry
|
||
`SRoh = 0.0`), so the `SRoh > 0` branch of `OverHarvestDemand` will be **unexercised**. I will
|
||
flag it as a hypothesis (rule 6) rather than claim it.
|
||
4. **`POPTYPE[0].out == 1.0f`, `POPTYPE[1].out == 0.33f`, `POPTYPE[1].maxpop == 20000000`,
|
||
`POPTYPE[0].maxpop == 50000000`** read live out of 0x00b104e8. If any of these differ from the
|
||
initialiser I read, my reading of the `fxch` rotation is wrong and everything in §3 is suspect.
|
||
5. `STATION_BONUS_IMPERIAL_OUTPUT == 0.1f` live (the file image value survives the data load).
|
||
|
||
### 4.2 Numbers, from `turn1-state.sav`, before the run
|
||
|
||
Gamma Cephei (`Idx 4`, human homeworld): `Pop = pbon = 1e9`, `Pop2 = {type 1, species 0, 5e8}`,
|
||
`Res 5000`, `MRes 155`, `ARes2 0`, `TRes 0`, `Infra = ibon = 1.0`, `OutMod 1.0`, `cm[0] = 75`,
|
||
`SRoh 0`, `rbfl 0`.
|
||
|
||
* `GroupPopulation(0, 0) = 2e9` → `GroupOutput(0,0,2e9) = 1.0 × sf × 1.8 × (2e9/5e5)` =
|
||
**7200 × sf**, `sf = 1 + 0.1k` for `k` stations.
|
||
* `GroupOutput(1, 0, 5e8 + surplus) = 0.33 × 1.8 × mo × ((5e8+surplus)/5e5)` = **594 × mo** plus
|
||
the surplus term.
|
||
* `StripMineFraction`: `cbrt(2e9/100) × 0.01 = 2.7144…` → clamps to `1.0`; `Fi = 2.0`;
|
||
`min(1.0, 2.0) = 1.0`. So `resTerm = (0 + resAvail) × 1.0 × 0.9` = **4500** without the AMine
|
||
flag, **4639.5** with it. *This is a live discriminator for `PID->[0x138]`.*
|
||
* `baseTerm = min(resAvail, max(f4c, 0)) × f50` (the `SRoh = 0` form).
|
||
|
||
So the returned total for Gamma Cephei should be
|
||
`(baseTerm + resTerm + 7200·sf + 594·mo) × p.OutMod × 1.0 × p.f224 × p.RebOutMod × p.ScOutMod`,
|
||
and with the four player multipliers at 1.0 and no stations it should sit in the low five figures.
|
||
|
||
**The single strongest check**: `total` must be *linear* in `Pop`. `Koa'Vo` has `Pop = 1.2e8`,
|
||
`pbon = 0` → imperial term `1.2e8/5e5 × 1.8 = 432`, exactly `2000/7200` of Gamma Cephei's, with
|
||
`Res 3394` giving `resTerm = 3054.6`. Two colonies, one ratio, no fitted parameter.
|
||
|
||
### 4.3 Falsification — how this model could be wrong, and the symptom of each
|
||
|
||
| way it could be wrong | symptom in the run |
|
||
|---|---|
|
||
| `[ecx+0x68]` in 0x007483b0 is not `Res` — I matched the offsets to `struct-recovery.md`'s memory column, which is 8 more than the wire column | the AMine-gated `+0x70 + +0x6c` sum would not match `MRes + ARes2`; `ours` diverges on **every** call by a fixed additive amount, and the traced `resAvail` will not equal any save field |
|
||
| my `fxch`-rotation reading of `InitPopTypeTable` is off by one register | `POPTYPE[1].out` reads live as `0.25` or `0.5` or `2.0` rather than `0.33`; the civilian term is then wrong by a constant ratio and only civilian-bearing systems diverge |
|
||
| the `max(v, 1.0)` I read at 0x74844c–0x748451 is really `min` | invisible here, because `SRoh = 0` on every save — so this is **explicitly unverified** and stays a hypothesis |
|
||
| `GroupOutput`'s station factor applies to civilians too (I read the `t == 0` test at 0x74b7e0 as excluding them) | systems with stations diverge; systems without do not — so a divergence that correlates with `NumSnF > 0` names this |
|
||
| the `MoraleOutputMod` thresholds are inclusive the other way (`>` vs `>=`) | a divergence only on systems whose `cm[sp]` sits exactly on a threshold; likely unexercised, so it will be reported as a hypothesis |
|
||
| the association of the additive chain differs (x87 is not associative) | a divergence in the last 1–2 ulps only, on the largest colonies |
|
||
| `p.f224` / `RebOutMod` / `ScOutMod` are not all 1.0 in these saves | `ours` diverges by a clean multiplicative factor on every call of one player — which identifies the slot rather than refuting the model |
|
||
|
||
A run with **0 divergences and only 2–3 distinct colony states** would be a weak result and will be
|
||
reported as one. The strong form is the `turn1 → turn2 → turn3` and Zuul `turn15 → turn23`
|
||
progressions, where `Pop`, `Res` and `Infra` all move.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 5. The live result
|
||
|
||
Two builds, two workloads, both on VM140. Hook set: `Game::ServerSystem::GroupOutput` and
|
||
`Game::ServerSystem::ComputeTotalOutput`, both in **compare** mode, every other hook off.
|
||
Traces `verify/traces/output-*.jsonl.gz`, reports `verify/results/compare/output-*.md`.
|
||
|
||
| run | build | workload | calls | compared | diverged | undeclared writes |
|
||
|---|---|---|---:|---:|---:|---:|
|
||
| 1 | `output-b48d860-20260908T1527Z` | `ref-turn2` load + one End Turn (Human, turn 2 -> 3) | 14,853 | 14,853 | 4,957 | **0** |
|
||
| 2 | `output2-b48d860-20260908T1553Z` | `zuul-turn5` load + one End Turn (Zuul, turn 5 -> 6) | 11,966 | 11,966 | **0** | **0** |
|
||
| 2 (cont.) | same | + `ref-turn2` reloaded | 24,357 | 24,357 | **1** | **0** |
|
||
|
||
Run 1's 4,957 divergences were **every one of them exactly one ulp**, and every one of them on
|
||
the civilian population row. The cause was not the model: it was that a 32-bit x87 build of
|
||
`game::sim` is allowed to leave an intermediate in a register at the register's own precision,
|
||
while the original's x87 rounds every multiply to double (its control word is 0x127f). Run 2
|
||
carries `sim::Narrow`, which forces the round the original performs anyway, and the population
|
||
term then matches **bit for bit on 13,105 calls across two species**.
|
||
|
||
### 5.1 Every prediction in §4.1, checked
|
||
|
||
| predicted | measured | verdict |
|
||
|---|---|---|
|
||
| `POPTYPE[0].out == 1.0f` | `1.0` | HELD |
|
||
| `POPTYPE[1].out == 0.33f` | `0.330000013` | HELD |
|
||
| `POPTYPE[0].maxpop == 50 000 000` | `50000000` | HELD |
|
||
| `POPTYPE[1].maxpop == 20 000 000` | `20000000` | HELD |
|
||
| `POPTYPE[2].maxpop == 0` | `0` | HELD |
|
||
| `STATION_BONUS_IMPERIAL_OUTPUT == 0.1f` | `0.100000001` | HELD |
|
||
| `SRoh == 0` on every call | 0 on all 24,357 | HELD (and the over-harvest branch is therefore **unexercised** -- see §5.3) |
|
||
| zero divergences on the returned double | 0 of 13,105 on `GroupOutput`, 1 of 11,252 on `ComputeTotalOutput` | HELD, with one residual |
|
||
| hundreds-to-thousands of calls, few distinct states | 24,357 calls, **13 distinct system states** | HELD, and the coverage is thin exactly as forecast |
|
||
|
||
So the reading of `InitPopTypeTable`'s x87 rotation was right: the imperial and civilian output
|
||
modifiers really are literals in the executable, and the shipped data changes neither.
|
||
|
||
Values the data files DO supply, read out of the running process (these were unknown before
|
||
this run):
|
||
|
||
| key | value |
|
||
|---|---|
|
||
| `STATION_BONUS_IMPERIAL_OUTPUT` | `0.1f` |
|
||
| `MORALE_INCREASE_OUTPUT` / `_MOD` | `85` / `1.5f` |
|
||
| `MORALE_DECREASE_OUTPUT` / `_MOD` | `20` / `0.5f` |
|
||
| `SLAVES_OUTPUT_MOD` | `3.0f` |
|
||
| `SpeciesDef +0x4c` (base resource demand) | **0** for Human and Tarkas, **10** for Zuul |
|
||
| `SpeciesDef +0x50` (resource output factor) | **10** for Human and Tarkas, **40** for Zuul |
|
||
|
||
The last two are the reason a species table is not optional: they differ by species and they
|
||
scale a whole term. Every colony in the corpus carries `cm = 75`, which sits strictly between
|
||
20 and 85, so **the morale multiplier is 1.0 on every call the corpus makes** -- both morale
|
||
branches are unexercised and stay hypotheses.
|
||
|
||
### 5.2 The prediction that was wrong, and it was mine, not the model's
|
||
|
||
§4.2 said Gamma Cephei's strip-mine fraction would be `min(1.0, 2.0) = 1.0` and its resource
|
||
term 4500. It is **2.0** and **9000**. The branch at 0x00747db4 is a *substitution*, not a
|
||
clamp: once the population term reaches `1 - 1e-4` the infrastructure term replaces it outright
|
||
rather than capping it, so a colony with a pending infrastructure bonus extracts at better than
|
||
unity. The disassembly said so and I mis-evaluated my own reading of it by hand. The model in
|
||
the code was right from the start; the arithmetic in the prediction was not. Caught by the very
|
||
first host test, before any VM time was spent.
|
||
|
||
### 5.3 What this run did NOT cover -- read this before quoting the zero
|
||
|
||
24,357 calls sounds like a lot. It is **13 distinct system states** and **12 distinct
|
||
`GroupOutput` rows**:
|
||
|
||
* **5 colonies** in total across both workloads (Gamma Cephei, Ke'Dolarra, Koa'Vo, Gallandro
|
||
and three more Zuul systems), and 4,832 of run 1's calls are the UI polling one of them.
|
||
* **`SRoh` is 0 on every single call**, so `OverHarvestDemand`'s whole `rate > 0` branch --
|
||
including the `max(v, 1.0)` floor, which is the one place I could most easily have read a
|
||
`min` as a `max` -- is **untested**. It is a hypothesis, not a finding.
|
||
* **The station factor is untested.** No system in either workload has a station, so
|
||
`stationFactor` was 1.0 on all 13,105 rows and the hook's own `ours` assumed zero stations.
|
||
A save with an imperial station would test it; none exists.
|
||
* **The slave term is untested.** `slaveGroupPop` was 0 on every call, including the Zuul
|
||
workload, so `SLAVES_OUTPUT_MOD = 3.0` was read but never used.
|
||
* **The morale multiplier is untested** (see above): every colony sits at 75.
|
||
* **The addiction multiplier is untested**: no colony reached phase 3.
|
||
* **The civilian capacity surplus is untested**: no colony was at its cap.
|
||
* Zuul have no civilian population at all, so run 2 exercises **only** the imperial row --
|
||
which is why its 11,966 calls found zero divergences even before you account for the fix.
|
||
|
||
The honest summary is: the *imperial* and *civilian* population laws are verified hard, on two
|
||
species and two workloads; every conditional hanging off them is unexercised and labelled.
|
||
|
||
### 5.4 The one residual
|
||
|
||
`ComputeTotalOutput` diverges on exactly one of 11,252 calls in run 2: Koa'Vo, the independent
|
||
colony, `9248.450318530804` against `9248.450318530802`. One ulp, in the five-multiplier tail
|
||
(`OutMod` there is 2.18500018, the only non-trivial one in the corpus). It is **immaterial to
|
||
every consumer**: the caller rounds this value half-to-even to an integer before splitting it
|
||
across the output channels, and 9248.4503 rounds to 9248 either way. It is recorded rather than
|
||
papered over, and it is not fixed.
|
||
|
||
---
|
||
|
||
## 6. What this unblocks, and what it does not
|
||
|
||
The brief's premise was that this term blocks `P01 P02 P03 P05 P06`. It is now verified -- and
|
||
that turns out **not** to be enough to unblock them, for a reason worth stating plainly.
|
||
|
||
`ComputeBudget`'s missing input is not a system's *output*; it is a system's **money**, which is
|
||
`ComputeMaxIncome(s) = max(ComputeOutputRates(s, maxMods)[3], 0)`. That runs the verified output
|
||
total through a second chain -- `TradePointsToMoney` -- which has its own population law
|
||
(`typeIncomeModifier / 14000`, no 1.8 factor, a different table column) and its own multipliers.
|
||
|
||
Lane Y's `tools/max_income_oracle.py` inverts the stored `BnkEl` to state the true
|
||
`Sum max(ComputeMaxIncome(s), 0)` for 25 player-records across the corpus. `tools/max_income_predict.py`
|
||
(new, this lane) computes the same number from colony state using the verified output term plus
|
||
that income chain and compares. Result:
|
||
|
||
```
|
||
6 of 25 records MATCH exactly; 19 differ.
|
||
```
|
||
|
||
The six that match are every record whose owner is the human player or the independent colony --
|
||
including `turn1/turn2/turn3-state` for both -- and they match to the unit on a five-figure
|
||
number, three of them on a moving workload. The nineteen that differ are all **AI-owned**, and
|
||
the single-system ones differ by a clean **factor of exactly 1.1** (`246992 x 1.1 = 271691`).
|
||
That factor is not on the wire: it is `DifficultyMods(owner)->+4`, the AI trade/income
|
||
difficulty multiplier that `TradePointsToMoney` applies and that `formula-gaps.md` Q3 already
|
||
names as unresolved. The multi-system AI empires differ by more, so there is at least one
|
||
further term there (candidates, in order: trade routes, stations, the suitability money cost on
|
||
non-ideal planets).
|
||
|
||
**So the blocker moves rather than clears**: the population -> base-output term is closed, and
|
||
what now stands between `src/app` and `systemIncome` is the income tail, not the output term.
|
||
That is a smaller and much better-specified target, and the 25-record oracle makes it falsifiable
|
||
without a VM.
|
||
|