19 KiB
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 byIsCombatTechName(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_TechIdNamestable at 0x009ff9e4 (stride 8:{const char* name, int}).TechId = 10000 + index(0xc5 = 197 = "none"). AtMasterTechTreeconstructionResolveTechIds(0x00581c10)_stricmps every name against the loaded.techlist and storesTechDef*[196]atMasterTechTree+0. Three helpers are the whole reader API:addr name (written back) signature 0x0057d5d0 MasterTechTree::IsTechbool (TechDef* def, int techId)—def == resolved[id-10000]0x0057d610 MasterTechTree::GetTechDefTechDef* (int techId)0x0057d810 TechTree::HasResearchedbool (int techId)— per-player node state == 40x00581cc0 (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 offlags[sp]=HasResearched(SpeciesDef(sp).xncTech[k])where the per-species XNC tech ids live in the hard-codedSpeciesDeftable (§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 misspelledTECHBEN_STRATRANGEcompare).
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 |
| 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 |
| — | 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.