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

8.3 KiB
Raw Blame History

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).