# game/effects — code-defined tech effects Module: `src/game/effects/`. Two data tables and a small apply layer over a plain `PlayerEconomyState`; no game state, no I/O. Depends on `game/sim` for `Species` and `TuningTable`. Build/test: `tests/game_effects/build_and_run.sh` (plain g++, `-Wall -Wextra -Werror`), or `-DSOTS_GAME_EFFECTS_TESTS=ON` once `src/game/effects` is added to the root CMake after `src/game/sim`. Confidence legend as in `game-sim.md`. ## The key space (`tech_id.h`) The game does not key effects on the `.tech` files. It carries a fixed list of 196 tech names; when the master tree is built, every name is matched case-insensitively against the loaded techs and its position becomes the tech's id, `10000 + index` (`10197` is the "none" sentinel). All hard-coded effects, runtime gates and design-option bitmasks read those ids. A tech absent from the list has no code effect beyond what its data file says (prerequisites, section/weapon availability). `TechId` reproduces that list as an enum with the same numeric values, generated from one X-macro so the enum and the name table cannot drift. Since B2 it is the **whole** table, read out of the executable: all 196 names, so `TechIdName` never returns `nullptr` for a valid id and `TechIdFromName` resolves every tech the code can key on. Enum identifiers are the data-file names except in the xenotech block, where the role-based names are kept (`XNC_Temperance_Hiver` = `"XNC_TEMPHVR"`) because `XenoTechId` is built on them. ### Xenotech block Ids 10114–10163 are the per-species xenotechs, nine families in flag-bit order, each a block of one tech per target species (`XenoTechId(level, species)`): | bit | family | block base | targets | |---|---|---|---| | 0 | Translation 1 | 10114 | Human, Hiver, Tarkas, Liir, Zuul, Morrigi | | 1 | Translation 2 | 10120 | same six | | 2 | Translation 3 | 10126 | same six (the Zuul slot's data name is `XNC_DOMZUUL`) | | 3 | Incorporate | 10132 | five: no Zuul | | 4 | Addict | 10137 | five | | 5 | Temperance | 10142 | five | | 6 | Subjugate | 10147 | six | | 7 | Accommodate | 10153 | five | | 8 | Proliferate | 10158 | **five** | Ids 10163 / 10164 — the two slots after the proliferate block — are the node-track techs `CCC_NDTRKHUM` / `CCC_NDTRKZUL`. The NPC race is never a target. Confidence: **high** throughout since B2 (the family order, the block bases, the species order inside a block and which families omit the Zuul are all read off the name table). Before B2 the proliferate block was modelled as six entries, which made `XenoTechId(Proliferate, Morrigi)` return the Human node-track tech. ### Node-track techs Seeing a species' node-space traffic is granted by a tech keyed by *name* in the species table rather than by the effect chain: `CCC_NDTRKHUM` (10163) for Human traffic, `CCC_NDTRKZUL` (10164) for Zuul traffic, none for the others. `NodeTrackTechName(species)` exposes the name and both now resolve through `TechIdFromName`. Confidence: high on the names and ids, medium on the reader semantics. ## The effects table (`tech_effects.h`) `EffectsOf(id)` returns the list of typed effects (`{kind, index, value}`) applied when the tech completes. Effects are additive per research event and permanent. 44 ids carry an entry; every other id returns an empty list. **Everything below is float32.** The modifiers are 4-byte floats in the player object and each step is `field = (float)((double)field OP k)`, where `k` is the *widened float32* literal the executable carries -- 0.05 is `(double)0.05f`, not 0.05. `PlayerEconomyState` therefore holds floats and the table's constants are written with an `f` suffix; see `docs/B2.md`. `PrGtTrf` is the exception: it is an `int`, raised with an integer max. | tech | effects | |---|---| | CCC_AdvSens | flag AdvancedSensors | | IND_Waldo, IND_ExpSys | ConMod[0..2] −0.10; OutMod +0.15 | | IND_CyberInt | ConMod[0..2] −0.05; OutMod +0.20 | | IND_OrbFound | SavMod[0..2] −0.05 | | IND_OrbDry | ConMod[1] −0.05; ConMod[2] −0.05 (index 0 untouched — ConMod is per hull class, medium) | | IND_GravCon | OutMod +0.30 | | IND_HvyPlat | OutMod +0.10 | | IND_AstMine | flag AsteroidMining | | IND_MsMine | MaxOH = max(MaxOH, 0.1); MinRate +1.0 | | BIO_GnMod | PopMod +0.10 | | BIO_AtmoAd | SuitTol +0.75; PopMod +0.06; TerraMod +0.35 | | BIO_EnvTail | PopMod +0.20; TerraMod +0.45 | | BIO_GrvAdpt | SuitTol +1.50; PopMod +0.10; TerraMod +0.35 | | IND_ArcCon | flag Arcology; PopMod +0.15; re-evaluate systems whose civilians sit at the cap (outcome) | | IND_EleNans | TerraMod +0.60 | | BIO_TerBac | TerraMod +0.45 | | IND_AtProc | TerraMod +0.50 | | DRV_TpGate / DRV_GatAmp | PrGtTrf = max(PrGtTrf, `PERGATETRAFFIC_DRV_TpGate` / `_GatAmp`) from the tuning table | | DRV_FarCast | CstR 10, CstE 2, CstT 1 | | CCC_AI / CCC_AIAdmin / CCC_AIFac | ResMod / IncMod / OutMod += the AI-bonus value for that slot, only while the AI benefit is on | | CCC_AIVrus | every owned system gets its AI flag (outcome) | | CCC_AISlv | same, plus the AI benefit is switched back on; `AiRebellionPossible` becomes false | | CCC_FtlEcon | flag TradeAllowed, unless the player is the rebel AI | | CCC_ComRaid | flag CommerceRaiding | | DRV_GrvSyn | flag GravSynth (client sync only; the drive modifier is design-side) | | CCC_DatCor | flag ViewIntel | | IND_HrdStrct | pddm ×0.25; OutMod ×0.90 — **multiplicative**, so order against the additive OutMod techs matters | | DRN_AdvRob | ConMod[0..2] −0.05 | | IND_CruisCon | Zuul only: IND_BrdPod is granted (outcome; the caller researches it) | | CCC_SpyBm, IND_SlvgTech | flag CaptureDesigns once both are researched | | BIO_PLGVAC / RTPLGVAC / BSTVAC / ASPLGVAC / CONNAN | HasVac, HasImm |= bit 0 / 1 / 2 / 3 / 4; cure that plague type on owned systems and ships (outcome) | | BIO_UNIANTI | same with mask 0x0f | | DRV_RIP / REND / RAD | node-bore parameters {45,15,3} / {65,35,4} / {95,60,5}, highest wins (re-derived in the tail, and the block is absent when none is researched) | Confidence: **high** on every constant above (each was read with its literal) and, since B2, **high** on the node-bore rule (the selector tests RAD, then REND, then RIP, and the first hit wins) and on the AI-benefit re-application. The three AI-bonus values are now recovered as well: **0.5 each**, and `AiRebellionOdds` carries the same table's odds column (0.1 for the three AI techs, 0.2 for `CCC_AIFRCON`). Three of the table's entries are not applied by the completing tech's own branch -- the game does them in the tail of *every* completion, so `ApplyTechCompletion` does too: * the two design-option masks (`ComputeDesignOptionMasks`, now available keyed by id); * the node-bore parameters, re-derived from the whole researched set; * the capture-designs pair test, which fires on whichever completion first sees both `CCC_SpyBm` and `IND_SlvgTech` researched -- not only on those two techs' own. Every completion also rebuilds `speciesFlags[]` (bit k of species sp = the level-k xenotech for sp is researched), ORs a sticky `translationKnownMask` bit for every non-NPC species whose level-1 translation is researched, and reports, in the outcome, every species whose temperance bit is held so the caller can cure addiction to it on owned systems. ### Where the modifiers are consumed `OutMod` → `TotalSystemOutput` (game/sim); `PopMod` → `PopulationGrowthDelta`; `TerraMod` → `TerraformDelta`; `SuitTol` → `HazardModifier` **and** `SuitabilityCostMod`; `ConMod[i]` → per-hull-class construction cost (medium); `SavMod[i]` → maintenance side (low); `pddm` → planetary-defence damage (low); `MaxOH` / `MinRate` → mining sliders; `PrGtTrf` → gate traffic capacity; the flags → the gates listed in the RE catalog. `hazardMod` is 1 (skipped) when `speciesFlags[sp]` has the accommodate bit or the player is the rebel AI. ### Design-option masks Two 32-bit words of "named tech researched" bits feed the design side; `kDesignOptionNamesA` (32 names) and `kDesignOptionNamesB` (29 names) are the tables and `ComputeDesignOptionMasks(hasResearchedByName)` builds the words from a by-name predicate, because not every name's id is recovered. Consumers are not modelled. Confidence: high on the tables. ## Apply layer ``` PlayerEconomyState s; // seeded by the caller (species, SuitTol start, ...) ApplyContext ctx{&tuning, aiBonus}; TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_HrdStrct, ctx); ``` `ApplyTechEffect` marks the id researched, applies its effects, runs the tail, and returns what the caller must do with real game state: `grantedTech`, `plagueCuredMask`, `flagSystemsAI`, `reevaluateCivilianCaps`, `temperanceSpeciesMask`, `nodeBoreParamsChanged`. Applying an invalid or already-researched id is a no-op (`applied == false`). `ApplyTechCompletion` is the same thing **without** the already-researched guard: that is what the game's callback is, because by the time it runs the node is already marked researched. A differential hook must use it. `ApplyTechEffectByName` resolves a data-file name first and also handles the node-track names. `SetAiBenefit(s, on, ctx)` adds or withdraws every researched AI tech's bonus (AI rebellion / AI slave tech). `RebuildSpeciesTechFlags` is also the load path. ## Not modelled / open - Nothing is left open in the key space: all 196 names are in the table since B2. - The plague-cure test in the game matches a vaccine tech **or any descendant of it**; `PlagueCureMask` is exact-id only, because the descendant relation needs the tech tree. - The events raised on completion (research complete / under budget), the plague-cure roll, the Zuul starting immunity/temperance flags, and the home-system bonus initialisation that reads the `*_HOME` tuning keys. - Runtime gates that read ids at use sites (advanced-sensor contact rules, spy-beam intel, tunnel sensors, hyper-com retargeting, hull regeneration, missile/beam/cannon variants, plague-type maps): documented in the RE catalog, to be modelled where those systems are built.