#include "game/sim/economy.h" #include #include #include "game/sim/numeric.h" namespace sots::sim { DifficultyMods DifficultyModsFor(int level, bool isAI, bool isNpc) { // The six floats of each row, as the image holds them: {AI triple, non-AI triple}. // Row 0 gives the break to the player; rows 1 and 2 give it to the AI. struct Row { float ai[3]; float other[3]; }; static constexpr Row kTable[kDifficultyLevels] = { {{1.0f, 1.0f, 1.0f}, {1.5f, 1.5f, 1.5f}}, // 0 {{3.0f, 1.1f, 1.5f}, {1.0f, 1.0f, 1.0f}}, // 1 {{1000000.0f, 1.7f, 2.0f}, {1.0f, 1.0f, 1.0f}}, // 2 }; // An out-of-range level keeps the all-ones default the original memcpy's in first. if (level < 0 || level >= kDifficultyLevels) return DifficultyMods{}; const Row& r = kTable[level]; const float* m = (isAI && !isNpc) ? r.ai : r.other; return DifficultyMods{static_cast(m[0]), static_cast(m[1]), static_cast(m[2])}; } int SavingsInterest(int savings, bool ownsSystems) { if (savings < 0 || !ownsSystems) return 0; return Ftol(static_cast(savings) * kSavingsInterestRate); } int DebtInterest(int savings) { if (savings >= 0) return 0; return Ftol(-static_cast(savings) * kDebtInterestRate); } int MaintenanceCost(int maintenance, double difficultyDivisor) { const int d = Ftol(difficultyDivisor); if (d == 0) return maintenance; return maintenance / d; } int ExpenseTotal(const std::vector& sliders, int availableBeforeExpenses) { constexpr int kUnlimited = 2000000000; const int availPre = availableBeforeExpenses; // The game multiplies the slider fraction by the available income in single // precision, so the income is rounded to a float before the product. const double availAsFloat = static_cast(static_cast(availPre)); std::int64_t minimums = 0; std::int64_t takes = 0; for (const ExpenseSlider& s : sliders) { const int minC = std::max(s.minimum, 0); int maxC = ClampT(s.maximum, 0, kUnlimited); if (maxC == 0) maxC = kUnlimited; const std::int64_t room = static_cast(maxC) - minC; const std::int64_t request = static_cast(Ftol(static_cast(s.fraction) * availAsFloat)) - minC; const std::int64_t take = std::min(std::max(request, 0), room); minimums += minC; takes += take; } const std::int64_t headroom = static_cast(availPre) - minimums; const std::int64_t total = minimums + std::min(std::max(takes, 0), headroom); return static_cast(ClampT(total, -2147483648LL, 2147483647LL)); } int ResearchPointsFromMoney(int researchMoney, double difficultyMult, double resMod, double shrm, double trm, double techMult, double serverResMod, double resScl) { const double base = (static_cast(researchMoney) / 50.0) * 1.15 * 0.5 * 0.85; const double rp = difficultyMult * base * (resMod + shrm + trm) * techMult * serverResMod * resScl; return Ftol(rp); } Budget ComputeBudget(const BudgetInputs& in, bool projected) { Budget b; b.savingsInterest = SavingsInterest(in.savings, in.ownsSystems); b.debtInterest = DebtInterest(in.savings); for (int inc : in.systemIncome) { if (inc >= 0) b.systemIncomePositive = SaturatingAdd(b.systemIncomePositive, inc); else b.systemIncomeNegative = SaturatingAdd(b.systemIncomeNegative, -inc); } b.tradeIncome = in.tradeIncome; b.secondaryManagerIncome = in.secondaryManagerIncome; b.shipCarriedPopIncome = in.shipCarriedPopIncome; b.maintenance = MaintenanceCost(in.maintenance, in.maintenanceDivisor); // Running total of the fixed lines; the slots filled later start at zero. auto running = [&]() -> std::int64_t { return static_cast(b.systemIncomePositive) + b.tradeIncome + b.shipCarriedPopIncome + b.secondaryManagerIncome + b.savingsInterest + b.bonusIncome - b.systemIncomeNegative - b.maintenance - b.researchMoneyKept - b.debtInterest - b.construction - b.expenses - b.researchMoneyGiven - b.savingsGiven; }; b.expenses = ExpenseTotal(in.expenses, static_cast(std::max(0, running()))); b.available = static_cast(std::max(0, running())); if (!in.isAI && b.available > 0) { b.construction = std::min(std::max(0, in.constructionDemand), b.available); } const int availAfterConstruction = b.available - b.construction; const double rate = projected ? 0.0 : in.researchRate; b.researchMoney = std::max(0, Ftol(static_cast(availAfterConstruction) * rate)); b.researchPoints = ResearchPointsFromMoney(b.researchMoney, in.researchDifficultyMult, in.resMod, in.shrm, in.trm, in.techResearchMult, in.serverResMod, in.resScl); b.totalResearchPoints = std::max(0, b.researchPoints + in.tra + in.trp); const int pct = ClampT(in.aidResearchPercent, 0, 100); b.researchMoneyGiven = static_cast(static_cast(b.researchMoney) * pct / 100); b.researchPointsGiven = static_cast(static_cast(b.totalResearchPoints) * pct / 100); b.totalResearchPoints -= b.researchPointsGiven; b.hasResearchAllocation = in.hasResearchTarget; // The research money is only actually charged when there is something to research: the // "kept" line and the allocation are written in the same branch, so a player with no // research target keeps the money in the treasury (B1 trace, docs/B1.md). b.researchMoneyKept = in.hasResearchTarget ? b.researchMoney - b.researchMoneyGiven : 0; // Tech income bonus: a share of the full net so far, only when that net is positive. const std::int64_t netBeforeBonus = running(); if (netBeforeBonus > 0) { b.bonusIncome = std::max(0, Ftol((in.techIncomeMult - 1.0) * static_cast(netBeforeBonus))); } // Savings aid: capped by the projected treasury after this turn (bonus included). if (in.aidSavings != 0) { const int netWithBonus = static_cast(ClampT(running(), -2147483648LL, 2147483647LL)); const int projectedSavings = SaturatingAdd(in.savings, netWithBonus); b.savingsGiven = std::min(std::max(projectedSavings, 0), std::max(in.aidSavings, 0)); } b.net = static_cast(ClampT(running(), -2147483648LL, 2147483647LL)); return b; } // ---- trade --------------------------------------------------------------------------- int TradeRoutesSupported(double civilianPop, double imperialPop, const TuningTable& t) { double routes = 0; if (t.TRADE_ROUTE_REQ_CIVPOPULATION > 0) routes += std::ceil(civilianPop / t.TRADE_ROUTE_REQ_CIVPOPULATION); if (t.TRADE_ROUTE_REQ_IMPPOPULATION > 0) routes += std::ceil(imperialPop / t.TRADE_ROUTE_REQ_IMPPOPULATION); return std::max(1, Ftol(routes)); } int TradeRouteGrossIncome(const TradeRouteState& route, const TuningTable& t) { if (route.ageTurns < t.TRADE_ROUTE_STARTUP_TURNS) return t.TRADE_ROUTE_STARTUP_INCOME; const int perFreighter[3] = {t.TRADE_ROUTE_INCOME_PERFREIGHTER_CRQ, t.TRADE_ROUTE_INCOME_PERFREIGHTER_CR, t.TRADE_ROUTE_INCOME_PERFREIGHTER_DE}; int capLeft = t.TRADE_ROUTE_MAX_FREIGHTERS; std::int64_t income = t.TRADE_ROUTE_MIN_INCOME; for (int c = 0; c < 3; ++c) { const int n = std::min(std::max(0, route.freighters[c]), std::max(0, capLeft)); income += static_cast(n) * perFreighter[c]; capLeft -= n; } double v = static_cast(income); v *= 1.0 + t.STATION_BONUS_TRADE_INCOME * route.tradeStationsAtSystem; if (route.partnerAddicted) v *= t.ADDICTION_TRADE_MOD; return Ftol(v); } int TradeRouteIncome(const TradeRouteState& route, bool asOwner, double difficultyTradeMult, const TuningTable& t) { const double share = Clamp01(t.TRADE_ROUTE_OWNERS_SHARE); const double part = asOwner ? share : 1.0 - share; return Ftol(static_cast(TradeRouteGrossIncome(route, t)) * part * difficultyTradeMult); } // ---- bankruptcy ----------------------------------------------------------------------- BankruptcyLimits ComputeBankruptcyLimits(int maxIncome, const TuningTable& t) { constexpr int kTreasuryLimit = 2000000000; BankruptcyLimits l; // Elimination: the debt whose 15 %/turn interest equals the maximum income. // // The divisor is NOT the decimal -0.15. The image's .rdata double is // -0.15000000596046448, i.e. (double)(float)-0.15f -- the compiler widened a float // literal once, at compile time. Lane K read it; lane N is fixing it here because the // difference is not the "one ulp on large empires" the old note claimed. The two // constants give a DIFFERENT truncated result for every maxIncome divisible by 3, // starting at maxIncome = 3 (-20 against -19), and the disagreement rate rises with // empire size to 100 % above about 3,000,000. Six of the twenty-five bankruptcy records // recoverable from the save corpus would come out wrong with the decimal. l.eliminationFloor = std::max(Ftol(static_cast(maxIncome) / kBankruptcyInterestDivisor), -kTreasuryLimit); // Protection: the tuned factor times the maximum income, never below the floor. // // The factor is NARROWED TO FLOAT32 first, and that is not a guess about the data file: // the two constants in this routine are loaded by different opcodes. The divisor above is // `DD /0` -- `fld qword`, a double in .rdata. The factor is // // mov ecx, [PTR_g_BANKRUPTCY_PROTECTION_LIMIT_FACTOR] // fmul dword ptr [ecx] ; D8 /1 == m32fp // // so whatever decimal the data file carries is a 4-byte float by the time it multiplies. // A `double` multiply here truncates to a different integer for roughly 1% of max-income // values at the corpus' empire size, and for more as the empire grows. The corpus cannot // see it -- all seven of its non-zero `BnkPr` records land where the two agree -- so this // is an instruction-stream reading, not a measured leaf (rule 23). l.protectionLimit = std::max(-Ftol(F32(t.BANKRUPTCY_PROTECTION_LIMIT_FACTOR) * static_cast(maxIncome)), l.eliminationFloor); return l; } int BankruptcyLevel(int savings, const BankruptcyLimits& limits) { if (savings < limits.eliminationFloor) return 2; if (savings < limits.protectionLimit) return 1; return 0; } BankruptcyDecision BankruptcyStep(BankruptcyState& state, int level, int currentTurn, const TuningTable& t) { const BankruptcyState old = state; if (level != state.warningLevel) { state.warningLevel = level; state.startTurn = level != 0 ? currentTurn : -1; } BankruptcyDecision d; d.costCutting = level != 0 && old.warningLevel != 0; d.eliminate = old.warningLevel == 2 && (currentTurn - old.startTurn) >= t.BANKRUPTCY_ELIMINATION_TURNS; return d; } } // namespace sots::sim