// Colonies: carrying capacity, population growth, infrastructure and terraforming // point conversion, slave deaths, output split, build-queue consumption. #pragma once #include #include #include #include "game/sim/species.h" #include "game/sim/tuning.h" namespace sots::sim { // Population group kinds (rows of the per-type population table). enum class PopGroup : int { Imperial = 0, Civilian = 1, Slaves = 2 }; // Xenotech level a player has reached against one species: bit k of the per-species // flag word is set when the k-th xenotech for that species is researched. The bit order // is the order of the xenotech families (translation 1/2/3, incorporate, addict, // temperance, subjugate, accommodate, proliferate). CONFIDENCE: high on the order. struct SpeciesTechFlags { bool translation1 = false; // bit 0: slave death rate x0.8 bool translation2 = false; // bit 1: slave death rate -0.2 bool translation3 = false; // bit 2: slave death rate -0.2 bool incorporate = false; // bit 3 bool addict = false; // bit 4 bool temperance = false; // bit 5: addiction to this species is cured bool subjugate = false; // bit 6 bool accommodate = false; // bit 7: no suitability penalty on capacity (hazard = 1) bool proliferate = false; // bit 8 static SpeciesTechFlags FromBits(unsigned bits) { SpeciesTechFlags f; f.translation1 = bits & 0x001; f.translation2 = bits & 0x002; f.translation3 = bits & 0x004; f.incorporate = bits & 0x008; f.addict = bits & 0x010; f.temperance = bits & 0x020; f.subjugate = bits & 0x040; f.accommodate = bits & 0x080; f.proliferate = bits & 0x100; return f; } }; // --------------------------------------------------------------------------------------- // Capacity // --------------------------------------------------------------------------------------- // Hazard modifier on carrying capacity from planet suitability vs the species' ideal: // clamp01(1 - |suit - ideal| / (SuitTol + 0.1)) // Linear, no exponent; the +0.1 gives every species a habitable band even at zero // tolerance. SuitTol starts at the species value and is raised by the atmospheric and // gravitational adaptation techs. Skipped (treated as 1) when the player has the // accommodate xenotech for the species or is the rebel AI -- the caller decides that. // CONFIDENCE: high -- read with its constant. double HazardModifier(double suitability, double idealSuitability, double suitTolerance); struct CapacityInputs { int planetSize = 0; // Size Species species = Species::Human; // species of the population group bool speciesCanLive = true; // species can survive on this planet class PopGroup group = PopGroup::Imperial; double groupCapacityMult = 1.0; // per-group-type capacity column double speciesGrowthFactor = 1.0; // per-species factor bool ownerIsDifferentSpecies = false; double crossSpeciesMod = 1.0; // applied when the owner is another species double hazardMod = 1.0; // from HazardModifier (or 1 when accommodated) bool arcologyTech = false; // adds a flat 1e8 (imperial) / 2e8 (civilian) bool groupMaxEnabled = false; // per-group hard cap present std::int64_t groupMax = 0; bool ownerIsNpc = false; // owner species is the independent race }; // Round a capacity down to a whole ten -- but only above ten. B4 finding: every carrying // capacity goes through this, so a colony's cap is always a multiple of 10 in practice. // CONFIDENCE: high -- read from the helper's truncate/divide/multiply sequence. std::int64_t QuantiseCapacity(std::int64_t v); // cap = quantise(ftoi64((Size x 1e8) x (hazard x (groupMult x speciesFactor x crossSpecies)))) // + arcology flat bonus (1e8 imperial / 2e8 civilian, **0 for slaves**); clamped to // the group max; x INDSYS_IMPERIAL_POPULATION_MOD and re-quantised when the owner is // the NPC race. The NPC species itself, and a species that cannot live there, get 0. // `Size x 1e8` is an exact 64-bit integer product; only the modifier chain is floating // point. CONFIDENCE: high on the shape, the order and the special cases. std::int64_t CarryingCapacity(const CapacityInputs& in, const TuningTable& t); // --------------------------------------------------------------------------------------- // Growth // --------------------------------------------------------------------------------------- struct GrowthInputs { std::int64_t pop = 0; bool blockaded = false; // growth halted at this system for this group double suitability = 0; // the planet's Suit double idealSuitability = 0; // the growing species' ideal double suitTolerance = 0; // the owner's SuitTol bool accommodated = false; // the owner ignores suitability entirely (RebAI etc.) double playerPopMod = 1.0; // PopMod, applied only when > 0 double extraFactor = 1.0; // the caller's per-call factor, applied only when > 0 double groupGrowthMult = 0.0; // per-group-type growth column; applied only if > 0 }; // The clamp the engine puts on the growth exponent before calling pow(). constexpr double kGrowthExponentMin = 0.009999999776482582; // (double)0.01f constexpr double kGrowthExponentMax = 1000.0; // Suitability distance the growth curve is built from: // min( |ideal - clamp(suit, 0, 20)| , tolerance ) // clamped through float32 at every step, and 0 when the owner is accommodated. The 0..20 // clamp on the planet's suitability is easy to miss and matters at the extremes. // CONFIDENCE: high. double GrowthSuitabilityDistance(double suit, double ideal, double tolerance, bool accommodated); // Growth fraction for one population group. // // B4 CORRECTION -- the curve is **not** logistic in population. There is no `pop / capacity` // term anywhere in the chain; the capacity is never even passed in. The base of the power // is a *suitability* term: // base = 1 - clamp01( GrowthSuitabilityDistance(...) / tolerance ) // g = clamp01( pow(base, clamp(POPULATION_GROWTH_EXP, 0.01f, 1000)) ) // if g > 0: g x= POPULATION_GROWTH_MOD (if > 0) x PopMod (if > 0) // x extraFactor (if > 0) x groupGrowthMult (if > 0) // Every guard is a strict `> 0` and every intermediate is stored back to a float32 slot. // A zero tolerance makes the quotient 0/0, which propagates NaN through the whole chain -- // a real hazard, reproduced rather than papered over. CONFIDENCE: high. double PopulationGrowthFraction(const GrowthInputs& in, const TuningTable& t); // The per-turn delta: `trunc(pop x g)`, or exactly 1 when that truncates to 0 while g is // strictly positive. A group with no population, or a blockaded one, yields 0. There is no // 50,000,000 cap here -- that lives in the apply. CONFIDENCE: high. std::int64_t PopulationGrowthDelta(const GrowthInputs& in, const TuningTable& t); // The largest population step a colony can take in one turn, up or down. constexpr std::int64_t kMaxPopulationStep = 50000000; // Apply growth to an imperial population: // delta = min(delta, 50,000,000) when delta >= 0 // new = pop + delta // new <= cap -> new // new > cap and pop <= cap -> exactly cap (no shrink at all) // new > cap and pop > cap -> max(pop - min(|cap - pop|, 50,000,000), min(pop, 100)) // finally, floored at 0 // B4 correction: the shrink is computed from the *old* population and only runs when the // colony was already over the cap; a colony that merely grows past the cap simply lands on // it. CONFIDENCE: high -- read branch by branch. std::int64_t ApplyImperialGrowth(std::int64_t pop, std::int64_t capacity, std::int64_t delta); // --------------------------------------------------------------------------------------- // Infrastructure and terraforming // --------------------------------------------------------------------------------------- // The infrastructure divisor is a TRUE double 3.3e-05 in the image (not a widened float). constexpr double kInfraPerPoint = 3.3e-5; // The two terraforming factors are widened *float* literals, and they differ: the "points // needed" side uses 1.8000000715255737 (= 1.5 x (double)1.2f, stored as one constant), // the yield side multiplies by the exact double 1.5 and then by (double)1.2f. The two // therefore agree exactly, which is what keeps need and yield consistent. constexpr double kTerraform12 = 1.2000000476837158; // (double)1.2f constexpr double kTerraformNeedFactor = 1.8000000715255737; // 1.5 x (double)1.2f // Points needed to bring infrastructure from `infra` to 1.0: ceil((1 - infra) / 3.3e-5). // A real ceil on the double quotient. CONFIDENCE: high. double InfrastructurePointsNeeded(double infra); // Infrastructure gained from spending `points`: `points / 500 x 0.01 x 1.65` evaluated in // that order on the x87, clamped at zero and narrowed to float32 once on the store. The // arithmetic is deliberately not folded into a single x3.3e-5. CONFIDENCE: high. double InfrastructureGain(double points); // Spend from a pool toward full infrastructure; returns the *delta* and reports the // points the pool has left (which cascade into the terraforming pool). CONFIDENCE: high. double ApplyInfrastructurePoints(double infra, double pool, double* pointsUnused); // Add an infrastructure delta to a colony: a no-op once infrastructure is at 1, clamped // to 1 otherwise, stored as float32. CONFIDENCE: high. double ApplyInfrastructureDelta(double infra, double delta); // Per-turn infrastructure loss of an unowned system. The image's constant is the widened // float literal `(double)0.02f` = 0.019999999552965164, not the decimal 0.02 (B4). constexpr double kUnownedInfraDecay = 0.019999999552965164; // Unowned systems lose `kUnownedInfraDecay` infrastructure per turn: the difference is stored // back into a 4-byte float and then floored, `result <= 0 -> 0`. CONFIDENCE: high -- read with // its constant's exact bits and the fcomp against the .rdata zero. double DecayUnownedInfrastructure(double infra); // Points needed to terraform from `suit` to `ideal`: // ceil( |float32(ideal - suit)| / |TerraMod x 1.8000000715255737 / 20000| ) // B4 correction: the player's terraforming modifier is inside the *need*, not only the // yield -- a better modifier needs proportionally fewer points, and that is what keeps // need and yield consistent. CONFIDENCE: high. double TerraformPointsNeeded(double suit, double ideal, double terraMod); // Suitability change from spending `points`: // float32( points x 1.5 x (double)1.2f x TerraMod x sign / 20000 ) // with `sign` = -1 only when `suit > ideal` strictly (a planet already at the ideal takes // +1). CONFIDENCE: high. double TerraformDelta(double points, double terraMod, double suit, double ideal); // Add a suitability delta, clamped so the planet stops at the ideal from whichever side // it approached; stored as float32. CONFIDENCE: high. double ApplyTerraformDelta(double suit, double delta, double ideal); // --------------------------------------------------------------------------------------- // Slaves // --------------------------------------------------------------------------------------- // The two xenotech factors of the slave-death modifier, as the image holds them. constexpr double kSlaveModBase = 0.800000011920929; // float32 0.8f, replaces the 1 constexpr double kSlaveModStep = 0.20000000298023224; // (double)0.2f, subtracted // Per-turn slave death rate: // mod = (translation1 ? 0.8f : 1) - 0.2 x translation2 - 0.2 x translation3 [no clamp] // rate = ((|ideal - suit| x BYHAZARD + DEATH_RATE) + SRs x BYOUTPUT) x mod // B4 correction: the hazard term is folded into the base *before* the output term, and every // step is stored back to a float32; an unowned system reports 1.0, not 0. CONFIDENCE: high. double SlaveDeathRate(double slaveOutputRate, double suit, double ideal, const SpeciesTechFlags& flags, const TuningTable& t, bool owned = true); // Deaths this turn: // d = ftoi64(slaves x (rate + plagueRate)) // then MIN_DEATHS/MAX_DEATHS, each **disabled by any negative value**, then clamped into // [0, slaves]. // B4 correction: the worst plague at the system contributes an *additive* rate term -- that // is the only path by which a plague kills slaves -- and both bounds are clamps, not just // the maximum. CONFIDENCE: high. std::int64_t SlaveDeaths(std::int64_t slaves, double rate, double plagueRate, const TuningTable& t); // --------------------------------------------------------------------------------------- // Output split // --------------------------------------------------------------------------------------- struct OutputRates { double trade = 0; // SRt double construction = 0; // SRsc double terraform = 0; // SRtf double infra = 0; // SRi }; // A slider at or below this counts as off, and it is also the value the all-zero fallback // seeds the unpinned channels with. The image holds the widened float literal // `(double)1e-4f`. constexpr double kOutputRateThreshold = 9.999999747378752e-05; // Normalise the player's output sliders. // // B4 CORRECTION -- this is not a symmetric "rescale all four to sum 1". The routine takes a // *pinned* channel, and every caller in the game passes the trade slider (explicitly or by // leaving the argument null, which selects it). So: // * terraform -> 0 when suitability is exactly at the ideal (an exact ==, no epsilon); // infra -> 0 when float32(infra + pending bonus) >= 1; // * any channel at or below (double)1e-4f -> 0; // * **only the trade slider** is clamped into [0, 1]; // * the sum covers construction + infra + terraform only, accumulated in float32; // * an exactly-zero sum seeds those three with 1e-4f (still honouring the two // suppressions) and re-sums -- an equal split over three channels, not four; // * each of the three becomes `(rate / sum) x (1 - trade)`, float32 at both stores. // Trade comes out unchanged. CONFIDENCE: high -- read step by step, including the pinned // argument's default and the float32 accumulation. OutputRates NormaliseOutputRates(const OutputRates& raw, bool suitAtIdeal, bool infraFull); // --------------------------------------------------------------------------------------- // Base output: the population, resource and over-harvest terms // // LANE N CORRECTION (2026-09-08). The previous model here treated the whole of a system's // output as one multiplicative chain, `base x morale x stationFactor x ...`, with `baseOutput` // an unresolved input. That is the wrong shape. The original sums **three independent terms** // and applies morale and the station bonus to only some of them; the five player/system // multipliers at the end are the only genuinely global factors. // // The population term itself is linear and is carried entirely by the executable: output // points per head are `typeOutputMod x 1.8 / 500000`. See // sots-re findings/subsystems/output-term.md. // --------------------------------------------------------------------------------------- // The per-population-type constants. The original builds this three-row table **in code** // from x87 literals; only the slave row reads the data files. CONFIDENCE: high (read out of // the initialiser, including its x87 register rotation). struct PopTypeConstants { double outputMod = 0; // multiplies the per-capita output rate double incomeMod = 0; // multiplies the per-capita income rate std::int64_t maxPopulation = 0; // the row's population cap }; // Slave-row values come from the data files, so they are taken from the tuning table. PopTypeConstants PopTypeOf(PopGroup g, const TuningTable& t); // Output points contributed per head, before the type modifier: `1.8 / 500000`. Both are // .rdata literals, i.e. facts about the algorithm rather than about the shipped data. constexpr double kOutputPopulationFactor = 1.7999999999999998; // the image's (double)1.8 constexpr double kOutputPopulationDivisor = 500000.0; struct GroupOutputInputs { PopGroup group = PopGroup::Imperial; std::int64_t count = 0; // heads in this group (imperial: Pop + pbon) int morale = 0; // the system's Morale int[7] entry for this species int stations = 0; // stations at the system bool owned = true; // the system has an owner bool independent = false; // sys.indi != null: morale is bypassed }; // One (group, species) row's contribution: // count <= 0 -> 0 // q = count / 500000 // sf = 1 + stations x STATION_BONUS_IMPERIAL_OUTPUT, imperial groups of an owned system only // (and only while the constant is > 0) // mo = morale multiplier, civilian groups only // max(0, typeOutputMod x (sf x 1.8) x mo x q) // CONFIDENCE: high -- read instruction by instruction, including the association of the // multiplies, which is not free in 80-bit x87. double GroupOutput(const GroupOutputInputs& in, const TuningTable& t); // Morale effect on output. A morale entry of exactly 0 means "no record", and the original // returns 1 rather than consulting the thresholds; each modifier is also ignored unless it // is strictly positive, which matters because an unloaded tuning table has them at 0. // CONFIDENCE: high (both guards read from the branch). double MoraleOutputMultiplier(int morale, const TuningTable& t); // Resource extraction efficiency: `min( clamp01(cbrt((Pop + pbon)/100) x 0.01), Infra + ibon )`, // narrowed to float32 on the way in and on the way out. The `>= 1 - 1e-4` branch substitutes // the infrastructure term outright. CONFIDENCE: high. struct StripMineInputs { std::int64_t population = 0; // Pop + pbon float infra = 0; // Infra float infraBonus = 0; // ibon }; float StripMineFraction(const StripMineInputs& in); // The over-harvest resource demand (0x007483b0). Also the term the resource ledger charges // against the stock, and -- multiplied by the species' resource-output factor -- one of the // three summands of a system's output. // B = overHarvestRate > 0 ? max(rate x resourcesAvailable x clamp01((Pop+pbon) x 1e-5), 1) : 0 // return min(resourcesAvailable, max(speciesBaseDemand + B, 0)) // CONFIDENCE: high on the structure; the `max(.., 1)` floor is UNVERIFIED behaviourally // because every save in the corpus carries SRoh = 0. struct OverHarvestInputs { double overHarvestRate = 0; // the normalised SRoh slider std::int64_t resourcesAvailable = 0; // Res, plus MRes + ARes2 when the owner strip-mines std::int64_t population = 0; // Pop + pbon int speciesBaseDemand = 0; // SpeciesDef +0x4c, from the data files bool owned = true; }; double OverHarvestDemand(const OverHarvestInputs& in); struct BaseOutputInputs { // population std::int64_t imperialPopulation = 0; // Pop + pbon, owner species only std::int64_t civilianPopulation = 0; // Pop2 + pbon2 of the owner species, incl. surplus std::int64_t slavePopulation = 0; // summed over species int imperialMorale = 0; // unused: imperial output ignores morale int civilianMorale = 0; int stations = 0; bool independent = false; // resources std::int64_t transitResources = 0; // TRes std::int64_t resourcesAvailable = 0; // Res (+ MRes + ARes2 when strip-mining) float infra = 0; float infraBonus = 0; // over-harvest double overHarvestRate = 0; // SRoh int speciesBaseDemand = 0; // SpeciesDef +0x4c double speciesResourceOutput = 0; // SpeciesDef +0x50 (a float, widened) }; // The sum of the three terms, before the global multipliers: // overHarvestDemand x speciesResourceOutput // + (TRes + resourcesAvailable) x stripMineFraction x 0.9 // + imperial + civilian + slave population output // CONFIDENCE: high on the terms; the summation order below reproduces the original's, // which matters because x87 addition is not associative. double SystemBaseOutput(const BaseOutputInputs& in, const TuningTable& t); struct OutputModifiers { double baseOutput = 0; // SystemBaseOutput bool addictionPhase3 = false; double scOutMod = 1.0; // ScOutMod (player +0x12c) double rebOutMod = 1.0; // RebOutMod (player +0x128) double techOutMod = 1.0; // the game-setup output multiplier (player +0x224) double systemOutMod = 1.0; // sys.OutMod (+0x7c) double playerOutMod = 1.0; // OutMod (player +0x124) bool owned = true; // no owner -> 0 bool rebelling = false; // rbfl != 0 -> 0 }; // total = roundHalfEven( addiction x base x OutMod x sys.OutMod x techOut x RebOutMod x ScOutMod ) // The five multipliers are applied in exactly that order in one uninterrupted 80-bit chain. // B4 correction retained: the engine's "round" is `fistp`/`fild` -- round to nearest, ties to // EVEN -- and the result stays a double that the channel splits multiply; only the reported // total slot truncates it. // CONFIDENCE: high. Note the return of the original is the *unrounded* double; the rounding // is done by its caller before the channel split, and is folded in here because every caller // in the game does it. double TotalSystemOutput(const OutputModifiers& m, const TuningTable& t); // The unrounded value the original returns, for a hook that compares its return bit for bit. double TotalSystemOutputRaw(const OutputModifiers& m, const TuningTable& t); struct OutputSplit { double trade = 0; double construction = 0; double terraform = 0; double infra = 0; }; // roundHalfEven(total x rate) for each channel, each kept as a double. CONFIDENCE: high. OutputSplit SplitOutput(double total, const OutputRates& rates); // Construction points after the shipyard station bonus. **Truncating**, not rounding. // CONFIDENCE: high. int ConstructionPoints(double constructionShare, int stations, const TuningTable& t); // Redistribute unspent construction points L over trade/terraform/infra. Weights are the // normalised rates of those three channels, or 1 / (suit != ideal) / (infra != 1) when the // construction rate is exactly 1. Each share is rounded half-to-even independently, so the // three need not add back up to L. CONFIDENCE: medium on the weights, high on the rounding. OutputSplit SplitLeftover(double leftover, const OutputRates& rates, bool suitAtIdeal, bool infraFull); // --------------------------------------------------------------------------------------- // Money: the population income law and a system's money output // --------------------------------------------------------------------------------------- // // The income chain is a SECOND chain off the same three-row population table, and it is not // the output chain with a different constant: income per head is `typeIncomeMod / 14000`, // with neither the 1.8 factor nor the 500000 divisor the output law uses, and it truncates // TWICE per (group, species) row -- once inside the per-row term and once after the morale // and addiction factors. Summing the species first and truncating once is wrong on any // colony carrying more than one species or a non-unit morale/addiction factor. // See sots-re findings/subsystems/income-term.md. // Money contributed per head, before the type modifier. An .rdata literal. constexpr double kIncomePopulationDivisor = 14000.0; // One (group, count) row's money: `ftol( typeIncomeMod x (count / 14000) )`. The type // modifier is a float32 in the table, so it is narrowed before the multiply. // CONFIDENCE: high -- read instruction by instruction (the whole function is five // instructions and a tail-jump into the float-to-int helper). int GroupIncome(PopGroup group, std::int64_t count, const TuningTable& t); struct PopIncomeRow { std::int64_t count = 0; // heads of this species in this group int morale = 0; // the system's Morale entry for this species bool addicted = false; // the system's addiction entry for this species is non-zero }; // The population income of one group type, summed over the seven species rows: // for each species with count > 0: // mo = civilian groups only: the morale multiplier (the SAME helper output uses) // ad = addicted ? ADDICTION_INCOME_MOD : 1 // sum += (double) ftol( (double)GroupIncome(group, count) x mo x ad ) // Slave groups take neither morale nor the civilian capacity surplus. // CONFIDENCE: high on the loop and both truncations; the civilian capacity-surplus term // (which the original adds to the owner species' civilian count when the colony is at its // cap) is the CALLER's business -- fold it into that row's `count`. No colony in the // corpus is at its cap, so that term is UNEXERCISED either way. double PopulationIncome(PopGroup group, const PopIncomeRow (&rows)[kSpeciesCount], bool owned, bool independent, const TuningTable& t); // Suitability term of the money cost: how far the planet is from the owner species' // ideal, capped by the owner's SuitTol (so the tolerance techs also cap the cost). // The rebel AI pays nothing, and so does a system carrying a von Neumann machine // (`vnh`, the very first test in the original); an unowned system is charged as if 20 away. // // `idealSuitability` is the SERVER's per-species baseline (`server->IdealSuit[species]`), // not the owner's own `IdealSuit` field, and the species is the system's population species // (`indi->indsp` on an independent colony) rather than the owner's. In the 11-save corpus // the two IdealSuit sources agree for every player, so **which one the original reads is // not falsifiable here** -- it is read from the instruction stream only. // CONFIDENCE: high. double SuitabilityCostMod(double suitability, double idealSuitability, double suitTolerance, bool rebelAI, bool owned, bool vonNeumann = false); struct SystemMoneyInputs { double tradePoints = 0; // the system's trade-channel output this turn double popIncomeImperial = 0; // PopulationIncome(Imperial, ...) double popIncomeCivilian = 0; // PopulationIncome(Civilian, ...), incl. surplus double slaveIncome = 0; // PopulationIncome(Slaves, ...) double speciesIncomeFactor = 1.0; // ConstantsOf(owner).incomeFactor (1 if unowned) double playerIncMod = 1.0; // IncMod (1 if unowned) double serverIncomeMod = 1.0; // game-option income modifier (Sim tag `IncMod`) double difficultyIncomeMult = 1.0; // DifficultyIncomeMod's table entry double speciesCostFactor = 1.0; // ConstantsOf(owner).hazardCostFactor double suitCostMod = 0; // from SuitabilityCostMod }; // Money a system contributes this turn (`TradePointsToMoney`, returning the DOUBLE the // original returns -- its caller is what truncates): // t = slaves + (civilian + (((trade - fmod(trade, 5)) x 5 + 0) + imperial)) // t = f32(speciesIncomeFactor) x t // t = f32(f32(difficultyIncomeMult) x f32(serverIncomeMod)) x (f32(IncMod) x t) // cost = f32(speciesCostFactor) x (suitCostMod x 10000 x 1.5) // return t - cost // Whole five-point blocks of trade are worth FIVE money each after being multiplied by // five -- i.e. 25 money per block of five points; the same literal is the modulus and the // multiplier. Every per-player multiplier is narrowed to float32 before it is applied, and // the additive association above is the original's, which x87 does not let us reorder. // CONFIDENCE: high -- read instruction by instruction. double SystemMoneyIncomeRaw(const SystemMoneyInputs& in); // The truncated form its callers store. CONFIDENCE: high. int SystemMoneyIncome(const SystemMoneyInputs& in); // `ServerSystem::ComputeMaxIncome` -- what `ComputeBudget` and `UpdateBankruptcyLimits` // actually sum. It runs the output total through `ComputeOutputFromRates` with the rate // vector "all output to trade" and takes the money channel: // trade = roundHalfEven(totalOutput) (rate 1.0, so the second rounding is a no-op) // return max( ftol(SystemMoneyIncomeRaw(...)), 0 ) // The `max(.., 0)` is the original's `jg`: a negative-income colony contributes zero to the // empire total rather than reducing it. // // The two cascade terms `ComputeOutputFromRates` can add to the money channel -- unspent // industry and unspent terraforming points -- are provably ZERO under this rate vector: // both are `0 - min(0, need)` with `need >= 0` (infra is clamped to 1, and the terraform // point count takes fabs after its sign multiply). They are therefore not inputs here. // CONFIDENCE: high; the science cascade being zero rests on the repair helper returning 0 // for zero science points, which is INFERRED rather than read. int SystemMaxIncome(double totalOutput, const SystemMoneyInputs& in); // --------------------------------------------------------------------------------------- // The turn path: `ServerSystem::ComputeOutput` // --------------------------------------------------------------------------------------- // // `ComputeBudget` has two modes and they take a system's money from DIFFERENT functions. // Projected mode calls `ComputeMaxIncome` (SystemMaxIncome above). The turn calls // `ComputeOutput`, which runs `ComputeOutputFromRates` with the system's OWN stored sliders, // so the construction, terraform and infrastructure channels are funded and three edges that // are provably dead under the max-income vector are live: // // * the build queue and the ship-repair pass consume construction points; // * whatever they leave over is redistributed across 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 the money channel -- two hops, not one. // // See sots-re findings/subsystems/output-turn-path.md. Note that the field the earlier // income-term note called "science" is the SHIP CONSTRUCTION slider; there is no science // channel in this function (research is bought with money at the empire level). // `ServerSystem::IdealSuitability` (0x00745d60) -- the suitability the terraform channel // aims at, and the `==` the rate normaliser tests against. // unowned -> the system's own suitability (so it is "at its ideal") // independent colony -> the SERVER's per-species baseline for `indi->indsp` // otherwise -> the OWNER's own `IdealSuit` field // and a system-level `dsu` override wins over all three when it is not the sentinel. // CONFIDENCE: high on the branch order. The sentinel is FLT_MAX: that is INFERRED from the // corpus (every system carries exactly FLT_MAX there) rather than read out of the data files. constexpr double kIdealSuitabilityNoOverride = 3.4028234663852886e+38; // FLT_MAX struct IdealSuitabilityInputs { bool owned = true; double systemSuitability = 0; // sys.Suit double ownerIdealSuitability = 0; // owner's IdealSuit field bool independent = false; // sys.hindi double serverIdealSuitability = 0; // server->IdealSuit[indi.indsp], independent only double systemOverride = kIdealSuitabilityNoOverride; // sys.dsu }; double IdealSuitability(const IdealSuitabilityInputs& in); // C3 note on `TerraformPointsNeeded` above (0x00746890): the original does NOT round -- the // `ceil` belongs to `ComputeOutputFromRates`, which applies it to the returned double. Our // version folds the `ceil` in, which is harmless because `ceil` is idempotent and every // caller applies it, but the boundary is worth stating. The sign multiply inside the divisor // is cancelled by a `fabs`, so the result is always >= 0, and a zero `TerraMod` yields +inf, // which makes the terraform channel absorb its whole pool with nothing cascading to money. // That branch is UNEXERCISED -- no corpus player carries TerraMod 0. // `ServerSystem::RepairShipsInOrbit` (0x00751590) -- **the side effect** that makes // `ComputeOutputFromRates` unsafe to call for its value. It hands each damaged ship of the // owner's fleets at the system a share of the construction points left over after the build // queue, round-robin, and returns what is left. // // The round robin is EQUIVALENT to `points - min(points, demand)` and this is a proof rather // than an observation: the per-pass share is `max(points / shipCount, 1)`, so every ship with // a positive remaining cost takes at least one point per pass, and the loop's only early exit // requires every remaining cost to be zero. So it ends either with the points exhausted or // with the demand met. CONFIDENCE: high; the equivalence is pinned by a test. struct RepairPassResult { int spent = 0; int left = 0; }; RepairPassResult RepairShipsInOrbit(int points, int repairDemand); struct SystemOutputInputs { // --- the rate vector, exactly as the system stores it (NOT normalised) --- OutputRates rates; // The two suppressions the normaliser applies. `suitAtIdeal` is an exact `==` against // IdealSuitability(); `infraFull` is `float32(Infra + ibon) >= 1`. bool suitAtIdeal = false; bool infraFull = false; // The leftover-weight test reads the RAW `Infra` against 1.0 and does NOT add the pending // bonus, so it is a different predicate from `infraFull` and is carried separately. bool infraExactlyOne = false; // --- the output total, unrounded (lane N's TotalSystemOutputRaw) --- double totalOutputRaw = 0; // --- construction --- int shipyardStations = 0; // StationCount(sys, owner, 1) int buildQueueDemand = 0; // sum of `conleft` over the system's build queue // Sum of `Ship::RepairCost` over the owner's damaged ships in orbit. NOT modelled from // the wire anywhere yet; a caller that cannot compute it must leave it 0 and say so. int repairDemand = 0; // --- infrastructure --- double infra = 0; // sys.Infra, for `ceil((1 - Infra) / 3.3e-5)` // --- terraforming --- double terraformPointsNeeded = 0; // TerraformPointsNeeded(...) bool terraformDown = false; // IdealSuitability() < sys.Suit double terraformMod = 1.0; // owner's TerraMod // --- money: every field except `tradePoints`, which this function computes --- SystemMoneyInputs money; }; struct SystemOutput { int totalOutput = 0; // out[0], truncated int money = 0; // out[3] <- the ONLY slot ComputeBudget reads int construction = 0; // out[7] int constructionToQueue = 0; // out[8] int constructionToRepair = 0; // out[9] double infraDelta = 0; // out[10], a float32 double suitabilityDelta = 0; // out[11], a float32 // Reported so a caller can see which edges actually carried anything. double tradePoints = 0; // round(total x SRt) double leftoverToTrade = 0; // the construction leftover's trade share double leftoverToMoney = 0; // the terraform leftover that reached the money channel OutputRates normalisedRates; }; // `ServerSystem::ComputeOutput` restricted to the channels the campaign has models for. // out[1], out[2], out[4], out[5] and out[6] -- the resource ledger, the trade-route income // pair and the repair demand -- are NOT produced here: none of them feeds `out[3]`, they have // their own inputs, and inventing them would be coverage theatre. // CONFIDENCE: high on the channel algebra and the rounding sites (every one read off the // instruction stream). The repair spend is only as good as `repairDemand`. SystemOutput ComputeSystemOutput(const SystemOutputInputs& in, const TuningTable& t); // --------------------------------------------------------------------------------------- // System bonus and build queue // --------------------------------------------------------------------------------------- // Both bonus-pool helpers reset the system's turns-developing counter when they fire on a // colony that is not the owner's home system. That is reported rather than written, so the // helpers stay pure; the turn pass applies it. struct BonusApplyResult { bool applied = false; // the pool actually moved bool resetTurnsDeveloping = false; // the non-home `ntdev = 0` the original performs }; // Apply a pending population bonus. Nothing happens unless `bonus > 0`; an unowned system // simply drops the whole pool. Otherwise `applied = min(bonus, cap - pop)` with the colony // already at or over the cap short-circuiting, `pop += applied`, `bonus -= applied`. // The population and the capacity are 32-bit in the original (it takes only the low word of // the capacity), which matters only for colonies past 2^31. CONFIDENCE: high -- read // instruction by instruction, including the unowned drop and the home-system test. BonusApplyResult ApplyPopulationBonus(std::int64_t& pop, std::int64_t capacity, std::int64_t& pendingBonus, bool owned = true, bool homeSystem = true); // Apply a pending infrastructure bonus. Nothing happens unless `bonus > 0` and // `infra < 1`; then `applied = min(bonus, 1 - infra)` and `infra` becomes **exactly 1.0** // when the pool covers the whole remainder (the original stores the literal, not the sum), // otherwise `infra + applied`. Every value is rounded to float32. `bonus -= applied`. // CONFIDENCE: high -- read instruction by instruction, including the exact-1.0 snap. BonusApplyResult ApplyInfrastructureBonus(double& infra, double& pendingBonus, bool homeSystem = true); struct SystemBonusInputs { bool stable = false; int turnsOwned = 0; // current turn - acquisition turn // Turns the colony has been *developing* (the system's `ntdev` counter, bumped by this // same pass just before the accrual and reset to 0 whenever the system is not stable). // B4 correction: the second gate is `ntdev`, not the rebellion turn `rbtn` -- read off // the two `cmp ... , SYSTEMBONUS_MINTURNS` at the head of the original. int turnsDeveloping = 0; bool ownerSpeciesEligible = true; // ConstantsOf(owner).systemBonusEligible (Zuul: false) std::int64_t capacity = 0; // imperial carrying capacity }; // Accrue the long-stability bonus when stable, owned for more than SYSTEMBONUS_MINTURNS // and developing for as long: // target = eligible ? ftol(max(POPBONUS, 0) x cap) : 0 // popBonus += min(max(ftol(POPBONUS_INC x cap), 0), max(target - popBonus, 0)) // infraBonus += min(max(INFRABONUS_INC, 0), max((eligible ? INFRABONUS : 0) - infraBonus, 0)) // The *_HOME keys are not used by the per-turn accrual (only when a home system's // bonus is initialised, which is not modelled here). Applied next turn by the Apply* // functions above. CONFIDENCE: high -- increment, target and gating read with constants. void AccrueSystemBonus(const SystemBonusInputs& in, std::int64_t& popBonus, double& infraBonus, const TuningTable& t); struct BuildOrder { int designId = 0; int orderId = 0; int constructionCost = 0; // con int constructionLeft = 0; // conleft int moneyCost = 0; // charged to the owner on completion (0 = free) bool moneyAvailable = true; // the owner's answer to the charge; false skips the order }; struct BuildQueueResult { std::vector completedOrderIds; int moneyCharged = 0; int pointsLeft = 0; // the function's return value: points not consumed by any order }; // FIFO consumption of construction points. // points <= 0 -> nothing happens, the points are returned unchanged // conleft > points -> `conleft -= points`, `points = 0`, **stop** // otherwise -> if the design costs money, ask the owner to pay; a refusal // SKIPS this order and moves to the next one rather than // stopping the pass. On success build the ship, // `points -= conleft`, `conleft = 0`, continue. // Removal is a separate sweep afterwards that unlinks **every** order with `conleft <= 0`, // including ones that were already at zero before this turn. More than one order can // complete in a turn. CONFIDENCE: high -- B4 corrected the money-refusal path (continue, not // stop), the removal sweep's predicate, and that the leftover is the return value. // // The `points <= 0` case skips **everything**, the removal sweep included: the entry test // branches to the epilogue, which returns the argument. Corrected by lane B6 from the // instruction stream, and carried as a LABELLED HYPOTHESIS rather than a result because no // save can exercise it: an order can only reach `conleft <= 0` inside this pass, and this // pass erases it before returning, so a queue never *starts* a turn with one -- unless a // design with zero construction cost is ever queued, which no corpus save has done. BuildQueueResult ProcessBuildQueue(std::vector& queue, int points); // --------------------------------------------------------------------------------------- // Per-player countdown nibbles (`Bats2`, `rcex`) // --------------------------------------------------------------------------------------- // // A system carries two 64-bit words in which each player owns a 4-bit counter: `Bats2` (a // battle cooldown) and `rcex` (a recon cooldown). Alongside each sits a 32-bit "someone is // counting" mask with one bit per player. The per-turn pass ticks every counter down by one // and clears the mask bit of a player whose counter has already reached zero. // // Only the first 15 players have a nibble: the original skips index >= 15 entirely (it neither // ticks nor clears), which is why the mask is a separate word. A counter is clamped to 15 on // write. CONFIDENCE: high -- read instruction by instruction, including the >= 15 guard. constexpr int kMaxCountdownPlayers = 15; struct ColonyCountdowns { std::uint64_t counters = 0; // player i's counter occupies bits [4i, 4i+4) std::uint32_t active = 0; // the companion mask, bit i per player }; int CountdownFor(const ColonyCountdowns& c, int player); void SetCountdown(ColonyCountdowns& c, int player, int value); // clamped to [0, 15] // One turn of decay over `playerCount` players. A zero counter clears the player's mask bit; // a non-zero one is written back one lower. Players at or past index 15 are untouched, and // the whole sweep is skipped when the counter word is already zero. CONFIDENCE: high. void TickCountdowns(ColonyCountdowns& c, int playerCount); // --------------------------------------------------------------------------------------- // Addiction phases and their morale events // --------------------------------------------------------------------------------------- // Morale-event ids raised by the addiction sweep, with the per-species morale delta each // carries (the deltas come from the event-id table the original inlines). constexpr int kMoraleEventAddictionSuppressed = 0x1b; // delta -1: the owner has temperance constexpr int kMoraleEventAddictionOnset = 0x1c; // delta +1: phase 1 constexpr int kMoraleEventAddictionTerminal = 0x1d; // delta -2: phase 3 enum class AddictionPhase : int { None = 0, Onset = 1, Established = 2, Terminal = 3 }; // Phase of one species' addiction at this system. `startTurn` is the system's `adt[species]` // stamp; a zero stamp means the species was never addicted here. // 0 -> None // elapsed > PHASE3_START -> Terminal, elapsed > PHASE2_START -> Established, else Onset // Both comparisons are strict. CONFIDENCE: high. AddictionPhase AddictionPhaseOf(int startTurn, int currentTurn, int phase2Start, int phase3Start); struct MoraleEventRaised { int species = 0; int eventId = 0; int moraleDelta = 0; }; // --------------------------------------------------------------------------------------- // The per-system turn pass (`ServerSystem::ProcessTurn`) // --------------------------------------------------------------------------------------- // // The original is a dispatcher: most of a colony turn happens in callees (plague, imperial // and civilian growth, resources, refuelling, slaves, rebellion, the build queue). What this // function reproduces is the set of words the dispatcher writes *itself*, plus the two // bonus-pool helpers whose arithmetic is already modelled above. Everything else is an input // boundary -- see docs/B4.md for the list and why each entry is there. struct ColonyTurnState { double infra = 0; // Infra double infraBonus = 0; // ibon, the pending infrastructure bonus std::int64_t pop = 0; // Pop (imperial); overwritten later in the turn by growth std::int64_t popBonus = 0; // pbon, the pending population bonus int turnsDeveloping = 0; // ntdev int totalResources = 0; // TRes, zeroed every turn bool growthHalted[3] = {false, false, false}; // haltv, cleared every turn ColonyCountdowns battles; // Bats2 + its mask ColonyCountdowns recon; // rcex + its mask }; struct ColonyTurnInputs { bool owned = false; // PID != null bool independent = false; // indi != null (an independent colony record exists) bool homeSystem = true; // this is the owner's home system bool stable = false; // IsStable() int playerCount = 0; // players in the game (bounds the countdown sweep) int currentTurn = 0; // StrategyServer ModCount int turnsOwned = 0; // ModCount - TAcq bool ownerSpeciesEligible = true; // the owner's species may accrue system bonuses std::int64_t imperialCapacity = 0; // MaxPop for the imperial group // Addiction sweep, one entry per species slot. bool civilianPresent[kSpeciesCount] = {}; // civilians (incl. pending bonus) > 0 bool temperance[kSpeciesCount] = {}; // the owner holds the temperance xenotech int addictionStart[kSpeciesCount] = {}; // adt[species] int addictionPhase2Start = 0; // ADDICTION_PHASE2_START int addictionPhase3Start = 0; // ADDICTION_PHASE3_START }; struct ColonyTurnResult { std::vector moraleEvents; // from the addiction sweep, in species order int rngDraws = 0; // words this pass consumes from the server generator }; // One `ServerSystem::ProcessTurn`, restricted to what the function body writes itself: // 1. unowned system: `Infra -= UNOWNED_INFRA_DECAY`, floored at 0 (`Infra <= 0` -> 0); // 2. `ApplyInfrastructureBonus` then `ApplyPopulationBonus` (both run even when unowned); // 2b. either bonus firing on a colony that is not the owner's home system resets `ntdev`; // 3. `ntdev` ++ when stable, else reset to 0; // 4. `AccrueSystemBonus` (gated on owned/stable/turnsOwned/ntdev); // 5. `TRes = 0`; // 6. `haltv[0..2] = false`; // 7. the two countdown sweeps; // 8. the addiction sweep, which raises one morale event per addicted species. // The order above is the original's. Steps 3-4 read `stable`, which is a callee's verdict and // therefore an input here. The pass consumes no RNG at all -- and neither do ProcessPlague, // the civilian growth pass or ProcessSlaves, all three swept to call depth one and provably // draw-free. **The only RNG consumer in a colony turn is ProcessRebellion**, and its draw // count is data dependent (one per million rebels in the counting pass, plus a // short-circuiting per-species roll), so a system with no rebellion must leave the generator // untouched -- which is what makes the generator a usable declared region here. // CONFIDENCE: high on every step listed; the sub-passes themselves are not modelled. ColonyTurnResult ProcessColonyTurn(ColonyTurnState& s, const ColonyTurnInputs& in, const TuningTable& t); } // namespace sots::sim