sots-re/findings/subsystems/formula-gaps.md

125 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Formula gaps from the game/sim port — answers from the binary
Evidence: annotated disassembly/decompiles in `handoff/tech-decompiles/` (files named by address). `ftol` = `_ftol2`
(0x00925220, truncation). Field names per `struct-recovery.md`.
## Q1. Which bankruptcy limit carries the 3.3 factor; when is the start turn stamped?
`ServerPlayer::UpdateBankruptcyLimits` 0x00818600 (disasm 0x0081863e–0x0081869e):
```
maxIncome = Σ_owned systems ComputeOutputMax(s)[3] // FUN_007521c0, int
BnkEl = ftol(maxIncome / -0.15) // FILD; FLD -0.15 (0x00a2ec30); FDIVR → -6.667 × maxIncome
if (BnkEl < -2,000,000,000) BnkEl = -2,000,000,000
BnkPr = -ftol(maxIncome × BANKRUPTCY_PROTECTION_LIMIT_FACTOR) // FMUL [g_BANKRUPTCY_PROTECTION_LIMIT_FACTOR] (3.3)
if (BnkPr < BnkEl) BnkPr = BnkEl // i.e. BnkPr = max(-3.3·maxIncome, BnkEl)
```
So the **3.3 factor is on the protection limit `BnkPr`**; the elimination limit `BnkEl` is `maxIncome / −0.15`
(the debt at which 15 %/turn interest equals maximum income). `BankruptcyLevel` (0x0080db10): 2 if `Sav < BnkEl`, 1 if
`Sav < BnkPr`, else 0.
Stamping: `ProcessBankruptcy` 0x007c0a50 per non-eliminated non-NPC player: `old = (BnkWrn, BnkTrn)`; `level =
BankruptcyLevel()`; `SetBankruptcyState(level)` (0x0080e260) = `if (level != BnkWrn) { BnkWrn = level; BnkTrn = level ?
ModCount : -1 }`; then decisions use **`old`**: cost-cutting (0x00889500) if `level != 0 && old.BnkWrn != 0`; elimination
if `old.BnkWrn == 2 && ModCount − old.BnkTrn ≥ BANKRUPTCY_ELIMINATION_TURNS`. Net: the stamp is written on the turn the
level changes (any change, 1↔2 included, resets it), and actions start the following turn.
Call sequence: `OnAllCombatDone_Tail` → … → `ProcessBankruptcy` → (later in the tail) `UpdateBankruptcyLimits`, so the
limits used by a turn's bankruptcy check are the ones computed at the end of the previous turn (and on load).
## Q2 (order as asked: 3). Suitability → carrying-capacity hazard curve
`HazardMod` 0x00747ae0: `clamp01(1 − |Suit − IdealSuit| / (SuitTol + 0.1))` (0x00a1a438 = 0.1). Linear, no exponent.
`SuitTol` starts at the species value and is raised by BIO_AtmoAd (+0.75) and BIO_GrvAdpt (+1.5) (tech-effects §1).
Skipped (=1.0) when species flag bit 7 (accommodate xenotech) or RebAI.
## Q3. Trade-points → money system-income tail
`ServerSystem::TradePointsToMoney(trade)` 0x007505b0 (disasm complete):
```
t = (trade − fmod(trade, 5.0)) × 5.0 // whole 5-point blocks, ×5
t += PopIncome(0) // 0x0074d760(sys, 0): Σ_species ftol(GroupIncome(0, pop_imperial(sp))) (0x00535e80 group-type table row 0)
t += PopIncome(1) // civilians (row 1); own species adds (cap − pop) surplus term when at cap
t += SlaveIncome() // 0x0074b700: Σ_species ftol(GroupIncome(2, slaves(sp))) (row 2, SLAVES_INCOME_MOD)
t *= SpeciesDef(owner).incomeFactor (+0x18: Zuul 1.1, Morrigi 0.8, else 1) [1 if unowned]
t *= owner.IncMod (+0x30c) [1 if unowned]
t *= srv.+0xbc × DifficultyMods(owner)->+4 // 0x0080f470: server income modifier × AI trade/income difficulty mult
cost = SpeciesDef(owner).costFactor (+0x24: Zuul 0.7) × CalcSuitMod × 10000 × 1.5
CalcSuitMod (0x007484d0) = min(|IdealSuit(species) − Suit|, owner.SuitTol) (0 if RebAI; 20 if no owner)
money = ftol(t − cost)
```
Note `SuitTol` therefore caps the hazard **cost** as well as extending the habitable range. `ADDICTION_INCOME_MOD` is
applied inside PopIncome (0x0074d760 → 0x00746910 morale/addiction factor) — not verified line by line (MEDIUM).
## Q4. `POPBONUS_INC` population increment
`ServerSystem::AccrueSystemBonus` 0x0074d4f0 (disasm 0x0074d53b–0x0074d5be):
```
if owner && IsStable && ModCount − TAcq > SYSTEMBONUS_MINTURNS && ntdev > SYSTEMBONUS_MINTURNS:
cap = MaxPop(imperial) // 0x0074ab20(0,0)
target = SystemBonusPopTarget(POPBONUS) // 0x0074b5a0: ftol(max(POPBONUS,0) × cap), 0 for species with SpeciesDef+0x5c == 0 (Zuul)
inc = ftol(POPBONUS_INC × cap) // FIMUL: 0.005 × cap
pbon += min(max(inc, 0), max(target − pbon, 0))
ibon += min(max(INFRABONUS_INC, 0), max(INFRABONUS − ibon, 0)) // INFRABONUS target 0 for Zuul
```
(`POPBONUS_HOME`/`INFRABONUS_HOME` are only used when the bonus is (re)initialised for a home system, 0x0074c680/
0x007477a0; the per-turn accrual always uses the non-HOME keys.) Applied next turn by `ApplyPopBonus` (`Pop += min(pbon,
cap − Pop)`).
## Q5. Expense-slider request term
`ComputeBudget` 0x00863030, loop 0x00863431–0x0086349f over `Nexp` entries `{xid, xmin, xmax, xper}` (16 B):
```
availPre = max(0, [6]−[14]−[13]−[11]−[9]−[7]+[3]+[1]−[12]−[8]−[10]+[5]+[4]+[2]) // net before expenses ([12] still 0)
for each entry:
xminC = max(xmin, 0)
xmaxC = clamp(xmax, 0, 2e9); if (xmaxC == 0) xmaxC = 2e9 // 0 = unlimited
room = xmaxC − xminC
req = ftol(xper × (float)availPre) − xminC // FLD [entry+8]; FMUL ST1 (float(availPre))
take = min(max(req, 0), room)
ΣXmin += xminC; ΣTake += take
[12] += ΣXmin + min(max(ΣTake, 0), availPre − ΣXmin)
```
So `xper` is a fraction of the pre-expense available income, the request is `xper × avail` minus the mandatory
minimum, clamped to `[0, xmax − xmin]`, and the total is capped by what is left after all minimums.
## Q6. Which running total the tech-income bonus and the savings aid read
Disasm 0x0086382b–0x00863899:
```
net1 = [5]−[9]−[10]−[11]−[12]−[13]−[14]−[8]−[7]+[3]+[4]+[2]+[1]+[6] // [6] = 0, [14] = 0 at this point
if (net1 > 0) [6] = max(0, ftol((p.+0x228 − 1.0) × net1)) // FLD [ESI+0x228]; FSUB 1.0; FIMUL net1
if (savAid != 0):
net2 = same sum, now including the new [6]
newSav = SatAdd(Sav, net2) // clamped ±2e9
[14] = min(max(newSav, 0), max(savAid, 0))
```
The bonus is a share of the **full net** (all income incl. interest and trade, minus maintenance, research money,
construction, expenses, research aid); savings aid is capped by **projected savings after this turn**, not by the turn
net. `p.+0x228` (and `+0x224` = output multiplier read by `ComputeOutputFromRates`, `+0x22c` = research multiplier)
are copied by 0x0077b620 from a per-player 0x50-byte setup record (`+0x48/+0x4c/+0x50`) at game creation/sync — the
game-setup handicap block, not a tech (MEDIUM).
## Q7. Node-line speed clamp at the influence radius
`NodeLine::Step` 0x00705510 + `BuildStutterSegments` 0x00705280: the travel line is intersected with every system's
sphere of radius `STUTTER_SYSTEM_INFLUENCE_RADIUS` (0x008a64f0 ray/sphere → `[t0,t1] × len`, entries dropped when
shorter than 0.01, sorted, overlaps merged at the midpoint). Per segment:
```
dist = DistPointToSegment(system.pos, segStart, segEnd) // 0x008e8eb0, t clamped to [0,1]
v = nodespeed × ((STUTTER_MAX_SPEED − STUTTER_MIN_SPEED) × (dist / RADIUS) + STUTTER_MIN_SPEED)
```
There is **no explicit clamp**: `dist ≤ RADIUS` holds by construction (the segment lies inside the sphere), so
`v ∈ [MIN, MAX] × nodespeed`; outside every sphere the remainder of the step moves at plain `nodespeed` (0x006fe3b0).
Also note the speed is per **segment** (closest approach of the whole chord), not re-evaluated per position.
## Q8. Does `DecayAllResearch` also hit the current target?
Yes. `TechTree::ProcessResearch` 0x005876c0, loop 0x00587c20–0x00587c90: for **every** node with `state == 2 &&
progress != 0`: `progress = max(0, progress − ftol(Cost(node) × 0.05))`. The just-funded node is not excluded; it only
escapes by completing (state 4) in the same call. Net gain of the current tech per turn is `spend − 5 %·cost`.
## Extras resolved on the way
* `PERGATETRAFFIC_DRV_TpGate/GatAmp` readers: `OnTechResearched` ids 10018/10019 → `PrGtTrf = max(PrGtTrf, value)`
(storage 0x00b23e2c / 0x00b23e30).
* `TRKSTL_REGENERATION_MOD` path: 0x0079b980 → 0x0079b770 gated by `HasResearched(IND_TRKSTL)`.
* Sensor range (system): `SENSORMOD[species] × (hadvs ? ADVSENS_SENSORS_MOD : 1) × 4.0` (0x0080b730).