sots-re/ghidra/addresses.d/lane-e1.json
alex bebdee1ceb income-term: the output -> money chain read from the instruction stream; the BnkEl oracle goes 6/25 -> 25/25
`output-term.md` §6 said the verified output total did not unblock the budget
because a system's MONEY is a second chain. This is that chain, disassembled to
the next function start throughout.

The multiplier `formula-gaps.md` Q3 could not name is a three-row table the
executable BUILDS IN CODE from .rdata float literals -- no data-file key, no
GlobalConst slot, the same shape lane N found for the pop-type table.
ServerPlayer+0x36c is an unnamed, unsaved pointer to {int id; float ai[3];
float other[3]}, filled from that table by ServerPlayer::Read and selected per
player by `is-AI && !NPC`. Every corpus save carries aidf == 1, whose AI income
column is 1.1f. The record's other two columns are a fleet-maintenance DIVISOR
and a RESEARCH multiplier -- Q3 called the third a trade multiplier and it is not.

25/25 on the oracle, from 6/25. The prediction of WHERE the remaining misses were
was wrong and is written down as wrong: the twelve Zuul records were not missing
the suitability cost (every corpus colony sits at its species' ideal, so that
whole term is multiplied by zero and stays unexercised). They were missing
SpeciesDef +0x4c/+0x50, which are PER SPECIES and were carried as one global
pair -- 400 output points, 2000 money, per Zuul colony, and the observed 4400 and
5566 shortfalls fall out to the unit.

New wire fact: the Sim block's ISsp/ISsu pairs ARE server->IdealSuit[], the
float[7] CalcSuitMod indexes. The array is randomised per game by the map
generator and cross-checks against every ServerPlayer's own IdealSuit field in
all 11 saves, so the suitability cost needs no data file.

Also states plainly what this does NOT unblock: ComputeBudget's turn path takes
its per-system money from ComputeOutput with the system's OWN rate sliders, not
from ComputeMaxIncome, so P01/P02/P03/P05/P06 and the two research RNG words stay
blocked on a strictly larger function.
2026-09-08 13:49:48 -04:00

101 lines
8.6 KiB
JSON

