tech effects (196-entry TechId table, OnTechResearched), 8 formula gaps answered, std::string settled
This commit is contained in:
parent
b24f13c613
commit
de7a2bef04
12 changed files with 1147 additions and 23 deletions
|
|
@ -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
|
||||
<!-- dashboard-metrics {"verified": 20, "mapped_plus": 36, "targets": 45, "loc": 13702, "tests": 64, "checks": 1923, "addr_verified": 91, "addr_total": 103, "layouts": 21, "open_q": 24} -->
|
||||
<!-- dashboard-metrics {"verified": 21, "mapped_plus": 38, "targets": 47, "loc": 13702, "tests": 64, "checks": 1923, "addr_verified": 123, "addr_total": 137, "layouts": 21, "open_q": 24} -->
|
||||
|
|
|
|||
|
|
@ -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<Descriptor>; 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) |
|
||||
|
|
|
|||
|
|
@ -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]])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
125
findings/subsystems/formula-gaps.md
Normal file
125
findings/subsystems/formula-gaps.md
Normal file
|
|
@ -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).
|
||||
192
findings/subsystems/tech-effects.md
Normal file
192
findings/subsystems/tech-effects.md
Normal file
|
|
@ -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<sp> | 0x00892241 | for each species with `flags[sp] & 0x20`: every owned system addicted to sp is cured (0x00745e40/0x00743800) → `EVENT_TEMPERANCE` (unless silent) |
|
||||
| any | — | 0x00891804 | if `!silent`: event `EVENT_RESEARCH_COMPLETE` when `progress/cost ≥ 0.8` else `EVENT_RESEARCH_UNDERBUDGET`; if def == `ResTNm` → clear current research and `ResErrRoll` (running plague-cure roll 0x0088df20 once). |
|
||||
|
||||
Where the multipliers are consumed (from `strategic-turn-internals.md`, now with producers known): `OutMod` in
|
||||
`ComputeOutputFromRates` total (×`p.OutMod`), `PopMod` in growth (`g *= PopMod`), `TerraMod` in terraform points,
|
||||
`SuitTol` in `HazardMod` (cap) **and** `CalcSuitMod` (money cost, see formula-gaps Q3/Q4), `ConMod[i]` = per-class
|
||||
construction cost multiplier (readers 0x00817f90 `ConstructionSpend`, 0x0079ac10, 0x00806ed0 …; MEDIUM),
|
||||
`SavMod[i]` readers 0x00745d60/0x00745dd0/0x00814c80 (maintenance side, LOW), `pddm` (planetary-defence damage
|
||||
multiplier? readers 0x0075a570/0x0075a6b0/0x0075bde0, LOW), `MaxOH` = max over-harvest slider, `MinRate` = mining
|
||||
rate, `PrGtTrf` = per-gate traffic capacity vs `GTraf`.
|
||||
|
||||
## 2. Other strategic readers (runtime gates by TechId)
|
||||
|
||||
| tech | where read | effect | conf |
|
||||
|---|---|---|---|
|
||||
| CCC_AdvSens 10000 | 0x00746390, 0x0075bd70, 0x0075cb20 | a player in "partial contact" state (2-bit field, `+0x24`) counts as known/visible when AdvSens is researched | MED |
|
||||
| CCC_SpyBm 10034, CCC_SPJAM 10112, CCC_FTLBRDB 10108 | 0x00788ff0 (from encounter detection 0x007d7f70) | spy-beam intel on a battle: FTLBRDB → also through cease-fire partners; SpyBm → yes unless the observed side has SPJAM | MED |
|
||||
| CCC_TUNSENS 10113 | 0x00815ab0 | station type 2 (or flag 0x800) with TUNSENS → tunnel-sensor detection | MED |
|
||||
| CCC_HYPCOM 10093 | 0x00700920 | without HYPCOM a fleet cannot be re-targeted while in flight to a moving target (waypoint kind 1 → forced stop) | MED |
|
||||
| IND_TRKSTL 10094 | 0x0079b980 → 0x0079b770 (uses `TRKSTL_REGENERATION_MOD` 0x007870d0) | post-combat hull regeneration for non-NPC owner | HIGH |
|
||||
| DRV_RIP/REND/RAD 10064/65/66 | 0x006e18e0 (via 0x008182c0 after research) | ServerPlayer+0x308 = {45,15,3} / {65,35,4} / {95,60,5} (Zuul node-bore parameters, highest wins) | MED |
|
||||
| DRV_FISSN/FUSN/ANTIMAT (0x0057d350), hull 0x0056e4c0, warheads 0x0058fd20 (NUKES, NUKEWHD, GMAWHD, FUSWHD, AMWHD) | 0x0080e490 / 0x0080e4d0 / 0x0080e510 | "tech level" 0..2 / 0..4 for the AI/diplomacy evaluator 0x006d4780 | HIGH |
|
||||
| WEP_MwMsl 10036, WEP_HvyPmsl 10037 | 0x00535c40 | missile-type availability bits (combat) | HIGH |
|
||||
| BIO_PLG..NANVIR 10044-48 / vaccines 10038-43 | 0x00535400, 0x00535480, 0x00535ef0, 0x00535f80, 0x00699950 | plague-type ↔ tech maps for `ProcessPlague`, bio-weapon sections and AI research picking | HIGH |
|
||||
| CCC_TRNSHUM..LIR 10114-117 | 0x0079f050 | scenario/encounter grants N random translation-1 techs (`SetResearched`) | MED |
|
||||
| IND_STLTHARM 10050, SLD_INTANG 10071 | 0x0081fc30 (StarShip flags), 0x0080b820 | all sections STLTHARM → ship flag 0x100000 (stealth); INTANG clears flag 0x80 | MED |
|
||||
| DRV_RECFISS 10072 / DRV_PLSMFOC 10051 | 0x0080b850, 0x0080ba30 (+3.0), 0x0081ede0 (+3.0 per section) | +3.0 to a design/ship stat (drive-family bonus; combat side) | LOW |
|
||||
| IND_CyberInt 10002, DRN_AdvRob 10031, DRN_CMBT 10049 | 0x00586600 (tree build, `mode`) | special-cased during per-species tree construction (scenario "all techs" path) | LOW |
|
||||
| CCC_ARMCOM/DATSYN/BTLCMP 10074-76 | 0x00695be0, 0x006964a0 | combat command-section level 0/1/2 | HIGH |
|
||||
| WEP_*LAS 10077/78/82, WEP_*CAN 10079-81 | 0x006907d0, 0x006ad5e0 | combat beam/cannon variants | HIGH |
|
||||
|
||||
## 3. Tech bitmasks (non-serialised `ServerPlayer+0x190/+0x194`)
|
||||
|
||||
`ComputeTechBitmasks` (0x004d7960): `+0x190` = OR over `g_TechBitmaskTableA` (0x00adf378, 32 pairs) and `+0x194` over
|
||||
`g_TechBitmaskTableB` (0x00adf478, 29 pairs). Table A bits 0..31: IND_REFCOAT, IND_IMPRFCT, IND_PLYALLOY, IND_MAGLAT,
|
||||
IND_QRKRES, IND_ADMALY, IND_PREDGUN, SLD_DEF, SLD_ERGAB, SLD_MKONE, SLD_MKTWO, SLD_MKTHREE, SLD_MKFOUR, SLD_CLK,
|
||||
SLD_IMPCLK, SLD_INTANG, WEP_VRFTECH, WEP_NUKEWHD, WEP_GMAWHD, WEP_FUSWHD, WEP_AMWHD, WEP_NEUTRND, CCC_INTSENS,
|
||||
CCC_SNSJAM, CCC_QNTCHAF, CCC_CMBTALG, CCC_HOLOTAC, CCC_ADVCNC, CCC_AdvSens, DRV_MCROFUS, DRV_INCTHRST, DRV_SMLFUS.
|
||||
Table B bits 0..28: DRV_NODE, NODFOC, NODPATH, STRWRP, IMPSTWRP, FLICKER, HYPER, DRV_HYPRFLD, WARP, SLD_MESSHLD,
|
||||
SLD_GRVSHLD, CCC_AIFRCON, DRV_RIP, REND, RAD, BIO_CONNAN, SLD_DISR, SLD_MAGNI, DRV_QNTCAP, CCC_FCCOM, IND_HRDELEC,
|
||||
IND_TRKSTL, DRV_VDCTR, VDCRV, VDMSTR, BIO_SMRTNAN, WEP_ACCAMP, WEP_MwMsl, WEP_HvyPmsl. These are the design/section
|
||||
"option available" masks (consumers on the design side, not traced).
|
||||
|
||||
## 4. `SpeciesDef` table (0x00b10a00, 7 × 0x184, `SpeciesDef::InitTable` 0x005453a0) — hard-coded species constants
|
||||
|
||||
| off | Human | Hiver | Tarkas | Liir | _NPC | Zuul | Morrigi | used by |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| +0x04 drive | 3 | 0 | 1 | 2 | 0 | 3 | 4 | |
|
||||
| +0x08 | 0.07 | 0.07 | 0.07 | 0.15 | 0.07 | 0.20 | 0.07 | (suit-related, unresolved) |
|
||||
| +0x0c | 1 | 0.85 | 0.9 | 1.5 | 1 | 0.55 | 1 | |
|
||||
| +0x10 | 1 | 1.15 | 1.1 | 0.9 | 1 | 1 | 1 | |
|
||||
| +0x14 | 1 | 1.2 | 1.1 | 0.8 | 1 | 1.7 | 0.85 | |
|
||||
| +0x18 income factor | 1 | 1 | 1 | 1 | 1 | 1.1 | 0.8 | `TradePointsToMoney` |
|
||||
| +0x1c | 1 | 1 | 1 | 1 | 1 | 1 | 1.3 | |
|
||||
| +0x20 | 1 | 1 | 1 | 1 | 1 | 1 | 2.0 | |
|
||||
| +0x24 cost factor | 1 | 1 | 1 | 1 | 1 | 0.7 | 1 | `TradePointsToMoney` hazard cost |
|
||||
| +0x28 | 0.9 | 0.75 | 1 | 1.2 | 1 | 0.2 | 0.5 | |
|
||||
| +0x2c | 0.75 | 0.8 | 0.7 | 0.1 | 1 | 1 | 0.25 | |
|
||||
| +0x30 | 0 | 0.5 | 0.25 | 0 | 0 | 0.75 | −0.5 | |
|
||||
| +0x34 | 1 | 1 | 0.85 | 0.95 | 1 | 1 | 1 | |
|
||||
| +0x38 | 1 | 1 | 1 | 1 | 1 | 0.5 | 1 | |
|
||||
| +0x4c | 10 | 10 | 10 | 10 | 10 | 0 | 10 | BaseOutput cap (turn notes §3.3) |
|
||||
| +0x50 | 10 | 10 | 10 | 10 | 10 | 40 | 10 | |
|
||||
| +0x54 | 1 | 1 | 1 | 1 | 1 | 2 | 1 | |
|
||||
| +0x58 | SENSORMOD_<sp> from species.txt (NPC 1.0) | | | | | | | sensor range |
|
||||
| +0x5c | 1 | 1 | 1 | 1 | 1 | **0** | 1 | system bonus eligible (Zuul never get SYSTEMBONUS) |
|
||||
| +0x74 node-track tech | CCC_NDTRKHUM | none | none | none | none | CCC_NDTRKZUL | none | `NPTrk` |
|
||||
| +0x78.. XNC ids (order of the 9 fillers) | TRNS1 10114+sp, TRNS2 10120+, TRNS3 10126+, INC 10132+ (Zuul/NPC none), ADCT 10137+, TEMP 10142+, SUB 10147+, ACC 10153+, PROF 10158+ | | | | | | | flag bits 0..8 (§1 "bit0/1/2 slave death" = translation levels?, see note) |
|
||||
| +0x144 flags | 1 | 0 | 0 | 0 | 0 | 2 | 0 | |
|
||||
| +0x148 | 0 | 0 | 0 | 0 | 0 | plague-cure mask \| 0x20 | 0 | Zuul start immune/temperate |
|
||||
|
||||
Note on the flag-bit semantics used by the colony code (turn notes §3): bits are in filler order, i.e. bit0/1/2 =
|
||||
translation 1/2/3, bit3 = incorporate, bit4 = addict, bit5 = temperance, bit6 = subjugate, bit7 = accommodate,
|
||||
bit8 = proliferate. That reassigns the earlier guesses ("bit3 civilian growth", "bit7 hazard immunity", "bit8 may seed
|
||||
civilians") to Incorporate / Accommodate / Proliferate respectively — consistent with the XNC tech names. The exact
|
||||
slot order was inferred from the call order in `InitTable` (fillers write `*in_EAX` successive dwords); HIGH on order,
|
||||
MEDIUM on the +0x78 base (the reader 0x0082bf10 starts at +0x78 and steps 4).
|
||||
|
||||
## 5. Techs with NO code effect (pure prerequisites / data-only)
|
||||
|
||||
Of the 196 TechIds, **no code reads** ids: 95 IND_SPNLMNT, 96 WEP_HCLAS, 97 WEP_PRTBM, 98 WEP_DSRPTR, 100-107 (NUKMINE,
|
||||
FUSMINE, DFMSL, GSDRVR, MASDRVR, HVYDRVR, VRFTECH*, PDTECH), 110 CCC_INTSENS*, 171 IND_QRKRES*, 173-175 SLD_MKONE-THREE*,
|
||||
177 WEP_NEUTRND*, 179-192 (CCC_ADVCNC*, DRV_MCROFUS*, INCTHRST*, SMLFUS*, HYPRFLD*, SLD_MESSHLD*, GRVSHLD*, DISR*,
|
||||
MAGNI*, DRV_QNTCAP*, CCC_FCCOM*, IND_HRDELEC*, BIO_SMRTNAN*, WEP_ACCAMP*). `*` = present only in a bitmask table (§3).
|
||||
Everything not in the enum at all (≈ half of the `.tech` files: most WEP_*, SLD_*, DRV_* section techs) has no code
|
||||
effect beyond `requires`/section availability driven by the data files. Count: **36 ids with a strategic effect
|
||||
(§1), ~45 with combat/design/AI gates (§2/§3), ~115 pure prerequisites.**
|
||||
|
||||
## 6. std::string layout (coordinator request)
|
||||
|
||||
`Mars::Stream::WriteString` (0x008b9d70): `CMP [str+0x14], 0x10; JC inline; MOV EAX,[str]` — i.e. `_Myres` at **+0x14**,
|
||||
buffer pointer / SSO buffer `_Bx` at **+0**. `basic_string::assign` (0x00425550) uses `param_1[4]` = `_Mysize` at
|
||||
**+0x10** and `param_1[5]` = `_Myres`. Vector-of-string strides are **0x1c** (`FUN_00699bd0` `pbVar5 += 0x1c`,
|
||||
`MasterTechTree` ctor `local_34 += 7` dwords), so `sizeof == 28` with the allocator/proxy dword at **+0x18**.
|
||||
**struct-recovery.md §0 is right (`_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18`); turn-spine.md §1.1 is wrong.**
|
||||
|
||||
## 7. Write-back (saved)
|
||||
|
||||
Functions renamed + plate comments: `ServerPlayer::OnTechResearched` 0x00891790, `MasterTechTree::IsTech/GetTechDef/
|
||||
ResolveTechIds` 0x0057d5d0/0x0057d610/0x00581c10, `TechTree::HasResearched` 0x0057d810, `IsCombatTechName` 0x006965c0,
|
||||
`ServerPlayer::RebuildSpeciesTechFlags` 0x0082bf10, `ServerSystem::HazardMod` 0x00747ae0, `ServerPlayer::
|
||||
UpdateBankruptcyLimits/SetBankruptcyState/BankruptcyLevel` 0x00818600/0x0080e260/0x0080db10, `ServerPlayer::
|
||||
ApplyAITechBonus/SetAIBenefit` 0x0080e330/0x008186b0, `TechDef::GetPlagueCureMask` 0x00537240, `ServerPlayer::
|
||||
SpeciesOfTranslationTech` 0x0080e410, `ComputeTechBitmasks` 0x004d7960, `ServerSystem::TradePointsToMoney/CalcSuitMod/
|
||||
AccrueSystemBonus/SystemBonusPopTarget` 0x007505b0/0x007484d0/0x0074d4f0/0x0074b5a0, `NodeLine::Step/
|
||||
BuildStutterSegments` 0x00705510/0x00705280, `DistPointToSegment` 0x008e8eb0, `SpeciesDef::InitTable/Get`
|
||||
0x005453a0/0x00545cc0, `ServerPlayer::GetIncMod/GetSpeciesCostFactor` 0x0080dd10/0x0080dd20. Labels: `g_TechIdNames`
|
||||
0x009ff9e4, `g_CombatTechNames` 0x00a19718, `g_SpeciesDefTable` 0x00b10a00, `g_TechBitmaskTableA/B` 0x00adf378/0x00adf478.
|
||||
|
||||
## 8. Method / confidence
|
||||
|
||||
Pass 1: vtable 0x00a327a4 slot 4 → callback; decompile + annotated disassembly (decompiler drops the pushed TechId,
|
||||
so every branch was mapped from `PUSH imm; CALL IsTech`). Pass 2: program-wide scan of scalar operands 10000..10196 →
|
||||
every reader of every id (`tech-decompiles/techfx2/techids.txt`). Pass 3: species table, bitmask tables, money-tail
|
||||
helpers. Pass 4: producers + write-back. HIGH = read from disassembly with constants; MEDIUM = decompile-level with
|
||||
an inferred field meaning; LOW = consumer identified but purpose inferred from context only.
|
||||
Not done: values of the 6-entry AI tech table (0x00a17888), the `+0x224/+0x228/+0x22c` player multipliers' producer
|
||||
(0x0077b620 copies them from a 0x50-byte per-player options record `+0x48/+0x4c/+0x50` — likely the game-setup
|
||||
difficulty/handicap block, MEDIUM), and the design-side consumers of the §3 masks.
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 <cstdint>
|
||||
|
|
@ -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<const char*,GlobalConst*,stricmp> 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<const char*,GlobalConst*,stricmp_less> /* +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
|
||||
|
|
|
|||
117
ghidra/scripts/TechFx.java
Normal file
117
ghidra/scripts/TechFx.java
Normal file
|
|
@ -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<Long> 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<String> 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<Function> 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<Long> 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<String> 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<Function> readerFns = new LinkedHashSet<>();
|
||||
if (best != 0) for (int i = 0; i < 196; i++) { Address ea = toAddr(best + 4*i); Set<String> 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<Long, Map<String,Integer>> 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<String,Integer> 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<String> 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");
|
||||
}
|
||||
}
|
||||
106
ghidra/scripts/TechFx2.java
Normal file
106
ghidra/scripts/TechFx2.java
Normal file
|
|
@ -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<Long> 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<String> 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<Integer, Set<String>> byId = new TreeMap<>();
|
||||
Set<Function> 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_<name> 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<Long> 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<Long, Map<String,Integer>> hits = new TreeMap<>();
|
||||
FunctionIterator fi = l.getFunctions(toAddr(0x00740000L), true);
|
||||
Set<Function> accCallers = new LinkedHashSet<>();
|
||||
while (fi.hasNext()) { Function f = fi.next(); if (f.getEntryPoint().getOffset() >= 0x008a0000L) break; long sz = f.getBody().getNumAddresses();
|
||||
Set<Long> 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<Long, Map<String,Integer>> e : hits.entrySet()) { fw.println(String.format("== disp 0x%x ==", e.getKey())); for (Map.Entry<String,Integer> 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");
|
||||
}
|
||||
}
|
||||
99
ghidra/scripts/TechFx3.java
Normal file
99
ghidra/scripts/TechFx3.java
Normal file
|
|
@ -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<String> 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<Long> 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<Long> 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<String> 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");
|
||||
}
|
||||
}
|
||||
140
ghidra/scripts/TechFx4.java
Normal file
140
ghidra/scripts/TechFx4.java
Normal file
|
|
@ -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<String> 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<Long> 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<String> 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<String,Integer> 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<<k (k=0..4); BIO_UNIANTI -> 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<<i.");
|
||||
rename(0x004d7960L, null, "ComputeTechBitmasks", "out[0] = OR of bit for each researched tech in table 0x00adf378 (32 x {techId,bit}); out[1] = same over 0x00adf478 (29 entries). Stored at ServerPlayer+0x190/+0x194 (design/section availability masks).");
|
||||
rename(0x007505b0L, "Game::ServerSystem", "TradePointsToMoney", "money = ((trade - fmod(trade,5))*5 + PopIncome(imperial) + PopIncome(civilian) + SlaveIncome) * speciesdef.incomeFactor(+0x18) * owner.IncMod(+0x30c) * srv.IncomeMod(+0xbc)*DifficultyMods->+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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue