// Code-defined tech effects: what researching a tech does to a player's strategic // modifiers, as a data table plus a small apply layer. // // Effects are additive per research event and permanent (there is no un-research). // The state below carries the player fields the effects touch, named after the save // tags where one exists; the apply function only ever reads and writes this struct and // reports the things it cannot do itself (touch systems, grant another tech) in an // outcome the caller acts on. #pragma once #include #include #include #include #include #include "game/effects/tech_id.h" #include "game/sim/species.h" #include "game/sim/tuning.h" namespace sots::effects { // Boolean abilities a tech switches on. enum class PlayerFlag : int { AdvancedSensors = 0, // hadvs: sensor ranges x ADVSENS_SENSORS_MOD; partial contacts count as known AsteroidMining, // AMine: asteroid resources counted in output Arcology, // harcc: +1e8 imperial / +2e8 civilian carrying capacity GravSynth, // hgs: synced to clients; the drive modifier is applied design-side TradeAllowed, // CnTrd: trade routes may be registered (not for the rebel AI) CommerceRaiding, // CnRad ViewIntel, // CnVItl: intel view on other empires (client side) CaptureDesigns, // cdp: observed/captured designs (needs both spy beam and salvage) Count }; constexpr int kPlayerFlagCount = static_cast(PlayerFlag::Count); // Slots of the AI-benefit tech bonus. enum class AiBonusSlot : int { Research = 0, Admin = 1, Factory = 2 }; enum class EffectKind : int { AddConstructionMod, // ConMod[0..2] += value AddConstructionModClass, // ConMod[index] += value AddSavingsMod, // SavMod[0..2] += value AddOutputMod, // OutMod += value MulOutputMod, // OutMod *= value AddPopulationMod, // PopMod += value AddTerraformMod, // TerraMod += value AddSuitTolerance, // SuitTol += value RaiseMaxOverharvest, // MaxOH = max(MaxOH, value) AddMiningRate, // MinRate += value MulDefenceDamageMod, // pddm *= value SetFlag, // flag(index) = true SetFlagUnlessRebel, // flag(index) = true unless the player is the rebel AI RaiseGateTraffic, // PrGtTrf = max(PrGtTrf, tuning key: index 0 = TpGate, 1 = GatAmp) SetFarCasting, // CstR = 10, CstE = 2, CstT = 1 AiTechBonus, // slot(index) += supplied bonus while the AI benefit is on FlagSystemsAI, // every owned system gets its AI flag (reported to the caller) EnableAiBenefit, // the AI benefit is switched (back) on CurePlague, // HasVac |= mask, HasImm |= mask (index = mask); cured on systems/ships GrantTechIfSpecies, // index = species: the tech in `value` is granted for free CaptureDesignsWith, // CaptureDesigns flag when this and tech(index) are both researched NodeBoreParams, // index = parameter row; the highest row wins ReevaluateCivilianCaps, // systems whose civilians sit at the cap are re-evaluated }; struct TechEffect { EffectKind kind; int index = 0; double value = 0; }; // The effects of one tech (empty for techs without a strategic effect). const std::vector& EffectsOf(TechId id); // Everything a tech touches on the player. Defaults are the "no tech yet" values; the // caller seeds species-dependent starts (SuitTol) from its own tables. // Every modifier below is a 4-byte float in the player object and every effect is applied // as `field = (float)((double)field OP constant)` -- the x87 loads the float, combines it // with a double literal and stores back through a float. Modelling them as `double` and // rounding once at the end drifts from the original after a few techs, so they are floats // here and `ApplyTechEffect` rounds at every step. See docs/B2.md. struct PlayerEconomyState { sots::sim::Species species = sots::sim::Species::Human; bool rebelAI = false; float conMod[3] = {1.f, 1.f, 1.f}; // ConMod: construction cost per hull class float savMod[3] = {1.f, 1.f, 1.f}; // SavMod float outMod = 1.f; // OutMod float popMod = 1.f; // PopMod float terraMod = 1.f; // TerraMod float suitTol = 0.f; // SuitTol: species start value + adaptation techs float maxOverharvest = 0.f; // MaxOH float miningRate = 0.f; // MinRate float defenceDamageMod = 1.f; // pddm float resMod = 1.f; // ResMod (AI research bonus lands here) float incMod = 1.f; // IncMod float castRange = 0.f; // CstR float castEfficiency = 0.f; // CstE float castThreshold = 0.f; // CstT int perGateTraffic = 0; // PrGtTrf -- an int, raised by an integer max // Node-bore parameters. In the player object these live in a separately allocated // 3-word block whose pointer is null while no bore drive is researched, so "absent" // is a state of its own rather than three zeroes. bool hasNodeBoreParams = false; int nodeBoreParams[3] = {0, 0, 0}; bool flags[kPlayerFlagCount] = {}; bool aiBenefit = true; // AIBn: false after an AI rebellion unsigned nodeTrackMask = 0; // NPTrk: bit per Species whose traffic is visible unsigned hasVaccine = 0; // HasVac unsigned hasImmunity = 0; // HasImm unsigned speciesFlags[sots::sim::kSpeciesCount] = {}; // xenotech bits per target species // Sticky "level-1 translation for this species has been researched" mask, one bit per // Species (never the NPC race, never cleared). Re-derived alongside speciesFlags. unsigned translationKnownMask = 0; std::bitset researched; bool Flag(PlayerFlag f) const { return flags[static_cast(f)]; } bool HasResearched(TechId id) const { return IsValidTechId(id) && researched.test(static_cast(TechIdIndex(id))); } }; // Values of the three AI-benefit bonuses. The game reads them from a 6-row table in the // executable ({tech id, rebellion odds, bonus value}); all three bonus values are 0.5 // (B2, table dumped). The caller may still override them. struct AiBonusValues { double research = 0.5; // added to ResMod (CCC_AI) double admin = 0.5; // added to IncMod (CCC_AIAdmin) double factory = 0.5; // added to OutMod (CCC_AIFac) }; struct ApplyContext { const sots::sim::TuningTable* tuning = nullptr; // gate traffic keys AiBonusValues aiBonus; }; // What the caller has to do after an apply because it needs game state this layer has // no access to. struct TechApplyOutcome { bool applied = false; // false when the id is invalid or already researched TechId grantedTech = TechId::None; // research this one too (Zuul boarding pods) unsigned plagueCuredMask = 0; // clear these plague types on owned systems and ships bool flagSystemsAI = false; // mark every owned system's AI flag bool reevaluateCivilianCaps = false; // re-evaluate systems whose civilians are at the cap unsigned temperanceSpeciesMask = 0; // bit per Species: cure addiction to it on owned systems bool nodeBoreParamsChanged = false; }; // Mark `id` researched and apply its effects. Every completion also rebuilds the // per-species xenotech flags and reports the temperance species. Applying an already // researched or invalid id does nothing. CONFIDENCE: high on the tabled effects (their // constants were read directly); medium on the AI-benefit re-application and the // node-bore "highest wins" rule. TechApplyOutcome ApplyTechEffect(PlayerEconomyState& s, TechId id, const ApplyContext& ctx); // The completion callback itself, with no already-researched guard: exactly what the game // runs every time a tech is marked researched, in the game's order -- the one matching // branch of the id chain, then the tail (plague mask, design-option masks are the caller's, // node-bore parameters, species flags, the Zuul grant, the capture-designs pair test, the // temperance sweep). `ApplyTechEffect` is this plus the guard; a differential hook must use // this one, because by the time the callback runs the tech is already marked researched. TechApplyOutcome ApplyTechCompletion(PlayerEconomyState& s, TechId id, const ApplyContext& ctx); // Just the tail of a completion: the node-bore selection, the species flag words and their // sticky translation mask, the capture-designs pair test and the temperance report. The // game runs it for *every* tech marked researched, including the ~half of the data files // that are not in the 196-name key space at all and so have no branch of their own. TechApplyOutcome RunCompletionTail(PlayerEconomyState& s); // Apply a completion by data-file name: resolves the id (case-insensitively), and also // handles the node-track techs, which are keyed by name in the species table rather than // by id. Returns the outcome; `applied` is false for names without a code effect. TechApplyOutcome ApplyTechEffectByName(PlayerEconomyState& s, std::string_view name, const ApplyContext& ctx); // Switch the AI benefit on or off, adding or removing the bonus of every researched AI // tech accordingly (off = AI rebellion; on again = the AI slave tech). CONFIDENCE: medium. void SetAiBenefit(PlayerEconomyState& s, bool on, const ApplyContext& ctx); // Recompute speciesFlags[sp] from the researched set: bit k is set when the level-k // xenotech aimed at species sp is researched. Also run on load. CONFIDENCE: high. void RebuildSpeciesTechFlags(PlayerEconomyState& s); // Whether an AI rebellion can still happen to this player: only while the AI slave tech // is not researched (and the player is not the NPC race). CONFIDENCE: high. bool AiRebellionPossible(const PlayerEconomyState& s); // Plague-cure bit of a vaccine tech (0 for anything else); the universal antidote // covers the first four plague types. CONFIDENCE: high. unsigned PlagueCureMask(TechId id); // Design-option availability masks: two words whose bits mean "the named tech is // researched". The names are game vocabulary; their ids are not all recovered, so the // masks are computed from a by-name predicate. Bit i of A is kDesignOptionNamesA[i], of // B kDesignOptionNamesB[i]. Consumers are design-side and not modelled. CONFIDENCE: high // on the tables. constexpr int kDesignOptionCountA = 32; constexpr int kDesignOptionCountB = 29; extern const char* const kDesignOptionNamesA[kDesignOptionCountA]; extern const char* const kDesignOptionNamesB[kDesignOptionCountB]; // The same two tables keyed the way the game keys them: bit i is "tech id kDesignOptionIdsX[i] // is researched". Recovered in B2 from the two {tech id, bit} tables in the executable. extern const TechId kDesignOptionIdsA[kDesignOptionCountA]; extern const TechId kDesignOptionIdsB[kDesignOptionCountB]; struct DesignOptionMasks { std::uint32_t a = 0; std::uint32_t b = 0; }; DesignOptionMasks ComputeDesignOptionMasks(const std::function& hasResearchedByName); // By id -- what the executable actually does, and what a differential hook needs. DesignOptionMasks ComputeDesignOptionMasks(const std::function& hasResearched); // Chance of an AI rebellion contributed by researching `id`, from the same 6-row table as // the bonus values: 0.1 for the three AI techs, 0.2 for CCC_AIFRCON, 0 for everything else. // The risk applies only while AiRebellionPossible(). CONFIDENCE: high (table dumped); // how the odds are consumed is not modelled here. double AiRebellionOdds(TechId id); } // namespace sots::effects