sots-re/findings/subsystems/tech-effects.md

275 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<sp> | 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_<sp> 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<float>`. `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.