{
"_note": "Lane E1, the output -> money chain. Six addresses this lane read are already held elsewhere and AGREE, so they are dropped from this fragment rather than duplicated (rule 14): ServerSystem_ComputeMaxIncome 0x007521c0 (lane-k.json), ServerSystem_ComputeOutputFromRates 0x00751bb0 and ServerSystem_TradePointsToMoney 0x007505b0 and ServerSystem_CalcSuitMod 0x007484d0 (addresses.json), GroupIncome 0x00535e80 and PopTypeRow 0x00535e00 (lane-n.json). Every one of those six was re-read from the instruction stream this lane and the address, convention and boundary matched what was already recorded.",
"entries": [
{
"name": "ServerSystem_PopIncome",
"addr": "0x0074d760",
"convention": "thiscall",
"prototype": "double (ServerSystem* sys, int groupType) // `ret 4`, real end 0x0074d8e1. The income analogue of PopOutput 0x0074d8f0, and NOT the same law: it sums, over species 0..6, `(double)ftol( (double)GroupIncome(groupType, count) x moraleMod x addictionMod )` -- so the value truncates TWICE per (group, species) row, once inside GroupIncome and once after both factors. moraleMod is 0x00746910 (the same helper the output term uses) and applies to groupType 1 only; addictionMod is the float behind slot 0x00aeca48 when the system's int[7] addiction table at +0x1e4 has a non-zero entry for that species. For groupType 1 and the owner's own species on a non-independent system the count first gains the capacity surplus from two calls to 0x0074a6d0 (out slot 4, then out slot 6), max(0, B - A)",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "ServerSystem_SlaveIncome",
"addr": "0x0074b700",
"convention": "thiscall",
"prototype": "double (ServerSystem* sys) // plain `ret`, real end 0x0074b793. groupType 2 of the same loop as PopIncome, over SlaveCount(species) (0x0074b610): no morale factor and no capacity surplus, but the addiction factor still applies. Unexercised: slave counts are 0 on every call in the corpus",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "ServerSystem_SlaveCount",
"addr": "0x0074b610",
"convention": "thiscall",
"prototype": "int64 (ServerSystem* sys, int species) // the slave-group population of one species; the group-2 counterpart of GroupPopulation 0x00747ba0",
"status": "unverified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08) -- reached from SlaveIncome, body not read"
},
{
"name": "ServerSystem_ComputeOutput",
"addr": "0x00751fb0",
"convention": "thiscall",
"prototype": "void (ServerSystem* sys, int out[12]) // zeroes `out`, returns immediately when the caller's pointer is null or the system has no owner (+0x100), else calls ComputeOutputFromRates(out, &sys->Rts /*+0x88*/) -- the system's OWN rate sliders, not a max-mods vector. THE DISTINCTION THAT MATTERS: ComputeBudget's real (non-projected) per-system money is this function's out[3], while its projected mode and UpdateBankruptcyLimits use ComputeMaxIncome 0x007521c0. They are different numbers: this path funds the science, construction and terraform channels, so the repair pass runs and the unspent-industry and unspent-terraforming cascades into the money channel are live",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "StrategyServer_IncomeDifficultyMod",
"addr": "0x0080f470",
"convention": "thiscall",
"prototype": "float (StrategyServer* srv, ServerPlayer* p) // `ret 4`, real end 0x0080f49f. Returns float32( float32(DifficultyMods_Select(p->diffMods /*+0x36c*/, p)[1]) x float32(srv->IncMod /*raw base +0xbc, the Sim block's `IncMod` tag*/) ), or just the server modifier when p is null. Every step is stored back through a 4-byte float. `ecx` here is the RAW StrategyServer base (ServerSystem+0x10), four bytes above the base the class's own methods get. THIS IS THE MISSING x1.1: at the difficulty level every corpus save carries (aidf == 1) the AI column of the table is 1.1f",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "DifficultyMods_Select",
"addr": "0x0059b490",
"convention": "thiscall",
"prototype": "float* (DifficultyMods* rec, ServerPlayer* p) // `ret 4`, real end 0x0059b4b3. Returns &rec->f[0] (the AI triple, at +0x04) when p is non-null AND p->[0xf9] (is-AI) is set AND p->NPC (+0xfb) is clear; otherwise &rec->f[3] (the non-AI triple, at +0x10). The three consumers read offset +0 (fleet maintenance divisor, ComputeBudget 0x0086338b), +4 (system and trade-route money, 0x0080f470 and 0x00833938) and +8 (research points bought with money, 0x0080e229 and 0x00863618)",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "LoadDifficultyRow",
"addr": "0x005a3990",
"convention": "cdecl",
"prototype": "void (int level, DifficultyMods* out) // real end 0x005a3a53 (Ghidra size 193 stops 2 bytes short). memcpy's the default {id 1, 1.0f x6} into `out` FIRST, then builds the table with BuildDifficultyTable 0x005a3870 and linear-searches it for id == level (stride 0x1c, from the 0x92492493 magic divide), copying the six floats on a hit. An out-of-range level therefore yields all ones rather than failing. Called from ServerPlayer::Read 0x008804d0 at 0x00880fa3, gated on 0 <= aidf < 3, which is also where ServerPlayer+0x368 (`aidf`) is stored",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "BuildDifficultyTable",
"addr": "0x005a3870",
"convention": "thiscall",
"prototype": "vector<DifficultyMods>* (vector<DifficultyMods>* out) // real end 0x005a3989. THE TABLE IS BUILT IN CODE, from .rdata float literals -- no data-file key, no GlobalConst slot, same shape as lane N's pop-type table. Three rows of {int id; float ai[3]; float other[3]} (0x1c): id 0 = ai {1,1,1} / other {1.5,1.5,1.5} (0x00a1b000); id 1 = ai {3.0 (0x00a0451c), 1.1 (0x009f957c), 1.5} / other {1,1,1}; id 2 = ai {1e6 (0x009ebd7c), 1.7 (0x009f9580), 2.0 (0x00a04518)} / other {1,1,1}. Read as: level 0 gives the break to the human player, levels 1 and 2 give it to the AI, and on level 2 the AI's fleet maintenance is divided by a million",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "ServerPlayer_GetIncMod",
"addr": "0x0080dd10",
"convention": "thiscall",
"prototype": "float (ServerPlayer* p) // seven bytes: `fld DWORD [ecx+0x30c]; ret`. The save's per-player `IncMod`",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "ServerPlayer_GetSpeciesCostFactor",
"addr": "0x0080dd20",
"convention": "thiscall",
"prototype": "float (ServerPlayer* p) // `SpeciesDef(p->Species /*+0x5c*/)->+0x24`, the multiplier on the suitability MONEY cost (Zuul 0.7). A data-file value",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "StrategyServer_IdealSuit",
"addr": "0x0080f4b0",
"convention": "thiscall",
"prototype": "float (StrategyServer* srv, int species) // `ret 4`; one instruction of work: `fld DWORD [ecx + species*4 + 0xf8]`. The per-species ideal-suitability array on the RAW server base. IT IS ON THE WIRE: the Sim block's `ISsp`/`ISsu` pairs are this float[7] in species-index order, and the array is randomised per game by the map generator -- verified against every ServerPlayer's own `IdealSuit` field in all 11 corpus saves. CalcSuitMod reads THIS, not the player's field",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
},
{
"name": "ServerSystem_TerraformPointsNeeded",
"addr": "0x00746890",
"convention": "thiscall",
"prototype": "double (ServerSystem* sys) // real end 0x0074690d. Returns 0 with no owner, else float32(|IdealSuit(species) - sys->Suit|) / |owner->TerraMod (+0x134) x [0x00a1f928] / 20000|. The sign term (-1.0 when the planet's suitability is STRICTLY above the ideal) is multiplied in BEFORE the fabs at 0x00746906 and therefore cancels: the result is always >= 0. That is why the unspent-terraform cascade into the money channel is provably zero under the max-income rate vector",
"status": "verified",
"source": "findings/subsystems/income-term.md (lane E1 2026-09-08)"
}
]
}