From de7a2bef04fbb92c8af75c72db79b4af0db55e1f Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 7 Sep 2026 22:05:49 -0400 Subject: [PATCH] tech effects (196-entry TechId table, OnTechResearched), 8 formula gaps answered, std::string settled --- campaign/DASHBOARD.md | 22 +-- campaign/board.md | 2 + campaign/open-questions.md | 1 + findings/control-flow/turn-spine.md | 2 + findings/subsystems/formula-gaps.md | 125 ++++++++++++ findings/subsystems/tech-effects.md | 192 +++++++++++++++++++ ghidra/addresses.json | 286 +++++++++++++++++++++++++++- ghidra/generated/sots_addresses.h | 78 +++++++- ghidra/scripts/TechFx.java | 117 ++++++++++++ ghidra/scripts/TechFx2.java | 106 +++++++++++ ghidra/scripts/TechFx3.java | 99 ++++++++++ ghidra/scripts/TechFx4.java | 140 ++++++++++++++ 12 files changed, 1147 insertions(+), 23 deletions(-) create mode 100644 findings/subsystems/formula-gaps.md create mode 100644 findings/subsystems/tech-effects.md create mode 100644 ghidra/scripts/TechFx.java create mode 100644 ghidra/scripts/TechFx2.java create mode 100644 ghidra/scripts/TechFx3.java create mode 100644 ghidra/scripts/TechFx4.java diff --git a/campaign/DASHBOARD.md b/campaign/DASHBOARD.md index 3133353..ab69d51 100644 --- a/campaign/DASHBOARD.md +++ b/campaign/DASHBOARD.md @@ -1,17 +1,17 @@ # SotS RE campaign — coverage dashboard -Generated 2026-09-08 02:01 UTC · `sots-re` @ 31153cd,2026-09-07 · `sots-engine` @ 4f0a9db,2026-09-07 (29 commits) · regenerate with `tools/dashboard.py` +Generated 2026-09-08 02:05 UTC · `sots-re` @ b24f13c,2026-09-07 · `sots-engine` @ 4f0a9db,2026-09-07 (29 commits) · regenerate with `tools/dashboard.py` > **North star:** A functional reimplementation of the engine — behavior-equivalent, NOT byte-for-byte ## 1. Map coverage (campaign/board.md) -45 targets · mapped-or-better **36/45** `[████████░░] 80%` · verified **20/45** `[████░░░░░░] 44%` +47 targets · mapped-or-better **38/47** `[████████░░] 81%` · verified **21/47** `[████░░░░░░] 45%` | Status | Count | % | |---|---:|---:| -| verified | 20 | 44% | -| mapped | 16 | 36% | +| verified | 21 | 45% | +| mapped | 17 | 36% | | in-progress | 2 | 4% | | backlog | 6 | 13% | | blocked | 1 | 2% | @@ -20,7 +20,7 @@ Generated 2026-09-08 02:01 UTC · `sots-re` @ 31153cd,2026-09-07 · `sots-engine |---|---:|---:|---:|---:|---:|---:| | objects | 5 | 2 | 0 | 0 | 0 | 7 | | control-flow | 0 | 2 | 0 | 0 | 0 | 2 | -| subsystems | 1 | 5 | 1 | 3 | 1 | 11 | +| subsystems | 2 | 6 | 1 | 3 | 1 | 13 | | engine | 7 | 1 | 0 | 0 | 0 | 8 | | verify | 4 | 2 | 0 | 0 | 0 | 6 | | phase2 | 1 | 0 | 1 | 3 | 0 | 5 | @@ -30,7 +30,7 @@ Generated 2026-09-08 02:01 UTC · `sots-re` @ 31153cd,2026-09-07 · `sots-engine - RTTI type descriptors: **1,924** (`Game::` 1,404, `Mars::` 194; serializable types 179) - Classes with recovered member layouts: **21** / 179 serializable types `[█░░░░░░░░░] 12%` — heuristic: distinct `Game::X`/`Mars::X` in `##`–`####` headings of `struct-recovery.md` + `schema-gaps-resolved.md` -- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **103**, verified **91** `[█████████░] 88%` +- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **137**, verified **123** `[█████████░] 90%` ## 3. Data layer @@ -78,7 +78,7 @@ Board `engine:` rows: verified **7**, mapped 1, in flight 0 (of 8) — verified ## 7. Open questions -Open **24** · resolved/parked 8 · backlog items: Now 4, Next 3, Later 2, Breadth queue 7, Parked 1, From the RE how-to 3 +Open **24** · resolved/parked 9 · backlog items: Now 4, Next 3, Later 2, Breadth queue 7, Parked 1, From the RE how-to 3 Most recent open: @@ -90,10 +90,10 @@ Most recent open: ## 8. Delta since previous dashboard -- verified targets: 19 → 20 (+1) · mapped-or-better: 35 → 36 (+1) -- engine LOC: 13,535 → 13,702 (+167) · test files: 64 → 64 (+0) · checks: 1,794 → 1,923 (+129) -- addresses verified: 91 → 91 (+0) · recovered layouts: 21 → 21 (+0) · open questions: 24 → 24 (+0) +- verified targets: 20 → 21 (+1) · mapped-or-better: 36 → 38 (+2) +- engine LOC: 13,702 → 13,702 (+0) · test files: 64 → 64 (+0) · checks: 1,923 → 1,923 (+0) +- addresses verified: 91 → 123 (+32) · recovered layouts: 21 → 21 (+0) · open questions: 24 → 24 (+0) --- warnings: mars-rng.md: no oracle total row parsed; mars-stream.md: no oracle total row parsed; mars-vfs.md: no oracle total row parsed - + diff --git a/campaign/board.md b/campaign/board.md index 752ea75..be8eaca 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -50,3 +50,5 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | engine: shim trace/compare emitter | engine | verified | high | 100% | 2026-09-07 | merged (sots-engine 9e110b4): emitter byte-exact vs mkfixture, tracer, snapshot/diff, Hook; ctest 18/18; tracecmp exits 0/1/2 as specified; shim links | | engine parity: first-wins keys + tokenizer rules | engine | verified | high | 100% | 2026-09-07 | merged: Mars::Script tokenizer rules, first-wins keys, trailing-pair drop in mars/parse+mars/text AND Python oracle; oracles 1531/1531, 64/64; only real-data effects: 2 dropped trailing pairs (engine uses defaults), systemnames.txt nesting | | save-format tag corrections | verify | verified | high | 100% | 2026-09-07 | byte-confirmed at offsets (otnF x62, nextid, FAIDes/DHide/DWep/DName x43, ords, wpts, paths); reader A()-matches them; 36 tests; strict 3/3, infos 259->197; SAVE_FORMAT §10 changelog | +| tech effects (code-defined) | subsystem | mapped | high | 90% | 2026-09-07 | findings/subsystems/tech-effects.md: g_TechIdNames 196 @0x009ff9e4 (TechId=10000+i), effects in ServerPlayer::OnTechResearched 0x00891790; SpeciesDef table 0x00b10a00; 36 strategic / ~45 gates / ~115 none. Feeds game/sim + game/data | +| strategic formula gaps | subsystem | verified | high | 100% | 2026-09-07 | findings/subsystems/formula-gaps.md: all 8 answered with code (bankruptcy 3.3 on BnkPr, linear hazard, money tail, pbon, slider, [6] net, no speed clamp, decay hits current) | diff --git a/campaign/open-questions.md b/campaign/open-questions.md index a814f44..be08708 100644 --- a/campaign/open-questions.md +++ b/campaign/open-questions.md @@ -39,3 +39,4 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re- - **RESOLVED: tech `allows` default** — unlisted species = 1.0 (confirmed in `FUN_005822d0` tree-creation roll, strategic-turn-internals §2); `game/data` uses 100. Still open from game/data: what the engine's converter does with the 34 malformed tokens (`force_right o`, `crew false`, `1.0f`, `0-5`, ``90\``); repeated scalars in a block are last-wins per the sequential if/else consumers (loader-prototypes §M3) — confirm on a fixture in compare mode. (from [[game-data]]) - **RESOLVED: SAVE_FORMAT tag names** — all corrections confirmed by bytes and applied to the Python reader + spec (§10). Residual doc debt: `findings/objects/save-editor-structs.md` and `verify/design-rules/SHIP_DESIGN_RULES.md` still quote R1's `ontF/faiDes/nextId` spellings (R1-provenance; annotate rather than rewrite). (from [[SAVE_FORMAT]]) - **Notes disagree on MSVC-2010 `std::string` layout** — `struct-recovery.md` §0 vs `turn-spine.md` §1.1 give different offsets; pin both from the `_Myres >= 16` SSO branch in `Stream::WriteString` and correct the loser. (from [[re-windows-2000s-howto]]) +- **RESOLVED: formula gaps (all 8)** — see [[formula-gaps]]; game/sim's low-confidence functions can now be pinned. **RESOLVED: std::string** = `_Bx@0,_Mysize@0x10,_Myres@0x14,_Alval@0x18`, sizeof 0x1c (struct-recovery §0 right; turn-spine §1.1 WRONG — annotated). **CORRECTION:** the 116-entry table @0x00a19718 is a name-membership list, not the effects table; use `g_TechIdNames` (196) + `OnTechResearched`. Open: values of the 6-entry AI-tech bonus table (0x00a17888); producer of ServerPlayer +0x224/+0x228/+0x22c beyond the setup-record copy (0x0077b620). (from [[tech-effects]]) diff --git a/findings/control-flow/turn-spine.md b/findings/control-flow/turn-spine.md index 201d954..6f47d28 100644 --- a/findings/control-flow/turn-spine.md +++ b/findings/control-flow/turn-spine.md @@ -12,6 +12,8 @@ Inputs: `findings/objects/struct-recovery.md` (member tables), `findings/objects --- +> **std::string layout note (2026-09-07):** §1.1's offsets are wrong; the verified MSVC-2010 layout is `_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18`, sizeof 0x1c (from `Mars::Stream::WriteString` 0x008b9d70). See findings/subsystems/tech-effects.md. + ## 1. Type write-back ### 1.1 Where the types live (important for anyone continuing in Ghidra) diff --git a/findings/subsystems/formula-gaps.md b/findings/subsystems/formula-gaps.md new file mode 100644 index 0000000..3fb0cdb --- /dev/null +++ b/findings/subsystems/formula-gaps.md @@ -0,0 +1,125 @@ +# Formula gaps from the game/sim port — answers from the binary + +Evidence: annotated disassembly/decompiles in `handoff/tech-decompiles/` (files named by address). `ftol` = `_ftol2` +(0x00925220, truncation). Field names per `struct-recovery.md`. + +## Q1. Which bankruptcy limit carries the 3.3 factor; when is the start turn stamped? + +`ServerPlayer::UpdateBankruptcyLimits` 0x00818600 (disasm 0x0081863e–0x0081869e): +``` +maxIncome = Σ_owned systems ComputeOutputMax(s)[3] // FUN_007521c0, int +BnkEl = ftol(maxIncome / -0.15) // FILD; FLD -0.15 (0x00a2ec30); FDIVR → -6.667 × maxIncome +if (BnkEl < -2,000,000,000) BnkEl = -2,000,000,000 +BnkPr = -ftol(maxIncome × BANKRUPTCY_PROTECTION_LIMIT_FACTOR) // FMUL [g_BANKRUPTCY_PROTECTION_LIMIT_FACTOR] (3.3) +if (BnkPr < BnkEl) BnkPr = BnkEl // i.e. BnkPr = max(-3.3·maxIncome, BnkEl) +``` +So the **3.3 factor is on the protection limit `BnkPr`**; the elimination limit `BnkEl` is `maxIncome / −0.15` +(the debt at which 15 %/turn interest equals maximum income). `BankruptcyLevel` (0x0080db10): 2 if `Sav < BnkEl`, 1 if +`Sav < BnkPr`, else 0. + +Stamping: `ProcessBankruptcy` 0x007c0a50 per non-eliminated non-NPC player: `old = (BnkWrn, BnkTrn)`; `level = +BankruptcyLevel()`; `SetBankruptcyState(level)` (0x0080e260) = `if (level != BnkWrn) { BnkWrn = level; BnkTrn = level ? +ModCount : -1 }`; then decisions use **`old`**: cost-cutting (0x00889500) if `level != 0 && old.BnkWrn != 0`; elimination +if `old.BnkWrn == 2 && ModCount − old.BnkTrn ≥ BANKRUPTCY_ELIMINATION_TURNS`. Net: the stamp is written on the turn the +level changes (any change, 1↔2 included, resets it), and actions start the following turn. +Call sequence: `OnAllCombatDone_Tail` → … → `ProcessBankruptcy` → (later in the tail) `UpdateBankruptcyLimits`, so the +limits used by a turn's bankruptcy check are the ones computed at the end of the previous turn (and on load). + +## Q2 (order as asked: 3). Suitability → carrying-capacity hazard curve + +`HazardMod` 0x00747ae0: `clamp01(1 − |Suit − IdealSuit| / (SuitTol + 0.1))` (0x00a1a438 = 0.1). Linear, no exponent. +`SuitTol` starts at the species value and is raised by BIO_AtmoAd (+0.75) and BIO_GrvAdpt (+1.5) (tech-effects §1). +Skipped (=1.0) when species flag bit 7 (accommodate xenotech) or RebAI. + +## Q3. Trade-points → money system-income tail + +`ServerSystem::TradePointsToMoney(trade)` 0x007505b0 (disasm complete): +``` +t = (trade − fmod(trade, 5.0)) × 5.0 // whole 5-point blocks, ×5 +t += PopIncome(0) // 0x0074d760(sys, 0): Σ_species ftol(GroupIncome(0, pop_imperial(sp))) (0x00535e80 group-type table row 0) +t += PopIncome(1) // civilians (row 1); own species adds (cap − pop) surplus term when at cap +t += SlaveIncome() // 0x0074b700: Σ_species ftol(GroupIncome(2, slaves(sp))) (row 2, SLAVES_INCOME_MOD) +t *= SpeciesDef(owner).incomeFactor (+0x18: Zuul 1.1, Morrigi 0.8, else 1) [1 if unowned] +t *= owner.IncMod (+0x30c) [1 if unowned] +t *= srv.+0xbc × DifficultyMods(owner)->+4 // 0x0080f470: server income modifier × AI trade/income difficulty mult +cost = SpeciesDef(owner).costFactor (+0x24: Zuul 0.7) × CalcSuitMod × 10000 × 1.5 + CalcSuitMod (0x007484d0) = min(|IdealSuit(species) − Suit|, owner.SuitTol) (0 if RebAI; 20 if no owner) +money = ftol(t − cost) +``` +Note `SuitTol` therefore caps the hazard **cost** as well as extending the habitable range. `ADDICTION_INCOME_MOD` is +applied inside PopIncome (0x0074d760 → 0x00746910 morale/addiction factor) — not verified line by line (MEDIUM). + +## Q4. `POPBONUS_INC` population increment + +`ServerSystem::AccrueSystemBonus` 0x0074d4f0 (disasm 0x0074d53b–0x0074d5be): +``` +if owner && IsStable && ModCount − TAcq > SYSTEMBONUS_MINTURNS && ntdev > SYSTEMBONUS_MINTURNS: + cap = MaxPop(imperial) // 0x0074ab20(0,0) + target = SystemBonusPopTarget(POPBONUS) // 0x0074b5a0: ftol(max(POPBONUS,0) × cap), 0 for species with SpeciesDef+0x5c == 0 (Zuul) + inc = ftol(POPBONUS_INC × cap) // FIMUL: 0.005 × cap + pbon += min(max(inc, 0), max(target − pbon, 0)) + ibon += min(max(INFRABONUS_INC, 0), max(INFRABONUS − ibon, 0)) // INFRABONUS target 0 for Zuul +``` +(`POPBONUS_HOME`/`INFRABONUS_HOME` are only used when the bonus is (re)initialised for a home system, 0x0074c680/ +0x007477a0; the per-turn accrual always uses the non-HOME keys.) Applied next turn by `ApplyPopBonus` (`Pop += min(pbon, +cap − Pop)`). + +## Q5. Expense-slider request term + +`ComputeBudget` 0x00863030, loop 0x00863431–0x0086349f over `Nexp` entries `{xid, xmin, xmax, xper}` (16 B): +``` +availPre = max(0, [6]−[14]−[13]−[11]−[9]−[7]+[3]+[1]−[12]−[8]−[10]+[5]+[4]+[2]) // net before expenses ([12] still 0) +for each entry: + xminC = max(xmin, 0) + xmaxC = clamp(xmax, 0, 2e9); if (xmaxC == 0) xmaxC = 2e9 // 0 = unlimited + room = xmaxC − xminC + req = ftol(xper × (float)availPre) − xminC // FLD [entry+8]; FMUL ST1 (float(availPre)) + take = min(max(req, 0), room) + ΣXmin += xminC; ΣTake += take +[12] += ΣXmin + min(max(ΣTake, 0), availPre − ΣXmin) +``` +So `xper` is a fraction of the pre-expense available income, the request is `xper × avail` minus the mandatory +minimum, clamped to `[0, xmax − xmin]`, and the total is capped by what is left after all minimums. + +## Q6. Which running total the tech-income bonus and the savings aid read + +Disasm 0x0086382b–0x00863899: +``` +net1 = [5]−[9]−[10]−[11]−[12]−[13]−[14]−[8]−[7]+[3]+[4]+[2]+[1]+[6] // [6] = 0, [14] = 0 at this point +if (net1 > 0) [6] = max(0, ftol((p.+0x228 − 1.0) × net1)) // FLD [ESI+0x228]; FSUB 1.0; FIMUL net1 +if (savAid != 0): + net2 = same sum, now including the new [6] + newSav = SatAdd(Sav, net2) // clamped ±2e9 + [14] = min(max(newSav, 0), max(savAid, 0)) +``` +The bonus is a share of the **full net** (all income incl. interest and trade, minus maintenance, research money, +construction, expenses, research aid); savings aid is capped by **projected savings after this turn**, not by the turn +net. `p.+0x228` (and `+0x224` = output multiplier read by `ComputeOutputFromRates`, `+0x22c` = research multiplier) +are copied by 0x0077b620 from a per-player 0x50-byte setup record (`+0x48/+0x4c/+0x50`) at game creation/sync — the +game-setup handicap block, not a tech (MEDIUM). + +## Q7. Node-line speed clamp at the influence radius + +`NodeLine::Step` 0x00705510 + `BuildStutterSegments` 0x00705280: the travel line is intersected with every system's +sphere of radius `STUTTER_SYSTEM_INFLUENCE_RADIUS` (0x008a64f0 ray/sphere → `[t0,t1] × len`, entries dropped when +shorter than 0.01, sorted, overlaps merged at the midpoint). Per segment: +``` +dist = DistPointToSegment(system.pos, segStart, segEnd) // 0x008e8eb0, t clamped to [0,1] +v = nodespeed × ((STUTTER_MAX_SPEED − STUTTER_MIN_SPEED) × (dist / RADIUS) + STUTTER_MIN_SPEED) +``` +There is **no explicit clamp**: `dist ≤ RADIUS` holds by construction (the segment lies inside the sphere), so +`v ∈ [MIN, MAX] × nodespeed`; outside every sphere the remainder of the step moves at plain `nodespeed` (0x006fe3b0). +Also note the speed is per **segment** (closest approach of the whole chord), not re-evaluated per position. + +## Q8. Does `DecayAllResearch` also hit the current target? + +Yes. `TechTree::ProcessResearch` 0x005876c0, loop 0x00587c20–0x00587c90: for **every** node with `state == 2 && +progress != 0`: `progress = max(0, progress − ftol(Cost(node) × 0.05))`. The just-funded node is not excluded; it only +escapes by completing (state 4) in the same call. Net gain of the current tech per turn is `spend − 5 %·cost`. + +## Extras resolved on the way + +* `PERGATETRAFFIC_DRV_TpGate/GatAmp` readers: `OnTechResearched` ids 10018/10019 → `PrGtTrf = max(PrGtTrf, value)` + (storage 0x00b23e2c / 0x00b23e30). +* `TRKSTL_REGENERATION_MOD` path: 0x0079b980 → 0x0079b770 gated by `HasResearched(IND_TRKSTL)`. +* Sensor range (system): `SENSORMOD[species] × (hadvs ? ADVSENS_SENSORS_MOD : 1) × 4.0` (0x0080b730). diff --git a/findings/subsystems/tech-effects.md b/findings/subsystems/tech-effects.md new file mode 100644 index 0000000..9e028b4 --- /dev/null +++ b/findings/subsystems/tech-effects.md @@ -0,0 +1,192 @@ +# 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. diff --git a/ghidra/addresses.json b/ghidra/addresses.json index c3a84f7..194aa5e 100644 --- a/ghidra/addresses.json +++ b/ghidra/addresses.json @@ -70,17 +70,17 @@ "name": "ServerPlayer_ComputeBudget", "addr": "0x00863030", "convention": "thiscall", - "prototype": "void (ServerPlayer* this, int* budget25)", - "status": "unverified", - "source": "findings/subsystems/strategic-turn-internals.md" + "prototype": "void (ServerPlayer* this, int out[25], bool projected)", + "status": "verified", + "source": "handoff/formula-gaps.md#q5" }, { "name": "TechTree_ProcessResearch", "addr": "0x005876c0", "convention": "thiscall", - "prototype": "void (TechTree* this, ...)", - "status": "unverified", - "source": "findings/control-flow/turn-spine.md" + "prototype": "void (TechTree* this, ?, vector<{node,int}>* alloc, int* overbudget) /* decay loop hits current target */", + "status": "verified", + "source": "handoff/formula-gaps.md#q8" }, { "name": "ServerSystem_Read", @@ -266,6 +266,30 @@ "status": "verified", "source": "handoff/loader-prototypes.md#m1" }, + { + "name": "GlobalConst_ParseRect", + "addr": "0x008b7040", + "convention": "cdecl", + "prototype": "void (int rect[4], const char* text) /* '%d %d %d %d' (x y w h); 2 keys (COMBATSETUP_CARD_*_RECT); layout inferred from the M1 trace, int vs float unconfirmed */", + "status": "verified-by-trace", + "source": "sots-engine docs/M1.md" + }, + { + "name": "GlobalConst_ParseVec3", + "addr": "0x008b7080", + "convention": "cdecl", + "prototype": "void (float xyz[3], const char* text) /* '%f %f %f'; 5 keys (HIVER_SPAWN_*_GATE_HEIGHT / _SHIP_OFFSET ...); inferred from the M1 trace */", + "status": "verified-by-trace", + "source": "sots-engine docs/M1.md" + }, + { + "name": "GlobalConst_ParseString", + "addr": "0x008b7670", + "convention": "cdecl", + "prototype": "void (std::string* storage, const char* text) /* *storage = text; MSVC2010 std::string 0x18 bytes {+0 char buf[16] | char* ptr when capacity > 15, +0x10 size, +0x14 capacity}; 146 keys (*_NAME, *_SOUND, *_TEXTURENAME, DERELICT_SECTION_nn ...); seen in the M1 trace */", + "status": "verified-by-trace", + "source": "sots-engine docs/M1.md" + }, { "name": "GlobalConsts_LoadAll", "addr": "0x008b76d0", @@ -825,6 +849,254 @@ "prototype": "void (Stream* s, const char* label, float* out, int flag) /* wrapper over vft[3] */", "status": "verified", "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "ServerPlayer_OnTechResearched", + "addr": "0x00891790", + "convention": "thiscall", + "prototype": "void (ServerPlayer* this, TechDef* def, bool silent) /* vft slot 4 of 0x00a327a4; hard-coded tech effects */", + "status": "verified", + "source": "handoff/tech-effects.md#1" + }, + { + "name": "MasterTechTree_IsTech", + "addr": "0x0057d5d0", + "convention": "thiscall", + "prototype": "bool (MasterTechTree* this, TechDef* def, int techId) /* techId = 10000+index into g_TechIdNames; 0xc5 = none */", + "status": "verified", + "source": "handoff/tech-effects.md#0" + }, + { + "name": "MasterTechTree_GetTechDef", + "addr": "0x0057d610", + "convention": "thiscall", + "prototype": "TechDef* (MasterTechTree* this, int techId)", + "status": "verified", + "source": "handoff/tech-effects.md#0" + }, + { + "name": "TechTree_HasResearched", + "addr": "0x0057d810", + "convention": "thiscall", + "prototype": "bool (TechTree* this, int techId)", + "status": "verified", + "source": "handoff/tech-effects.md#0" + }, + { + "name": "MasterTechTree_ResolveTechIds", + "addr": "0x00581c10", + "convention": "thiscall", + "prototype": "void (MasterTechTree* this) /* fills TechDef*[196] at this+0 from g_TechIdNames */", + "status": "verified", + "source": "handoff/tech-effects.md#0" + }, + { + "name": "IsCombatTechName", + "addr": "0x006965c0", + "convention": "cdecl", + "prototype": "int (std::string* name) /* membership in g_CombatTechNames (116) */", + "status": "verified", + "source": "handoff/tech-effects.md#0" + }, + { + "name": "ServerPlayer_RebuildSpeciesTechFlags", + "addr": "0x0082bf10", + "convention": "fastcall", + "prototype": "void (ServerPlayer* this) /* flags[7] at +0x348 from SpeciesDef xnc tech ids */", + "status": "verified", + "source": "handoff/tech-effects.md#0" + }, + { + "name": "ServerPlayer_ApplyAITechBonus", + "addr": "0x0080e330", + "convention": "thiscall", + "prototype": "void (ServerPlayer* this, TechDef* def)", + "status": "verified", + "source": "handoff/tech-effects.md#1" + }, + { + "name": "ServerPlayer_SetAIBenefit", + "addr": "0x008186b0", + "convention": "thiscall", + "prototype": "void (ServerPlayer* this, bool on)", + "status": "verified", + "source": "handoff/tech-effects.md#1" + }, + { + "name": "TechDef_GetPlagueCureMask", + "addr": "0x00537240", + "convention": "cdecl", + "prototype": "bool (TechDef* def, uint* maskOut)", + "status": "verified", + "source": "handoff/tech-effects.md#1" + }, + { + "name": "ComputeTechBitmasks", + "addr": "0x004d7960", + "convention": "cdecl", + "prototype": "uint* (uint out[2], TechTree* tree) /* EBX-passed tree in caller; tables 0x00adf378 / 0x00adf478 */", + "status": "unverified", + "source": "handoff/tech-effects.md#3" + }, + { + "name": "SpeciesDef_Get", + "addr": "0x00545cc0", + "convention": "cdecl", + "prototype": "SpeciesDef* (uint species) /* 0x00b10a00 + i*0x184 */", + "status": "verified", + "source": "handoff/tech-effects.md#4" + }, + { + "name": "SpeciesDef_InitTable", + "addr": "0x005453a0", + "convention": "cdecl", + "prototype": "void (void)", + "status": "verified", + "source": "handoff/tech-effects.md#4" + }, + { + "name": "ServerPlayer_UpdateBankruptcyLimits", + "addr": "0x00818600", + "convention": "fastcall", + "prototype": "void (ServerPlayer* this)", + "status": "verified", + "source": "handoff/formula-gaps.md#q1" + }, + { + "name": "ServerPlayer_SetBankruptcyState", + "addr": "0x0080e260", + "convention": "thiscall", + "prototype": "void (ServerPlayer* this, int level)", + "status": "verified", + "source": "handoff/formula-gaps.md#q1" + }, + { + "name": "ServerPlayer_BankruptcyLevel", + "addr": "0x0080db10", + "convention": "fastcall", + "prototype": "int (ServerPlayer* this)", + "status": "verified", + "source": "handoff/formula-gaps.md#q1" + }, + { + "name": "ServerSystem_HazardMod", + "addr": "0x00747ae0", + "convention": "cdecl", + "prototype": "double (double suit, double ideal, double tol) /* clamp01(1-|suit-ideal|/(tol+0.1)) */", + "status": "verified", + "source": "handoff/formula-gaps.md#q2" + }, + { + "name": "ServerSystem_TradePointsToMoney", + "addr": "0x007505b0", + "convention": "thiscall", + "prototype": "double (ServerSystem* this, double tradePoints) /* RET 8 */", + "status": "verified", + "source": "handoff/formula-gaps.md#q3" + }, + { + "name": "ServerSystem_CalcSuitMod", + "addr": "0x007484d0", + "convention": "thiscall", + "prototype": "double (ServerSystem* this, int species)", + "status": "verified", + "source": "handoff/formula-gaps.md#q3" + }, + { + "name": "ServerSystem_AccrueSystemBonus", + "addr": "0x0074d4f0", + "convention": "fastcall", + "prototype": "void (ServerSystem* this)", + "status": "verified", + "source": "handoff/formula-gaps.md#q4" + }, + { + "name": "ServerSystem_SystemBonusPopTarget", + "addr": "0x0074b5a0", + "convention": "thiscall", + "prototype": "int (ServerSystem* this, float frac)", + "status": "verified", + "source": "handoff/formula-gaps.md#q4" + }, + { + "name": "NodeLine_Step", + "addr": "0x00705510", + "convention": "cdecl", + "prototype": "void (Vector3* posOut, bool* arrivedOut, float nodespeed, float dt, void* lines, Vector3* from, Vector3* to)", + "status": "unverified", + "source": "handoff/formula-gaps.md#q7" + }, + { + "name": "NodeLine_BuildStutterSegments", + "addr": "0x00705280", + "convention": "cdecl", + "prototype": "void (vector* segsOut, vector* systems, Vector3* from, Vector3* to)", + "status": "unverified", + "source": "handoff/formula-gaps.md#q7" + }, + { + "name": "g_TechIdNames", + "addr": "0x009ff9e4", + "convention": "data", + "prototype": "struct { const char* name; int x; }[196] /* TechId = 10000+i */", + "status": "verified", + "source": "handoff/tech-effects.md#0" + }, + { + "name": "g_CombatTechNames", + "addr": "0x00a19718", + "convention": "data", + "prototype": "const char*[116]", + "status": "verified", + "source": "handoff/tech-effects.md#0" + }, + { + "name": "g_SpeciesDefTable", + "addr": "0x00b10a00", + "convention": "data", + "prototype": "SpeciesDef[7] (0x184 B each, .bss, filled by SpeciesDef_InitTable)", + "status": "verified", + "source": "handoff/tech-effects.md#4" + }, + { + "name": "g_TechBitmaskTableA", + "addr": "0x00adf378", + "convention": "data", + "prototype": "struct { int techId; uint bit; }[32]", + "status": "verified", + "source": "handoff/tech-effects.md#3" + }, + { + "name": "g_TechBitmaskTableB", + "addr": "0x00adf478", + "convention": "data", + "prototype": "struct { int techId; uint bit; }[29]", + "status": "verified", + "source": "handoff/tech-effects.md#3" + }, + { + "name": "g_AITechValueTable", + "addr": "0x00a17888", + "convention": "data", + "prototype": "struct { int techId; int a; float value; }[6] /* values not dumped */", + "status": "unverified", + "source": "handoff/tech-effects.md#1" + }, + { + "name": "g_PERGATETRAFFIC_DRV_TpGate", + "addr": "0x00b23e2c", + "convention": "data", + "prototype": "int (config storage; PTR slot 0x00aedfe0)", + "status": "verified", + "source": "handoff/tech-effects.md#1" + }, + { + "name": "g_PERGATETRAFFIC_DRV_GatAmp", + "addr": "0x00b23e30", + "convention": "data", + "prototype": "int (config storage; PTR slot 0x00aedfe4)", + "status": "verified", + "source": "handoff/tech-effects.md#1" } ] -} +} \ No newline at end of file diff --git a/ghidra/generated/sots_addresses.h b/ghidra/generated/sots_addresses.h index e466db2..0380869 100644 --- a/ghidra/generated/sots_addresses.h +++ b/ghidra/generated/sots_addresses.h @@ -1,5 +1,5 @@ // GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). -// Source: sots-re ghidra/addresses.json @ 254a077, generated 2026-09-07 by tools/gen_addresses.py +// Source: sots-re ghidra/addresses.json @ b24f13c, generated 2026-09-07 by tools/gen_addresses.py // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). #pragma once #include @@ -23,9 +23,9 @@ constexpr uint32_t StrategyClient_EndTurn = 0x00383be0; constexpr uint32_t StrategyServer_BeginProcessTurn = 0x003d98e0; // thiscall void (StrategyServer* this) [unverified] constexpr uint32_t StrategyServer_ProcessTurn = 0x003dc6c0; -// thiscall void (ServerPlayer* this, int* budget25) [unverified] +// thiscall void (ServerPlayer* this, int out[25], bool projected) [verified] constexpr uint32_t ServerPlayer_ComputeBudget = 0x00463030; -// thiscall void (TechTree* this, ...) [unverified] +// thiscall void (TechTree* this, ?, vector<{node,int}>* alloc, int* overbudget) /* decay loop hits current target */ [verified] constexpr uint32_t TechTree_ProcessResearch = 0x001876c0; // thiscall void (ServerSystem* this /*+8 IStreamable*/, Stream* s) [verified-by-save] constexpr uint32_t ServerSystem_Read = 0x0035d4b0; @@ -69,10 +69,16 @@ constexpr uint32_t GlobalConst_Register = 0x004b7610; constexpr uint32_t GlobalConst_ParseInt = 0x004b7000; // cdecl void (float* storage, const char* text) [verified] constexpr uint32_t GlobalConst_ParseFloat = 0x004b7020; -// cdecl void (float* storage, const char* text) /* *storage = sscanf(%f) * *(float*)0x00af5210 */ [verified] +// cdecl void (float* storage, const char* text) /* sscanf(%f) into *storage, then *storage = (float)((double)*storage * *(double*)0x00af5210) */ [verified] constexpr uint32_t GlobalConst_ParseFloatScaled = 0x004b70e0; // cdecl void (float rgba[4], const char* text) /* '%d %d %d %d', defaults 255, /255, clamp 0..1 */ [verified] constexpr uint32_t GlobalConst_ParseColour = 0x004b7110; +// cdecl void (int rect[4], const char* text) /* '%d %d %d %d' (x y w h); 2 keys (COMBATSETUP_CARD_*_RECT); layout inferred from the M1 trace, int vs float unconfirmed */ [verified-by-trace] +constexpr uint32_t GlobalConst_ParseRect = 0x004b7040; +// cdecl void (float xyz[3], const char* text) /* '%f %f %f'; 5 keys (HIVER_SPAWN_*_GATE_HEIGHT / _SHIP_OFFSET ...); inferred from the M1 trace */ [verified-by-trace] +constexpr uint32_t GlobalConst_ParseVec3 = 0x004b7080; +// cdecl void (std::string* storage, const char* text) /* *storage = text; MSVC2010 std::string 0x18 bytes {+0 char buf[16] | char* ptr when capacity > 15, +0x10 size, +0x14 capacity}; 146 keys (*_NAME, *_SOUND, *_TEXTURENAME, DERELICT_SECTION_nn ...); seen in the M1 trace */ [verified-by-trace] +constexpr uint32_t GlobalConst_ParseString = 0x004b7670; // cdecl void (void) /* called once from Application::Initialize; sets g_GlobalConstsLoaded */ [verified] constexpr uint32_t GlobalConsts_LoadAll = 0x004b76d0; // cdecl void (const char* file, GlobalConstMap* consts) /* consts = std::map of the keys registered for this file; MSVC2010 layout: map {+4 head node, +8 size}; node {+0 left, +4 parent, +8 right, +0xc const char* key, +0x10 GlobalConst*, +0x14 color, +0x15 isnil}; head: isnil=1, parent=root. Consumed keys are erased from the map. Hook this for the file-level read */ [verified] @@ -81,7 +87,7 @@ constexpr uint32_t GlobalConsts_LoadFile = 0x004b73c0; constexpr uint32_t g_GlobalConstsLoaded = 0x0072d740; // data std::map /* +4 (0x00b2d748) = header node pointer */ [verified] constexpr uint32_t g_GlobalConstRegistry = 0x0072d744; -// data float /* multiplier applied by GlobalConst_ParseFloatScaled after the %f scan */ [verified] +// data double /* pi/180 as a DOUBLE (0x3f91df46a2529d39); GlobalConst_ParseFloatScaled does float word *= this in x87 and stores float */ [verified] constexpr uint32_t g_GlobalConstFloatScale = 0x006f5210; // thiscall Script* (Script* this) /* sizeof(Script) = 0x28 */ [verified] constexpr uint32_t Script_ctor = 0x004cd700; @@ -213,5 +219,67 @@ constexpr uint32_t Stream_ReadInt = 0x004b9d20; constexpr uint32_t Stream_ReadBool = 0x004b9c00; // cdecl void (Stream* s, const char* label, float* out, int flag) /* wrapper over vft[3] */ [verified] constexpr uint32_t Stream_ReadFloat = 0x004b9bc0; +// thiscall void (ServerPlayer* this, TechDef* def, bool silent) /* vft slot 4 of 0x00a327a4; hard-coded tech effects */ [verified] +constexpr uint32_t ServerPlayer_OnTechResearched = 0x00491790; +// thiscall bool (MasterTechTree* this, TechDef* def, int techId) /* techId = 10000+index into g_TechIdNames; 0xc5 = none */ [verified] +constexpr uint32_t MasterTechTree_IsTech = 0x0017d5d0; +// thiscall TechDef* (MasterTechTree* this, int techId) [verified] +constexpr uint32_t MasterTechTree_GetTechDef = 0x0017d610; +// thiscall bool (TechTree* this, int techId) [verified] +constexpr uint32_t TechTree_HasResearched = 0x0017d810; +// thiscall void (MasterTechTree* this) /* fills TechDef*[196] at this+0 from g_TechIdNames */ [verified] +constexpr uint32_t MasterTechTree_ResolveTechIds = 0x00181c10; +// cdecl int (std::string* name) /* membership in g_CombatTechNames (116) */ [verified] +constexpr uint32_t IsCombatTechName = 0x002965c0; +// fastcall void (ServerPlayer* this) /* flags[7] at +0x348 from SpeciesDef xnc tech ids */ [verified] +constexpr uint32_t ServerPlayer_RebuildSpeciesTechFlags = 0x0042bf10; +// thiscall void (ServerPlayer* this, TechDef* def) [verified] +constexpr uint32_t ServerPlayer_ApplyAITechBonus = 0x0040e330; +// thiscall void (ServerPlayer* this, bool on) [verified] +constexpr uint32_t ServerPlayer_SetAIBenefit = 0x004186b0; +// cdecl bool (TechDef* def, uint* maskOut) [verified] +constexpr uint32_t TechDef_GetPlagueCureMask = 0x00137240; +// cdecl uint* (uint out[2], TechTree* tree) /* EBX-passed tree in caller; tables 0x00adf378 / 0x00adf478 */ [unverified] +constexpr uint32_t ComputeTechBitmasks = 0x000d7960; +// cdecl SpeciesDef* (uint species) /* 0x00b10a00 + i*0x184 */ [verified] +constexpr uint32_t SpeciesDef_Get = 0x00145cc0; +// cdecl void (void) [verified] +constexpr uint32_t SpeciesDef_InitTable = 0x001453a0; +// fastcall void (ServerPlayer* this) [verified] +constexpr uint32_t ServerPlayer_UpdateBankruptcyLimits = 0x00418600; +// thiscall void (ServerPlayer* this, int level) [verified] +constexpr uint32_t ServerPlayer_SetBankruptcyState = 0x0040e260; +// fastcall int (ServerPlayer* this) [verified] +constexpr uint32_t ServerPlayer_BankruptcyLevel = 0x0040db10; +// cdecl double (double suit, double ideal, double tol) /* clamp01(1-|suit-ideal|/(tol+0.1)) */ [verified] +constexpr uint32_t ServerSystem_HazardMod = 0x00347ae0; +// thiscall double (ServerSystem* this, double tradePoints) /* RET 8 */ [verified] +constexpr uint32_t ServerSystem_TradePointsToMoney = 0x003505b0; +// thiscall double (ServerSystem* this, int species) [verified] +constexpr uint32_t ServerSystem_CalcSuitMod = 0x003484d0; +// fastcall void (ServerSystem* this) [verified] +constexpr uint32_t ServerSystem_AccrueSystemBonus = 0x0034d4f0; +// thiscall int (ServerSystem* this, float frac) [verified] +constexpr uint32_t ServerSystem_SystemBonusPopTarget = 0x0034b5a0; +// cdecl void (Vector3* posOut, bool* arrivedOut, float nodespeed, float dt, void* lines, Vector3* from, Vector3* to) [unverified] +constexpr uint32_t NodeLine_Step = 0x00305510; +// cdecl void (vector* segsOut, vector* systems, Vector3* from, Vector3* to) [unverified] +constexpr uint32_t NodeLine_BuildStutterSegments = 0x00305280; +// data struct { const char* name; int x; }[196] /* TechId = 10000+i */ [verified] +constexpr uint32_t g_TechIdNames = 0x005ff9e4; +// data const char*[116] [verified] +constexpr uint32_t g_CombatTechNames = 0x00619718; +// data SpeciesDef[7] (0x184 B each, .bss, filled by SpeciesDef_InitTable) [verified] +constexpr uint32_t g_SpeciesDefTable = 0x00710a00; +// data struct { int techId; uint bit; }[32] [verified] +constexpr uint32_t g_TechBitmaskTableA = 0x006df378; +// data struct { int techId; uint bit; }[29] [verified] +constexpr uint32_t g_TechBitmaskTableB = 0x006df478; +// data struct { int techId; int a; float value; }[6] /* values not dumped */ [unverified] +constexpr uint32_t g_AITechValueTable = 0x00617888; +// data int (config storage; PTR slot 0x00aedfe0) [verified] +constexpr uint32_t g_PERGATETRAFFIC_DRV_TpGate = 0x00723e2c; +// data int (config storage; PTR slot 0x00aedfe4) [verified] +constexpr uint32_t g_PERGATETRAFFIC_DRV_GatAmp = 0x00723e30; } // namespace sots::addr diff --git a/ghidra/scripts/TechFx.java b/ghidra/scripts/TechFx.java new file mode 100644 index 0000000..b188f12 --- /dev/null +++ b/ghidra/scripts/TechFx.java @@ -0,0 +1,117 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.program.model.scalar.*; +import ghidra.program.model.lang.*; +import ghidra.app.decompiler.*; +import java.util.*; +import java.util.regex.*; +import java.io.*; + +// Tech-effects recon: ServerPlayer vtable callback, 196-entry tech table, readers of per-species flag words, part-B decompiles. +public class TechFx extends GhidraScript { + DecompInterface decomp; Memory mem; ReferenceManager rm; + static final String OUT = "/tmp/techfx"; + String cstr(Address a) { try { byte[] b = new byte[80]; mem.getBytes(a, b); int i = 0; while (i < 80 && b[i] != 0 && (b[i]&0xff) >= 0x20 && (b[i]&0xff) < 0x7f) i++; if (i >= 1 && i < 80 && b[i] == 0) return new String(b, 0, i, "ISO-8859-1"); } catch (Exception e) {} return null; } + String constAt(long a) { + try { Address ad = toAddr(a); int iv = mem.getInt(ad); long lv = mem.getLong(ad); + float f = Float.intBitsToFloat(iv); double d = Double.longBitsToDouble(lv); + String s = cstr(ad); if (s != null) return "\"" + s + "\""; + StringBuilder sb = new StringBuilder(); sb.append("i=" + iv); + if (!Float.isNaN(f) && Math.abs(f) < 1e12 && (Math.abs(f) > 1e-7 || f == 0)) sb.append(" f=" + f); + if (!Double.isNaN(d) && Math.abs(d) < 1e12 && (Math.abs(d) > 1e-7 || d == 0)) sb.append(" d=" + d); + return sb.toString(); } catch (Exception e) { return "?"; } + } + String subst(String c) { + Matcher m = Pattern.compile("&?(DAT|PTR_s_|s_[A-Za-z0-9_]*|PTR_DAT|u_[A-Za-z0-9_]*|_DAT|PTR_PTR|_PTR)_([0-9a-f]{8})").matcher(c); + StringBuffer sb = new StringBuffer(); + while (m.find()) { long a = Long.parseLong(m.group(2), 16); Address ad = toAddr(a); String s = cstr(ad); String rep = m.group(0); + if (s != null && s.length() <= 64 && !m.group(1).equals("PTR_DAT")) rep = "\"" + s + "\""; + else if (a >= 0x9dd000L && a < 0xb40000L) { rep = m.group(0) + "/*" + constAt(a) + "*/"; } + m.appendReplacement(sb, Matcher.quoteReplacement(rep)); } + m.appendTail(sb); return sb.toString(); + } + String decompRaw(Function f) { try { DecompileResults res = decomp.decompileFunction(f, 600, monitor); if (res == null || !res.decompileCompleted()) return "[decompile failed]"; return res.getDecompiledFunction().getC(); } catch (Exception e) { return "[exception " + e.getMessage() + "]"; } } + Set done = new HashSet<>(); + void dumpFunc(Function f, PrintWriter idx, boolean disasm) throws Exception { + if (f == null || done.contains(f.getEntryPoint().getOffset())) return; + done.add(f.getEntryPoint().getOffset()); + String t = String.format("%08x", f.getEntryPoint().getOffset()); + PrintWriter w = new PrintWriter(new FileWriter(OUT + "/" + t + ".c")); + w.println("// " + f.getName(true) + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); + StringBuilder calls = new StringBuilder(); for (Function cf : f.getCalledFunctions(monitor)) calls.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); + w.println("// CALLS: " + calls); + StringBuilder callers = new StringBuilder(); for (Function cf : f.getCallingFunctions(monitor)) callers.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); + w.println("// CALLERS: " + callers); + w.println(subst(decompRaw(f))); + if (disasm) { w.println("// ---- DISASM ----"); Listing l = currentProgram.getListing(); InstructionIterator ii = l.getInstructions(f.getBody(), true); + while (ii.hasNext()) { Instruction in = ii.next(); StringBuilder ann = new StringBuilder(); + for (Reference r : in.getReferencesFrom()) { long ta = r.getToAddress().getOffset(); if (ta >= 0x9dd000L && ta < 0xb40000L) { Symbol s = currentProgram.getSymbolTable().getPrimarySymbol(r.getToAddress()); ann.append(" ; " + (s != null ? s.getName() : String.format("%08x", ta)) + "=" + constAt(ta)); } else if (r.getReferenceType().isCall()) { Function cf = getFunctionAt(r.getToAddress()); if (cf != null) ann.append(" ; ->" + cf.getName(true)); } } + w.println(String.format("%08x %-40s%s", in.getAddress().getOffset(), in.toString(), ann)); } } + w.close(); + idx.println("decompiled " + t + " " + f.getName(true) + " size=" + f.getBody().getNumAddresses()); idx.flush(); + } + String readersOf(Address ga) { Set seen = new TreeSet<>(); ReferenceIterator gi = rm.getReferencesTo(ga); while (gi.hasNext()) { Reference gr = gi.next(); Function rf = getFunctionContaining(gr.getFromAddress()); seen.add(rf == null ? "(nofunc " + gr.getFromAddress() + ")" : rf.getName(true) + "@" + rf.getEntryPoint()); } return String.join(" ", seen); } + + @Override public void run() throws Exception { + mem = currentProgram.getMemory(); rm = currentProgram.getReferenceManager(); + decomp = new DecompInterface(); decomp.openProgram(currentProgram); + new File(OUT).mkdirs(); + PrintWriter idx = new PrintWriter(new FileWriter(OUT + "/index.txt")); + PrintWriter rep = new PrintWriter(new FileWriter(OUT + "/report.txt")); + + // 1. ServerPlayer vtable + long vt = 0x00a327a4L; + rep.println("== ServerPlayer vtable 0x00a327a4 =="); + List vfns = new ArrayList<>(); + for (int i = 0; i < 8; i++) { int p = mem.getInt(toAddr(vt + 4*i)); Function f = getFunctionAt(toAddr(p & 0xffffffffL)); rep.println(String.format("slot %d (+0x%x): %08x %s", i, 4*i, p, f == null ? "?" : f.getName(true) + " size=" + f.getBody().getNumAddresses())); if (f != null) vfns.add(f); } + Function cb = getFunctionAt(toAddr(mem.getInt(toAddr(vt + 0x10)) & 0xffffffffL)); + if (cb != null) { dumpFunc(cb, idx, true); + for (Function c1 : cb.getCalledFunctions(monitor)) { if (c1.getBody().getNumAddresses() < 30000) dumpFunc(c1, idx, false); + for (Function c2 : c1.getCalledFunctions(monitor)) if (c2.getBody().getNumAddresses() < 6000 && c2.getBody().getNumAddresses() > 40) dumpFunc(c2, idx, false); } } + for (Function f : vfns) if (f.getBody().getNumAddresses() < 30000) dumpFunc(f, idx, false); + + // 2. 0x0058b870 and the 196-entry table + Function ld = getFunctionAt(toAddr(0x0058b870L)); dumpFunc(ld, idx, false); + rep.println("== data refs from FUN_0058b870 =="); + Set cands = new TreeSet<>(); + if (ld != null) { InstructionIterator ii = currentProgram.getListing().getInstructions(ld.getBody(), true); + while (ii.hasNext()) { Instruction in = ii.next(); for (Reference r : in.getReferencesFrom()) { long ta = r.getToAddress().getOffset(); if (ta >= 0x9dd000L && ta < 0xb40000L && r.getReferenceType().isData()) { rep.println(String.format(" %08x -> %08x %s", in.getAddress().getOffset(), ta, constAt(ta))); cands.add(ta); } } + for (int oi = 0; oi < in.getNumOperands(); oi++) for (Object o : in.getOpObjects(oi)) if (o instanceof Scalar) { long v = ((Scalar)o).getUnsignedValue(); if (v >= 0xad9000L && v < 0xb40000L) { rep.println(String.format(" %08x imm %08x", in.getAddress().getOffset(), v)); cands.add(v); } } } } + PrintWriter tw = new PrintWriter(new FileWriter(OUT + "/techtable196.txt")); + List names = new ArrayList<>(); + for (int i = 0; i < 196; i++) { long a = 0x009ff9e4L + 8*i; int p = mem.getInt(toAddr(a)); int q = mem.getInt(toAddr(a+4)); String s = (p > 0x9dd000 && p < 0xad9000) ? cstr(toAddr(p & 0xffffffffL)) : null; names.add(s == null ? "?" : s); tw.println(String.format("%3d %08x name=%s second=%08x(%d)", i, a, s, q, q)); } + tw.close(); + // Try candidates as base of TechDef*[196]: score by xref count + rep.println("== candidate array bases =="); + long best = 0; int bestScore = -1; + for (long c : cands) { int score = 0; for (int i = 0; i < 196; i++) { ReferenceIterator gi = rm.getReferencesTo(toAddr(c + 4*i)); while (gi.hasNext()) { gi.next(); score++; } } rep.println(String.format(" %08x xrefs-over-196-slots=%d", c, score)); if (score > bestScore) { bestScore = score; best = c; } } + rep.println("best base " + String.format("%08x", best)); + PrintWriter rw = new PrintWriter(new FileWriter(OUT + "/techreaders.txt")); + Set readerFns = new LinkedHashSet<>(); + if (best != 0) for (int i = 0; i < 196; i++) { Address ea = toAddr(best + 4*i); Set seen = new TreeSet<>(); ReferenceIterator gi = rm.getReferencesTo(ea); while (gi.hasNext()) { Reference gr = gi.next(); Function rf = getFunctionContaining(gr.getFromAddress()); if (rf != null) { readerFns.add(rf); seen.add(rf.getName(true) + "@" + rf.getEntryPoint() + "(" + String.format("%08x", gr.getFromAddress().getOffset()) + ")"); } else seen.add("(nofunc " + gr.getFromAddress() + ")"); } rw.println(String.format("%3d %08x %-16s %s", i, best + 4*i, names.get(i), String.join(" ", seen))); } + rw.close(); + for (Function f : readerFns) if (f.getBody().getNumAddresses() < 40000 && !f.getEntryPoint().equals(toAddr(0x0058b870L))) dumpFunc(f, idx, false); + + // 3. instruction scan for ServerPlayer field displacements + long[] disp = {0x348,0x34c,0x350,0x354,0x358,0x35c,0x360,0x364,0x224,0x228,0x22c,0x124,0x128,0x12c,0xc0,0xc4,0x104,0x160,0xd0,0xdc,0xe0}; + Map> hits = new TreeMap<>(); + for (long d : disp) hits.put(d, new TreeMap<>()); + InstructionIterator all = currentProgram.getListing().getInstructions(true); + while (all.hasNext()) { Instruction in = all.next(); for (int oi = 0; oi < in.getNumOperands(); oi++) { if ((in.getOperandType(oi) & OperandType.ADDRESS) == 0 && (in.getOperandType(oi) & OperandType.DYNAMIC) == 0) continue; boolean hasReg = false; Scalar sc = null; for (Object o : in.getOpObjects(oi)) { if (o instanceof Register) hasReg = true; if (o instanceof Scalar) sc = (Scalar)o; } if (!hasReg || sc == null) continue; long v = sc.getSignedValue(); if (hits.containsKey(v)) { Function f = getFunctionContaining(in.getAddress()); String k = f == null ? "(nofunc)" : f.getName(true) + "@" + f.getEntryPoint(); hits.get(v).merge(k, 1, Integer::sum); } } } + PrintWriter dw = new PrintWriter(new FileWriter(OUT + "/dispscan.txt")); + for (long d : disp) { dw.println(String.format("== disp 0x%x ==", d)); for (Map.Entry e : hits.get(d).entrySet()) dw.println(" " + e.getKey() + " x" + e.getValue()); } + dw.close(); + // dump readers of 0x348..0x364 that are strategy-side (0x0074xxxx..0x0089xxxx) and small + for (long d : new long[]{0x348,0x34c,0x350,0x354,0x358,0x35c,0x360,0x364}) for (String k : hits.get(d).keySet()) { Matcher m = Pattern.compile("@([0-9a-f]{8})").matcher(k); if (m.find()) { long a = Long.parseLong(m.group(1), 16); if (a >= 0x740000L && a < 0x8a0000L) { Function f = getFunctionAt(toAddr(a)); if (f != null && f.getBody().getNumAddresses() < 12000) dumpFunc(f, idx, false); } } } + + // 4. part-B functions + String[] partB = {"00818600","007c0a50","0074d4f0","00863030","00705510","005876c0","00747ae0","007505b0","0080db10","00754220","0074b230","00536fb0","00537140","0074ab20","00746910","0080dd10","0080dd20","0074c680","0074f1e0","007d92a0","00705280","00705940","00817f90","0080dd30"}; + String[] partBdis = {"00818600","007505b0","0074d4f0","0080dd10","0080dd20","00747ae0","00705510","0074c680"}; + Set disSet = new HashSet<>(Arrays.asList(partBdis)); + for (String t : partB) { done.remove(Long.parseLong(t, 16)); dumpFunc(getFunctionAt(toAddr(Long.parseLong(t, 16))), idx, disSet.contains(t)); } + rep.close(); idx.close(); decomp.dispose(); println("done"); + } +} diff --git a/ghidra/scripts/TechFx2.java b/ghidra/scripts/TechFx2.java new file mode 100644 index 0000000..be664d4 --- /dev/null +++ b/ghidra/scripts/TechFx2.java @@ -0,0 +1,106 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.program.model.scalar.*; +import ghidra.program.model.lang.*; +import ghidra.app.decompiler.*; +import java.util.*; +import java.util.regex.*; +import java.io.*; + +// Pass 2: every immediate 10000..10196 (tech ids) anywhere in code, ServerPlayer accessor stubs, and their callers. +public class TechFx2 extends GhidraScript { + DecompInterface decomp; Memory mem; ReferenceManager rm; + static final String OUT = "/tmp/techfx2"; + String cstr(Address a) { try { byte[] b = new byte[80]; mem.getBytes(a, b); int i = 0; while (i < 80 && b[i] != 0 && (b[i]&0xff) >= 0x20 && (b[i]&0xff) < 0x7f) i++; if (i >= 1 && i < 80 && b[i] == 0) return new String(b, 0, i, "ISO-8859-1"); } catch (Exception e) {} return null; } + String constAt(long a) { + try { Address ad = toAddr(a); int iv = mem.getInt(ad); long lv = mem.getLong(ad); + float f = Float.intBitsToFloat(iv); double d = Double.longBitsToDouble(lv); + String s = cstr(ad); if (s != null) return "\"" + s + "\""; + StringBuilder sb = new StringBuilder(); sb.append("i=" + iv); + if (!Float.isNaN(f) && Math.abs(f) < 1e12 && (Math.abs(f) > 1e-7 || f == 0)) sb.append(" f=" + f); + if (!Double.isNaN(d) && Math.abs(d) < 1e12 && (Math.abs(d) > 1e-7 || d == 0)) sb.append(" d=" + d); + return sb.toString(); } catch (Exception e) { return "?"; } + } + String subst(String c) { + Matcher m = Pattern.compile("&?(DAT|PTR_s_|s_[A-Za-z0-9_]*|PTR_DAT|u_[A-Za-z0-9_]*|_DAT|PTR_PTR|_PTR)_([0-9a-f]{8})").matcher(c); + StringBuffer sb = new StringBuffer(); + while (m.find()) { long a = Long.parseLong(m.group(2), 16); Address ad = toAddr(a); String s = cstr(ad); String rep = m.group(0); + if (s != null && s.length() <= 64 && !m.group(1).equals("PTR_DAT")) rep = "\"" + s + "\""; + else if (a >= 0x9dd000L && a < 0xb40000L) { rep = m.group(0) + "/*" + constAt(a) + "*/"; } + m.appendReplacement(sb, Matcher.quoteReplacement(rep)); } + m.appendTail(sb); return sb.toString(); + } + String decompRaw(Function f) { try { DecompileResults res = decomp.decompileFunction(f, 600, monitor); if (res == null || !res.decompileCompleted()) return "[decompile failed]"; return res.getDecompiledFunction().getC(); } catch (Exception e) { return "[exception " + e.getMessage() + "]"; } } + Set done = new HashSet<>(); + void dumpFunc(Function f, PrintWriter idx, boolean disasm) throws Exception { + if (f == null || done.contains(f.getEntryPoint().getOffset())) return; + done.add(f.getEntryPoint().getOffset()); + String t = String.format("%08x", f.getEntryPoint().getOffset()); + PrintWriter w = new PrintWriter(new FileWriter(OUT + "/" + t + ".c")); + w.println("// " + f.getName(true) + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); + StringBuilder calls = new StringBuilder(); for (Function cf : f.getCalledFunctions(monitor)) calls.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); + w.println("// CALLS: " + calls); + StringBuilder callers = new StringBuilder(); for (Function cf : f.getCallingFunctions(monitor)) callers.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); + w.println("// CALLERS: " + callers); + w.println(subst(decompRaw(f))); + if (disasm) { w.println("// ---- DISASM ----"); Listing l = currentProgram.getListing(); InstructionIterator ii = l.getInstructions(f.getBody(), true); + while (ii.hasNext()) { Instruction in = ii.next(); StringBuilder ann = new StringBuilder(); + for (Reference r : in.getReferencesFrom()) { long ta = r.getToAddress().getOffset(); if (ta >= 0x9dd000L && ta < 0xb40000L) { Symbol s = currentProgram.getSymbolTable().getPrimarySymbol(r.getToAddress()); ann.append(" ; " + (s != null ? s.getName() : String.format("%08x", ta)) + "=" + constAt(ta)); } else if (r.getReferenceType().isCall()) { Function cf = getFunctionAt(r.getToAddress()); if (cf != null) ann.append(" ; ->" + cf.getName(true)); } } + w.println(String.format("%08x %-40s%s", in.getAddress().getOffset(), in.toString(), ann)); } } + w.close(); + idx.println("decompiled " + t + " " + f.getName(true) + " size=" + f.getBody().getNumAddresses()); idx.flush(); + } + + @Override public void run() throws Exception { + mem = currentProgram.getMemory(); rm = currentProgram.getReferenceManager(); + decomp = new DecompInterface(); decomp.openProgram(currentProgram); + new File(OUT).mkdirs(); + PrintWriter idx = new PrintWriter(new FileWriter(OUT + "/index.txt")); + List names = new ArrayList<>(); + for (int i = 0; i < 196; i++) { long a = 0x009ff9e4L + 8*i; int p = mem.getInt(toAddr(a)); String s = (p > 0x9dd000 && p < 0xad9000) ? cstr(toAddr(p & 0xffffffffL)) : null; names.add(s == null ? "?" : s); } + + // 1. immediates 10000..10196 (+ 0xc5 sentinel ignored) anywhere + PrintWriter iw = new PrintWriter(new FileWriter(OUT + "/techids.txt")); + Map> byId = new TreeMap<>(); + Set idFns = new LinkedHashSet<>(); + Listing l = currentProgram.getListing(); + InstructionIterator all = l.getInstructions(true); + while (all.hasNext()) { Instruction in = all.next(); + for (int oi = 0; oi < in.getNumOperands(); oi++) { if ((in.getOperandType(oi) & OperandType.SCALAR) == 0) continue; for (Object o : in.getOpObjects(oi)) { if (!(o instanceof Scalar)) continue; long v = ((Scalar)o).getSignedValue(); if (v < 10000 || v > 10196) continue; int id = (int)(v - 10000); + Function f = getFunctionContaining(in.getAddress()); + // find next call within 8 instructions + String nextCall = "-"; Instruction n = in; for (int k = 0; k < 8 && n != null; k++) { n = n.getNext(); if (n == null) break; if (n.getMnemonicString().equals("CALL")) { for (Reference r : n.getReferencesFrom()) if (r.getReferenceType().isCall()) { Function cf = getFunctionAt(r.getToAddress()); nextCall = cf == null ? r.getToAddress().toString() : cf.getName(true) + "@" + cf.getEntryPoint(); } if (nextCall.equals("-")) nextCall = n.toString(); break; } } + String line = String.format("%08x %-8s id=%d %-16s in %s -> %s", in.getAddress().getOffset(), in.getMnemonicString(), (int)v, names.get(id), f == null ? "(nofunc)" : f.getName(true) + "@" + f.getEntryPoint(), nextCall); + iw.println(line); byId.computeIfAbsent(id, k -> new TreeSet<>()).add(f == null ? "(nofunc)" : f.getName(true) + "@" + f.getEntryPoint() + "(" + String.format("%08x", in.getAddress().getOffset()) + ")"); if (f != null) idFns.add(f); } } } + iw.println("== by id =="); for (int i = 0; i < 196; i++) iw.println(String.format("%3d %-16s %s", i, names.get(i), byId.containsKey(i) ? String.join(" ", byId.get(i)) : "(none)")); + iw.close(); + // also pointer-table refs: PTR_s_ entries at 0x009ff9e4 + 8i referenced from code (e.g. switch tables) + for (Function f : idFns) if (f.getBody().getNumAddresses() < 20000) dumpFunc(f, idx, f.getBody().getNumAddresses() < 5000); + + // 2. accessor stubs in ServerPlayer/StrategyServer code range reading small [reg+disp] + long[] disp = {0xb4,0xb8,0xfc,0xfd,0xfe,0xff,0x100,0x101,0x102,0x103,0x104,0x108,0x10c,0x110,0x114,0x118,0x11c,0x120,0x124,0x128,0x12c,0x130,0x134,0x138,0x13c,0x140,0x144,0x148,0x14c,0x150,0x154,0x158,0x15c,0x160,0x190,0x194,0x224,0x228,0x22c,0x288,0x28c,0x290,0x330,0x348}; + Set dset = new HashSet<>(); for (long d : disp) dset.add(d); + PrintWriter aw = new PrintWriter(new FileWriter(OUT + "/accessors.txt")); + PrintWriter fw = new PrintWriter(new FileWriter(OUT + "/fieldrefs.txt")); + Map> hits = new TreeMap<>(); + FunctionIterator fi = l.getFunctions(toAddr(0x00740000L), true); + Set accCallers = new LinkedHashSet<>(); + while (fi.hasNext()) { Function f = fi.next(); if (f.getEntryPoint().getOffset() >= 0x008a0000L) break; long sz = f.getBody().getNumAddresses(); + Set seenD = new TreeSet<>(); + InstructionIterator ii = l.getInstructions(f.getBody(), true); + while (ii.hasNext()) { Instruction in = ii.next(); for (int oi = 0; oi < in.getNumOperands(); oi++) { boolean hasReg = false; Scalar sc = null; for (Object o : in.getOpObjects(oi)) { if (o instanceof Register) hasReg = true; if (o instanceof Scalar) sc = (Scalar)o; } if (!hasReg || sc == null) continue; if ((in.getOperandType(oi) & (OperandType.ADDRESS|OperandType.DYNAMIC)) == 0) continue; long v = sc.getSignedValue(); if (dset.contains(v)) { seenD.add(v); hits.computeIfAbsent(v, k -> new TreeMap<>()).merge(f.getName(true) + "@" + f.getEntryPoint() + " sz=" + sz, 1, Integer::sum); } } } + if (sz <= 64 && !seenD.isEmpty()) { StringBuilder cs = new StringBuilder(); for (Function c : f.getCallingFunctions(monitor)) { cs.append(c.getName(true) + "@" + c.getEntryPoint() + " "); accCallers.add(c); } aw.println(String.format("%s@%s sz=%d disp=%s callers: %s", f.getName(true), f.getEntryPoint(), sz, seenD, cs)); dumpFunc(f, idx, true); } } + aw.close(); + for (Map.Entry> e : hits.entrySet()) { fw.println(String.format("== disp 0x%x ==", e.getKey())); for (Map.Entry e2 : e.getValue().entrySet()) fw.println(" " + e2.getKey() + " x" + e2.getValue()); } + fw.close(); + for (Function c : accCallers) if (c.getBody().getNumAddresses() < 20000) dumpFunc(c, idx, false); + + // 3. specific helpers + String[] extra = {"0082bf10","00537240","0080e410","0080e330","008186b0","004d7960","0074d070","0074a870","00745e40","00743800","004231c0","0080dad0","0080e490","0080e4d0","0080e510","0082bbf0","0082bc20","0082bc60","0082bc80","0082bcd0","0082bd10","0082bdb0","0082bdf0","0082bf00","0082c190","0082d220","0080b730","0081e9d0","008672c0","007870d0","006fe0e0","006ff370","0080b820","0080b850","0080ba30","0081eb30","0081ede0","00827540","00826af0","004d7890","0053bd40","004d77c0","00690f70","006962b0","0069d2c0","005862d0","0088df20","008182c0","0078c7e0","0074b110","0074b610","0074c400","0074ef70","00758170","0075bde0","0075fb20","0080f060","00812eb0","0081adf0","0081e3d0","00833fa0","008340a0","0083bed0","008402b0","00868400","00878060","0087fac0","008822c0","0088f9b0","008924d0","0059c7d0","0059dd00","00761930","00762170","00764e30","00766560","0077b510","0077c1f0","0078dac0","007e8fc0","007e9b60","0073ec20","0074ad90","0074b230","006b3ed0"}; + for (String t : extra) { Function f = getFunctionAt(toAddr(Long.parseLong(t, 16))); if (f != null && f.getBody().getNumAddresses() < 25000) dumpFunc(f, idx, f.getBody().getNumAddresses() < 2500); } + idx.close(); decomp.dispose(); println("done"); + } +} diff --git a/ghidra/scripts/TechFx3.java b/ghidra/scripts/TechFx3.java new file mode 100644 index 0000000..7c10b98 --- /dev/null +++ b/ghidra/scripts/TechFx3.java @@ -0,0 +1,99 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.program.model.scalar.*; +import ghidra.program.model.lang.*; +import ghidra.app.decompiler.*; +import java.util.*; +import java.util.regex.*; +import java.io.*; + +// Pass 3: money-tail helpers, species-def table, tech bitmask tables, node segment builder, ComputeBudget disasm. +public class TechFx3 extends GhidraScript { + DecompInterface decomp; Memory mem; ReferenceManager rm; + static final String OUT = "/tmp/techfx3"; + List names = new ArrayList<>(); + String cstr(Address a) { try { byte[] b = new byte[80]; mem.getBytes(a, b); int i = 0; while (i < 80 && b[i] != 0 && (b[i]&0xff) >= 0x20 && (b[i]&0xff) < 0x7f) i++; if (i >= 1 && i < 80 && b[i] == 0) return new String(b, 0, i, "ISO-8859-1"); } catch (Exception e) {} return null; } + String techName(long v) { return (v >= 10000 && v <= 10195) ? names.get((int)(v - 10000)) : (v == 0xc5 ? "NONE" : null); } + String constAt(long a) { + try { Address ad = toAddr(a); int iv = mem.getInt(ad); long lv = mem.getLong(ad); + float f = Float.intBitsToFloat(iv); double d = Double.longBitsToDouble(lv); + String s = cstr(ad); if (s != null) return "\"" + s + "\""; + StringBuilder sb = new StringBuilder(); sb.append("i=" + iv); String tn = techName(iv & 0xffffffffL); if (tn != null) sb.append("(" + tn + ")"); + if (!Float.isNaN(f) && Math.abs(f) < 1e12 && (Math.abs(f) > 1e-7 || f == 0)) sb.append(" f=" + f); + if (!Double.isNaN(d) && Math.abs(d) < 1e12 && (Math.abs(d) > 1e-7 || d == 0)) sb.append(" d=" + d); + return sb.toString(); } catch (Exception e) { return "?"; } + } + String subst(String c) { + Matcher m = Pattern.compile("&?(DAT|PTR_s_|s_[A-Za-z0-9_]*|PTR_DAT|u_[A-Za-z0-9_]*|_DAT|PTR_PTR|_PTR)_([0-9a-f]{8})").matcher(c); + StringBuffer sb = new StringBuffer(); + while (m.find()) { long a = Long.parseLong(m.group(2), 16); Address ad = toAddr(a); String s = cstr(ad); String rep = m.group(0); + if (s != null && s.length() <= 64 && !m.group(1).equals("PTR_DAT")) rep = "\"" + s + "\""; + else if (a >= 0x9dd000L && a < 0xb40000L) { rep = m.group(0) + "/*" + constAt(a) + "*/"; } + m.appendReplacement(sb, Matcher.quoteReplacement(rep)); } + m.appendTail(sb); + // annotate bare tech-id literals + Matcher m2 = Pattern.compile("0x27([0-9a-f]{2})\\b").matcher(sb.toString()); StringBuffer sb2 = new StringBuffer(); + while (m2.find()) { long v = Long.parseLong("27" + m2.group(1), 16); String tn = techName(v); m2.appendReplacement(sb2, Matcher.quoteReplacement(m2.group(0) + (tn != null ? "/*" + tn + "*/" : ""))); } + m2.appendTail(sb2); return sb2.toString(); + } + String decompRaw(Function f) { try { DecompileResults res = decomp.decompileFunction(f, 600, monitor); if (res == null || !res.decompileCompleted()) return "[decompile failed]"; return res.getDecompiledFunction().getC(); } catch (Exception e) { return "[exception " + e.getMessage() + "]"; } } + Set done = new HashSet<>(); + void dumpFunc(Function f, PrintWriter idx, boolean disasm) throws Exception { + if (f == null || done.contains(f.getEntryPoint().getOffset())) return; + done.add(f.getEntryPoint().getOffset()); + String t = String.format("%08x", f.getEntryPoint().getOffset()); + PrintWriter w = new PrintWriter(new FileWriter(OUT + "/" + t + ".c")); + w.println("// " + f.getName(true) + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); + StringBuilder calls = new StringBuilder(); for (Function cf : f.getCalledFunctions(monitor)) calls.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); + w.println("// CALLS: " + calls); + StringBuilder callers = new StringBuilder(); for (Function cf : f.getCallingFunctions(monitor)) callers.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); + w.println("// CALLERS: " + callers); + w.println(subst(decompRaw(f))); + if (disasm) { w.println("// ---- DISASM ----"); Listing l = currentProgram.getListing(); InstructionIterator ii = l.getInstructions(f.getBody(), true); + while (ii.hasNext()) { Instruction in = ii.next(); StringBuilder ann = new StringBuilder(); + for (Reference r : in.getReferencesFrom()) { long ta = r.getToAddress().getOffset(); if (ta >= 0x9dd000L && ta < 0xb40000L) { Symbol s = currentProgram.getSymbolTable().getPrimarySymbol(r.getToAddress()); ann.append(" ; " + (s != null ? s.getName() : String.format("%08x", ta)) + "=" + constAt(ta)); } else if (r.getReferenceType().isCall()) { Function cf = getFunctionAt(r.getToAddress()); if (cf != null) ann.append(" ; ->" + cf.getName(true)); } } + for (int oi = 0; oi < in.getNumOperands(); oi++) for (Object o : in.getOpObjects(oi)) if (o instanceof Scalar) { String tn = techName(((Scalar)o).getUnsignedValue()); if (tn != null) ann.append(" ; tech " + tn); } + w.println(String.format("%08x %-40s%s", in.getAddress().getOffset(), in.toString(), ann)); } } + w.close(); + idx.println("decompiled " + t + " " + f.getName(true) + " size=" + f.getBody().getNumAddresses()); idx.flush(); + } + void dumpRegion(PrintWriter w, String title, long start, long end) throws Exception { + w.println("== " + title + String.format(" %08x..%08x ==", start, end)); + for (long a = start; a < end; a += 4) { int iv = mem.getInt(toAddr(a)); String s = null; if (iv > 0x9dd000 && iv < 0xb40000) s = cstr(toAddr(iv & 0xffffffffL)); Symbol sym = currentProgram.getSymbolTable().getPrimarySymbol(toAddr(a)); + w.println(String.format("%08x %s%s%s", a, constAt(a), s != null ? " ->\"" + s + "\"" : "", sym != null ? " [" + sym.getName() + "]" : "")); } + } + + @Override public void run() throws Exception { + mem = currentProgram.getMemory(); rm = currentProgram.getReferenceManager(); + decomp = new DecompInterface(); decomp.openProgram(currentProgram); + new File(OUT).mkdirs(); + PrintWriter idx = new PrintWriter(new FileWriter(OUT + "/index.txt")); + for (int i = 0; i < 196; i++) { long a = 0x009ff9e4L + 8*i; int p = mem.getInt(toAddr(a)); String s = (p > 0x9dd000 && p < 0xad9000) ? cstr(toAddr(p & 0xffffffffL)) : null; names.add(s == null ? "?" : s); } + PrintWriter rep = new PrintWriter(new FileWriter(OUT + "/report.txt")); + + // species def accessor and its table + Function sp = getFunctionAt(toAddr(0x00545cc0L)); dumpFunc(sp, idx, true); + Set bases = new TreeSet<>(); + if (sp != null) { InstructionIterator ii = currentProgram.getListing().getInstructions(sp.getBody(), true); while (ii.hasNext()) { Instruction in = ii.next(); for (Reference r : in.getReferencesFrom()) { long ta = r.getToAddress().getOffset(); if (ta >= 0x9dd000L && ta < 0xb40000L) bases.add(ta); } for (int oi = 0; oi < in.getNumOperands(); oi++) for (Object o : in.getOpObjects(oi)) if (o instanceof Scalar) { long v = ((Scalar)o).getUnsignedValue(); if (v >= 0xad9000L && v < 0xb40000L) bases.add(v); rep.println(String.format(" 545cc0 scalar %08x %d", in.getAddress().getOffset(), v)); } } } + rep.println("species accessor data refs: " + bases); + for (long b : bases) dumpRegion(rep, "speciesdef candidate", b, b + 7 * 0xc0); + // tech bitmask tables used by FUN_004d7960 + dumpRegion(rep, "bitmask tables", 0x00adf360L, 0x00adf5a0L); + // AI tech table near DAT_00aea2ec + dumpRegion(rep, "AI tech table", 0x00aea2d0L, 0x00aea340L); + // 116-entry table header/neighbourhood (what precedes/follows) + dumpRegion(rep, "0x00a19700 neighbourhood", 0x00a196f0L, 0x00a19720L); + dumpRegion(rep, "0x009ff9e4 neighbourhood", 0x009ff9c0L, 0x009ff9f0L); + + String[] want = {"0074d760","0074b700","0080f470","007484d0","005453a0","00690f70","0074b5a0","00747740","00705280","008e8eb0","0057d220","0057d7e0","00580e30","0074b110","0074ab20","0074a4a0","0074c680","0074f1e0","007477a0","00817f90","0059b490","0080f4b0","00536c80","0074a6d0","0053bd40","004d7890","004d77c0","00690f70","006922c0","00696420","006964a0","00695be0","00696550","00694f40","0053dd60","00535f80","00535400","00535480","005352f0","00535ef0","00578920","00578d20","00552600","00552900","00555fc0","0079f050","0079f290","007911f0","007867e0","0078aa70","007b41c0","007cb080","008375d0","00875fa0","006c3600","006bb420","006da6a0","006a2bf0","006c2510","006ad5e0","006acba0","006aea70","0057c3c0","006cdbb0","0075cb20","0062d310","0080dad0","00815ab0","00815960","0075bd70","0075bde0","006e18e0","008188b0","00788ff0","0080e510","0080e490","00746390","0080e4d0","00535c40","00818860","00820380","00700920","0079b980","006d1e30","006fb1a0","006b3ed0","0074b7a0","00746830","00750480","007483b0"}; + String[] dis = {"0074d760","0074b700","0080f470","007484d0","0074b5a0","00747740","0057d220","0057d7e0","00705280","008e8eb0","0080dad0","0080e510","0080e490","0080e4d0","00815ab0","00815960","0075bd70","008188b0","00818860","00746390","00535c40"}; + Set disSet = new HashSet<>(Arrays.asList(dis)); + for (String t : want) { Function f = getFunctionAt(toAddr(Long.parseLong(t, 16))); if (f != null && f.getBody().getNumAddresses() < 25000) dumpFunc(f, idx, disSet.contains(t) || f.getBody().getNumAddresses() < 900); } + // ComputeBudget with disasm + dumpFunc(getFunctionAt(toAddr(0x00863030L)), idx, true); + rep.close(); idx.close(); decomp.dispose(); println("done"); + } +} diff --git a/ghidra/scripts/TechFx4.java b/ghidra/scripts/TechFx4.java new file mode 100644 index 0000000..7f8016c --- /dev/null +++ b/ghidra/scripts/TechFx4.java @@ -0,0 +1,140 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.program.model.scalar.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.data.*; +import ghidra.app.decompiler.*; +import java.util.*; +import java.util.regex.*; +import java.io.*; + +// Pass 4: remaining decompiles (+0x228 producers, WriteString for std::string layout) and write-back of names/comments/labels. +public class TechFx4 extends GhidraScript { + DecompInterface decomp; Memory mem; ReferenceManager rm; + static final String OUT = "/tmp/techfx4"; + List names = new ArrayList<>(); + String cstr(Address a) { try { byte[] b = new byte[80]; mem.getBytes(a, b); int i = 0; while (i < 80 && b[i] != 0 && (b[i]&0xff) >= 0x20 && (b[i]&0xff) < 0x7f) i++; if (i >= 1 && i < 80 && b[i] == 0) return new String(b, 0, i, "ISO-8859-1"); } catch (Exception e) {} return null; } + String techName(long v) { return (v >= 10000 && v <= 10195) ? names.get((int)(v - 10000)) : (v == 0xc5 ? "NONE" : null); } + String constAt(long a) { + try { Address ad = toAddr(a); int iv = mem.getInt(ad); long lv = mem.getLong(ad); + float f = Float.intBitsToFloat(iv); double d = Double.longBitsToDouble(lv); + String s = cstr(ad); if (s != null) return "\"" + s + "\""; + StringBuilder sb = new StringBuilder(); sb.append("i=" + iv); String tn = techName(iv & 0xffffffffL); if (tn != null) sb.append("(" + tn + ")"); + if (!Float.isNaN(f) && Math.abs(f) < 1e12 && (Math.abs(f) > 1e-7 || f == 0)) sb.append(" f=" + f); + if (!Double.isNaN(d) && Math.abs(d) < 1e12 && (Math.abs(d) > 1e-7 || d == 0)) sb.append(" d=" + d); + return sb.toString(); } catch (Exception e) { return "?"; } + } + String subst(String c) { + Matcher m = Pattern.compile("&?(DAT|PTR_s_|s_[A-Za-z0-9_]*|PTR_DAT|u_[A-Za-z0-9_]*|_DAT|PTR_PTR|_PTR)_([0-9a-f]{8})").matcher(c); + StringBuffer sb = new StringBuffer(); + while (m.find()) { long a = Long.parseLong(m.group(2), 16); Address ad = toAddr(a); String s = cstr(ad); String rep = m.group(0); + if (s != null && s.length() <= 64 && !m.group(1).equals("PTR_DAT")) rep = "\"" + s + "\""; + else if (a >= 0x9dd000L && a < 0xb40000L) { rep = m.group(0) + "/*" + constAt(a) + "*/"; } + m.appendReplacement(sb, Matcher.quoteReplacement(rep)); } + m.appendTail(sb); + Matcher m2 = Pattern.compile("0x27([0-9a-f]{2})\\b").matcher(sb.toString()); StringBuffer sb2 = new StringBuffer(); + while (m2.find()) { long v = Long.parseLong("27" + m2.group(1), 16); String tn = techName(v); m2.appendReplacement(sb2, Matcher.quoteReplacement(m2.group(0) + (tn != null ? "/*" + tn + "*/" : ""))); } + m2.appendTail(sb2); return sb2.toString(); + } + String decompRaw(Function f) { try { DecompileResults res = decomp.decompileFunction(f, 600, monitor); if (res == null || !res.decompileCompleted()) return "[decompile failed]"; return res.getDecompiledFunction().getC(); } catch (Exception e) { return "[exception " + e.getMessage() + "]"; } } + Set done = new HashSet<>(); + void dumpFunc(Function f, PrintWriter idx, boolean disasm) throws Exception { + if (f == null || done.contains(f.getEntryPoint().getOffset())) return; + done.add(f.getEntryPoint().getOffset()); + String t = String.format("%08x", f.getEntryPoint().getOffset()); + PrintWriter w = new PrintWriter(new FileWriter(OUT + "/" + t + ".c")); + w.println("// " + f.getName(true) + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); + StringBuilder calls = new StringBuilder(); for (Function cf : f.getCalledFunctions(monitor)) calls.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); + w.println("// CALLS: " + calls); + StringBuilder callers = new StringBuilder(); for (Function cf : f.getCallingFunctions(monitor)) callers.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); + w.println("// CALLERS: " + callers); + w.println(subst(decompRaw(f))); + if (disasm) { w.println("// ---- DISASM ----"); Listing l = currentProgram.getListing(); InstructionIterator ii = l.getInstructions(f.getBody(), true); + while (ii.hasNext()) { Instruction in = ii.next(); StringBuilder ann = new StringBuilder(); + for (Reference r : in.getReferencesFrom()) { long ta = r.getToAddress().getOffset(); if (ta >= 0x9dd000L && ta < 0xb40000L) { Symbol s = currentProgram.getSymbolTable().getPrimarySymbol(r.getToAddress()); ann.append(" ; " + (s != null ? s.getName() : String.format("%08x", ta)) + "=" + constAt(ta)); } else if (r.getReferenceType().isCall()) { Function cf = getFunctionAt(r.getToAddress()); if (cf != null) ann.append(" ; ->" + cf.getName(true)); } } + w.println(String.format("%08x %-40s%s", in.getAddress().getOffset(), in.toString(), ann)); } } + w.close(); + idx.println("decompiled " + t + " " + f.getName(true) + " size=" + f.getBody().getNumAddresses()); idx.flush(); + } + void rename(long a, String ns, String name, String comment) throws Exception { + Function f = getFunctionAt(toAddr(a)); if (f == null) { println("no function at " + Long.toHexString(a)); return; } + Namespace n = currentProgram.getGlobalNamespace(); + if (ns != null) for (String part : ns.split("::")) n = currentProgram.getSymbolTable().getOrCreateNameSpace(n, part, SourceType.USER_DEFINED); + if (f.getName().startsWith("FUN_")) { f.setName(name, SourceType.USER_DEFINED); f.setParentNamespace(n); } + if (comment != null) { String old = f.getComment(); f.setComment((old == null || old.isEmpty()) ? comment : old + "\n" + comment); } + } + void label(long a, String name, String comment) throws Exception { + Address ad = toAddr(a); Symbol s = currentProgram.getSymbolTable().getPrimarySymbol(ad); + if (s == null || s.getName().startsWith("DAT_") || s.getName().startsWith("PTR_")) currentProgram.getSymbolTable().createLabel(ad, name, SourceType.USER_DEFINED).setPrimary(); + if (comment != null) currentProgram.getListing().setComment(ad, CodeUnit.PLATE_COMMENT, comment); + } + + @Override public void run() throws Exception { + mem = currentProgram.getMemory(); rm = currentProgram.getReferenceManager(); + decomp = new DecompInterface(); decomp.openProgram(currentProgram); + new File(OUT).mkdirs(); + PrintWriter idx = new PrintWriter(new FileWriter(OUT + "/index.txt")); + for (int i = 0; i < 196; i++) { long a = 0x009ff9e4L + 8*i; int p = mem.getInt(toAddr(a)); String s = (p > 0x9dd000 && p < 0xad9000) ? cstr(toAddr(p & 0xffffffffL)) : null; names.add(s == null ? "?" : s); } + PrintWriter rep = new PrintWriter(new FileWriter(OUT + "/report.txt")); + + // 1. decompiles + String[] want = {"007cf560","0077b620","00747ba0","0053be20","00581cc0","00747d30","0074aff0","00745aa0","0080dcb0","0080ddb0","0080dde0","0080af60","0080ba90","008102f0","0081bf50","0080baf0","0080bb40","0080bb60","0080d080","0087fac0","008822c0","0080b730","0081e9d0","008672c0","007870d0"}; + String[] dis = {"007cf560","00747ba0","0053be20","00581cc0","0080dcb0","0080ddb0","0080dde0","0080af60","0080ba90","0080baf0","0080bb40","0080bb60","0080d080","0080b730"}; + Set disSet = new HashSet<>(Arrays.asList(dis)); + for (String t : want) { Function f = getFunctionAt(toAddr(Long.parseLong(t, 16))); if (f != null && f.getBody().getNumAddresses() < 30000) dumpFunc(f, idx, disSet.contains(t) || f.getBody().getNumAddresses() < 600); } + // WriteString candidates: functions whose name mentions Stream and Write/String + rep.println("== Stream write/string functions =="); + FunctionIterator fi = currentProgram.getListing().getFunctions(true); + while (fi.hasNext()) { Function f = fi.next(); String n = f.getName(true); if ((n.contains("Stream") && (n.contains("Write") || n.contains("String"))) || n.contains("WriteString") || n.contains("ReadString")) { rep.println(" " + n + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); if (f.getBody().getNumAddresses() < 4000) dumpFunc(f, idx, true); } } + // also the IStreamable vtable slot +0x18 of ServerPlayer sub-vftable 0x00a32794 + rep.println("== IStreamable sub-vftable 0x00a32794 =="); + for (int i = 0; i < 8; i++) { int p = mem.getInt(toAddr(0x00a32794L + 4*i)); Function f = getFunctionAt(toAddr(p & 0xffffffffL)); rep.println(String.format(" slot %d: %08x %s", i, p, f == null ? "?" : f.getName(true) + " size=" + f.getBody().getNumAddresses())); } + // Mars::Stream vtable guess: find callers of ServerPlayer::Write's string helper — dump ServerPlayer::Write's first 200 instructions calls + Function wr = getFunctionAt(toAddr(0x008563e0L)); + if (wr != null) { rep.println("== ServerPlayer::Write callees =="); Map cnt = new TreeMap<>(); for (Function cf : wr.getCalledFunctions(monitor)) cnt.put(cf.getName(true) + "@" + cf.getEntryPoint() + " sz=" + cf.getBody().getNumAddresses(), 1); for (String k : cnt.keySet()) rep.println(" " + k); + for (Function cf : wr.getCalledFunctions(monitor)) { long sz = cf.getBody().getNumAddresses(); if (sz > 40 && sz < 700) { // look for functions that test [x+0x14] >= 0x10 (SSO) + InstructionIterator ii = currentProgram.getListing().getInstructions(cf.getBody(), true); boolean sso = false; while (ii.hasNext()) { Instruction in = ii.next(); String s = in.toString(); if ((s.contains("0x14]") || s.contains("0x14 ]")) && (s.startsWith("CMP"))) sso = true; } + if (sso) { rep.println(" SSO-candidate " + cf.getName(true) + "@" + cf.getEntryPoint()); dumpFunc(cf, idx, true); } } } } + + // 2. write-back + int tx = currentProgram.startTransaction("tech-effects writeback"); + try { + rename(0x00891790L, "Game::ServerPlayer", "OnTechResearched", "ServerPlayer::OnTechResearched(TechDef* def, bool silent) - vft slot 4, called from TechTree::SetResearched. Hard-coded strategic tech effects: chain of MasterTechTree::IsTech(def, 10000+i) tests writing ServerPlayer fields (ConMod/SavMod/OutMod/PopMod/TerraMod/SuitTol/MaxOH/MinRate/pddm/CstR-E-T/PrGtTrf/flags). See /srv/re-lab/handoff/tech-effects.md for the id->effect table. Tail: plague-cure mask (HasImm/HasVac), tech bitmasks (+0x190/+0x194), RebuildSpeciesTechFlags, Zuul CruisCon->free BrdPod, cdp=SpyBm&&SlvgTech, EVENT_TEMPERANCE cure."); + rename(0x0057d5d0L, "Game::MasterTechTree", "IsTech", "bool IsTech(TechDef* def, int techId): techId = 10000+index into g_TechIdNames (0x009ff9e4, 196 entries); 0xc5 = none. Compares def against the resolved TechDef*[196] at *this."); + rename(0x0057d610L, "Game::MasterTechTree", "GetTechDef", "TechDef* GetTechDef(int techId 10000..10195) from the resolved table at *this; 0 if none/0xc5."); + rename(0x0057d810L, "Game::TechTree", "HasResearched", "bool HasResearched(int techId): master->resolved[techId-10000] -> node state == 4."); + rename(0x00581c10L, "Game::MasterTechTree", "ResolveTechIds", "Fills this->resolved[196] (TechDef*) by _stricmp-looking up each name of g_TechIdNames (0x009ff9e4, stride 8) in the sorted tech list. Called at the end of the MasterTechTree ctor."); + rename(0x006965c0L, null, "IsCombatTechName", "Returns 1 if the string (std::string at param+4) matches any of the 116 names in g_CombatTechNames (0x00a19718). Pure membership test (weapon/section tech families for the design/combat side); not the effects table."); + rename(0x0082bf10L, "Game::ServerPlayer", "RebuildSpeciesTechFlags", "Recomputes flags[7] at +0x348 from the tech tree: for species i, speciesdef(i)+0x78..+0x9c hold tech ids (translation1/2/3, incorporate, addict, temperance, subjugate, accommodate, proliferate order per SpeciesDef init) -> bits 0..8 = HasResearched(id). Called after every research completion and on load."); + rename(0x00747ae0L, "Game::ServerSystem", "HazardMod", "double HazardMod(suit, ideal, tol) = clamp01(1 - |suit-ideal| / (tol + 0.1)). Linear, no curve."); + rename(0x00818600L, "Game::ServerPlayer", "UpdateBankruptcyLimits", "maxIncome = sum ServerSystem::ComputeOutputMax()[3]; BnkEl = max(ftol(maxIncome / -0.15), -2e9); BnkPr = max(-ftol(maxIncome * BANKRUPTCY_PROTECTION_LIMIT_FACTOR), BnkEl). The 3.3 factor is on the PROTECTION limit; elimination limit is -6.67x max income (15% debt interest = max income)."); + rename(0x0080e260L, "Game::ServerPlayer", "SetBankruptcyState", "if (level != BnkWrn) { BnkWrn = level; BnkTrn = level ? ModCount : -1; } - stamp only on transitions; ProcessBankruptcy acts on the previous turn's (BnkWrn,BnkTrn)."); + rename(0x0080db10L, "Game::ServerPlayer", "BankruptcyLevel", "2 if Sav < BnkEl, 1 if Sav < BnkPr, else 0."); + rename(0x0080e330L, "Game::ServerPlayer", "ApplyAITechBonus", "For CCC_AI/CCC_AIAdmin/CCC_AIFac: v = techStrategyValue(def)->+8 * (AIBn ? +1 : -1); adds to ResMod / IncMod(+0x30c) / OutMod respectively. AIVrus/AISlv: no numeric effect here."); + rename(0x008186b0L, "Game::ServerPlayer", "SetAIBenefit", "SetAIBenefit(bool on): flips AIBn (+0xfe) and re-applies the sign of every researched AI tech bonus (6-entry table at 0x00aea2f0)."); + rename(0x00537240L, "Game::TechDef", "GetPlagueCureMask", "If def is (or descends from) BIO_PLGVAC/RTPLGVAC/BSTVAC/ASPLGVAC/CONNAN -> mask = 1< 0x0f. Used by OnTechResearched: HasImm |= mask, HasVac |= mask, cure plagues."); + rename(0x0080e410L, "Game::ServerPlayer", "SpeciesOfTranslationTech", "Returns species index i whose speciesdef(i)+0x74 (CCC_NDTRKHUM/ZUL 'node track' tech) == def, else -1; OnTechResearched sets NPTrk |= 1<+4 - speciesdef.costFactor(+0x24) * min(|IdealSuit-Suit|, SuitTol) * 10000 * 1.5"); + rename(0x007484d0L, "Game::ServerSystem", "CalcSuitMod", "min(|IdealSuit(species) - Suit|, owner.SuitTol); 0 if RebAI owner; 20 if no owner (logs)."); + rename(0x0074d4f0L, "Game::ServerSystem", "AccrueSystemBonus", "Requires owner, IsStable, ModCount-TAcq > SYSTEMBONUS_MINTURNS, ntdev > MINTURNS. pbon += min(max(ftol(POPBONUS_INC*cap),0), max(ftol(POPBONUS*cap) - pbon, 0)); ibon += min(max(INFRABONUS_INC,0), max(INFRABONUS - ibon, 0)). Species with speciesdef+0x5c == 0 (Zuul) get no bonus."); + rename(0x0074b5a0L, "Game::ServerSystem", "SystemBonusPopTarget", "ftol(max(frac,0) * MaxPop(imperial)) if speciesdef(owner)+0x5c else 0."); + rename(0x00705510L, "Game::NodeLine", "Step", "Node-line travel: segments from FUN_00705280 (line clipped to each system's STUTTER_SYSTEM_INFLUENCE_RADIUS sphere); per segment speed = nodespeed * ((MAX-MIN) * dist(system, segment)/RADIUS + MIN); outside spheres full nodespeed."); + rename(0x00705280L, "Game::NodeLine", "BuildStutterSegments", "Clips the travel line against every system's influence sphere (FUN_008a64f0 ray/sphere), keeps [t0,t1]*len entries, sorts, and merges overlaps at the midpoint. Guarantees dist <= RADIUS inside a segment (the implicit clamp)."); + rename(0x008e8eb0L, null, "DistPointToSegment", "Distance from point to segment (t clamped to [0,1])."); + rename(0x005453a0L, "Game::SpeciesDef", "InitTable", "Hard-coded per-species constants table at 0x00b10a00 (7 x 0x184): drive type, growth/hazard/income/cost factors, XNC tech ids at +0x78.., NDTRK tech at +0x74, sensor mod, colour. See tech-effects.md section 4."); + rename(0x00545cc0L, "Game::SpeciesDef", "Get", "SpeciesDef* Get(int species 0..6) -> 0x00b10a00 + i*0x184."); + rename(0x0080dd10L, "Game::ServerPlayer", "GetIncMod", null); + rename(0x0080dd20L, "Game::ServerPlayer", "GetSpeciesCostFactor", "speciesdef(Species)+0x24 (Zuul 0.7, else 1.0)"); + label(0x009ff9e4L, "g_TechIdNames", "TechId name table: 196 x {const char* name, int}; TechId = 10000 + index. Resolved per game into MasterTechTree resolved[] by ResolveTechIds. This is the table the hard-coded effects key on."); + label(0x00a19718L, "g_CombatTechNames", "116 x const char* - membership list used by IsCombatTechName (0x006965c0); not an effects table."); + label(0x00b10a00L, "g_SpeciesDefTable", "7 x SpeciesDef (0x184 B), filled by SpeciesDef::InitTable (0x005453a0)."); + label(0x00adf378L, "g_TechBitmaskTableA", "32 x {techId, bit} -> ServerPlayer+0x190"); + label(0x00adf478L, "g_TechBitmaskTableB", "29 x {techId, bit} -> ServerPlayer+0x194"); + } finally { currentProgram.endTransaction(tx, true); } + rep.close(); idx.close(); decomp.dispose(); println("done"); + } +}