phase 2: address contract + generator, turn-internals filed, M0-M4 rows; sibling repo sots-engine
This commit is contained in:
parent
daea98feba
commit
2d298b1e98
7 changed files with 678 additions and 1 deletions
|
|
@ -46,3 +46,7 @@ A 4-agent crew (defined in `~/.claude/agents/re-*.md`) runs the exploration:
|
|||
- `ghidra/` — exported scripts + datatype archives.
|
||||
|
||||
Approach & north star: `findings/00-strategy.md`. Binary facts: `findings/01-fingerprint.md`.
|
||||
|
||||
## Sibling repo
|
||||
`alex/sots-engine` — the from-scratch engine source (clean-room, public-capable). This repo keeps the
|
||||
evidence + planning for both; binary facts cross over only via `ghidra/addresses.json` → `tools/gen_addresses.py`.
|
||||
|
|
|
|||
|
|
@ -32,4 +32,10 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| real save for verification | verify | verified | high | 100% | 2026-09-07 | 6 saves, 3 distinct turn states in verify/results/saves/ (byte-deterministic per state). Game runs on VM140 via DXVK 3.1 + lavapipe; recipe findings/subsystems/running-the-game.md |
|
||||
| save_reader.py | verify | verified | high | 100% | 2026-09-07 | --strict exit 0 on all 3 real saves; 0 resyncs / 0 hint-failures; only raw = RNG blob (2503 B). 29 tests. SAVE_FORMAT.md confirmed |
|
||||
| Ghidra type write-back | meta | verified | high | 100% | 2026-09-07 | structs saved in project (ServerSystem 87f, ServerPlayer 110f, StarFleet, StarShip, StrategyServer partial, 22 nested); 52 serializers + primitives + ~60 spine fns renamed; decompile shows field names |
|
||||
| strategic turn internals (economy/research/colony/movement/diplomacy) | subsystem | in-progress | — | 0% | 2026-09-07 | R5 breadth: formulas + constant->code map from ServerPlayer/ServerSystem::ProcessTurn, TechTree::ProcessResearch, MoveFleet |
|
||||
| strategic turn internals (economy/research/colony/movement/diplomacy) | subsystem | mapped | high | 90% | 2026-09-07 | findings/subsystems/strategic-turn-internals.md: budget/RP/trade/bankruptcy formulas, research rolls (unlisted race=1.0, Zuul x2), colony growth, movement, RNG=MT19937; ~600 g_KEY labels in Ghidra |
|
||||
| P2-M0 shim bootstrap | phase2 | in-progress | — | 0% | 2026-09-07 | sots-engine: binkw32 proxy + MinHook + trace hook on Application::Initialize; game to menu |
|
||||
| P2-M1 flat-KV config loader | phase2 | backlog | — | 0% | 2026-09-07 | CfgVar_RegisterKey 0x008b76a0; trace->compare->replace; save byte-identical oracle |
|
||||
| P2-M2 manifest/id registry | phase2 | backlog | — | 0% | 2026-09-07 | _weapons.txt/_shipsections.txt stable ids |
|
||||
| P2-M3 Mars brace-block parser | phase2 | backlog | — | 0% | 2026-09-07 | + .effect TXT reader; compare over full catalog load |
|
||||
| P2-M4 gobio VFS read | phase2 | backlog | — | 0% | 2026-09-07 | ZIP entries + NativeFileSystem override order |
|
||||
| loader prototypes (Ghidra) | meta | in-progress | — | 0% | 2026-09-07 | R6: cfgvar parser, manifest loader, brace-block parser entry+node model, gobio read -> ghidra/addresses.json |
|
||||
|
|
|
|||
|
|
@ -28,3 +28,4 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re-
|
|||
- **RESOLVED: save framing** — padding is joint; `"."` = NULL tag (VectorHelper count+elements, Vector3 bodies); empty strings are 4 zero bytes; `CDT` is a tagged frame + opaque `CD` frames. All three real saves parse `--strict` clean. Note: `SvSctOb` IS present in all saves (patch doc §7 was wrong). (from [[SAVE_FORMAT]])
|
||||
- **(parked) Combat loader job API** — does tactical-battle loading go through the job helpers `0x8fc160`/`0x8fa5b0` with the `TryEnterCriticalSection→Sleep(1000)` completion poll (`0x0071ea60`)? If yes, that is a plausible many-core stall (lost try-lock = +1 s per poll). End Turn also has a deliberate `Sleep(computed)` ≈0.85 s at `0x00838da9`. (from [[battle-load-profile]])
|
||||
- **Ship-design code rules (Ghidra, from SHIP_DESIGN_RULES.md §8)** — hidden default rider designs are built WITHOUT tech gating (where?); is section class-equality enforced or merely unoffered; confirm the turret class-accept table (`standard ← {standard,missile,grapple}`, `strafe ← {standard}`; grapple weakest); hull-class tech injection (`IND_CruisCon`/`IND_DreadCon`) + station exemption; option defaults/auto-upgrade and what `option_cost` multiplies; weapon cost per bank vs per mount; mass/speed formulas; `faiDes`/`dWep` meaning. (from [[SHIP_DESIGN_RULES]])
|
||||
- **Corrections from turn internals** — `ServerPlayer+0xf9` is the AI flag (not bTurnDone); `FUN_007d7f70` is encounter detection; ProcessTurn phase 2 is trade, not diplomacy; the spine's `FUN_00840fe0` 'income' is special projects — income is `FUN_00863030` ComputeBudget. `TECHBEN_*` data blocks are inert (effects hard-coded via the 116-entry tech-name table @0x00a19718). Open: TRA/TRP producers, `FPsp2` derivation, `PERGATETRAFFIC_*` readers, exact BnkEl/BnkPr expression. (from [[strategic-turn-internals]])
|
||||
|
|
|
|||
554
findings/subsystems/strategic-turn-internals.md
Normal file
554
findings/subsystems/strategic-turn-internals.md
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
# Strategic-layer turn internals — Sword of the Stars (2006), breadth pass
|
||||
|
||||
Program `sots` / "Sword of the Stars.exe" (ImageBase 0x00400000, MSVC-2010, 32-bit). Ghidra 12.0.4 headless.
|
||||
Inputs: `findings/control-flow/turn-spine.md`, `findings/objects/struct-recovery.md`, `findings/subsystems/data-model.md`.
|
||||
Scripts (CT111 `/root/`, copies in `/srv/re-lab/handoff/scripts/`): `TurnDump.java` (spine + direct callees),
|
||||
`TurnDump2.java` (config-key → global map with **`g_<KEY>` labels saved in the project**, RNG class, species,
|
||||
tech-name xrefs, float-constant table), `TurnDump3.java` (third-tier economy/colony), `TurnDump4.java` (.tech
|
||||
parser). Raw decompiles with string-literal substitution: `/srv/re-lab/handoff/turn-decompiles/{turn,turn2,turn3,turn4}/<addr>.c`
|
||||
(also `constants.txt`, `floatconsts.txt`, `rng.txt`, `species.txt`, `technames.txt`, `techtable.txt`, `disasm.txt`).
|
||||
All addresses are VAs. Function names in `Game::` are the ones already in the project; `FUN_xxxxxxxx` are still
|
||||
unnamed (my proposed names are given in *italics* and were **not** written back — only the `g_<KEY>` data labels were).
|
||||
|
||||
Conventions used below: `p` = `Game::ServerPlayer*`, `sys` = `Game::ServerSystem*`, `srv` = `Game::StrategyServer*`,
|
||||
`ftol()` = MSVC `_ftol2` (`FUN_00925220`, truncation), `ftoi64()` = `FUN_00925256`, `rand01()` = `Mars::RNG::NextFloat`,
|
||||
`randint(n)` = `Mars::RNG::NextInt`. Field names are the save-tag names from struct-recovery.md.
|
||||
|
||||
---
|
||||
|
||||
## 0. How data-file constants reach the code (applies to every section)
|
||||
|
||||
Every `KEY value` line of `Data/Strategy/StrategyVars.txt`, `Data/globals.txt`, `Data/species.txt`,
|
||||
`Data/encounters.txt` is registered by a **32-byte static-initialiser stub** in the still-undisassembled region
|
||||
0x009ab000–0x009c1400:
|
||||
|
||||
```
|
||||
push <parser> ; 0x008b7000 = int, 0x008b7020 = float, 0x008b70e0 = (angular/deg float), 0x008b7110 = colour "r g b"
|
||||
push "KEY"
|
||||
push &g_KEY ; the storage word (int or float) the code reads
|
||||
mov ecx, &cfgvar_KEY ; a 16-byte ConfigVar object
|
||||
call 0x008b76a0 ; ConfigVar::Register(name, &storage, parser) (proposed name)
|
||||
```
|
||||
|
||||
The file loader later walks the registry and overwrites the storage words, so **the data file is authoritative; the
|
||||
image contains fallback defaults**, several of which differ from the shipped file (image → file):
|
||||
`TRADE_SECTOR_SIZE` 7.0→10.0, `INDSYS_SYSTEM_ODDS` 0.1→0.04, `TRADE_RAID_ODDS_PLAYER` 0.2→0.07,
|
||||
`POPULATION_GROWTH_MOD` 1.0→1.2, `POPULATION_GROWTH_EXP` 2.0→1.85, `MORALE_EVENT_HISTORY_MAXEVENTS` 5→10,
|
||||
`DEFENCEFLEET_PLANET_RADIUS` 5000→1800, `DEFENCEFLEET_STATION_RADIUS` 3000→1000, `TRADE_FREIGHTER_*_BONUS_*`
|
||||
(50000/50000/25000/25000 → 50000/75000/25000/50000). A reimplementation must read the file, not trust "defaults".
|
||||
|
||||
Code reads the value through a pointer slot next to the storage (`*PTR_g_KEY`), e.g. `*(int*)PTR_g_SYSTEMBONUS_MINTURNS_00aeca10`.
|
||||
Every storage word now carries a `g_<KEY>` label in the Ghidra project (≈600 labels), so any future decompile shows
|
||||
which constant a function reads. The full key → storage → reader-function map is in
|
||||
`turn-decompiles/turn2/constants.txt`; the strategic subset is folded into the tables of §1–§5.
|
||||
|
||||
Species enum (from `FUN_0053b030` / `FUN_0053b110`, the name↔index switch used by `StrategyServer::Read/Write`):
|
||||
**0 Human, 1 Hiver, 2 Tarkas, 3 Liir, 4 _NPC, 5 Zuul, 6 Morrigi**. Every per-species table in the sim
|
||||
(`Morale.int[7]`, `adt[7]`, `ServerPlayer+0x348[7]`, tech-edge `avail[7]`, `PrisonerHold` counts) is indexed by this
|
||||
7-value enum; index 4 is the NPC/independent race, which is why "species 4" gets no imperial growth and the
|
||||
`INDSYS_IMPERIAL_POPULATION_MOD` cap (§3.2). (The `.rdata` name-table order Human..Morrigi is *not* the enum.)
|
||||
|
||||
---
|
||||
|
||||
## 1. Economy (per-player income → budget → savings)
|
||||
|
||||
### 1.1 Function map
|
||||
| addr | name (existing / *proposed*) | role |
|
||||
|---|---|---|
|
||||
| 0x00891340 | `Game::ServerPlayer::ProcessTurn` | per-player turn driver (order in 1.2) |
|
||||
| 0x00863030 | *`ServerPlayer::ComputeBudget(int out[25], bool projected)`* | THE income/expense formula; also builds the research allocation |
|
||||
| 0x00817990 | *`SatAdd(a,b)`* | saturating add clamped to ±2,000,000,000 — `Sav = SatAdd(Sav, budget.net)` |
|
||||
| 0x00751fb0 / 0x007521c0 | *`ServerSystem::ComputeOutput(int out[12])`* (projected / max) | per-system output & income (see 3.3) |
|
||||
| 0x00751bb0 | *`ServerSystem::ComputeOutputFromRates(out, rates[7])`* | the actual split formula |
|
||||
| 0x0083a520 | *`ServerTradeManager::GetPlayerTradeIncome(p)`* | Σ route incomes |
|
||||
| 0x00833790 | *`TradeRoute::Income(route, asOwner)`* | per-route income formula |
|
||||
| 0x00819d20 | *`ServerSystem::NumTradeRoutesSupported`* | ceil(civ/REQ_CIV)+ceil(imp/REQ_IMP), min 1 |
|
||||
| 0x0086b300 | *`ServerTradeManager::ProcessTurn`* | **ProcessTurn phase 2 is trade, not diplomacy** (freighter→route allocation) |
|
||||
| 0x007adc80 | *`StrategyServer::RegisterTradeSystems`* | phase 2b: every owned system whose owner has `CnTrd` registers `NumTradeRoutesSupported` routes |
|
||||
| 0x00817f90 | *`ServerPlayer::ConstructionSpend(limit)`* | Σ over build orders in state 1/2 of min(cost, ftol(order.+0x10 × …)) |
|
||||
| 0x00840fe0 | *`ServerPlayer::ProcessSpecialProjects`* | the spine's "income/savings (FUN_00840fe0)" is wrong — this is special projects ("SpecialProject: %s completed investigation phase…") |
|
||||
| 0x0059b490 | *`StrategyServer::GetDifficultyMods(p)`* | returns `srv+0x10` for humans/NPC, `srv+4` for AI (`p+0xf9`≠0 && !NPC): a 3-float table {maintenance divisor @+0, trade mult @+4, research mult @+8} |
|
||||
| 0x00818600 | *`ServerPlayer::UpdateBankruptcyLimits`* | `BnkEl`, `BnkPr` from Σ max system income |
|
||||
| 0x0080db10 | *`ServerPlayer::BankruptcyLevel`* | 2 if `Sav < BnkEl`, else (1 if `Sav < BnkPr`, else 0 — tail not decompiled) |
|
||||
| 0x007c0a50 | *`StrategyServer::ProcessBankruptcy`* | warnings, cost cutting (0x00889500), elimination |
|
||||
| 0x007d92a0 | *`StrategyServer::OnAllCombatDone_Tail`* | the deferred end-of-turn tail (calls bankruptcy, ProcessNodeSpaceTravel, `UpdateBankruptcyLimits`, …) |
|
||||
|
||||
### 1.2 `ServerPlayer::ProcessTurn` order
|
||||
1. `ComputeBudget(this, &B, false)`; `Sav = SatAdd(Sav, B.net)`.
|
||||
2. zero per-turn accumulators (`(+0x3d8)->+0x10`, `+0xcc`, `+0xc8`); if `+0xf9` → `ConstructionSpend` (AI path).
|
||||
3. `ProcessSpecialProjects` (0x00840fe0).
|
||||
4. research (§2): `if (ResT && !RollResearchAccident()) TechTree::ProcessResearch(tree, ?, B.researchAlloc, &B.overbudget)`.
|
||||
5. `TRA = TRM = TRP = 0`, `+0xe0 = +0xdc = 0` (per-turn contributions consumed).
|
||||
6. `RebAI` players: `RebOutMod = clamp(RebOutMod − 0.04, 1.0, 2.0)` (0x00a17870/68/6c).
|
||||
7. `PR` vector `{float PRm; int PRBt}`: `TRM += PRm; if (--PRBt <= 0) erase` — timed research-multiplier bonuses.
|
||||
8. `if (ResT && ResErrRoll && progress/cost > 0.5) { RollPlagueContainment (0x0088df20); ResErrRoll = false; }`
|
||||
(0x00820380 logs "Plague:/Base containment/Resch containment/Aggression/Odds" — the research-error roll is the
|
||||
plague-cure research mechanic).
|
||||
9. no `ResT`: list available techs (0x00584e50); if none but some tech is state 2 → "no research" event.
|
||||
10. 0x00863cf0 (special-project tail).
|
||||
|
||||
### 1.3 The budget (`ComputeBudget`, `out[]` indices as the code uses them)
|
||||
```
|
||||
[0] Sav
|
||||
[5] savingsInterest = Sav >= 0 && ownsSystems ? ftol(Sav * 0.01) : 0 // 0x009e31c0 (double 0.01)
|
||||
[10] debtInterest = Sav < 0 ? ftol(-Sav * 0.15) : 0 // 0x009ed188 (double 0.15)
|
||||
for each owned system s with !s.Abdn:
|
||||
inc = ComputeOutput(s)[3] (projected ? from current OutputRates : max) // money from the system
|
||||
[1] += max(inc,0); [7] += max(-inc,0)
|
||||
[2] tradeIncome = GetPlayerTradeIncome(p) // Σ TradeRoute::Income(r, owner)
|
||||
[4] (srv->vft[2]())->vft[1](p) // second manager's income — unidentified (spy/trade partner share?)
|
||||
[3] shipPopIncome = Σ fleets(p) Σ ships whose design.flags & 0x4000000 : Σ_type income of carried Population // slaver/colony ships
|
||||
[8] maintenance = Maint / ftol(DifficultyMods(p)->+0)
|
||||
[12] expenses = Σ Nexp.xmin + min( Σ clamp(ftol(?)-xmin, 0, xmax-xmin) , avail - Σxmin ) // Nexp = {xid,xmin,xmax,xper} entries (per-category expense sliders)
|
||||
[15] avail = max(0, [1]+[2]+[3]+[4]+[5]+[6] - [7]-[8]-[9]-[10]-[11]-[12]-[13]-[14])
|
||||
[11] construction = (p+0xf9==0 && avail>0) ? min(ConstructionSpend(p, avail), avail) : 0
|
||||
[16] researchMoney = max(0, ftol(avail' * ResRate)) // ResRate = research slider (0..1), 0 when 'projected'
|
||||
[17] researchPoints = ftol( DifficultyMods(p)->+8
|
||||
* ((researchMoney / 50.0) * 1.15 * 0.5 * 0.85) // 0x009e2328, 0x00a1a4b8, 0x009e20a0, 0x00a2d818
|
||||
* (ResMod + shrm + TRM) // player research multipliers (+0xc0,+0x160,+0xd0)
|
||||
* p->+0x22c * srv->ResMod // tech multiplier (float @0x22c) × game-option research modifier
|
||||
* ResScl ) // +0xc4
|
||||
i.e. RP ≈ money × 0.009775 × (ResMod+shrm+TRM) × techMult × srv.ResMod × ResScl × difficulty
|
||||
[18] = TRA, [20] = TRP, [21] totalRP = max(0, [17] + TRA + TRP) // TRA/TRP: per-turn RP contributions (trade research)
|
||||
aid (vector<PlayerAid> @+0x310, 0x18 stride): pctRes = clamp(Σ entries with +0xc>0 of +0x8, 0..100), savAid = Σ entries with +0x14>0 of +0x10
|
||||
[13] researchMoneyGiven = researchMoney * pctRes/100 ; [19] RPgiven = totalRP * pctRes/100 ; [21] -= [19]
|
||||
if (ResT) researchAlloc.push_back({ResT, [21]}); [9] = researchMoney - [13]
|
||||
[6] bonusIncome = ftol((p->+0x228 - 1.0) * remaining) // second tech multiplier (float @0x228) applied to remaining net
|
||||
[14] savingsGiven = min(max(remaining,0), savAid)
|
||||
net = [1]+[2]+[3]+[4]+[5]+[6] - [7]-[8]-[9]-[10]-[11]-[12]-[13]-[14]
|
||||
```
|
||||
So the "budget sliders" are `ResRate` (research share of net) and the `Nexp` entries; construction is not a slider —
|
||||
it is whatever the build queues demand up to `avail`. The UI pie chart (`PIECHARTCOLOR_INCOME/COST_FLEETS/COST_RESEARCH/
|
||||
COST_RESEARCH_AID/COST_FLEETS_QUEUED/COST_FLEETS_TOTAL`) is drawn from this same array.
|
||||
|
||||
### 1.4 Trade (`StrategyVars` → code)
|
||||
| key | reader | use |
|
||||
|---|---|---|
|
||||
| `TRADE_ROUTE_REQ_CIVPOPULATION`, `_IMPPOPULATION` | 0x00819d20 | routes a system supports = ceil(civ/REQ_CIV) + ceil(imp/REQ_IMP), min 1 |
|
||||
| `TRADE_ROUTE_STARTUP_TURNS`, `_STARTUP_INCOME` | 0x00833790 | route younger than N turns pays `STARTUP_INCOME` flat |
|
||||
| `TRADE_ROUTE_MIN_INCOME`, `_MAX_FREIGHTERS`, `_INCOME_PERFREIGHTER_{CRQ,CR,DE}` | 0x00833790, 0x0081a3e0 | income = MIN_INCOME + Σ_class min(n_class, capLeft) × PERFREIGHTER[class], class 0=CRQ,1=CR,2=DE, capLeft starts at MAX_FREIGHTERS |
|
||||
| `STATION_BONUS_TRADE_INCOME` | 0x00833790 | × (1 + bonus × #trade stations at the system (0x00815b10 type 4)) |
|
||||
| `ADDICTION_TRADE_MOD` | 0x00833790 | × mod when the partner is addicted |
|
||||
| `TRADE_ROUTE_OWNERS_SHARE` | 0x00833790 | owner gets share, partner (1−share), clamped 0..1; AI multiplier `DifficultyMods->+4` |
|
||||
| `TRADE_ROUTE_MAX_FREIGHTERS`, `STATION_TRADE_NUMROUTES` | 0x0086b300, 0x008209e0, 0x0083a5e0 | freighter allocation per turn; trade station adds NUMROUTES |
|
||||
| `TRADE_ROUTE_FREIGHTER_WARNING_TURNS` | 0x0088ad60 | warning event |
|
||||
| `TRADE_RAID_ODDS_{PLAYER,NPC,REFUGEE}`, `TRADE_{MIN,MAX}_NPCRAIDERS` | 0x00893290, 0x0088f070 | raid encounter generation |
|
||||
| `TRADE_FREIGHTER_{CAPTURED,KILLED}_BONUS_{DE,CR}` | 0x0080edd0, 0x0080ee00 | combat-result payouts |
|
||||
| `TRADE_SECTOR_SIZE` | 0x007c4140, 0x00841bf0, 0x008483c0, `BuildTurnEvents` | sector partition of the map |
|
||||
| `SLAVES_INCOME_MOD`, `_OUTPUT_MOD`, `_REPAIR_MOD` | 0x00535ca0 | fills the 3-row PopulationGroup-type table (0 imperial, 1 civilian, 2 slaves) at 0x00b104e8 (48-byte rows: caps 50M/20M, mult columns) |
|
||||
|
||||
### 1.5 Bankruptcy
|
||||
* `UpdateBankruptcyLimits` (end-of-turn tail and on load): `maxIncome = Σ_systems ComputeOutput_max(s)[3]`;
|
||||
`BnkEl = clamp(ftol(f1(maxIncome)))`, `BnkPr = max(BnkEl, −ftol(f2(maxIncome)))` where one of f1/f2 multiplies by
|
||||
`BANKRUPTCY_PROTECTION_LIMIT_FACTOR` (3.3) — the decompiler dropped the FP ops; shape: **the debt floor is
|
||||
−3.3 × maximum possible income** (matches the data-file comment).
|
||||
* `BankruptcyLevel`: 2 when `Sav < BnkEl` (elimination pending), 1 when below the protection limit, 0 otherwise.
|
||||
* `ProcessBankruptcy` (0x007c0a50, every non-eliminated non-NPC player, in the deferred tail): level≠0 →
|
||||
cost-cutting 0x00889500 (cancel build orders 0x0082be30, scrap ships 0x00839b70/0x008574a0, events
|
||||
`PTR_PTR_00af0a4c..`), sets `BnkWrn` (state) / `BnkTrn` (turn it started); state 2 and
|
||||
`ModCount − BnkTrn ≥ BANKRUPTCY_ELIMINATION_TURNS` → eliminate (0x007bd930(p,1,0,0)) + event. Debt interest 15 %/turn
|
||||
(above) is the other pressure.
|
||||
|
||||
Confidence: HIGH on the budget line items, the research-points formula (verified in disassembly 0x008635b5–0x00863629),
|
||||
trade income and route counts; MEDIUM on the meaning of `[4]`, `[6]`, `Nexp`, and the exact BnkEl/BnkPr expression.
|
||||
Open: what sets `TRA/TRP/TRM` (trade research?) and `p+0x228/+0x22c` (searched: written by the tech callback; not traced).
|
||||
Note for struct-recovery: `ServerPlayer+0xf9` behaves as **bIsAI** (selects AI difficulty table, skips human resume),
|
||||
not "bTurnDone".
|
||||
|
||||
---
|
||||
|
||||
## 2. Research
|
||||
|
||||
### 2.1 Function map
|
||||
| addr | name | role |
|
||||
|---|---|---|
|
||||
| 0x005876c0 | `Game::TechTree::ProcessResearch(tree, ?, vector<{techIdx,int points}>&, int* overbudget)` | progress + completion roll + decay + unlock events |
|
||||
| 0x0057da00 | *`TechTree::Cost(def)`* | `INT_MAX` if none; else `max(1, ftol(baseCost × CostMult(def)))` |
|
||||
| 0x0080db50 | *`ServerPlayer::TechCostMult(def)`* | `1.0 − 0.25 × n`, n = how many of three species-table "research bonus" techs (speciesdef[6]+0x78/+0x7c/+0x80) the player owns that apply to `def` (0x0057d220) — floor 0.25 |
|
||||
| 0x00581e10 | *`TechTree::SetResearched(def, flags)`* | state 4, turn/order stamps, player callback `vft+0x10(def, flags&4)`, unlock children |
|
||||
| 0x0057d8e0 | *`TechTree::PrereqsMet(def+0x88)`* | AND over groups of OR'd techs (each must contain a state-4 tech) — NOT a race roll |
|
||||
| 0x005822d0 | *`TechTree::BuildForSpecies(species, ?, mode, allowedList, rootDef)`* | **the per-race availability roll, done once at tree creation** |
|
||||
| 0x00889dc0 | `ServerPlayer::RollResearchAccident` | "ResearchBoost" lab accident |
|
||||
| 0x0047d830 / 0x004271c0 | *`Mars::RNG::NextFloat / NextInt`* | rolls (§6) |
|
||||
| 0x0058b050 | *`TechDef::Parse`* (.tech brace block: `allows`, `strategy`, `threat`, `requires`) | see 2.5 |
|
||||
|
||||
TechTree object: `+0xc` owner `ServerPlayer*`, `+0x10/+0x14` `vector<Node*>` indexed by tech id, `+0x20` research
|
||||
order counter. Node (0x34 B, ctor in 0x005822d0): `[0] TechDef*`, `+4..+0xc vector<Edge*> children`,
|
||||
`+0x14 state` (0 hidden, 1 parent researched, 2 available, 4 researched), `+0x18 costRP` (0x7fffffff = none;
|
||||
min over researched parents' edge cost), `+0x1c progress`, `+0x20 turnAvailable`, `+0x24 turnResearched`,
|
||||
`+0x28 order`, `+0x2c flag` (1 default, 0 "completed early", 2 "over-budget notified"). Edge: `+0x1c RP cost`,
|
||||
`+0x20 + 4×species` float availability (0..1), `+0x40 child TechDef*`.
|
||||
|
||||
### 2.2 Tree creation — the race probability
|
||||
`BuildForSpecies` walks from the roots; for every `allows` edge:
|
||||
```
|
||||
p = edge.avail[species] // parsed from "Human:50 Zuul:0 …" as percent/100
|
||||
include child if mode==2 (everything)
|
||||
or (p > 0 && (mode==1 || p >= 1.0 || rand01() <= p)) // mode 0 = normal game
|
||||
```
|
||||
then recurses into the child. Default for a species **not listed** on the `allows` line: see 2.5 (turn4 result).
|
||||
`mode` comes from the caller 0x00586600 (scenario/"all techs" option). So the famous per-game random tech tree
|
||||
is a single pass of `rand01() <= pct/100` per (edge, species) at game start; the strategic RNG is the server RNG (§6).
|
||||
|
||||
### 2.3 Per-turn progress (`ProcessResearch`)
|
||||
```
|
||||
for each {node, points} in alloc: // one entry: the current ResT with totalRP (from budget[21])
|
||||
cost = Cost(node.def); lo = cost*50/100; hi = cost*150/100 (lo≥0, hi≥lo)
|
||||
wasDone = cost <= node.progress
|
||||
spend = min(points, hi - node.progress); *overbudget += points - spend
|
||||
node.progress += spend; nowDone = cost <= node.progress
|
||||
if (node.progress < hi) {
|
||||
if (spend == 0) { odds = 0; roll = 1 } // nothing spent → cannot complete
|
||||
else { odds = (progress - lo) / hi; // 0 at 50 %, 1/3 at 100 %, 2/3 at 150 %
|
||||
roll = rand01(); // eps 0x009e1e68 == 0.0
|
||||
if (owner.Species == 5 /*Zuul*/) roll = min(roll, rand01()); } // Zuul roll twice, keep the better
|
||||
} else { odds = 1; roll = 0 } // ≥150 % of cost: guaranteed
|
||||
if (odds < roll) { // not complete
|
||||
if (!wasDone && nowDone && owner) { EVENT_RESEARCH_OVERBUDGET; node.flag = 2 }
|
||||
} else {
|
||||
log "Research completed at %d of %d (%.1f%%). (Odds: %.2f, Roll: %.2f)"
|
||||
if (progress/cost < 0.8) node.flag = 0 // 0x009e20c8
|
||||
SetResearched(node.def, 2)
|
||||
}
|
||||
for each node with state==2 && progress>0: progress = max(0, progress - ftol(Cost(node)*0.05)) // 0x009e5060: 5 %/turn decay
|
||||
for each node with state==2 && turnAvailable == currentTurn && parent researched → EVENT_TECHS_UNLOCKED (names list)
|
||||
```
|
||||
Net effect: the current tech gains `spend − 5 %·cost`; every other partially researched tech decays 5 % of its cost per turn.
|
||||
|
||||
### 2.4 Unlocking (`SetResearched`)
|
||||
state←4, `turnResearched = ModCount`, `order = tree.counter++`, owner callback `vft+0x10(def, flags>>2&1)` (this is
|
||||
where hard-coded tech effects are applied — see 2.6); for each child edge: child state 0→1, `child.cost = min(child.cost,
|
||||
edge.cost)`; then every node in state 1 whose `PrereqsMet(def+0x88)` holds becomes state 2 with `turnAvailable =
|
||||
ModCount`; a state-2 node with `Cost()==0` is researched immediately (recursive).
|
||||
|
||||
### 2.5 `.tech` parsing (`TechDef::Parse` 0x0058b050)
|
||||
Handles `name/family/type/threat/requires/allows/strategy/ship`. `allows "CHILD RP:cost Human:% …"` → Edge with
|
||||
`cost` and `avail[species] = pct/100`. **Default for unlisted species: 1.0 (always available).** The edge parser
|
||||
`FUN_00584160` allocates the 0x44-byte Edge with all seven `avail[]` floats (`+0x20..+0x3b`) preset to `0x3f800000`
|
||||
= 1.0 and `cost` (+0x1c) = 0, then overwrites only the species actually named on the `allows` line (name → index via
|
||||
`FUN_0053b110`, value = pct/100). So a race omitted from `allows` is guaranteed the tech; an explicit `Race:0`
|
||||
excludes it. Unknown child name → log `"%s cannot allow %s. %s not found."`. `+0x3c` = parent TechDef, `+0x40` =
|
||||
child TechDef.
|
||||
`strategy { inc/dec TECHBEN_X }` is stored as a `vector<std::string>` on the TechDef.
|
||||
|
||||
### 2.6 `TECHBEN_*` — dead data; effects are hard-coded
|
||||
Only one `TECHBEN_` string exists in the executable: `TECHBEN_STRATRANGE` (0x00a1a924), compared by
|
||||
0x00699bd0 (`_stricmp` over the TechDef's strategy-string vector). The data file spells it `TECHBEN_STRATRNGE`, so
|
||||
it never matches; the other 22 benefit names (`TECHBEN_INDOUTPUT`, `_HULLSTR`, `_SHIPCONCOST`, `_POPGROW`, …) have
|
||||
**no consumer at all**. The real strategic effects are keyed on tech *names* in code:
|
||||
* a 116-entry tech-name pointer table at **0x00a19718** (`IND_Waldo, IND_RefCoat, IND_ImpRfCt, IND_PlyAlloy,
|
||||
IND_StlthArm, …`; dump in `turn3/techtable.txt`) — index = internal `TechID` enum; 0x006965c0 looks a name up by
|
||||
`_stricmp` over the 116 entries;
|
||||
* a second 196-entry table at 0x009ff9e4 (starts `CCC_AdvSens`) resolved at load by 0x00581c10 into per-player
|
||||
TechDef pointers;
|
||||
* per-player **species-tech flag words** `ServerPlayer+0x348 .. +0x364` (7 × uint32, one per species index) set by the
|
||||
research callback and tested all over the colony code: bit0/1/2 slave-death techs, bit3 civilian-growth permission,
|
||||
bit5 addiction immunity, bit7 hazard-immunity (no suitability penalty), bit8 "may seed civilians";
|
||||
* explicit checks by name: `CCC_FCCom` (0x005744e0), `WEP_GrnLas/UvLas/XryLas` (0x006962b0, 0x0069d2c0),
|
||||
`IND_Waldo/IND_RefCoat` (0x006965c0); and the readers of `TRKSTL_REGENERATION_MOD` (0x007870d0),
|
||||
`ADVSENS_SENSORS_MOD` (0x0080b730, 0x0081e9d0, 0x008672c0), `STATION_BONUS_*`, `FLOCKDRIVE_*`.
|
||||
For a reimplementation: ignore `strategy{}` blocks; port the effect table by tech name.
|
||||
|
||||
### 2.7 Lab accidents (`RollResearchAccident`)
|
||||
Only when `ResT` is set and the research **boost** (extra funding, `ftol(...) > 0`) is non-zero:
|
||||
`roll = randint(100)`; odds computed from the boost; log `"ResearchBoost: Rolling for lab accident. Rolled %d. Odds %d. %s"`;
|
||||
no accident if `roll ≥ odds` (or the plague/containment path 0x00889d60 says so). Accident: if 0x0057d480 flags the
|
||||
tech as catastrophic → applied to every system (0x0059f1a0 loop); otherwise progress loss
|
||||
`ceil(clamp(rand01()×(maxLoss−minLoss)+minLoss, 0..1) × 100)` %, event `EVENT_LABACCIDENT_{SMALL,MEDIUM,LARGE}`
|
||||
by `def+0xac` size class (0/1/2). Research skipped that turn.
|
||||
|
||||
Confidence: HIGH (progress/odds/decay verified against disassembly 0x00587846–0x005878f0 and 0x00587c20–0x00587c90;
|
||||
tree roll from 0x005822d0). MEDIUM: cost-multiplier trio identity, accident odds function.
|
||||
|
||||
---
|
||||
|
||||
## 3. Colonies (`Game::ServerSystem::ProcessTurn` 0x007598e0)
|
||||
|
||||
### 3.1 Function map (in call order)
|
||||
| addr | name / *proposed* | what |
|
||||
|---|---|---|
|
||||
| — | inline | unowned: `Infra −= 0.02` (0x009e9170), floor 0 |
|
||||
| 0x00746780 | *`ApplyInfraBonus`* | `Infra += min(ibon, 1−Infra)`, `ibon −=` same; non-home system resets `rbtn` |
|
||||
| 0x0074b510 | *`ApplyPopBonus`* | `Pop += min(pbon, MaxPop(0,0) − Pop)`, `pbon −=` |
|
||||
| 0x007514f0 ×2 | *`IndependentPopDrift(0→1 / 1→0, indsp, mod)`* | independent systems: imperial↔civilian transfer (params `DAT_00aeca78`=10000, `DAT_00aeca7c`=0) |
|
||||
| 0x0074ad90 | *`IsStable`* | → `ntdev` (turns developing) ++ / 0 |
|
||||
| 0x0074d4f0 | *`AccrueSystemBonus`* | `SYSTEMBONUS_*` (below) |
|
||||
| 0x00756a90 | `ProcessPlague` | outbreak/cure/colony destroyed (no constants) |
|
||||
| 0x00752500 → 0x00890d50 | `ProcessBuildQueue` → `BuildQueue::ProcessTurn` | 3.4 |
|
||||
| 0x0074b230 | *`GrowImperialPop`* | 3.2 |
|
||||
| 0x00754220 | *`GrowCivilianPops`* + morale events | 3.2 |
|
||||
| 0x00745f30 | *`AdjustResources(delta, mode)`* | `Res/ARes2/MRes` (mode 3 = normal, 0 = `AMine` asteroid mining split) |
|
||||
| 0x007463f0 | *`RefuelInOrbit(1)`* | ships of fleets at the system in state 3 |
|
||||
| — | inline | `Bats2` / `rcex` 64-bit nibble arrays: per-player 4-bit countdowns (battle / recon cooldown) |
|
||||
| 0x007537b0 | `ProcessSlaves` | 3.5 |
|
||||
| 0x007583b0 | `ProcessRebellion` | 3.6 |
|
||||
| — | inline | addiction: per species `adt[i]` start turn vs `ADDICTION_PHASE2_START/PHASE3_START` → morale events 0x1b/0x1c/0x1d (skipped if player flag bit5) |
|
||||
|
||||
### 3.2 Population
|
||||
**Carrying capacity** (0x0074ab20 / 0x0074a4a0, species `sp`, group type `t` 0 imperial / 1 civilian / 2 slaves):
|
||||
```
|
||||
if sp == 4 → 0 // see open question
|
||||
if !SpeciesCanLive(sp) (0x0053bb40) → 0
|
||||
cap = ftoi64( Size × 1e8 × groupdef[t].+0x20 × SpeciesGrowthFactor(sp) (0x0053bb00)
|
||||
× (owner species ≠ sp ? CrossSpeciesMod (0x0053bb20) : 1)
|
||||
× HazardMod(Suit, IdealSuit(p), SuitTol) (0x00747ae0; 1.0 if player flag bit7 or RebAI) )
|
||||
+ (p.harcc(+0x104) ? 1e8 (imperial) / 2e8 (civilian) : 0) // 0x0080dd30
|
||||
cap = min(cap, groupdef[t].max @+0x28 if @+0x2c >= 0)
|
||||
if owner species == 4: cap *= INDSYS_IMPERIAL_POPULATION_MOD
|
||||
```
|
||||
**Growth per turn** (0x00748100 → 0x00537140 → 0x00536fb0):
|
||||
```
|
||||
if haltv[t] (blockade) → 0 ; if system flagged in player's +0xdc mask → factor 0x00a1b000 instead of 1
|
||||
g = clamp01( (1 − clamp01(Pop/cap)) ^ POPULATION_GROWTH_EXP )
|
||||
if g > 0: g *= POPULATION_GROWTH_MOD ; g *= p.PopMod ; g *= hazard/species factor ; g *= groupdef[t].+4 (if >0)
|
||||
delta = ftoi64(Pop × g) (min 1 if g>0), capped at 50,000,000
|
||||
imperial: Pop = min(cap, Pop + delta); if cap < Pop: Pop −= min(50,000,000, Pop − cap), floor min(Pop,100)
|
||||
```
|
||||
Civilians (0x00754220): per species with `+0x348` flag bit8 clear: if no group yet and cap > 0 → seed 1000 (own species
|
||||
via 0x007502d0, others 0x0074ec50 with `MORALE_DEFAULT_VALUE`); growth as above per group; totals capped at
|
||||
`0xfffffffffd050f80`-style guards; morale events 0x10 (growth) / 0x11 (decline) with the inlined event-id→delta table
|
||||
(ids 1,3,7,0xe,0x12,0x21 → −5; 2,9,0x18,0x25 → −10; 4,5,0xa,0xb,0x23 → +10; 6,0x11,0x1d,0x26 → −2; 8,0x20 → +5;
|
||||
0xc,0xd,0x14,0x24 → +2; 0xf,0x1a → −3; 0x10,0x19,0x1b → −1; 0x13,0x16,0x1c → +1; 0x15 → +3; 0x17 → −15; 0x1e → +30).
|
||||
|
||||
**System bonus** (0x0074d4f0; keys `SYSTEMBONUS_*`): when `IsStable` and `ModCount − TAcq > MINTURNS` and
|
||||
`rbtn > MINTURNS`: `pbon += min(POPBONUS_INC-derived amount, cap×POPBONUS(_HOME) − pbon)`, `ibon += min(INFRABONUS_INC,
|
||||
INFRABONUS(_HOME) − ibon)`; applied next turn by `ApplyPopBonus/ApplyInfraBonus`. `pbon2` is the civilian analogue.
|
||||
|
||||
### 3.3 Output → construction / infra / terraform / money (`ComputeOutputFromRates` 0x00751bb0)
|
||||
```
|
||||
rates = normalise(OutputRates{SRt trade, SRsc construction, SRtf terraform, SRi infra}) (0x00747390):
|
||||
entries < 0x009e22c8 → 0; SRtf → 0 if Suit == IdealSuit; SRi → 0 if Infra + ibon ≥ 1; rescale to Σ=1 (all-zero → equal split)
|
||||
total = round( BaseOutput × mods ) // 0x00750480:
|
||||
BaseOutput from pop (0x007483b0: (pbon+Pop)/… resource-availability factor, capped by species +0x4c)
|
||||
× morale (0x00746910: ≥MORALE_INCREASE_OUTPUT → ×MORALE_INCREASE_OUTPUT_MOD, ≤MORALE_DECREASE_OUTPUT → ×DECREASE_MOD)
|
||||
× (1 + STATION_BONUS_IMPERIAL_OUTPUT × stations) (0x0074b7a0)
|
||||
× ADDICTION_OUTPUT_MOD (phase ≥ 3)
|
||||
× p.ScOutMod(+0x12c) × p.RebOutMod(+0x128) × p.+0x224 × sys.OutMod × p.OutMod(+0x124)
|
||||
trade = round(total×SRt); cons = round(total×SRsc); terra = round(total×SRtf); infra = round(total×SRi)
|
||||
resources: need = ftol(f(total)) + CivilianConsumption (0x0074c6f0: CIVILIAN_RESOURCES_CONSUMED × civ-share curve)
|
||||
out[2] = min(need, Res [+ MRes + ARes2 if p.AMine]) → consumed by AdjustResources; out[1] = shortfall term
|
||||
construction: out[7] = cons × (1 + STATION_BONUS_SHIPCON × stations) (0x00746830); out[8] = min(out[7], queue demand);
|
||||
leftover → repair ships in orbit (0x00751590); out[9] = remaining
|
||||
unspent leftover L split back over trade/terra/infra by rates (or 1 / (Suit≠ideal) / (Infra≠1) when SRsc==1)
|
||||
infra: need = ceil((1 − Infra) / 3.3e-5); pool = infra + L_infra; spend = min(pool, need)
|
||||
out[10] = (spend / 500) × 0.01 × 1.65 = spend × 3.3e-5 → Infra += out[10] (≈30 300 points for 0→1)
|
||||
terraform: need = |Ideal − Suit| / (1.5 × 1.2 / 20000); pool = terra + L_terra + carry; spend = min
|
||||
out[11] = spend × 1.5 × 1.2 × p.TerraMod × (Suit > Ideal ? −1 : 1) / 20000 → Suit += out[11] (0x00748270)
|
||||
money: out[3] = ftol( Income(trade + L_trade) ) (0x007505b0: × species income factor (speciesdef+0x18) × p income mult (0x0080dd10) − p cost term (0x0080dd20))
|
||||
```
|
||||
`TnsOH` (turns over-harvesting) increments while `SRoh > 0 && out[1] > 0` (ProcessBuildQueue).
|
||||
|
||||
### 3.4 Build queue (`BuildQueue::ProcessTurn(queue, sys, points)` 0x00890d50)
|
||||
Orders are a `std::list` of `ShipBuildOrder{desID, con, sav, conleft, ordID, ShipDesign*}` processed FIFO:
|
||||
`if points < conleft { conleft −= points; break }` else (if `design.cost(+0xc0) > 0` the queue's owner must pay via
|
||||
`vft[9](sys, cost)`) create the ship (0x004f41a0), attach to the system's fleet (0x0059f1a0), refuel/repair/initialise,
|
||||
bump `ShipRecords` (per-class counters at `p+0x1b4`, per-design list at `p+0x1e4`), raise `SEBuildCompleted` with
|
||||
the `ShipBuildOrderDef`, `points −= conleft; conleft = 0`; finished orders are unlinked afterwards. Ship-borne queues
|
||||
(0x00789500) reuse it.
|
||||
|
||||
### 3.5 Slaves (`ProcessSlaves`; `SLAVES_*`)
|
||||
```
|
||||
rate = ( SRs × SLAVES_DEATH_RATE_BYOUTPUT + |IdealSuit − Suit| × SLAVES_DEATH_RATE_BYHAZARD + SLAVES_DEATH_RATE )
|
||||
× mod, mod = (flag bit0 ? 0.8 : 1) − 0.2×bit1 − 0.2×bit2 // 0x0074b110; flags = p+0x348[sp]
|
||||
deaths(sp) = clamp(ftoi64(slaves(sp) × rate), SLAVES_MIN_DEATHS, SLAVES_MAX_DEATHS (−1 = none))
|
||||
```
|
||||
when the last slave dies → `EVENT_SLAVES_DEAD`; pending slave-transfer object at `sys+0x2c0` consumed.
|
||||
`SLAVES_MAX_PERSYSTEM` is registered but has **no code reader** (dead key).
|
||||
|
||||
### 3.6 Rebellion / independence
|
||||
`ProcessRebellion`: `rbfl` (bitmask of rebelling species) — 0x0074fbe0 computes rebels per species (units of 1e6),
|
||||
0x00753c60 rolls new rebellions (per-species roll count `local_f4` × 0x008e6dd0), events `EVENT_SYSTEM_REBELLION_CONTINUES`
|
||||
with the same morale table; outcome 0 → 0x00756350 (suppressed), 1/2 → continues / 0x007522b0 (system lost).
|
||||
Independence: 0x00748fa0 = unowned system with civilians isolated ≥ `INDSYS_MINTURNS_ISOLATED` turns may turn
|
||||
independent; 0x0075fa10 drafts `INDSYS_DRAFTED_PERCENTAGE` of civilians into imperial pop; `INDSYS_MIN_CHAOS_POPULATION`
|
||||
in 0x007b9df0 (abandon/chaos); `INDSYS_SYSTEM_ODDS` at map generation (0x007471b0); `INDSYS_IMPERIAL_POPULATION_MOD` in
|
||||
the cap formula; `INDSYS_SURRENDER_MAXIMPERIAL` **has no reader** (dead key).
|
||||
|
||||
### 3.7 Morale
|
||||
`Morale` = `int[7]` per species at `sys+0x120` (default `MORALE_DEFAULT_VALUE`); colonies below
|
||||
`MORALE_DISABLED_POPULATION` are pinned at `MORALE_DISABLED_VALUE` (0x00749460, 0x0075b5c0); history capped at
|
||||
`MORALE_EVENT_HISTORY_MAXEVENTS` (`MORALE_EVENT_HISTORY_MINTURNS` has no reader). Output effect in 3.3.
|
||||
|
||||
Confidence: HIGH on the order of operations, growth curve, slave deaths, system bonus, infra/terraform point
|
||||
conversions; MEDIUM on the money-income tail (0x007505b0 lost its FP chain) and civilian seeding rules.
|
||||
Species index 4 (no imperial growth, cap × `INDSYS_IMPERIAL_POPULATION_MOD`) is `_NPC` — the independent/NPC race —
|
||||
per the enum in §0 (resolved).
|
||||
|
||||
---
|
||||
|
||||
## 4. Movement
|
||||
|
||||
### 4.1 Function map
|
||||
| addr | name | role |
|
||||
|---|---|---|
|
||||
| 0x007da9a0 | `StrategyServer::ProcessFleetMovement` | buckets fleets, several `MoveFleet` passes, gate traffic, `OnFleetArrived` |
|
||||
| 0x007d9ee0 | `StrategyServer::MoveFleet(fleet, dt)` | one movement step (recursive for leftover dt) |
|
||||
| 0x00705510 | *`NodeLine::Step`* | node-line travel with the "stutter" speed profile |
|
||||
| 0x00702e20 | *`ServerNodeGraph::FindNodeLines`* | candidate node lines from the fleet position |
|
||||
| 0x007b6700 | *`ProbabilisticJump`* | waypoint type 5 (uses `CstE`/`CstT`, RNG) |
|
||||
| 0x006ff6a0 | *`StarFleet::MinRange`* | min over ships of `StarShip.Range` |
|
||||
| 0x0074d1c0 | *`ServerSystem::FleetDeparts`* | removes fleet from system, ownership bits |
|
||||
| 0x007a0e20 | `ProcessNodeSpaceTravel` | node-space loss / multipoint checks (deferred tail) |
|
||||
| 0x0056e6e0 / 0x0056e720 | *`IsGateTransitWaypoint / IsNodeWaypoint`* | waypoint-type predicates |
|
||||
|
||||
### 4.2 `ProcessFleetMovement`
|
||||
Fleets are sorted into `std::set`s by state (departing, in-transit with node route, arriving, other). Passes:
|
||||
`MoveFleet(f, 0.5)` for set A; set B: 0x00706f10 + `MoveFleet(f, 0.5)` and arrival bookkeeping; `MoveFleet(f, 0.5)`
|
||||
again for A; every fleet in no set → `MoveFleet(f, 1.0)`; remaining set → `MoveFleet(f, 1.0)` (dt constants
|
||||
0x009e2ea0 = 0.5, 0x3f800000 = 1.0). Then `FPogn2` (fleet+0xec) := current waypoint's target position; per-player
|
||||
**gate traffic** `GTraf(+0x14c) = Σ fleet.+0xc0 (int16)` over fleets whose current waypoint is a gate transit;
|
||||
`OnFleetArrived` (EVENT_FLEET_ARRIVED); flag 0x100 ("moved this turn") cleared.
|
||||
|
||||
### 4.3 `MoveFleet(fleet, dt)`
|
||||
```
|
||||
if flag 0x100 or no waypoints → 0
|
||||
dest = ResolveWaypoint(wpts[0]) (0x00701390) ; 0 → log "Destination of fleet doesn't exist. Stopping fleet."
|
||||
if not departed yet (0x0078c9e0==0): for each ship still acting: log "Ship leaving %s is still doing %s. Cancelling action." + cancel (0x00849280); FleetDeparts(sys)
|
||||
switch wpt.Tp:
|
||||
5: ProbabilisticJump(fleet, dest): roll = rand01()×p.CstE; if roll > p.CstT → stop at fraction `roll` along the vector (event, pnd set) else arrive exactly
|
||||
4: teleport: pos = dest.pos (gate)
|
||||
2: node line: lines = FindNodeLines(fleet.pos, dest.pos, STUTTER_SYSTEM_INFLUENCE_RADIUS)
|
||||
step = NodeLine::Step(…, nodespeed = FPsp2, dt): along the line
|
||||
v = nodespeed × ((STUTTER_MAX_SPEED − STUTTER_MIN_SPEED) × (distToLineSystem / STUTTER_SYSTEM_INFLUENCE_RADIUS) + STUTTER_MIN_SPEED)
|
||||
else: step = FPsp2 × dt // FPsp2 = fleet strategic speed (+0xd8)
|
||||
range = MinRange(fleet) − 0.05 (0x00a1d2c0); if range < distance and MinRange(fleet)==0 → step 0 // out of fuel
|
||||
move = min(step, range, distance); pos += dir × move (snap to dest when move == distance)
|
||||
if !IsNodeWaypoint: (fraction used for recursion); for each ship without flag 0x1000: ship.Range −= move (floor 0); tankers (0x00703500/0x00829550/0x00854ad0) refuel
|
||||
if pos == dest.pos: SEFleetArrived {playerId, fleetId, destId};
|
||||
dest kind (+0x14): 0 system → 0x0074f240 enter, 0x007ac630, 0x007b4a30; more waypoints → continue via 0x00703730 (3 = blocked → stop) / 0x007d5080
|
||||
1 fleet → 0x0074f240/0x007ac630/0x00706f10; 2 point → 0x0083a3b0, 0x007b4920
|
||||
PrvPos = old pos if moved; if fraction < 0.9999 (0x00a261f0): MoveFleet(fleet, (1 − fraction) × dt) // multi-waypoint in one turn
|
||||
```
|
||||
Speed model: `FPsp2` is set when the flight plan is built (ApplyTurnCommands/`SetDestination`, not decompiled this
|
||||
round) from the design's engine section `ftlspeed` (straight-line races) or `nodespeed` (Human node lines; Hiver
|
||||
gates are type 4 teleports plus `PERGATETRAFFIC_*` capacity), with Morrigi flock bonus `FLOCKDRIVE_CLASSBONUS_{DE,CR,DN}`
|
||||
(+0.05/0.10/0.15 per class up to `_SHIPLIMIT`, × `_GRAVSYN_MOD`) applied in 0x006fe0e0 / 0x006ff370.
|
||||
Fuel: `StarShip.Range` is remaining strategic range; fleets cannot overshoot the slowest tank; `DefaultAutoRefuel`
|
||||
(game option) handles refuelling at friendly systems (0x007463f0). Node-space hazards (`EVENT_LOSTINNODESPACE_NOBORE/
|
||||
ENGINES`, `EVENT_FLEET_MULTIPOINT_NONODE`) are evaluated in `ProcessNodeSpaceTravel` from the deferred tail.
|
||||
|
||||
Confidence: HIGH on the step/clamp/arrival logic and the stutter formula; MEDIUM on the pass bucketing semantics and
|
||||
the identity of waypoint type 5 (a probabilistic jump using the player's `CstE/CstT` — consistent with the Zuul
|
||||
node-bore / Morrigi gravity casting; `CstR` unused here).
|
||||
|
||||
---
|
||||
|
||||
## 5. Diplomacy and per-turn upkeep
|
||||
|
||||
### 5.1 What actually changes per turn
|
||||
Diplomatic state is just `PlayerAlliances {ALid, AL, NA, CF}` at `ServerPlayer+0x168..0x174` (alliance id + three
|
||||
per-player bitmasks). It is changed **only by `ApplyTurnCommands`** (0x007b18b0): each player's 16-byte alliance record
|
||||
from the `SNMUpdate` TurnCommands is diffed against the current one — leave alliance (`ALid → −1`, events
|
||||
`PTR_PTR_00aee290/2a0`), join (`PTR_PTR_00aee2b0`; if the alliance now holds more than half the players a morale
|
||||
event fires), NAP bit set/cleared and CF bit set/cleared per pair (events at 0x007b1cxx/0x007b1exx) — then the four
|
||||
words are copied in. Nothing in `StrategyServer::ProcessTurn` itself is diplomacy: the spine's "phase 2 alliance/
|
||||
diplomacy upkeep" (0x0086b300 / 0x007adc80) is **trade** (§1.4).
|
||||
|
||||
### 5.2 `DiplomacyStats` (vector at `+0x230`, one entry per other player, 0x24 B)
|
||||
Updated by 0x007898c0 right after the alliance diff (called from `ApplyTurnCommands`):
|
||||
* for every pair with a current relation (0x0080e050: 1 ally, 2 NAP, 3 cease-fire) the matching `last*` int16
|
||||
(`lastally` @+0x10, `lastnap` @+8, `lastcf` @+0x18) is set to the current `ModCount`;
|
||||
* for every pair whose relation was broken this turn (three input bitmask vectors: alliance-broken, NAP-broken,
|
||||
CF-broken) the entry is fetched/created (0x008180e0) and, if the corresponding `last*` equals `ModCount − 1`
|
||||
(treaty made last turn and broken immediately) the betrayal counter `bty*`/`bkn*` is incremented.
|
||||
* `deadhome` (+0x20) is set elsewhere (home-system loss). 0x007acb50 dumps the "Alliances / Non Aggression / Cease
|
||||
fires" tables to the log.
|
||||
So `DiplomacyStats` is a **ledger for the AI** (consumed with `Data/Strategy/AI/stock_diplomacy_messages.csv`); there is
|
||||
no attitude/relationship score in the sim.
|
||||
|
||||
### 5.3 Other per-turn upkeep in the deferred tail (`OnAllCombatDone_Tail` 0x007d92a0 → `ProcessTurn` tail)
|
||||
`ProcessAid` 0x007ad100 (transfers from the `aid` vector, `EVENT_GIVE_SAVINGS/RESEARCH`, 100 % const 0x009e20e8),
|
||||
`ProcessSpecialProjects` 0x007a3310, `ProcessSurrenders` 0x007d0d10 (`EVENT_PLAYER_SURRENDERED_`, `EVENT_SYSTEM_SURRENDERED`,
|
||||
threshold 0.2 at 0x009e1fc0), `ProcessBankruptcy` (§1.5), `UpdateBankruptcyLimits`, `ProcessNodeSpaceTravel`,
|
||||
encounter-result application (per-encounter 0x74-byte records; 0x007d7f70 is **encounter detection**, not diplomacy:
|
||||
pairs of fleets at one location with relation 0 = war build the records), `ProcessStations` / `ProcessDefenceSats`
|
||||
(scuttle events, `STATION_MAINTENANCE_PENALTY` in 0x00814c80, `STATION_MINPOP_*` in 0x00815ce0/0x00815da0/0x0084be30).
|
||||
|
||||
Confidence: HIGH on where diplomacy state lives and changes; MEDIUM on the exact `DiplomacyStats` field ↔ relation
|
||||
mapping (index 8/0/4 → ally/nap/cf inferred from the int16 layout).
|
||||
|
||||
---
|
||||
|
||||
## 6. RNG (deterministic lockstep)
|
||||
|
||||
`Mars::RNG` (vftable 0x009e9aec = {dtor 0x004f7230, Read 0x008e6c30, Write 0x008e6ca0}) is a textbook **MT19937**:
|
||||
|
||||
| addr | *proposed name* | detail |
|
||||
|---|---|---|
|
||||
| 0x0049fdf0 | `Mars::RNG::RNG(uint seed)` | `mt[0]=seed; mt[i]=1812433253×(mt[i−1]^(mt[i−1]>>30))+i` (0x6c078965), then twist |
|
||||
| 0x0049cd20 | `Mars::RNG::SeedFromClock` | seed from `_time64`/`clock` |
|
||||
| 0x00426e00 | `Mars::RNG::Twist` (`__fastcall this=&mt`) | N=624 (0x270), M=397 (0x18d), MATRIX_A 0x9908b0df, three-loop form; resets `next=&mt[0]`, `left=624` |
|
||||
| 0x0047d830 | `Mars::RNG::NextFloat` | `y = *next++; left--` (twist when 0); tempering `y^=y>>11; y^=(y<<7)&0x9d2c5680; y^=(y<<15)&0xefc60000; y^=y>>18`; returns `(float)(y × 2^−32)` ∈ [0,1) (0x009e61b0 = 2^−32, 0x009e61b8 = 2^32 unsigned fix-up) |
|
||||
| 0x004271c0 | `Mars::RNG::NextInt(uint n)` | same tempering; rejection sampling with a power-of-two mask ≥ n → uniform [0, n) |
|
||||
|
||||
Layout: `{vptr @0; uint32 mt[624] @+4; uint32* next @+0x9c4; int left @+0x9c8}` = **0x9cc = 2508 bytes**; the
|
||||
`NextFloat/NextInt` methods take `this = &mt` (object+4) — that is why they index `+0x9c0/+0x9c4`. Serialisation
|
||||
(`"RNG"` tag in `StrategyServer::Write`, `StreamableHelper<Mars::RNG>`) writes **0x9c4 raw bytes = mt[624] + left**;
|
||||
`Read` restores both and recomputes `next = &mt[624 − left]` — the ~2500 B blob in saves is exactly this, so a
|
||||
reimplementation must reproduce MT19937 bit-for-bit and consume the stream in the same order. The strategy sim
|
||||
uses one instance at `StrategyServer+0x16c` (created in the ctor 0x007d78d0); every roll listed in this document
|
||||
(tree creation, research completion, lab accident, plague, rebellion, jump scatter, encounters, raids) draws from it,
|
||||
so given identical commands all lockstep peers stay in sync. The CRT `rand()` import exists but is not used by the sim.
|
||||
Consumers of the twist (19 functions, `turn2/rng.txt`) give the full RNG API surface (e.g. 0x007c2fa0, 0x007a7f30,
|
||||
0x0079f7d0, 0x007c4140, 0x007aa240 are StrategyServer-side users: map generation, encounters, raids).
|
||||
|
||||
---
|
||||
|
||||
## 7. Constant → code map (strategic keys; `turn2/constants.txt` has all ~600)
|
||||
|
||||
| key | storage | readers (addr) |
|
||||
|---|---|---|
|
||||
| BANKRUPTCY_ELIMINATION_TURNS | 0x00b23e24 | 0x007c0a50 ProcessBankruptcy |
|
||||
| BANKRUPTCY_PROTECTION_LIMIT_FACTOR | 0x00b23e28 | 0x00818600 UpdateBankruptcyLimits |
|
||||
| SLAVES_DEATH_RATE / _BYHAZARD / _BYOUTPUT | 0x00b21f84/88/8c | 0x0074b110 |
|
||||
| SLAVES_MIN_DEATHS / MAX_DEATHS | 0x00b21f90/94 | ProcessSlaves |
|
||||
| SLAVES_INCOME/OUTPUT/REPAIR_MOD | 0x00b0e9ac/b0/b4 | 0x00535ca0 (pop-type table), 0x007525c0 |
|
||||
| SLAVES_MAX_PERSYSTEM | 0x00b0e9a8 | **none** |
|
||||
| SYSTEMBONUS_MINTURNS / POPBONUS_INC / INFRABONUS_INC | 0x00aeca0c/34/3c | 0x0074d4f0 |
|
||||
| SYSTEMBONUS_POPBONUS(_HOME) / INFRABONUS(_HOME) | 0x00aeca14/24 / 1c/2c | 0x0074c680, 0x0074f1e0 / 0x00747740, 0x007477a0 |
|
||||
| POPULATION_GROWTH_MOD / _EXP | 0x00ae2e90/98 | 0x00536fb0 |
|
||||
| CIVILIAN_RESOURCES_CONSUMED | 0x00ae2ea0 | 0x0074c6f0 |
|
||||
| CIVILIAN_BURDEN_RATIO | 0x00aeca64 | 0x0057b6c0, 0x0074a6d0, 0x007fe470, 0x0080f570 |
|
||||
| MORALE_* | 0x00aec758.. | 0x0074ec50, 0x0074fbe0, 0x0075bde0 (default), 0x00749460/0x0075b5c0/0x00792750 (disabled), 0x00746910 (output) |
|
||||
| ADDICTION_PHASE2/3_START | 0x00aeca54/5c | ServerSystem::ProcessTurn, 0x00743b70, 0x00745e40 |
|
||||
| ADDICTION_INCOME_MOD / OUTPUT_MOD / TRADE_MOD / SPREAD_ODDS | 0x00aeca44/4c, 0x00af0c54/4c | 0x0074b700+0x0074d760 / 0x00750480 / 0x00833790 / 0x0082cca0 |
|
||||
| STATION_BONUS_IMPERIAL_OUTPUT / SHIPCON / RESEARCH / TRADE_INCOME | 0x00af08f0/e8, 0x00af0878, 0x00af0c3c | 0x0074b7a0 / 0x00746830 / 0x0081f930 / 0x00833790 |
|
||||
| STATION_MAINTENANCE_PENALTY, STATION_TRADE_NUMROUTES, STATION_MINPOP_* | 0x00af0880, 0x00af0c44, 0x00af08f8/900 | 0x00814c80; 0x008209e0, 0x0083a5e0; 0x00815ce0/0x00815da0/0x0084be30 |
|
||||
| TRADE_* | 0x00af0b90.. | see §1.4 |
|
||||
| INDSYS_* | 0x00b21f80, 0x00aec6c4.. | §3.6 (SURRENDER_MAXIMPERIAL: none) |
|
||||
| FLOCKDRIVE_* | 0x00b212b8.. | 0x006fe0e0, 0x006ff370 |
|
||||
| STUTTER_SYSTEM_INFLUENCE_RADIUS / MIN_SPEED / MAX_SPEED (globals.txt) | 0x00b212cc/d0/d4 | 0x00705280, 0x00705510, 0x00705940, MoveFleet |
|
||||
| SHIPSCUTTLE_*_OUTPUT_BOOST_FACTOR | 0x00b23e40.. | 0x00792480 |
|
||||
| SYSTEM_MIN/MAX_ASTEROID/MISC_RESOURCES | 0x00b23e4c.. | 0x00788850, 0x00798040 (map gen) |
|
||||
| TRKSTL_REGENERATION_MOD, ADVSENS_SENSORS_MOD | 0x00aeddd0, 0x00af0678 | 0x007870d0; 0x0080b730/0x0081e9d0/0x008672c0 |
|
||||
| DEFENCEFLEET_PLANET/STATION_RADIUS | 0x00af05a0/a8 | 0x00437140, 0x007f9080 (combat placement) |
|
||||
| STARMAP_NUMSYSTEMS_* | 0x00b23e34.. | 0x005ade80, 0x005bdae0, 0x00711fa0, 0x007171f0, 0x00719500/910/db0 |
|
||||
| PERGATETRAFFIC_DRV_TpGate / GatAmp | (not resolved this round) | gate capacity vs `GTraf` |
|
||||
| species.txt SENSORMOD_* | see constants.txt | sensor range |
|
||||
|
||||
Hard-coded literals worth knowing (`.rdata`): `0x009e1e68` = 0.0 (the "eps" everywhere), `0x009e1ef0` = 1.0,
|
||||
`0x009e20e8` = 100.0, `0x009e2328` = 50.0, `0x009e20a0` = 0.5, `0x00a1a4b8` = 1.15, `0x00a2d818` = 0.85,
|
||||
`0x009e20c8` = 0.8, `0x009e5060` = 0.05, `0x009e31c0` = 0.01, `0x009ed188` = 0.15, `0x009e9170` = 0.02,
|
||||
`0x00a1f930` = 3.3e-5, `0x009e2348` = 500, `0x00a1a448` = 0.01, `0x00a1f920` = 1.65, `0x009e90b8` = 1.5,
|
||||
`0x009e62b8` = 1.2, `0x00a0ffa8` = 20000, `0x009e5d70` = 0.25, `0x009e3030` = 0.8, `0x009e20d8` = 0.2,
|
||||
`0x00a1d2c0` = 0.05, `0x00a261f0` = 0.9999, `0x009e2ea0` = 0.5.
|
||||
|
||||
---
|
||||
|
||||
## 8. Corrections to earlier notes / open questions
|
||||
* `turn-spine.md` §2.4: phase 2 (0x0086b300/0x007adc80) is **trade**, not alliance/diplomacy upkeep; phase 8's
|
||||
"income/savings (FUN_00840fe0)" is **special projects** — income is 0x00863030 (*ComputeBudget*).
|
||||
* `turn-spine.md` §2.4 step 12 / §1.1: **0x007d7f70 is encounter detection** (pairs of co-located fleets whose
|
||||
relation `FUN_0080e050` == 0 (war) produce the 0x74-byte encounter records consumed by the combat round), not a
|
||||
per-team diplomacy pass.
|
||||
* `struct-recovery.md` §2 / `turn-spine.md` §1.1: **`ServerPlayer+0xf9` is the AI-controlled flag (bIsAI)**, not
|
||||
"bTurnDone_nonser": it selects the AI difficulty-modifier table in `GetDifficultyMods` (0x0059b490), gates the AI
|
||||
construction path in `ServerPlayer::ProcessTurn`, and is why `+0xf9==0` players get `Status=1` (resume) at turn end.
|
||||
* `struct-recovery.md`: the species enum has 7 values (index 4 = `_NPC`), so `Species` 5 = Zuul, 6 = Morrigi.
|
||||
* `data-model.md` §4.1: the `strategy{TECHBEN_*}` block is inert; race gating is rolled once at tree creation with
|
||||
unlisted races defaulting to 100 %.
|
||||
* Open: (a) `TRA/TRP/TRM` and `p+0x228/+0x22c` producers; (b) budget `[4]`/`[6]` semantics; (c) `FPsp2` derivation
|
||||
from `ftlspeed/nodespeed`; (d) `PERGATETRAFFIC_*` readers; (e) the money-income tail 0x007505b0; (f) lab-accident
|
||||
odds function; (g) exact BnkEl/BnkPr expression.
|
||||
* reva-server was stopped for each headless run and restarted after; no git commit was made; the only project
|
||||
write-back is the `g_<KEY>` labels (Save succeeded).
|
||||
31
ghidra/addresses.json
Normal file
31
ghidra/addresses.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"_schema": "facts about Sword of the Stars.exe (GOG 1.8.1, build 19833). rva = file address - 0x00400000. Runtime address = GetModuleHandle(NULL) + rva (exe is ASLR-relocated). Prototypes marked 'unverified' are best current reading; a Ghidra round confirms them.",
|
||||
"image_base": "0x00400000",
|
||||
"entries": [
|
||||
{"name":"Mars_Application_Initialize","addr":"0x008a0e50","convention":"thiscall","prototype":"void (Application* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"},
|
||||
{"name":"Mars_Application_Run","addr":"0x0089f5b0","convention":"thiscall","prototype":"int (Application* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"},
|
||||
{"name":"Process_PinAffinity","addr":"0x0089ee70","convention":"cdecl","prototype":"void (int coreIndex)","status":"unverified","source":"findings/objects/ghidra-recon.md"},
|
||||
{"name":"CfgVar_RegisterKey","addr":"0x008b76a0","convention":"thiscall","prototype":"void (CfgVar* this, void* parser, const char* key, void* storage)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#config-plumbing"},
|
||||
{"name":"DemoApp_OnTick","addr":"0x0089a640","convention":"thiscall","prototype":"void (DemoApp* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"},
|
||||
{"name":"StrategyClient_EndTurn","addr":"0x00783be0","convention":"thiscall","prototype":"void (StrategyClient* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"},
|
||||
{"name":"StrategyServer_BeginProcessTurn","addr":"0x007d98e0","convention":"thiscall","prototype":"void (StrategyServer* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"},
|
||||
{"name":"StrategyServer_ProcessTurn","addr":"0x007dc6c0","convention":"thiscall","prototype":"void (StrategyServer* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"},
|
||||
{"name":"ServerPlayer_ComputeBudget","addr":"0x00863030","convention":"thiscall","prototype":"void (ServerPlayer* this, int* budget25)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md"},
|
||||
{"name":"TechTree_ProcessResearch","addr":"0x005876c0","convention":"thiscall","prototype":"void (TechTree* this, ...)","status":"unverified","source":"findings/control-flow/turn-spine.md"},
|
||||
{"name":"ServerSystem_Read","addr":"0x0075d4b0","convention":"thiscall","prototype":"void (ServerSystem* this /*+8 IStreamable*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"ServerSystem_Write","addr":"0x00749630","convention":"thiscall","prototype":"void (ServerSystem* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"ServerPlayer_Read","addr":"0x008804d0","convention":"thiscall","prototype":"void (ServerPlayer* this /*+0x3a0*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"ServerPlayer_Write","addr":"0x008563e0","convention":"thiscall","prototype":"void (ServerPlayer* this /*+0x3a0*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"StarFleet_Read","addr":"0x00702470","convention":"thiscall","prototype":"void (StarFleet* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"StarFleet_Write","addr":"0x00701070","convention":"thiscall","prototype":"void (StarFleet* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"StarShip_Read","addr":"0x00853fa0","convention":"thiscall","prototype":"void (StarShip* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"StarShip_Write","addr":"0x008291f0","convention":"thiscall","prototype":"void (StarShip* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"StrategyServer_Read","addr":"0x007d27a0","convention":"thiscall","prototype":"void (StrategyServer* this, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"StrategyServer_Write","addr":"0x0079fa70","convention":"thiscall","prototype":"void (StrategyServer* this, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"Stream_WriteNetworkObjectId","addr":"0x00816490","convention":"thiscall","prototype":"void (Stream* this, const char* tag, NetworkObject* obj)","status":"unverified","source":"findings/objects/struct-recovery.md"},
|
||||
{"name":"RNG_Twist","addr":"0x00426e00","convention":"thiscall","prototype":"void (RNG* this)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#rng"},
|
||||
{"name":"RNG_Seed","addr":"0x0049fdf0","convention":"thiscall","prototype":"void (RNG* this, uint32_t seed)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#rng"},
|
||||
{"name":"RNG_NextFloat","addr":"0x0047d830","convention":"thiscall","prototype":"float (RNG* this)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#rng"},
|
||||
{"name":"RNG_NextInt","addr":"0x004271c0","convention":"thiscall","prototype":"uint32_t (RNG* this)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#rng"}
|
||||
]
|
||||
}
|
||||
61
ghidra/generated/sots_addresses.h
Normal file
61
ghidra/generated/sots_addresses.h
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ daea98f, 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>
|
||||
|
||||
namespace sots::addr {
|
||||
constexpr uint32_t IMAGE_BASE = 0x00400000;
|
||||
|
||||
// thiscall void (Application* this) [unverified]
|
||||
constexpr uint32_t Mars_Application_Initialize = 0x004a0e50;
|
||||
// thiscall int (Application* this) [unverified]
|
||||
constexpr uint32_t Mars_Application_Run = 0x0049f5b0;
|
||||
// cdecl void (int coreIndex) [unverified]
|
||||
constexpr uint32_t Process_PinAffinity = 0x0049ee70;
|
||||
// thiscall void (CfgVar* this, void* parser, const char* key, void* storage) [unverified]
|
||||
constexpr uint32_t CfgVar_RegisterKey = 0x004b76a0;
|
||||
// thiscall void (DemoApp* this) [unverified]
|
||||
constexpr uint32_t DemoApp_OnTick = 0x0049a640;
|
||||
// thiscall void (StrategyClient* this) [unverified]
|
||||
constexpr uint32_t StrategyClient_EndTurn = 0x00383be0;
|
||||
// thiscall void (StrategyServer* this) [unverified]
|
||||
constexpr uint32_t StrategyServer_BeginProcessTurn = 0x003d98e0;
|
||||
// thiscall void (StrategyServer* this) [unverified]
|
||||
constexpr uint32_t StrategyServer_ProcessTurn = 0x003dc6c0;
|
||||
// thiscall void (ServerPlayer* this, int* budget25) [unverified]
|
||||
constexpr uint32_t ServerPlayer_ComputeBudget = 0x00463030;
|
||||
// thiscall void (TechTree* this, ...) [unverified]
|
||||
constexpr uint32_t TechTree_ProcessResearch = 0x001876c0;
|
||||
// thiscall void (ServerSystem* this /*+8 IStreamable*/, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t ServerSystem_Read = 0x0035d4b0;
|
||||
// thiscall void (ServerSystem* this /*+8*/, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t ServerSystem_Write = 0x00349630;
|
||||
// thiscall void (ServerPlayer* this /*+0x3a0*/, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t ServerPlayer_Read = 0x004804d0;
|
||||
// thiscall void (ServerPlayer* this /*+0x3a0*/, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t ServerPlayer_Write = 0x004563e0;
|
||||
// thiscall void (StarFleet* this /*+8*/, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t StarFleet_Read = 0x00302470;
|
||||
// thiscall void (StarFleet* this /*+8*/, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t StarFleet_Write = 0x00301070;
|
||||
// thiscall void (StarShip* this /*+8*/, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t StarShip_Read = 0x00453fa0;
|
||||
// thiscall void (StarShip* this /*+8*/, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t StarShip_Write = 0x004291f0;
|
||||
// thiscall void (StrategyServer* this, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t StrategyServer_Read = 0x003d27a0;
|
||||
// thiscall void (StrategyServer* this, Stream* s) [verified-by-save]
|
||||
constexpr uint32_t StrategyServer_Write = 0x0039fa70;
|
||||
// thiscall void (Stream* this, const char* tag, NetworkObject* obj) [unverified]
|
||||
constexpr uint32_t Stream_WriteNetworkObjectId = 0x00416490;
|
||||
// thiscall void (RNG* this) [unverified]
|
||||
constexpr uint32_t RNG_Twist = 0x00026e00;
|
||||
// thiscall void (RNG* this, uint32_t seed) [unverified]
|
||||
constexpr uint32_t RNG_Seed = 0x0009fdf0;
|
||||
// thiscall float (RNG* this) [unverified]
|
||||
constexpr uint32_t RNG_NextFloat = 0x0007d830;
|
||||
// thiscall uint32_t (RNG* this) [unverified]
|
||||
constexpr uint32_t RNG_NextInt = 0x000271c0;
|
||||
|
||||
} // namespace sots::addr
|
||||
20
tools/gen_addresses.py
Executable file
20
tools/gen_addresses.py
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Emit include/generated/sots_addresses.h for sots-engine from ghidra/addresses.json.
|
||||
Only facts (name, RVA, convention, prototype, status) — never decompiler text."""
|
||||
import json, os, subprocess, sys, datetime
|
||||
root = os.path.join(os.path.dirname(__file__), "..")
|
||||
j = json.load(open(os.path.join(root, "ghidra", "addresses.json")))
|
||||
base = int(j["image_base"], 16)
|
||||
try: rev = subprocess.check_output(["git","-C",root,"rev-parse","--short","HEAD"]).decode().strip()
|
||||
except Exception: rev = "unknown"
|
||||
out = [ "// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).",
|
||||
f"// Source: sots-re ghidra/addresses.json @ {rev}, generated {datetime.date.today()} by tools/gen_addresses.py",
|
||||
"// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).",
|
||||
"#pragma once", "#include <cstdint>", "", "namespace sots::addr {", f"constexpr uint32_t IMAGE_BASE = 0x{base:08x};", "" ]
|
||||
for e in j["entries"]:
|
||||
rva = int(e["addr"],16) - base
|
||||
out.append(f"// {e['convention']:8s} {e['prototype']} [{e['status']}]")
|
||||
out.append(f"constexpr uint32_t {e['name']} = 0x{rva:08x};")
|
||||
out += ["", "} // namespace sots::addr", ""]
|
||||
dest = sys.argv[1] if len(sys.argv) > 1 else os.path.join(root, "ghidra", "generated", "sots_addresses.h")
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True); open(dest, "w").write("\n".join(out)); print("wrote", dest, f"({len(j['entries'])} entries)")
|
||||
Loading…
Add table
Reference in a new issue