# Hard-coded tech effects — Sword of the Stars (2006) `Sword of the Stars.exe` Ghidra 12 headless on project `sots` (ImageBase 0x00400000). Scripts `TechFx.java`..`TechFx4.java` (copies in `handoff/scripts/`), raw decompiles in `handoff/tech-decompiles/`. Renames/comments/labels listed in §7 were written back and saved. Companion: `formula-gaps.md`, `addresses-effects.json`. ## 0. Corrections to `strategic-turn-internals.md` §2.6 * The **116-entry table at 0x00a19718 is NOT the effects table.** It is a plain name list (`g_CombatTechNames`) used by `IsCombatTechName` (0x006965c0, one caller 0x006b3ed0): returns 1 if a string matches any entry. No index is derived. * The real key space is the **196-entry `g_TechIdNames` table at 0x009ff9e4** (stride 8: `{const char* name, int}`). `TechId = 10000 + index` (0xc5 = 197 = "none"). At `MasterTechTree` construction `ResolveTechIds` (0x00581c10) `_stricmp`s every name against the loaded `.tech` list and stores `TechDef*[196]` at `MasterTechTree+0`. Three helpers are the whole reader API: | addr | name (written back) | signature | |---|---|---| | 0x0057d5d0 | `MasterTechTree::IsTech` | `bool (TechDef* def, int techId)` — `def == resolved[id-10000]` | | 0x0057d610 | `MasterTechTree::GetTechDef` | `TechDef* (int techId)` | | 0x0057d810 | `TechTree::HasResearched` | `bool (int techId)` — per-player node state == 4 | | 0x00581cc0 | (unnamed) | `bool (const char* name, int techId)` — section tech-list variant | * The species flag words `ServerPlayer+0x348[7]` are **not hard-coded tech names**: `ServerPlayer::RebuildSpeciesTechFlags` (0x0082bf10) sets bit k of `flags[sp]` = `HasResearched(SpeciesDef(sp).xncTech[k])` where the per-species XNC tech ids live in the hard-coded `SpeciesDef` table (§4). So the bits are "xenotech level reached against species sp". * `strategy{TECHBEN_*}` remains dead (confirmed again: nothing reads the TechDef strategy vector except the one misspelled `TECHBEN_STRATRANGE` compare). Reimplementation rule: key on the **tech name → TechId enum** below, apply the callback table (§1) on research completion (and re-derive the flag words / bitmasks on load), then use the runtime gates in §2/§3. ## 1. `ServerPlayer::OnTechResearched(def, bool silent)` — 0x00891790 (vft slot 4, called by `TechTree::SetResearched`) Field names are the save-tag names from `struct-recovery.md` §2 (`ConMod[3]`@+0x10c, `SavMod[3]`@+0x118, `OutMod`@+0x124, `PopMod`@+0x130, `TerraMod`@+0x134, `SuitTol`@+0xb4, `MaxOH`@+0xb8, `MinRate`@+0x140, `pddm`@+0x108, `CstR/E/T`@+0x150, `PrGtTrf`@+0x148). All constants verified in the disassembly (doubles in `.rdata`). Effects are **additive per research event and permanent** (no un-research). Confidence HIGH unless marked. | id | tech | call site | effect | |---|---|---|---| | 10000 | CCC_AdvSens | 0x008919f9 | `hadvs = 1` → sensor range × `ADVSENS_SENSORS_MOD` (readers 0x0080b730 system sensors `SENSORMOD[sp] × mod × 4.0`, 0x0081e9d0, 0x008672c0). Also re-derived on load (0x00875fa0). | | 10001 | IND_Waldo | 0x00891a7f | `ConMod[0..2] −= 0.10`; `OutMod += 0.15` | | 10002 | IND_CyberInt | 0x00891a27 | `ConMod[0..2] −= 0.05`; `OutMod += 0.20` | | 10003 | IND_ExpSys | 0x00891a7f | same block as Waldo: `ConMod[0..2] −= 0.10`; `OutMod += 0.15` | | 10004 | IND_OrbFound | 0x00891ad7 | `SavMod[0..2] −= 0.05` | | 10005 | IND_OrbDry | 0x00891b1d | `ConMod[1] −= 0.05`; `ConMod[2] −= 0.05` (index 0 untouched → ConMod is per hull class DE/CR/DN, MEDIUM) | | 10006 | IND_GravCon | 0x00891b43 | `OutMod += 0.30` | | 10007 | IND_HvyPlat | 0x00891b6b | `OutMod += 0.10` | | 10008 | IND_AstMine | 0x00891b93 | `AMine = 1` (asteroid mining: `AdjustResources` mode 0, `MRes/ARes2` counted in output; §3.3 of turn notes) | | 10009 | IND_MsMine | 0x00891bb0 | `MaxOH = max(MaxOH, 0.1)`; `MinRate += 1.0` | | 10010 | BIO_GnMod | 0x00891c02 | `PopMod += 0.10` | | 10011 | BIO_AtmoAd | 0x00891c2a | `SuitTol += 0.75`; `PopMod += 0.06`; `TerraMod += 0.35` | | 10012 | BIO_EnvTail | 0x00891c76 | `PopMod += 0.20`; `TerraMod += 0.45` | | 10013 | BIO_GrvAdpt | 0x00891c9e | `SuitTol += 1.50`; `PopMod += 0.10`; `TerraMod += 0.35` | | 10014 | IND_ArcCon | 0x00891cee | `harcc = 1` (carrying cap +1e8 imperial / +2e8 civilian, 0x0080dd30); `PopMod += 0.15`; every owned system whose civilian total == civilian cap is re-evaluated (0x00756040/0x00753510/0x00751b60: morale/civ recalculation, LOW on exact purpose) | | 10015 | IND_EleNans | 0x00891e42 | `TerraMod += 0.60` | | 10016 | BIO_TerBac | 0x00891e6a | `TerraMod += 0.45` | | 10017 | IND_AtProc | 0x00891e92 | `TerraMod += 0.50` | | 10018 | DRV_TpGate | 0x00891eba | `PrGtTrf = max(PrGtTrf, g_PERGATETRAFFIC_DRV_TpGate)` (storage 0x00b23e2c via PTR 0x00aedfe0) — resolves the open "PERGATETRAFFIC readers" item | | 10019 | DRV_GatAmp | 0x00891ed3 | `PrGtTrf = max(PrGtTrf, g_PERGATETRAFFIC_DRV_GatAmp)` (0x00b23e30 via 0x00aedfe4) | | 10020 | DRV_FarCast | 0x00891f07 | `CstR = 10.0; CstE = 2.0; CstT = 1.0` (Morrigi far-casting; used by waypoint type 5 `ProbabilisticJump`) | | 10021/2/3 | CCC_AI / CCC_AIAdmin / CCC_AIFac | 0x00891f5f | if `AIBn` (AI benefit on): `ApplyAITechBonus(def)` (0x0080e330): `v = aiTable(def).value(+8) × (+1)`; CCC_AI → `ResMod += v`, CCC_AIAdmin → `IncMod(+0x30c) += v`, CCC_AIFac → `OutMod += v`. `SetAIBenefit(false)` (0x008186b0, AI rebellion) subtracts them again for every researched AI tech. The 6-entry value table is at **0x00a17888** (12-byte `{techId, ?, float value}`), values not dumped this round (MEDIUM). | | 10024 | CCC_AIVrus | 0x00891f8a | every owned system `+0x188 = 1`; if `AIRebellion` object exists → 0x00691030 | | 10025 | CCC_AISlv | 0x00891fe4 | every owned system `+0x188 = 1`; `SetAIBenefit(true)`; if AIR → 0x00691020. Also: `HasResearched(CCC_AISlv)` **removes the AI-rebellion research risk** (0x00820380: odds = aiTable(def).+4 only while !AISlv && !NPC && no AIR) and gates 0x006d1e30/0x0080dad0 (AI rebellion possible?). | | (any) | species "node-track" tech (`SpeciesDef+0x74`: CCC_NDTRKHUM for Human, CCC_NDTRKZUL for Zuul, none otherwise) | 0x0089202f | `NPTrk |= 1 << species` (0x0080e410). Readers 0x0080ddb0 `(NPTrk >> p.Species) & 1`, 0x0080dde0 `NPTrk != 0`, client 0x0080ba90/0x008102f0/0x0081bf50 → "can see that species' node-space traffic" (MEDIUM). | | 10026 | CCC_FtlEcon | 0x00892071 | if `!RebAI`: `CnTrd = 1` (trade allowed; `RegisterTradeSystems` 0x007adc80 only registers routes for owners with CnTrd; other readers 0x00819f00, 0x00833220, 0x0083a690) | | 10027 | CCC_ComRaid | 0x0089209b | `CnRad = 1` (commerce raiding permission; consumer not isolated — +0x100 offset collides with ServerSystem.owner, LOW) | | 10028 | DRV_GrvSyn | 0x008920b8 | `hgs = 1` — only synced to clients (0x0077b620, SynchronizePlayer); no server-side reader found. FLOCKDRIVE_GRAVSYN_MOD is applied from the design side (0x006fe0e0/0x006ff370). | | 10029 | CCC_DatCor | 0x008920d5 | `CnVItl = 1` — readers are client/UI (0x007f4670, 0x007f6d20, 0x007fc7f0, 0x007fed30): view intel on other empires (MEDIUM) | | 10030 | IND_HrdStrct | 0x008920ef | `pddm *= 0.25`; `OutMod *= 0.90` (multiplicative!) | | 10031 | DRN_AdvRob | 0x00892126 | `ConMod[0..2] −= 0.05` | | 10038..10043 | BIO_PLGVAC / RTPLGVAC / BSTVAC / ASPLGVAC / CONNAN / UNIANTI | 0x00892156 (tail) | `mask = GetPlagueCureMask(def)` (0x00537240: bit 0..4, UNIANTI = 0x0f; also matches descendants via 0x0057d220) → `HasVac |= mask; HasImm |= mask;` 0x0078c7e0 clears plague type k on all owned systems and ships ("PLAGUE: Player %d(%s) cured of %s"). | | — | every completion | tail | `(+0x190,+0x194) = ComputeTechBitmasks()` (0x004d7960; §3), `RebuildSpeciesTechFlags()`, 0x008182c0 → Zuul rip/rend/rad tunnelling params | | 10032 (+10033) | IND_CruisCon, Zuul only | 0x008921e1 | if `Species == 5`: `SetResearched(GetTechDef(IND_BrdPod), 1,1,0)` — Zuul get Boarding Pods free with Cruiser Construction | | 10034+10035 | CCC_SpyBm && IND_SlvgTech | 0x00892209 | `cdp = 1` (readers 0x00793d60, 0x007b8460, 0x00809fa0, 0x0086baa0, 0x00878060 — design-capture/observed-design feature, MEDIUM) | | flag bit 5 (temperance) | XNC_TEMP | 0x00892241 | for each species with `flags[sp] & 0x20`: every owned system addicted to sp is cured (0x00745e40/0x00743800) → `EVENT_TEMPERANCE` (unless silent) | | any | — | 0x00891804 | if `!silent`: event `EVENT_RESEARCH_COMPLETE` when `progress/cost ≥ 0.8` else `EVENT_RESEARCH_UNDERBUDGET`; if def == `ResTNm` → clear current research and `ResErrRoll` (running plague-cure roll 0x0088df20 once). | Where the multipliers are consumed (from `strategic-turn-internals.md`, now with producers known): `OutMod` in `ComputeOutputFromRates` total (×`p.OutMod`), `PopMod` in growth (`g *= PopMod`), `TerraMod` in terraform points, `SuitTol` in `HazardMod` (cap) **and** `CalcSuitMod` (money cost, see formula-gaps Q3/Q4), `ConMod[i]` = per-class construction cost multiplier (readers 0x00817f90 `ConstructionSpend`, 0x0079ac10, 0x00806ed0 …; MEDIUM), `SavMod[i]` readers 0x00745d60/0x00745dd0/0x00814c80 (maintenance side, LOW), `pddm` (planetary-defence damage multiplier? readers 0x0075a570/0x0075a6b0/0x0075bde0, LOW), `MaxOH` = max over-harvest slider, `MinRate` = mining rate, `PrGtTrf` = per-gate traffic capacity vs `GTraf`. ## 2. Other strategic readers (runtime gates by TechId) | tech | where read | effect | conf | |---|---|---|---| | CCC_AdvSens 10000 | 0x00746390, 0x0075bd70, 0x0075cb20 | a player in "partial contact" state (2-bit field, `+0x24`) counts as known/visible when AdvSens is researched | MED | | CCC_SpyBm 10034, CCC_SPJAM 10112, CCC_FTLBRDB 10108 | 0x00788ff0 (from encounter detection 0x007d7f70) | spy-beam intel on a battle: FTLBRDB → also through cease-fire partners; SpyBm → yes unless the observed side has SPJAM | MED | | CCC_TUNSENS 10113 | 0x00815ab0 | station type 2 (or flag 0x800) with TUNSENS → tunnel-sensor detection | MED | | CCC_HYPCOM 10093 | 0x00700920 | without HYPCOM a fleet cannot be re-targeted while in flight to a moving target (waypoint kind 1 → forced stop) | MED | | IND_TRKSTL 10094 | 0x0079b980 → 0x0079b770 (uses `TRKSTL_REGENERATION_MOD` 0x007870d0) | post-combat hull regeneration for non-NPC owner | HIGH | | DRV_RIP/REND/RAD 10064/65/66 | 0x006e18e0 (via 0x008182c0 after research) | ServerPlayer+0x308 = {45,15,3} / {65,35,4} / {95,60,5} (Zuul node-bore parameters, highest wins) | MED | | DRV_FISSN/FUSN/ANTIMAT (0x0057d350), hull 0x0056e4c0, warheads 0x0058fd20 (NUKES, NUKEWHD, GMAWHD, FUSWHD, AMWHD) | 0x0080e490 / 0x0080e4d0 / 0x0080e510 | "tech level" 0..2 / 0..4 for the AI/diplomacy evaluator 0x006d4780 | HIGH | | WEP_MwMsl 10036, WEP_HvyPmsl 10037 | 0x00535c40 | missile-type availability bits (combat) | HIGH | | BIO_PLG..NANVIR 10044-48 / vaccines 10038-43 | 0x00535400, 0x00535480, 0x00535ef0, 0x00535f80, 0x00699950 | plague-type ↔ tech maps for `ProcessPlague`, bio-weapon sections and AI research picking | HIGH | | CCC_TRNSHUM..LIR 10114-117 | 0x0079f050 | scenario/encounter grants N random translation-1 techs (`SetResearched`) | MED | | IND_STLTHARM 10050, SLD_INTANG 10071 | 0x0081fc30 (StarShip flags), 0x0080b820 | all sections STLTHARM → ship flag 0x100000 (stealth); INTANG clears flag 0x80 | MED | | DRV_RECFISS 10072 / DRV_PLSMFOC 10051 | 0x0080b850, 0x0080ba30 (+3.0), 0x0081ede0 (+3.0 per section) | +3.0 to a design/ship stat (drive-family bonus; combat side) | LOW | | IND_CyberInt 10002, DRN_AdvRob 10031, DRN_CMBT 10049 | 0x00586600 (tree build, `mode`) | special-cased during per-species tree construction (scenario "all techs" path) | LOW | | CCC_ARMCOM/DATSYN/BTLCMP 10074-76 | 0x00695be0, 0x006964a0 | combat command-section level 0/1/2 | HIGH | | WEP_*LAS 10077/78/82, WEP_*CAN 10079-81 | 0x006907d0, 0x006ad5e0 | combat beam/cannon variants | HIGH | ## 3. Tech bitmasks (non-serialised `ServerPlayer+0x190/+0x194`) `ComputeTechBitmasks` (0x004d7960): `+0x190` = OR over `g_TechBitmaskTableA` (0x00adf378, 32 pairs) and `+0x194` over `g_TechBitmaskTableB` (0x00adf478, 29 pairs). Table A bits 0..31: IND_REFCOAT, IND_IMPRFCT, IND_PLYALLOY, IND_MAGLAT, IND_QRKRES, IND_ADMALY, IND_PREDGUN, SLD_DEF, SLD_ERGAB, SLD_MKONE, SLD_MKTWO, SLD_MKTHREE, SLD_MKFOUR, SLD_CLK, SLD_IMPCLK, SLD_INTANG, WEP_VRFTECH, WEP_NUKEWHD, WEP_GMAWHD, WEP_FUSWHD, WEP_AMWHD, WEP_NEUTRND, CCC_INTSENS, CCC_SNSJAM, CCC_QNTCHAF, CCC_CMBTALG, CCC_HOLOTAC, CCC_ADVCNC, CCC_AdvSens, DRV_MCROFUS, DRV_INCTHRST, DRV_SMLFUS. Table B bits 0..28: DRV_NODE, NODFOC, NODPATH, STRWRP, IMPSTWRP, FLICKER, HYPER, DRV_HYPRFLD, WARP, SLD_MESSHLD, SLD_GRVSHLD, CCC_AIFRCON, DRV_RIP, REND, RAD, BIO_CONNAN, SLD_DISR, SLD_MAGNI, DRV_QNTCAP, CCC_FCCOM, IND_HRDELEC, IND_TRKSTL, DRV_VDCTR, VDCRV, VDMSTR, BIO_SMRTNAN, WEP_ACCAMP, WEP_MwMsl, WEP_HvyPmsl. These are the design/section "option available" masks (consumers on the design side, not traced). ## 4. `SpeciesDef` table (0x00b10a00, 7 × 0x184, `SpeciesDef::InitTable` 0x005453a0) — hard-coded species constants | off | Human | Hiver | Tarkas | Liir | _NPC | Zuul | Morrigi | used by | |---|---|---|---|---|---|---|---|---| | +0x04 drive | 3 | 0 | 1 | 2 | 0 | 3 | 4 | | | +0x08 | 0.07 | 0.07 | 0.07 | 0.15 | 0.07 | 0.20 | 0.07 | (suit-related, unresolved) | | +0x0c | 1 | 0.85 | 0.9 | 1.5 | 1 | 0.55 | 1 | | | +0x10 | 1 | 1.15 | 1.1 | 0.9 | 1 | 1 | 1 | | | +0x14 | 1 | 1.2 | 1.1 | 0.8 | 1 | 1.7 | 0.85 | | | +0x18 income factor | 1 | 1 | 1 | 1 | 1 | 1.1 | 0.8 | `TradePointsToMoney` | | +0x1c | 1 | 1 | 1 | 1 | 1 | 1 | 1.3 | | | +0x20 | 1 | 1 | 1 | 1 | 1 | 1 | 2.0 | | | +0x24 cost factor | 1 | 1 | 1 | 1 | 1 | 0.7 | 1 | `TradePointsToMoney` hazard cost | | +0x28 | 0.9 | 0.75 | 1 | 1.2 | 1 | 0.2 | 0.5 | | | +0x2c | 0.75 | 0.8 | 0.7 | 0.1 | 1 | 1 | 0.25 | | | +0x30 | 0 | 0.5 | 0.25 | 0 | 0 | 0.75 | −0.5 | | | +0x34 | 1 | 1 | 0.85 | 0.95 | 1 | 1 | 1 | | | +0x38 | 1 | 1 | 1 | 1 | 1 | 0.5 | 1 | | | +0x4c | 10 | 10 | 10 | 10 | 10 | 0 | 10 | BaseOutput cap (turn notes §3.3) | | +0x50 | 10 | 10 | 10 | 10 | 10 | 40 | 10 | | | +0x54 | 1 | 1 | 1 | 1 | 1 | 2 | 1 | | | +0x58 | SENSORMOD_ from species.txt (NPC 1.0) | | | | | | | sensor range | | +0x5c | 1 | 1 | 1 | 1 | 1 | **0** | 1 | system bonus eligible (Zuul never get SYSTEMBONUS) | | +0x74 node-track tech | CCC_NDTRKHUM | none | none | none | none | CCC_NDTRKZUL | none | `NPTrk` | | +0x78.. XNC ids (order of the 9 fillers) | TRNS1 10114+sp, TRNS2 10120+, TRNS3 10126+, INC 10132+ (Zuul/NPC none), ADCT 10137+, TEMP 10142+, SUB 10147+, ACC 10153+, PROF 10158+ | | | | | | | flag bits 0..8 (§1 "bit0/1/2 slave death" = translation levels?, see note) | | +0x144 flags | 1 | 0 | 0 | 0 | 0 | 2 | 0 | | | +0x148 | 0 | 0 | 0 | 0 | 0 | plague-cure mask \| 0x20 | 0 | Zuul start immune/temperate | Note on the flag-bit semantics used by the colony code (turn notes §3): bits are in filler order, i.e. bit0/1/2 = translation 1/2/3, bit3 = incorporate, bit4 = addict, bit5 = temperance, bit6 = subjugate, bit7 = accommodate, bit8 = proliferate. That reassigns the earlier guesses ("bit3 civilian growth", "bit7 hazard immunity", "bit8 may seed civilians") to Incorporate / Accommodate / Proliferate respectively — consistent with the XNC tech names. The exact slot order was inferred from the call order in `InitTable` (fillers write `*in_EAX` successive dwords); HIGH on order, MEDIUM on the +0x78 base (the reader 0x0082bf10 starts at +0x78 and steps 4). ## 5. Techs with NO code effect (pure prerequisites / data-only) Of the 196 TechIds, **no code reads** ids: 95 IND_SPNLMNT, 96 WEP_HCLAS, 97 WEP_PRTBM, 98 WEP_DSRPTR, 100-107 (NUKMINE, FUSMINE, DFMSL, GSDRVR, MASDRVR, HVYDRVR, VRFTECH*, PDTECH), 110 CCC_INTSENS*, 171 IND_QRKRES*, 173-175 SLD_MKONE-THREE*, 177 WEP_NEUTRND*, 179-192 (CCC_ADVCNC*, DRV_MCROFUS*, INCTHRST*, SMLFUS*, HYPRFLD*, SLD_MESSHLD*, GRVSHLD*, DISR*, MAGNI*, DRV_QNTCAP*, CCC_FCCOM*, IND_HRDELEC*, BIO_SMRTNAN*, WEP_ACCAMP*). `*` = present only in a bitmask table (§3). Everything not in the enum at all (≈ half of the `.tech` files: most WEP_*, SLD_*, DRV_* section techs) has no code effect beyond `requires`/section availability driven by the data files. Count: **36 ids with a strategic effect (§1), ~45 with combat/design/AI gates (§2/§3), ~115 pure prerequisites.** ## 6. std::string layout (coordinator request) `Mars::Stream::WriteString` (0x008b9d70): `CMP [str+0x14], 0x10; JC inline; MOV EAX,[str]` — i.e. `_Myres` at **+0x14**, buffer pointer / SSO buffer `_Bx` at **+0**. `basic_string::assign` (0x00425550) uses `param_1[4]` = `_Mysize` at **+0x10** and `param_1[5]` = `_Myres`. Vector-of-string strides are **0x1c** (`FUN_00699bd0` `pbVar5 += 0x1c`, `MasterTechTree` ctor `local_34 += 7` dwords), so `sizeof == 28` with the allocator/proxy dword at **+0x18**. **struct-recovery.md §0 is right (`_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18`); turn-spine.md §1.1 is wrong.** ## 7. Write-back (saved) Functions renamed + plate comments: `ServerPlayer::OnTechResearched` 0x00891790, `MasterTechTree::IsTech/GetTechDef/ ResolveTechIds` 0x0057d5d0/0x0057d610/0x00581c10, `TechTree::HasResearched` 0x0057d810, `IsCombatTechName` 0x006965c0, `ServerPlayer::RebuildSpeciesTechFlags` 0x0082bf10, `ServerSystem::HazardMod` 0x00747ae0, `ServerPlayer:: UpdateBankruptcyLimits/SetBankruptcyState/BankruptcyLevel` 0x00818600/0x0080e260/0x0080db10, `ServerPlayer:: ApplyAITechBonus/SetAIBenefit` 0x0080e330/0x008186b0, `TechDef::GetPlagueCureMask` 0x00537240, `ServerPlayer:: SpeciesOfTranslationTech` 0x0080e410, `ComputeTechBitmasks` 0x004d7960, `ServerSystem::TradePointsToMoney/CalcSuitMod/ AccrueSystemBonus/SystemBonusPopTarget` 0x007505b0/0x007484d0/0x0074d4f0/0x0074b5a0, `NodeLine::Step/ BuildStutterSegments` 0x00705510/0x00705280, `DistPointToSegment` 0x008e8eb0, `SpeciesDef::InitTable/Get` 0x005453a0/0x00545cc0, `ServerPlayer::GetIncMod/GetSpeciesCostFactor` 0x0080dd10/0x0080dd20. Labels: `g_TechIdNames` 0x009ff9e4, `g_CombatTechNames` 0x00a19718, `g_SpeciesDefTable` 0x00b10a00, `g_TechBitmaskTableA/B` 0x00adf378/0x00adf478. ## 8. Method / confidence Pass 1: vtable 0x00a327a4 slot 4 → callback; decompile + annotated disassembly (decompiler drops the pushed TechId, so every branch was mapped from `PUSH imm; CALL IsTech`). Pass 2: program-wide scan of scalar operands 10000..10196 → every reader of every id (`tech-decompiles/techfx2/techids.txt`). Pass 3: species table, bitmask tables, money-tail helpers. Pass 4: producers + write-back. HIGH = read from disassembly with constants; MEDIUM = decompile-level with an inferred field meaning; LOW = consumer identified but purpose inferred from context only. Not done: values of the 6-entry AI tech table (0x00a17888), the `+0x224/+0x228/+0x22c` player multipliers' producer (0x0077b620 copies them from a 0x50-byte per-player options record `+0x48/+0x4c/+0x50` — likely the game-setup difficulty/handicap block, MEDIUM), and the design-side consumers of the §3 masks. --- ## 9. B2 corrections (2026-09-08) — read from the instruction stream, ReVa + `objdump -b binary -m i386` Nine things this note had wrong, missing, or at lower confidence than the binary supports. Where the two disagree, the binary wins. 1. **Every modifier is a float32 and every constant is a widened float literal.** The shape is `fld dword [field]; fadd/fmul qword [k]; fstp dword [field]`. The doubles at the `.rdata` addresses listed in §1 are *float32 values widened to double*, not the decimals: 0x009e5060 = 0x3FA99999A0000000 = `(double)0.05f`, 0x009e2318 = `(double)0.1f`, 0x009ed188 = `(double)0.15f`, 0x009e20d8 = `(double)0.2f`, 0x009e5df0 = `(double)0.3f`, 0x009f15b0 = `(double)0.35f`, 0x00a33358 = `(double)0.45f`, 0x009e5068 = `(double)0.6f`, 0x00a33360 = `(double)0.06f`, 0x009e23a0 = `(double)0.9f`. Exact: 0x009e5d70 = 0.25, 0x009e20a0 = 0.5, 0x009e8d28 = 0.75, 0x009e90b8 = 1.5, 0x009e1ef0 = 1.0. `MaxOH`'s 0x009e22f0 is a **float32** 0.1f loaded with `fld dword` and passed to `std::max`. `DRV_FarCast` writes float32 10.0 (0x00a2bed0), 2.0 (0x009e5840) and `FLD1`. A reimplementation must round through float32 at *every* step. 2. **`g_AITechValueTable` (0x00a17888) is dumped.** Six 12-byte rows `{int techId, float odds, float value}`, count at 0x00aea2ec = 6: `{10021,0.1,0.5} {10022,0.1,0.5} {10023,0.1,0.5} {10083,0.2,0.0} {10024,0,0} {10025,0,0}`. So the CCC_AI / AIAdmin / AIFac bonus is **0.5** into ResMod / IncMod(+0x30c) / OutMod, and the AI-rebellion odds column is 0.1 for those three and 0.2 for **CCC_AIFRCON (10083)** — which is in the table for its odds only; `ApplyAITechBonus` has no branch for it. `ApplyAITechBonus` multiplies the value by `AIBn ? 1.0f : (float)[0x009e1fac] = -1.0f` and narrows the product to float32 before adding. 3. **`PrGtTrf` (+0x148) is an int.** `mov ecx,[PTR]; mov edx,[esi+0x148]; cmp edx,[ecx]; jl` — a signed integer max against an integer config word, not a float compare. §1's "PrGtTrf = max(...)" is right, the type was not stated. 4. **`ServerPlayer+0x308` is a pointer, not three inline words.** `ServerPlayer:: UpdateNodeBoreParams` (0x008182c0, called unconditionally in the tail) builds a local 3-word struct via `SelectNodeBoreParams` (0x006e18e0); on success it `operator new(0xc)`s the block if +0x308 is null and copies the three words in; on failure it `operator delete`s the block and nulls +0x308. `SelectNodeBoreParams(out, player)` tests **RAD (0x2752) first, then REND (0x2751), then RIP (0x2750)**, first hit wins — "highest wins" confirmed, HIGH not MEDIUM — returning {95,60,5} / {65,35,4} / {45,15,3}, and false with out = {INT_MAX, INT_MAX, 0} when none. **There is no species check**: these are not Zuul-gated in code. 5. **`RebuildSpeciesTechFlags` (0x0082bf10) has an undocumented second pass.** After filling `flags[7]` at +0x348 (and writing the count word 7 at +0x364 — the 0x20 bytes are assigned as a unit), it loops species 0..6 and, for each `sp != 4` (_NPC) whose `flags[sp] & 1` (translation level 1) is set, ORs `1 << sp` into **`ServerPlayer+0x1a4`**. Sticky: only ever ORed, never cleared, so it survives the first pass clearing a flag. Not previously recorded. 6. **Three tail steps are not effects of their tech.** `cdp` is a tail check — `if (!cdp) { if HasResearched(10034) && HasResearched(10035) then cdp = 1 }` — evaluated on every completion, not only on those two techs'. So are the +0x190/+0x194 masks and the node-bore selection. §1's table reads as if they belong to the completing tech. 7. **The callback has no already-researched guard**, because `SetResearched` sets node state 4 before invoking it. 8. **`*(int*)TechDef` is NOT the TechId.** `MasterTechTree::IsTech(def, id)` (0x0057d5d0) does `if (id == 0xc5) fail; idx = id - 10000; if ((unsigned)idx > 0xc3) fail; return def && master[0][idx] && def == master[0][idx]`. `TechTree::HasResearched(id)` (0x0057d810) does the same map through `this+4` (the MasterTechTree) to reach a `TechDef`, and *then* uses that def's first word as the index into `this+0x10` (the node vector), testing `node->state == 4`. So the def's first word is a node index in a larger key space, the TechId key space is exactly 10000..10195, and 197 is the "none" sentinel special-cased before the subtraction. `TechTree+4` = `MasterTechTree*` is worth adding to the object map. 9. **`g_TechIdNames` (0x009ff9e4) is dumped in full.** 196 × `{const char* name, int}`; the second word is `10001 + index` and is not the id (the id is `10000 + index`, per `IsTech`). The names, in position order, are now in `sots-engine`'s `src/game/effects/tech_id.h`. Anchors all agree: 0 CCC_AdvSens (0x2710), 30 IND_HrdStrct (0x272e), 38..43 the six vaccines (0x2736..0x273b), 64..66 DRV_RIP/REND/RAD (0x2750..0x2752). Consequences for §4's filler-order note: the block species order is **Human, Hiver, Tarkas, Liir, [Zuul,] Morrigi**; the families that omit the Zuul are Incorporate (132-136), Addict (137-141), Temperance (142-146), Accommodate (153-157) **and Proliferate (158-162)** — proliferate is five entries, not six. Index 130, the Zuul slot of the level-3 translation block, is `XNC_DOMZUUL`. **Ids 163 and 164 are `CCC_NDTRKHUM` and `CCC_NDTRKZUL`**, the node-track techs of §1 — previously unlocated. §5's "no code reads them" list can now be stated by name rather than by index. 10. **`g_TechBitmaskTableA/B` are `{int techId, uint bit}`.** Joined with §3's bit-ordered name lists every id matches the name table above, so §3's two lists are confirmed and are keyed by id, not by name: A = 10092, 10170, 10091, 10090, 10171, 10169, 10084, 10111, 10172, 10173-10176, 10088, 10073, 10071, 10106, 10166, 10167, 10099, 10168, 10177, 10110, 10109, 10178, 10085, 10086, 10179, 10000, 10180, 10181, 10182; B = 10056, 10057, 10058, 10061, 10062, 10063, 10059, 10183, 10060, 10184, 10185, 10083, 10064, 10065, 10066, 10042, 10186, 10187, 10188, 10189, 10190, 10094, 10067, 10068, 10069, 10191, 10192, 10036, 10037. Also confirmed unchanged: the chain is a strict if/else-if over `push 0x2710`..`push 0x272f` (10000..10031) with no gaps and at most one branch taken; the tail then tests 0x2730/0x2731 (CruisCon → GetTechDef(BrdPod) → SetResearched(def,1,1,0), gated on `Species == 5`) and 0x2732/0x2733 via HasResearched. `TechDef::GetPlagueCureMask` loops i = 0..5 over ids 10038..10042 with a NULL at i == 5, returning `1 << i` on the first hit, then falls through to 10043 returning 0x0f — and the per-id test is `0x0057d220(def, target)`, i.e. **def is the target or a descendant of it**, not an exact match. Every magnitude in §1 is confirmed.