`ServerSystem::ComputeOutput` 0x00751fb0 and the whole of `ComputeOutputFromRates`
0x00751bb0, read from the instruction stream to the next function start. This is the
function lane E1 correctly identified as the real blocker on P01/P02/P03/P05/P06 after
its own chain came out 25/25 on BnkEl and did not unblock them.
The shape, and the fact everything turns on: construction points the build queue and
the ship-repair pass do not spend are redistributed over trade / terraform /
infrastructure, and the TRADE share is added to the money channel. Unspent
infrastructure points cascade into the terraform pool and unspent terraform points
cascade into money -- two hops, not one. So a colony with an empty build queue earns
the same money whichever way its sliders point, which is why the turn path's money
sits within one trade point of the projected path on this corpus.
Eleven addresses, five of them dropped as agreeing duplicates. New: the repair pass
0x00751590 (the B1 double-run side effect, and its round robin is provably a `min`),
the construction-point helper 0x00746830, the queue-demand walker 0x008251e0, the
ideal-suitability resolver 0x00745d60, and two helpers with non-standard conventions
that would be silently wrong read as thiscall -- 0x0074c6f0 takes `this` in ESI and
0x007460b0 takes the system in EBX.
Corrections in place: `income-term.md`'s "leftover science points" are leftover SHIP
CONSTRUCTION points (there is no science channel in this function), its
science-cascade bullet moves from inferred to instruction-verified, and
`output-term.md`'s 0x00751fa8 boundary is padding -- the `ret 8` is at 0x00751fa0.
Measured, with the engine change (sots-engine wip/output2 d3ee453):
turn1-state -> turn2-state 209 -> 157 closed 52 regressed 0 (was 51 / 0)
turn2-state -> turn3-state 108 -> 86 closed 22 regressed 0 (was 21 / 0)
Closed 1 per pair, regressed 0. Every prediction in the doc's section 4 held; the one
that was off is named and explained. The strongest check is not the leaf count but
the eleven-save self-check in section 5.3, where every difference between the turn
path and the projected path decomposes to the unit into the build queue's points.
505 lines
29 KiB
Markdown
505 lines
29 KiB
Markdown
# `ComputeOutput` on the turn path (lane C3, 2026-09-08)
|
||
|
||
`ServerSystem::ComputeOutput` 0x00751fb0 and the whole of `ComputeOutputFromRates`
|
||
0x00751bb0 — the money `ComputeBudget` actually sums during a turn, as opposed to the
|
||
projected-mode `ComputeMaxIncome` that lane E1 closed at 25/25 on the `BnkEl` oracle.
|
||
|
||
Evidence: own `objdump -d` pass over `Sword of the Stars.exe`. Both ranges 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* is not.
|
||
|
||
This supersedes two labels in earlier notes:
|
||
|
||
* `income-term.md` §2.2 calls `[ebp-0x28]` "leftover **science** points". There is no science
|
||
channel in this function. `OutputRates+4` is the **ship-construction** slider `SRsc`, the
|
||
helper at 0x00746830 turns it into **construction points**, and the "leftover" is what the
|
||
build queue and the ship-repair pass did not spend. `strategic-turn-internals.md` §3.3 had
|
||
this right; the income lane's paraphrase did not.
|
||
* `output-term.md` §1 gives `ComputeOutputFromRates`'s end as 0x00751fa8. The `ret 8` is at
|
||
**0x00751fa0**; 0x00751fa3..0x00751faf is `int3` padding. E1's 0x00751fa0 is correct.
|
||
|
||
---
|
||
|
||
## 1. The call chain
|
||
|
||
| addr | name | conv / boundary | note |
|
||
|---|---|---|---|
|
||
| 0x00751fb0 | `ServerSystem::ComputeOutput(int out[12])` | thiscall `ret 4`, ends 0x00752070 | Ghidra size 192 = correct |
|
||
| 0x00751bb0 | `ServerSystem::ComputeOutputFromRates(int out[12], OutputRates*)` | thiscall `ret 8`, ends 0x00751fa0 | **repairs ships in orbit** |
|
||
| 0x00747390 | `NormaliseOutputRates(float rates[4], ServerSystem*, float* pinned)` | **cdecl** | pinned = null → trade |
|
||
| 0x00750480 | `ServerSystem::ComputeTotalOutput(double SRoh)` | thiscall `ret 8` | lane N; side-effect free |
|
||
| 0x00746830 | `ServerSystem::ConstructionPoints(double consShare)` → int | thiscall `ret 8`, ends 0x00746883 | shipyard-station bonus |
|
||
| 0x00815c10 | `ServerSystem::StationCount(ServerPlayer*, int kind)` | cdecl-through-thiscall | **kind 1** here (shipyards) |
|
||
| 0x008251e0 | `BuildQueue::TotalConstructionLeft()` → int | thiscall `ret 0`, 23 bytes | Σ `conleft` over the order list |
|
||
| 0x00751590 | `ServerSystem::RepairShipsInOrbit(int points, bool estimateOnly)` → int | thiscall `ret 8`, ends 0x007517b5 | **the side effect**; returns points LEFT |
|
||
| 0x00745d60 | `ServerSystem::IdealSuitability()` → double | thiscall `ret 0`, ends 0x00745dc0 | §2.6 |
|
||
| 0x00746890 | `ServerSystem::TerraformPointsNeeded()` → double | thiscall `ret 0`, ends 0x0074690d | §2.5 |
|
||
| 0x0074c6f0 | `ServerSystem::CivilianConsumption()` → int | **reads `esi` as `this`** | resource ledger only |
|
||
| 0x007483b0 | `ServerSystem::OverHarvestDemand(double SRoh)` | thiscall `ret 8` | lane N |
|
||
| 0x007505b0 | `ServerSystem::TradePointsToMoney(double trade)` → double | thiscall `ret 8` | lane E1 |
|
||
| 0x008e5660 | `round(double)` | cdecl | `fistp`/`fild`, ties to **even** |
|
||
| 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]` |
|
||
|
||
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
|
||
**EBX**. Both are compiler-local helpers that inherit a register the caller happens to hold.
|
||
|
||
---
|
||
|
||
## 2. The formula
|
||
|
||
### 2.1 `ComputeOutput(out)` — 0x00751fb0 (instruction-verified)
|
||
|
||
```
|
||
if (out == 0) return
|
||
memset(out, 0, 48) // 12 dwords; slots 10 and 11 are FLOATS
|
||
if (sys->PID /*+0x100*/ == 0) return
|
||
ComputeOutputFromRates(out, &sys->Rts) // sys+0x88 -- the system's OWN sliders, unnormalised
|
||
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[3]` — the money — is written entirely inside `ComputeOutputFromRates`.** Nothing after
|
||
that call touches it, so the trade-route and repair-demand slots are reporting only.
|
||
|
||
`ComputeBudget` 0x00863030 confirms this at 0x008631fd:
|
||
|
||
```
|
||
for s in player->systems:
|
||
if (s->Abdn /*+0xc4*/) continue
|
||
if (projected) money = ComputeMaxIncome(s) // 0x007521c0
|
||
else { int o[12]; ComputeOutput(s, o); money = o[3]; } // [ebp-0xe0 + 0xc]
|
||
if (money > 0) income += money // budget+0x04
|
||
if (money < 0) expense += -money // budget+0x1c
|
||
```
|
||
|
||
### 2.2 The `OutputRates` memory layout (instruction-verified)
|
||
|
||
`ServerSystem+0x88`, 0x1c bytes, seven dwords. The **memory** order is not the wire order:
|
||
|
||
| offset | field | who reads it |
|
||
|---|---|---|
|
||
| +0x00 | `SRt` trade | trade points, and the leftover weight |
|
||
| +0x04 | `SRsc` **ship construction** | construction points |
|
||
| +0x08 | `SRtf` terraform | zeroed by normalisation when `Suit == IdealSuitability()` |
|
||
| +0x0c | `SRi` infrastructure | zeroed by normalisation when `f32(Infra + ibon) >= 1` |
|
||
| +0x10 | `SRoh` over-harvest | `ComputeTotalOutput` / `OverHarvestDemand` |
|
||
| +0x14 | `SRs` | not read here (slave death rate) |
|
||
| +0x18 | `SRnr` (int) | not read here |
|
||
|
||
The suppressions are read off `NormaliseOutputRates` 0x00747390: `[edi+0x8] := 0` under the
|
||
`Suit == IdealSuitability()` branch and `[edi+0xc] := 0` under `1.0 <= f32(ibon + Infra)`.
|
||
The wire order (`SRs, SRt, SRsc, SRtf, SRi, SRoh, SRnr`) is a serialiser choice.
|
||
|
||
### 2.3 `ComputeOutputFromRates(out, rates)` — 0x00751bb0 (instruction-verified)
|
||
|
||
```
|
||
memset(out, 0, 48)
|
||
if (sys->PID == 0) return
|
||
r = *rates // 7 dwords copied to the stack: the caller's
|
||
NormaliseOutputRates(&r, sys, 0) // struct is never modified
|
||
|
||
T = round( ComputeTotalOutput(r.SRoh) ) // ties to even, kept as a double
|
||
tradePts = round( T * r.SRt )
|
||
consPts = round( r.SRsc * T )
|
||
terraPts = round( r.SRtf * T )
|
||
infraPts = round( r.SRi * T )
|
||
out[0] = ftol(T)
|
||
|
||
// --- resource ledger (independent of everything below) -------------------------
|
||
need = ftol(OverHarvestDemand(r.SRoh)) + CivilianConsumption()
|
||
resAvail = sys->Res + (PID->[0x138] ? sys->MRes + sys->ARes2 : 0)
|
||
out[2] = min(resAvail, max(need, 0))
|
||
out[1] = min(out[2], ftol(OverHarvestDemand(r.SRoh)))
|
||
|
||
// --- construction ---------------------------------------------------------------
|
||
queueNeed = sys->BQ ? BuildQueue::TotalConstructionLeft(sys->BQ) : 0
|
||
out[7] = ConstructionPoints(consPts) // ftol(c + stations*BONUS*c)
|
||
out[8] = min(queueNeed, out[7])
|
||
rem = max(out[7] - out[8], 0)
|
||
out[9] = 0
|
||
if (rem > 0):
|
||
left = RepairShipsInOrbit(rem, /*estimateOnly=*/false)
|
||
out[9] = rem - left
|
||
rem = left
|
||
|
||
// --- the leftover redistribution -------------------------------------------------
|
||
Ltrade = Lterra = Linfra = 0.0
|
||
if (rem > 0):
|
||
P = (double) rem
|
||
if (r.SRsc == 1.0): // an exact ==
|
||
wT = 1.0
|
||
wF = (sys->Suit != IdealSuitability()) ? 1.0 : 0.0
|
||
wI = (sys->Infra != 1.0) ? 1.0 : 0.0
|
||
else:
|
||
wT = r.SRt ; wF = r.SRtf ; wI = r.SRi
|
||
S = wI + (wF + wT) // that association
|
||
Ltrade = round( wT * P / S )
|
||
Lterra = round( wF * P / S )
|
||
Linfra = round( wI * P / S )
|
||
|
||
// --- infrastructure --------------------------------------------------------------
|
||
infraNeed = ceil( (1.0 - sys->Infra) / 3.3e-5 ) // 0x00a1f930
|
||
poolI = Linfra + infraPts
|
||
spendI = min(poolI, infraNeed)
|
||
leftI = max(poolI - spendI, 0)
|
||
out[10] = (float) max( spendI / 500.0 * 0.01 * 1.65, 0 ) // three 80-bit steps
|
||
|
||
// --- terraforming ----------------------------------------------------------------
|
||
terraNeed = ceil( TerraformPointsNeeded() )
|
||
poolF = (Lterra + terraPts) + leftI // the infra cascade lands HERE
|
||
spendF = min(poolF, terraNeed)
|
||
leftF = max(poolF - spendF, 0)
|
||
sign = (IdealSuitability() < sys->Suit) ? -1.0 : 1.0
|
||
out[11] = (float) ( sign * (spendF * 1.5 * 1.2f * PID->TerraMod) / 20000.0 )
|
||
|
||
// --- money -----------------------------------------------------------------------
|
||
out[3] = ftol( TradePointsToMoney( (Ltrade + tradePts) + leftF ) )
|
||
```
|
||
|
||
Two things this makes explicit that no earlier note stated:
|
||
|
||
1. **The infrastructure leftover cascades into the terraform pool, and the terraform
|
||
leftover cascades into money.** There is no direct industry→money edge; it is two hops.
|
||
`income-term.md` §2.4's "both cascade sources are zero" is right for the max-income rate
|
||
vector (where `infraPts = terraPts = 0` *and* both needs are `>= 0`) but says nothing
|
||
about the turn path.
|
||
2. **The construction leftover's trade share is added to the money channel.** On a colony
|
||
whose build queue is empty, every construction point the system produced comes back as
|
||
trade points. A system with `SRt = 0, SRsc = 1` and an empty queue therefore earns
|
||
**exactly as much money** as one with `SRt = 1`. That is the single most consequential
|
||
fact in this function and it is why the corpus's turn-path money is so close to its
|
||
projected money (§4).
|
||
|
||
### 2.4 `RepairShipsInOrbit(points, estimateOnly)` — 0x00751590 (instruction-verified)
|
||
|
||
```
|
||
if (points == 0) return 0
|
||
need = 0 ; cand = vector<StarShip*>()
|
||
for i in 0 .. sys->vtbl[2]()-1: // fleets at the system
|
||
f = sys->vtbl[4](i)
|
||
if (f->[0x58] != sys->PID) continue
|
||
if (!Fleet::TestFlags(f, 0, 8)) continue
|
||
for sh in f->ships (+0xa4 .. +0xa8):
|
||
if (!Ship::TestFlags(sh, 0, 8)) continue
|
||
c = Ship::RepairCost(sh, 1) // 0x00815180
|
||
if (c <= 0) continue
|
||
need += c
|
||
if (!estimateOnly) cand.push_back(sh)
|
||
if (estimateOnly) return max(points - need, 0)
|
||
// round robin
|
||
progress = true
|
||
while (progress && points > 0):
|
||
share = max(points / cand.size(), 1) // integer divide
|
||
progress = false
|
||
for sh in cand:
|
||
if (points <= 0) return points
|
||
take = min(Ship::RepairCost(sh, 1), share)
|
||
if (take > 0) { Ship::ApplyRepair(sh, take); progress = true; points -= take }
|
||
return points
|
||
```
|
||
|
||
**The round robin is equivalent to `points - min(points, need)`** and this is a proof, not an
|
||
observation: `share >= 1` always, so every ship with a positive remaining cost takes at least
|
||
one point per pass; the loop can only stop early via `progress == false`, which requires every
|
||
remaining cost to be zero. So the pass ends with `points == 0` (need was the binding
|
||
constraint) or with every cost zero (points was). The engine models it as the `min` and pins
|
||
the equivalence in a test rather than reproducing the round robin.
|
||
|
||
`ComputeOutputFromRates` passes `estimateOnly = 0`, so **this is the B1 replace double-run
|
||
hazard**: calling the original to harvest a value repairs ships. The engine never does.
|
||
|
||
### 2.5 `TerraformPointsNeeded()` — 0x00746890 (instruction-verified)
|
||
|
||
```
|
||
if (PID == 0) return 0.0
|
||
sign = (IdealSuitability() < sys->Suit) ? -1.0 : 1.0 // 0x009e3348 = -1.0
|
||
rate = f32(PID->TerraMod /*+0x134*/) * 1.8000000715255737 // 0x00a1f928 = (double)1.8f
|
||
d = fabs( f32(IdealSuitability() - sys->Suit) )
|
||
return d / fabs( rate * sign / 20000.0 )
|
||
```
|
||
|
||
The `fabs` is after the sign multiply, so the sign cancels and the result is always `>= 0`
|
||
(E1's §2.4 reading confirmed). Note `1.8f` here is the same number as `1.5 * 1.2f` in
|
||
`out[11]` — `1.5 * 1.2000000476837158 == 1.8000000715255737` exactly — so the point count and
|
||
the point value use one rate, folded differently.
|
||
|
||
`TerraMod == 0` makes this `+inf`, `ceil(+inf) = +inf`, and the terraform channel then absorbs
|
||
its whole pool with nothing cascading to money. No corpus player has `TerraMod == 0` (Human
|
||
and Tarkas carry 1.0, species 4 carries 3.7), so that branch is **unexercised**.
|
||
|
||
### 2.6 `IdealSuitability()` — 0x00745d60 (instruction-verified)
|
||
|
||
```
|
||
if (sys->PID == 0) return (double) sys->Suit // an unowned system is "at its ideal"
|
||
v = PID->IdealSuit /*+0xb0*/
|
||
if (sys->indi /*+0x1c8*/) v = StrategyServer::IdealSuit(sys->server, sys->indi->indsp)
|
||
if (sys->dsu /*+0x118*/ != SENTINEL) v = sys->dsu // SENTINEL is the float at 0x00aeca6c
|
||
return v
|
||
```
|
||
|
||
This is **not** the same source `CalcSuitMod` uses in the money cost (E1 §2.6 reads the
|
||
server's per-species array there). Here the owner's own `IdealSuit` field is the default and
|
||
the server array is only consulted for an independent colony. In the 11-save corpus the two
|
||
agree, so the difference is instruction-verified only.
|
||
|
||
`dsu` is `3.4028235e38` (`FLT_MAX`) on **every system of every corpus save**, so the override
|
||
never fires and the sentinel's identity is *inferred* from that (the value at 0x00aeca6c is a
|
||
data-file-loaded `GlobalConst` slot, not a `.rdata` literal, so it was not read directly).
|
||
|
||
### 2.7 `ConstructionPoints(cons)` — 0x00746830 (instruction-verified)
|
||
|
||
```
|
||
if (PID == 0) return ftol(cons)
|
||
k = StationCount(sys, PID, 1) // 1 = shipyard
|
||
b = (STATION_BONUS_SHIPCON > 0) ? STATION_BONUS_SHIPCON : 0
|
||
return ftol( k * (b * cons) + cons )
|
||
```
|
||
|
||
Note the association `((b * cons) * k) + cons`, and that the bonus is ignored unless strictly
|
||
positive — the same "unloaded tuning table reads as 1.0, not 0.0" guard the output term has.
|
||
|
||
---
|
||
|
||
## 3. What this needs that the max-income path did not
|
||
|
||
| input | source | held? |
|
||
|---|---|---|
|
||
| `Rts` (5 floats) | wire, `Sys/Rts` | yes |
|
||
| `IdealSuitability()` | `Player/IdealSuit`, `Sys/dsu`, `Sim/ISsu[]` | yes |
|
||
| `Infra`, `ibon` | wire | yes |
|
||
| shipyard station count | `Sys/NumSnF` | yes (0 on every corpus system) |
|
||
| build-queue demand Σ`conleft` | wire, `Sys/BQ/ords` | yes |
|
||
| **ship repair demand in orbit** | needs `Ship::RepairCost` 0x00815180 over the fleets at the system | **NO** |
|
||
| `TerraMod` | wire, `Player/TerraMod` | yes |
|
||
| the output total | lane N | yes |
|
||
|
||
**One input of nine is missing**, and it is the repair demand. It is zero whenever no ship in
|
||
orbit is damaged. On the two reference pairs the human's and the AI's colonies carry no fleet
|
||
at all in the input state except Ke'Dolarra (turn 2) and Koa'Vo, so the exposure is small but
|
||
not provably nil; the engine takes it as an explicit input defaulting to 0 and reports it.
|
||
|
||
---
|
||
|
||
## 4. PREDICTION — written before the build
|
||
|
||
The corpus turns out to make this function much simpler than it looks, and the predictions
|
||
below are the consequences.
|
||
|
||
Measured facts about the corpus, from the wire (`Sys/Rts`, `Sys/dsu`, `Sys/BQ`, `Sys/NumSnF`,
|
||
`Player/IdealSuit`, `Player/TerraMod`), before any model was written:
|
||
|
||
* every owned colony in all 11 saves has `Suit == IdealSuit` **exactly** and `Infra == 1.0`
|
||
exactly, so `NormaliseOutputRates` zeroes both `SRtf` and `SRi` on every one of them;
|
||
* consequently `infraNeed = ceil(0/3.3e-5) = 0`, `terraNeed = ceil(0/rate) = 0`, and
|
||
`infraPts = terraPts = 0`;
|
||
* no system carries a station, so the shipyard bonus is 1;
|
||
* `dsu` is `FLT_MAX` everywhere, so the ideal-suitability override never fires;
|
||
* the sliders that occur are `{t .5, sc .5}`, `{t 0, sc 1}`, `{t .04, sc .96}` and
|
||
`{.25,.25,.25,.25}`; after suppression they normalise to `{t, 1-t, 0, 0}`.
|
||
|
||
### 4.1 What I expect
|
||
|
||
1. **The leftover redistribution collapses to "all of it to trade" on every corpus colony.**
|
||
In the `SRsc == 1` branch the weights are `(1, 0, 0)` because suit is at the ideal and
|
||
infra is exactly 1; in the other branch they are `(SRt, 0, 0)`. Either way
|
||
`Ltrade = round(P) = P` and `Lterra = Linfra = 0`.
|
||
2. **Therefore `out[3] = ftol(TradePointsToMoney(round(T*SRt) + ftol(round(T*SRsc))))`**, and
|
||
with an empty build queue and no repairs the argument is `2*round(T/2)` for a 50/50 colony
|
||
and `T` for a pure-construction one — i.e. **within ±1 of `ComputeMaxIncome`'s `T`**.
|
||
3. **The turn path's money therefore equals the projected path's money to ±1 trade point on
|
||
this corpus.** If that is right, the `BnkEl` oracle — which constrains only the projected
|
||
path — becomes an *indirect* check on the turn path here, and only here.
|
||
4. Numerically, using the `BnkEl` the oracle saves already carry (inverted through
|
||
`ftol(maxIncome / -0.15000000596046448)`):
|
||
|
||
| player | pair | oracle's `Sav` move | decomposition |
|
||
|---|---|---|---|
|
||
| 576 Independent Colony | 1→2 | `0 → 98871` | `99871 − 1000 maint + 0 interest` |
|
||
| 576 | 2→3 | `98871 → 198730` | `99871 + 988 interest − 1000 maint` |
|
||
| 16 human | 1→2 | `50000 → 289688` | `239188 + 500 interest`, and `239189` is the max income of the **post-growth** turn-2 colony |
|
||
| 16 human | 2→3 | `289688 → 532369` | `239785 + 2896 interest`, again post-growth |
|
||
|
||
So `ComputeBudget` runs on the colony state **after** `S11 SystemTurn` — the strategic
|
||
driver runs the system turn at index 11 and the player driver at index 13 — and the
|
||
human's system income is the post-growth number.
|
||
|
||
5. **What will therefore close, and what will not:**
|
||
* Player 576's income does **not** move with growth (Koa'Vo has no civilian population and
|
||
its `BnkEl` is identical in all three saves), so its `Sav` should come out **exactly**
|
||
right on both pairs. **+1 leaf per pair.**
|
||
* Player 16's income **does** move with growth, and our `S11` does not commit civilian
|
||
growth (it needs the carrying capacity). Our number will be short by **597** on pair 1
|
||
and **596** on pair 2, so `Sav` stays wrong — but it was already wrong, so **0 regressed**.
|
||
* Player 32 (the AI) picks a research target and changes `ResRate` during the turn; that is
|
||
Rung B. Not closed.
|
||
* The 24 `turnstats` leaves need the budget **and** ship construction (lane E2's finding).
|
||
Not closed by this lane alone.
|
||
* `PvSav` is `S00`'s snapshot, not `P02`'s write, and player 32's `PvSav` is not its
|
||
pre-turn `Sav`, so it is left alone.
|
||
|
||
**Predicted total: pair 1 closed 1 regressed 0; pair 2 closed 1 regressed 0.**
|
||
6. The two research RNG words stay out of the generator model, and for a **new** reason: the
|
||
allocation is now computable, but no player in either input state carries a research
|
||
target (`ResTNm` is empty on 16 and 32 in `turn1-state`), so `ProcessResearch` has nothing
|
||
to fund. The blocker moves from "the money is unmodelled" to "the AI's target selection
|
||
is unmodelled".
|
||
|
||
### 4.2 Falsification — how this could be wrong, and the symptom of each
|
||
|
||
| way it could be wrong | symptom |
|
||
|---|---|
|
||
| `OutputRates+4` is not construction but something else, so `SRsc == 1` is not what the leftover branch tests | player 576 (whose rates are `.25/.25/.25/.25`, the only four-way vector) would miss while the `{0,1}` colonies matched, or vice versa |
|
||
| the leftover weights are the **raw** sliders, not the normalised ones | 576 misses by a clean ratio (`.25` vs `.75` denominators); the `{t,1-t}` colonies are unaffected because the ratio is the same |
|
||
| `Ltrade` is *not* added to the money channel (i.e. I misread the slot reuse of `[ebp-0x28]`) | every colony's money collapses to `round(T*SRt)`, so the `{t 0, sc 1}` colonies earn **zero** and player 32's and the turn-2/3 colonies' income falls by 100 % — a very loud failure |
|
||
| the budget runs **before** `S11`, and the human's +597 has another source | player 576, whose income does not move with growth, would still match, so this is NOT separable on 576 — it is separable only on 16, where a pre-growth budget predicts `289092` and a post-growth one `289688` |
|
||
| the repair pass consumes points on a corpus colony | player 576 (Koa'Vo has one fleet in every save) misses low by the repair spend; 16's homeworld has no fleet in `turn1-state`, so a 576-only miss names this |
|
||
| the round robin is not equivalent to the `min` | only visible where `points < need`; unexercised here |
|
||
| `ConstructionPoints` truncates where I read a round | off-by-one on colonies whose `T*SRsc` is not integral — but `T` is already an integer and the sliders are exact binary fractions on most colonies, so this is nearly invisible; `{.04, .96}` (Downbelow) is the one colony that would show it, and it is not on either reference pair |
|
||
| `sys->[0x230]` gates something that feeds money | it cannot: `out[3]` is written before that test and never again |
|
||
|
||
A run that closes player 576 and nothing else is a **weak** result and will be reported as
|
||
one: it is one leaf per pair, and it is the leaf where the missing upstream input happens not
|
||
to matter.
|
||
|
||
---
|
||
|
||
## 5. The result
|
||
|
||
Engine: `sim::ComputeSystemOutput` (`src/game/sim/colony.{h,cpp}`) plus
|
||
`sim::IdealSuitability` and `sim::RepairShipsInOrbit`, wired into `P01`/`P02` through
|
||
`BuildBudgetFeeds` in `src/app/turn.cpp`. Host `ctest` 46/46, clean-room OK, both run as
|
||
separate commands.
|
||
|
||
```
|
||
reference pair turn1-state -> turn2-state 209 -> 157 closed 52 regressed 0 (was 51 / 0)
|
||
second pair turn2-state -> turn3-state 108 -> 86 closed 22 regressed 0 (was 21 / 0)
|
||
```
|
||
|
||
**Closed 1 per pair, regressed 0 per pair.** The leaf is
|
||
`/Sim/players/Player[576 "Independent Colony"]/Sav` on both.
|
||
|
||
### 5.1 Every prediction in §4.1, checked
|
||
|
||
| predicted | outcome | verdict |
|
||
|---|---|---|
|
||
| the leftover redistribution collapses to "all to trade" on every corpus colony | it does; `leftoverToTrade == leftover` on all of them | HELD |
|
||
| the turn path equals the projected path to within the trade-point rounding when the queue is empty | **exactly equal on 5 of the 11 saves** — every save whose build queues are all empty | HELD |
|
||
| player 576's `Sav` comes out exactly right on both pairs | `0 -> 98871` and `98871 -> 198730`, both exact | HELD |
|
||
| player 16's `Sav` is short by the uncommitted civilian growth | `289092` against the oracle's `289688` | HELD (the amount was predicted as 597 and is **596** — see §5.2) |
|
||
| player 32 is not closed | `297492` against `92651` | HELD |
|
||
| the `turnstats` leaves are not closed | not closed | HELD |
|
||
| pair 1 closed 1 regressed 0; pair 2 closed 1 regressed 0 | exactly that | HELD |
|
||
| the two research RNG words stay out of the generator model | they do, for the predicted reason | HELD (§5.5) |
|
||
|
||
Nothing in §4.2's falsification table fired. In particular the loudest one — "the construction
|
||
leftover does not reach money" — would have collapsed every `{SRt 0, SRsc 1}` colony's income
|
||
to zero, and those colonies price correctly.
|
||
|
||
### 5.2 The prediction that was off, and why
|
||
|
||
§4.1 said player 16 would be short by **597** on pair 1. It is short by **596**. The
|
||
arithmetic: our pre-growth output gives a max income of 238,592 and the post-growth one gives
|
||
239,189, a gap of 597 — but the oracle's own turn-path number is 239,188, one *below* its
|
||
projected 239,189, because `2 x round(T/2)` lost a point on the post-growth total and did not
|
||
on the pre-growth one. So the two errors partly cancel, and the residual is the growth gap
|
||
minus one trade point. The mechanism was predicted; the sign of its interaction was not.
|
||
|
||
### 5.3 The strongest check available without a VM
|
||
|
||
The engine now runs every owned system through **both** paths on the same colony state and
|
||
reports the difference, every run. The projected sum is the number the save's own `BnkEl`
|
||
states and that lane E1 scored 25/25 against, so a large delta is the new model failing and it
|
||
is visible on load.
|
||
|
||
| save | landed players agreeing exactly | worst \|delta\| | explanation |
|
||
|---|---|---|---|
|
||
| `turn1-state`, `turn2-state`, `turn3-state` | 3 of 3 | 0 | every build queue empty |
|
||
| `human-turn2-orders` | 2 of 2 | 0 | every build queue empty |
|
||
| `zuul-turn15-orders` | 2 of 2 | 0 | every build queue empty |
|
||
| `human-turn3-noderoute` | 1 of 2 | **37** | queues empty; one trade point crossed a five-point block: `5 money x 5 x IncMod 1.48 = 37` |
|
||
| `zuul-turn23-fleet23` | 1 of 2 | **2035** | Gallandro's queue holds 370 points: `370 x 5 x 1.1 = 2035` |
|
||
| `zuul-turn17-orders2`, `-rollpending` | 0 of 2 | **21010** | W'kanda's queue holds 3818: `3820 x 5 x 1.1 = 21010` (the block count moves by 3820, not 3818, because of the two `mod 5` offsets) |
|
||
| `zuul-turn16-noderoute` | 0 of 2 | **36438** | W'kanda's queue holds 6626: `6625 x 5 x 1.1 = 36437.5`, and two independent truncations put it at 36438 |
|
||
| `zuul-turn5-species5` | 1 of 2 | **39160** | W'kanda's queue holds 7120: `7120 x 5 x 1.1 = 39160` |
|
||
|
||
**Every delta decomposes, to the unit, into the build queue's construction points priced
|
||
through the money chain** — three of the four exactly, and the fourth within the one unit that
|
||
two independent truncations of a `.5` allow. That is the closest thing to an oracle this path
|
||
has, and it is a much stronger statement than "the two agree": it says the two paths differ
|
||
by *exactly the thing the disassembly says they differ by*.
|
||
|
||
### 5.4 What this run did NOT cover — read this before quoting the numbers
|
||
|
||
* **One leaf per pair is a weak result** and it is the one leaf where the missing upstream
|
||
input does not bite. Player 576 owns one colony with no civilian population and an empty
|
||
queue; it is the easy case.
|
||
* **The ship-repair demand is taken as 0 everywhere.** `Ship::RepairCost` (0x00815180) is
|
||
unread, so no save can supply it. Every colony with a damaged fleet in orbit therefore reads
|
||
**high**, and the corpus cannot tell us whether any does.
|
||
* **No corpus colony is off its ideal suitability, and none is below full infrastructure.** So
|
||
the terraform channel, the infrastructure channel, `infraNeed`, `terraNeed`, the
|
||
infra→terraform cascade, the terraform→money cascade and the non-trivial half of the
|
||
`SRsc == 1` leftover branch are **all unexercised by the corpus**. They are covered by host
|
||
tests written from the instruction stream, which is a much weaker claim.
|
||
* **No corpus system has a station**, so the shipyard bonus in `ConstructionPoints` — including
|
||
the "strictly positive" guard and the association this lane corrected — is unexercised.
|
||
* **`SRoh` is 0 on every save**, so the over-harvest term is unexercised here too (lane N's
|
||
finding, unchanged).
|
||
* **`dsu` is FLT_MAX everywhere**, so the ideal-suitability override is unexercised and the
|
||
sentinel's identity is inferred from that fact rather than read.
|
||
* **The build queue is exercised only on Zuul saves that are not on either reference pair.**
|
||
The two pairs the metric is measured on have empty queues throughout, so the queue edge is
|
||
checked by the §5.3 self-check and not by the leaf count.
|
||
* The `[0x230]` gate, `out[4]`, `out[5]` and `out[6]` are read but **not modelled**; nothing
|
||
downstream of `out[3]` needs them.
|
||
* **Neither `NormaliseOutputRates`' tail (past 0x007474d4) nor `CivilianConsumption`'s ratio
|
||
was read this lane.** The first is B4's; the second feeds only the resource ledger.
|
||
|
||
### 5.5 The two budget-downstream RNG words
|
||
|
||
They still cannot enter the generator model, and the reason has changed again.
|
||
|
||
`income-term.md` §6.3 named them: `ProcessResearch`'s `RNG::Chance(odds)` on the funded node,
|
||
and the draw inside `OnTechResearched`'s effect callback. Both are downstream of the research
|
||
**allocation**, which is `ComputeBudget`'s research money — and that money is now computable,
|
||
because its only missing input was the per-system money this lane closed.
|
||
|
||
What blocks them now is one step further out: **the only corpus player that reaches
|
||
`ProcessResearch` with a research target is the AI**, and its research rate (`ResRate`
|
||
0.25 → 0.8) and its target (`ResTNm` '' → 'IND_Waldo') are *its own orders, generated during
|
||
the same turn*. Feeding our budget into `ProcessResearch` would allocate against the wrong
|
||
rate. On `turn1-state` no player has a target at all, so the phase has nothing to fund and
|
||
both words cost zero on the reference pair.
|
||
|
||
That last sentence is a claim about the **workload**, not about the phase, and rule 20 applies
|
||
to it: a word count cannot separate "`ProcessResearch` was not entered" from "it was entered
|
||
and drew nothing". An entry probe on `ProcessResearch` would settle it in minutes and is the
|
||
cheap next step. Until then the two words are best described as *unblocked in formula, blocked
|
||
in workload*, and the honest ledger entry is that the generator model is unchanged at 16 words
|
||
modelled.
|
||
|
||
### 5.6 What is now blocked on what
|
||
|
||
| phase | was blocked on | is blocked on |
|
||
|---|---|---|
|
||
| P01 `ComputeBudget` | the per-system money output | nothing here; `S11`'s uncommitted civilian growth upstream, and the unread ship-repair demand |
|
||
| P02 `ApplyNetToSavings` | P01 | committed; exact where the upstream is |
|
||
| P03 `RecordBudgetDerivedFields` | the budget | two player words that are still not identified on the wire — **unchanged, this lane does not help it** |
|
||
| P05 `ProcessResearch` | the allocation | the AI's own research rate and target (Rung B) |
|
||
| P06 `ResearchRefund` | P01 and P05 | P05 |
|
||
| T36 turn record | the budget **and** ship construction | ship construction (lane B6) plus the same growth gap; the money half is now available |
|
||
|
||
The single highest-value next item this lane can name is **committing `S11`'s civilian
|
||
growth**: it is what stands between player 16's `Sav` and the oracle on both pairs, it is also
|
||
what stands between `BnkEl`/`BnkPr` and the oracle (four more leaves), and it is the same
|
||
`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.
|
||
|