250 lines
24 KiB
Markdown
250 lines
24 KiB
Markdown
# Ghidra write-back + turn/main-loop spine — Sword of the Stars (2006)
|
||
|
||
Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000. All addresses are VAs. Ghidra 12.0.4 headless,
|
||
project `/srv/re-lab/ghidra-projects/sots`; all changes below are **saved in the project** (four headless runs, each
|
||
"Save succeeded"). Scripts (CT111 `/root/`, copies in `/srv/re-lab/handoff/scripts/`): `SpineRecon.java`..`SpineRecon4.java`
|
||
(read-only recon), `WriteBack.java` (types + renames + labels), `FixThis.java` (class-struct merge + serializer `this`
|
||
views), `FixPlayer.java`. Raw decompiles used for the analysis: CT111 `/tmp/spine/*.c`; the verification decompiles are in
|
||
`/srv/re-lab/handoff/ghidra-verify/`.
|
||
|
||
Inputs: `findings/objects/struct-recovery.md` (member tables), `findings/objects/ghidra-recon.md` (affinity, vftables),
|
||
`findings/subsystems/ui-screen-map.md` §3 (SE*/SNM* names).
|
||
|
||
---
|
||
|
||
## 1. Type write-back
|
||
|
||
### 1.1 Where the types live (important for anyone continuing in Ghidra)
|
||
Ghidra resolves the `this` type of a `__thiscall` function in class namespace `Game::X` to the structure
|
||
**`/Game/X`** (`VariableUtilities.findOrCreateClassStruct`). The recovered structs were therefore merged *into those
|
||
class structs*, so every function placed in the class namespace decompiles with field names automatically:
|
||
|
||
| Ghidra data type | size | fields | note |
|
||
|---|---|---|---|
|
||
| `/Game/ServerSystem` | 0x2d8 | 87 | abs offsets from struct-recovery §1 (+ base vptrs/netId/owner) |
|
||
| `/Game/ServerPlayer` | 0x3e0 | 110 | §2; IStreamable sub-object at +0x3a0 |
|
||
| `/Game/StarFleet` | 0x120 | 21 | §3 |
|
||
| `/Game/StarShip` | 0xb0 | 26 | §4 |
|
||
| `/Game/StrategyServer` | 0x320 | 41 | **partial**: offsets from `StrategyServer::Write` + the spine functions (Frame/turn @+0xc, Systems @+0x44, Players @+0x54, Fleets @+0x64, Acts @+0x74, OnEventCallback @+0x170, StrategyEvents list @+0x2b8, bProcessingTurn @+0x2f1, …) |
|
||
| `/Game/Population` 0x14, `PopulationGroup` 0x18, `Morale` 0x20, `MoraleEvent` 0x50, `IndependenceInfo` 0x70, `PlayerColorID` 4, `ShipBuildOrder` 0x18, `DiplomacyStats` 0x24, `PlayerReport` 0x30, `PlayerAlliances` 0x10, `NodeRoute` 0x10, `FlightPlan` 0x38, `FlightPlan/Waypoint` 0x1c, `ShipHealth` 0x10, `PrisonerHold` 0x18, `EventStorage` 0x1c, `CivilianRatios` 0x2c, `ShipRecords` 0x44, `SpyReport` 0x34, `/Game/StarSystem/OutputRates` 0x1c, `/Game/StarSystem/PlayerView` 0x9c, `/Mars/Vector3` 12 | | | nested types from §1.1–§2.5 / §3.1 |
|
||
| `/SOTS/std::string` 0x1c, `std::vector` 12, `std::list` 8, `std::map` 8, `Game::FleetLayout` 0x24 | | | helpers. `std::string` layout **verified** from `Stream::WriteString` (`if (s->_Myres > 15) p = s->_Ptr`): `_Bx`@0 (16), `_Mysize`@0x10, `_Myres`@0x14, `_Alval`@0x18 |
|
||
| `/SOTS/Game::ServerSystem_ser8`, `StarFleet_ser8`, `StarShip_ser8`, `ServerPlayer_ser928` | | | **serializer views**: the same fields shifted by the IStreamable COL offset (8 / 0x3a0), because inside `X::Read/Write` `this = object + COL`. Applied (custom storage, `this` in ECX) to the eight Read/Write functions. For ServerPlayer only the +0x3a0.. tail is visible; the rest stays as `this + -0x378` etc. |
|
||
| `/Game/BuildQueue`, `TechTree`, `ShipDesign`, `Tech`, `CommMessageContainer`, `FleetNameGenerator`, `AIRebellion`, `AIEncounterFlags`, `ServerNodeGraph`, `ServerTradeManager`, `IServerSpyManager`, `AttribMap`, `SVScriptObject`, `StrategyEvent`, `/Mars/Stream` | 0 | | opaque, pointer targets only |
|
||
|
||
Every struct carries a description citing `struct-recovery.md`; gaps are left undefined. One extra member was added from
|
||
the spine work: `ServerPlayer+0xf9 bTurnDone_nonser` (tested by `StrategyServer::OnPlayerEndTurn`, not serialized), and
|
||
`ServerPlayer+0x164 Status` is the field `SNMSetPlayerStatus` writes (4 = "done", 1 = playing).
|
||
|
||
### 1.2 Renames (all with plate comments citing the finding doc)
|
||
*Serializers* (52): `Game::ServerSystem::Read/Write` (0x0075d4b0/0x00749630), `Game::ServerPlayer::Read/Write`
|
||
(0x008804d0/0x008563e0), `Game::StarFleet::Read/Write` (0x00702470/0x00701070), `Game::StarShip::Read/Write`
|
||
(0x00853fa0/0x008291f0), `Game::StrategyServer::Read/Write` (0x007d27a0/0x0079fa70), `Game::StarMapNode::Read/Write`
|
||
(0x00727790/0x00727820), `Game::StarSystem::PlayerView::Read/Write`, `::OutputRates::Read/Write`, `Population`,
|
||
`PopulationGroup`, `IndependenceInfo`, `Morale`, `MoraleEvent`, `ShipBuildOrder`, `PlayerNotes`, `SpyReport`,
|
||
`PlayerReport`, `DiplomacyStats::Write`, `FlightPlan`, `FlightPlan::Waypoint`, `NodeRoute`, `PrisonerHold`,
|
||
`EventStorage::Write`, `PlayerAlliances::Write`, `ShipHealth::Write`, `PlayerColorID::Write`, `Mars::Vector3::Write`,
|
||
`TechTree::Write`, `ShipRecords::Write`, `CivilianRatios::Write` — exactly the addresses in struct-recovery §0.
|
||
|
||
*Stream primitives* (`Mars::Stream::…`): `WriteString` 0x008b9d70, `WriteBool` 0x008b9c20, `WriteFloat` 0x008b9be0,
|
||
`WriteInt` 0x008b9d50, `WriteInt16AsInt` 0x008b9d00, `WriteInt8AsInt` 0x008b9cb0, `WriteInt64` 0x008b9c60,
|
||
`WriteHandleId` 0x00816490, `ReadFloat` 0x008b9bc0, `ReadInt` 0x008b9d20, `ReadBool` 0x008b9c00, `ReadString`
|
||
0x008b9d90, `ReadInt64` 0x008b9c40, `ReadInt16` 0x008b9cd0, `ReadHandle` 0x008164d0. Each plate comment lists the Stream
|
||
vftable slot map (+0x10 ReadIntRef, +0x14 ReadNested, +0x18 String, +0x1c Bool, +0x20 Float, +0x24 Int, +0x28 Nested,
|
||
+0x30 Raw). The `Mars::Stream` vftable itself was not located this round (the wrappers are the only anchor).
|
||
|
||
*Affinity / app*: `Process_PinAffinity` 0x0089ee70; `Mars::Application::Initialize` 0x008a0e50 (+ secondary label
|
||
`AppStartup_ReadConfig` at the same address, as requested); `WinMain` 0x0089dd30; `Mars::Application::Run` 0x0089f5b0;
|
||
`Mars::Application::PumpMessages` 0x0089f1c0; `Mars::Application::Application` 0x008a0170; `CreateAppWindow` 0x0089fe70;
|
||
`Mars::FrameTimer::Update` 0x0090c700; `Mars::TimerList::Dispatch` 0x008e5ac0; `Mars::AppStartup::OnConfigToken`
|
||
0x0089f4d0; `Mars::ConfigParser::ParseFile` 0x008cd820; `Game::DemoApp::{DemoApp 0x0089c950, OnStartup 0x0089d610,
|
||
OnShutdown 0x0089dfb0, OnUpdate 0x00898800, OnTick 0x0089a640, OnRender 0x00899210, ShouldSleepWhenInactive 0x008986a0,
|
||
CreateStrategyGame 0x00898b00}`. Globals: `g_pApplication` 0x00b2d540, `g_pDemoApp` 0x00b2d0bc.
|
||
|
||
*Threads, game creation, turn spine*: see §2/§3 (every function named there was renamed; ~60 functions).
|
||
|
||
*Network message registry*: 82 registry entries labelled `NetMsgReg_<name>` (EOL comment = id/factory), the 21 unnamed
|
||
factory functions renamed `Game::<Msg>::Create`, `Mars::NetMessageRegistry::Register` 0x008d2290,
|
||
`g_NetMsgRegistryById` 0x00b2ddf8. EOL comments on the three `CreateThread` sites.
|
||
|
||
### 1.3 Verification (re-decompile with the new types)
|
||
`Game::ServerSystem::Write` (0x00749630), `this` typed `ServerSystem_ser8*` — field names appear for every tag
|
||
(`/srv/re-lab/handoff/ghidra-verify/verify_ServerSystem_Write.c`, 125 `this->` references; Read: 205):
|
||
```c
|
||
void __thiscall Game::ServerSystem::Write(ServerSystem_ser8 *this, void *stream)
|
||
...
|
||
StarMapNode::Write(stream);
|
||
Mars::Stream::WriteFloat(pvVar3,"R",&this->R,0xffffffff,uVar5);
|
||
Mars::Stream::WriteFloat(pvVar3,"G",&this->G,0xffffffff);
|
||
Mars::Stream::WriteFloat(pvVar3,"B",&this->B,0xffffffff);
|
||
Mars::Stream::WriteFloat(pvVar3,"A",&this->A,0xffffffff);
|
||
Mars::Stream::WriteInt(pvVar3,"Idx",&this->Idx,0xffffffff);
|
||
Mars::Stream::WriteInt(pvVar3,"Size",&this->Size,0xffffffff);
|
||
Mars::Stream::WriteFloat(pvVar3,"Suit",&this->Suit,0xffffffff);
|
||
Mars::Stream::WriteInt(pvVar3,"Res",&this->Res,0xffffffff);
|
||
Mars::Stream::WriteInt(pvVar3,"ARes2",&this->ARes2,0xffffffff);
|
||
Mars::Stream::WriteInt(pvVar3,"MRes",&this->MRes,0xffffffff);
|
||
Mars::Stream::WriteBool(pvVar3,"NoRebAI",&this->NoRebAI,0xffffffff);
|
||
Mars::Stream::WriteInt(pvVar3,"TRes",&this->TRes,0xffffffff);
|
||
Mars::Stream::WriteInt(pvVar3,"Pop",&this->Pop,0xffffffff);
|
||
local_1c = &this->Pop2; local_24 = Mars::StreamableHelper<class_Game::Population>::vftable;
|
||
(**(code **)(*(int *)pvVar3 + 0x28))("Pop2",&local_24);
|
||
Mars::Stream::WriteFloat(pvVar3,"Infra",&this->Infra,0xffffffff);
|
||
Mars::Stream::WriteInt(pvVar3,"PvPop",&this->PvPop,0xffffffff);
|
||
...
|
||
```
|
||
`Game::StrategyServer::Write` (COL 0, plain `StrategyServer*`): `WriteString(stream,"KeyPath",&this->KeyPath)`,
|
||
`local_60 = (this->NodeMapLines._Mylast - _Myfirst)/0x14` (NMSz), `WriteInt(stream,"ModCount",&this->ModCount)`,
|
||
`WriteInt(stream,"Frame",&this->Frame)`, `("GameID",this->GameID)`, `this->Attrib`, `&this->GameName`, `this->Map`,
|
||
`&this->IncMod`, `&this->ResMod` … (69 refs). Spine functions after the merge: `BeginProcessTurn`:
|
||
`this->Frame = this->Frame + 1; this->bProcessingTurn = true; log("Begin processing turn %d", this->Frame)`;
|
||
`OnPlayerEndTurn`: `piVar10 = this->Players._Myfirst … if (this->OnEventCallback == NULL) … (*this->OnEventCallback)(id,0x27,&ev)`;
|
||
`ServerSystem::ProcessTurn`: `if (this->PID == NULL) this->Infra -= …; if (this->indi) iVar4 = this->indi->indsp; this->ntdev = 0`.
|
||
Every save tag in the decompile lines up with the field of the same name — the offset tables in struct-recovery.md are
|
||
confirmed by the decompiler itself.
|
||
|
||
---
|
||
|
||
## 2. Turn / main-loop spine
|
||
|
||
### 2.1 Entry → init → main loop
|
||
```
|
||
entry 0x00925794 → ___tmainCRTStartup 0x00925501 → WinMain 0x0089dd30 (hInst=0x400000, cmdline, nShow)
|
||
├ single-instance mutex "Kerberos_SwordOfTheStars_Mutex" (bypass: /concurrent); FindWindow+SetForegroundWindow otherwise
|
||
├ new Game::DemoApp (0x1b8) 0x0089c950 → Mars::Application::Application 0x008a0170 (g_pApplication = this)
|
||
├ Mars::Application::Initialize 0x008a0e50 ("AppStartup_ReadConfig")
|
||
│ CPU/ForceSingleCore > 0 → Process_PinAffinity 0x0089ee70 (SetProcessAffinityMask 1<<core)
|
||
│ "/startup:" cmdline → Mars::ConfigParser::ParseFile 0x008cd820 with Mars::AppStartup (vft[1] = OnConfigToken 0x0089f4d0; "conlevel")
|
||
│ display.cfg, audio.cfg, CoInitialize, Direct3DCreate9(0x20|0x1f), CreateAppWindow 0x0089fe70, DrawDevice 0x008df760, ClipCursor
|
||
│ timers → CreateEvent×2 + CreateThread(SoundSystem::StreamingUpdateThreadProc) prio 15 ← THREAD 2
|
||
│ GlobalConsts, textures, effect pool, sprites, keymap, font, SimplePainter, PanelManager 0x0090f1e0
|
||
│ IApplication::OnStartup (vft+0x10) = Game::DemoApp::OnStartup 0x0089d610
|
||
│ banner "Sword of the Stars 1.8.1 (Wed Dec 13 03:38:31 2017)", profiles, Mars::Network::Startup 0x00902470 → NetworkManager::Create 0x00902350 ← THREAD 1
|
||
│ ParticleSystemManager, Mesh, Model, StringTable, GUIResources, SoundSystem, sound_ui.csv, SpeechEvents, GUIAppearances, colour/badge/avatar tables, main menu / "/join"
|
||
└ Mars::Application::Run 0x0089f5b0 ← MAIN LOOP
|
||
do {
|
||
Mars::FrameTimer::Update 0x0090c700
|
||
PanelManager->vft+0x60() (UI/input update)
|
||
app->vft+0x18 = DemoApp::OnUpdate 0x00898800 (Mars network pump FUN_00902ad0; current game object(+0x158)->Update; strategy game (+0x150) → FUN_007879c0)
|
||
if (!focused && app->vft+0xc ShouldSleepWhenInactive) Sleep(20)
|
||
_controlfp(...); every _DAT_00a36860 s → Mars::TimerList::Dispatch 0x008e5ac0
|
||
ok = app->vft+0x1c = DemoApp::OnTick 0x0089a640 (sound/console/panels + top-level state machine: loads/unloads strategy game @+0x150 and combat @+0x158; 0 when quitting)
|
||
if (ok && DrawDevice) { dev->Begin; if (!dev->IsLost) { dev->+8; app->vft+0x20 = DemoApp::OnRender 0x00899210 (PanelManager->vft+0x64 draw / movie player); dev->+0xc; dev->Present } }
|
||
} while (Mars::Application::PumpMessages 0x0089f1c0 (PeekMessage/Translate/Dispatch, 0 on WM_QUIT) && ok);
|
||
```
|
||
The strategy game is created from the lobby via `DemoApp::CreateStrategyGame` 0x00898b00 → `StrategyApp::CreateGame`
|
||
0x00888e80 (GameOptions/AIProcessMinTime, DefaultAutoRefuel, starcolors.txt, `TurnCommands_v5`) → `StrategyServer`
|
||
ctor 0x007d78d0 (+0x170 OnEventCallback from a ctor argument), `StrategyServer::InitGame` 0x007c8d90 (SEInitGame,
|
||
SEAddDesign, SynchronizePlayer), `StrategyServer::LoadGame` 0x007dd530 ("loaded from file"), star-map UI
|
||
(FUN_00778f40 → … → `StarMapPanelBase` ctor 0x00741cd0 ← THREAD 3).
|
||
|
||
### 2.2 Event plumbing (how `SE*` reach the client)
|
||
`Game::SE*` are `Game::StrategyEvent` subclasses with a single virtual (dtor) — there is **no dynamic_cast dispatch**;
|
||
the server hands each event to a callback with an **integer event type**: `StrategyServer+0x170 OnEventCallback
|
||
(playerNetId, int type, StrategyEvent** ev)`; `StrategyServer::BroadcastEvent` 0x00789710 loops over `Players`.
|
||
Missing callback → "StrategyServer: OnEvent() called, but no callback function specified." (31 sites). Types seen:
|
||
`0x21 SETurnEndPending` (client side, `StrategyClient::RaiseEvent` 0x00783ee0), `0x24 SEProcessTurn`, `0x26
|
||
SEResumePlaying`, `0x27 SELastPlaying`. The client counterpart is the `StrategyClient` (vftable 0x00a2298c).
|
||
|
||
`SNM*` are network messages registered by static initialisers (`.text` 0x009be0cc–0x009c138c, code Ghidra had not
|
||
disassembled) via `Mars::NetMessageRegistry::Register` 0x008d2290 → `g_NetMsgRegistryById[id]` 0x00b2ddf8. SNM entry
|
||
address = 0x00b2bc94 + 12·id (`{name, id, factory}`), so a handler comparing `msg->GetType()->id` against
|
||
`DAT_00b2xxxx` is comparing against `entry+4`. Ids that matter for the turn:
|
||
|
||
| id | message | id | message |
|
||
|---|---|---|---|
|
||
| 0x29 | SNMUpdate (host → all: every player's TurnCommands) | 0x3f | SNMEndTurn (client → host) |
|
||
| 0x2a/0x2b | SNMHostCombat / Reply | 0x40 | SNMUpdateComplete |
|
||
| 0x2c/0x2d/0x2e | SNMLaunchCombat / JoinCombat / JoinCombatReply | 0x43/0x44 | SNMResumePlaying / …Received |
|
||
| 0x2f | SNMAllCombatDone | 0x3d / 0x3e | SNMRunAI / SNMKillAI |
|
||
| 0x30 / 0x31 | SNMEncounterQueryResults / SNMEncounterResults | 0x53 | SNMTurnInfo |
|
||
| 0x32 | SNMSetPlayerStatus | 0x5b | SNMQueryEndTurnDone |
|
||
| 0x3c | SNMDoEncounterQuery | 0x52 | SNMSetTimers |
|
||
|
||
(full table of 82 ids: `NetMsgReg_*` labels in Ghidra; ids 0x00–0x0f are the Mars transport `TNM*`/`FNM*`/`NM*`.)
|
||
|
||
### 2.3 The turn, end to end (addresses = renamed functions)
|
||
```
|
||
[strategic round] UI End Turn (FUN_005e4f80 / FUN_00579310)
|
||
→ Game::StrategyClient::EndTurn 0x00783be0 (marks +0x57/+0x132, QPC timestamp, EndTurnDelay 0x0076ab90)
|
||
→ RaiseEvent(0x21, SETurnEndPending) 0x00783ee0 [SETurnEndPending: "Done", cancellable → CancelEndTurn 0x007856f0 → SETurnEndCancelled]
|
||
→ SendEndTurn 0x00783980 → SNMEndTurn (factory 0x0088c7b0; TurnCommands @+4, AIEncounterFlags @+0x1b8)
|
||
StrategyClient::Update 0x007842b0: strategy timer → SETurnTimeExpired / EndTurnForced 0x00783d30 / SNMQueryEndTurnDone
|
||
|
||
[host] Game::StrategyNetworkClient::OnMessage 0x00784640 case 0x3f SNMEndTurn
|
||
→ StrategyServer::OnPlayerEndTurn 0x007d9af0 (if exactly one human still not done → SELastPlaying 0x27 to him)
|
||
→ StrategyServer::StorePlayerTurnCommands 0x007893c0
|
||
host StrategyNetworkServer::Update 0x007cda40 → when all done broadcasts SNMUpdate (0x29) carrying ALL TurnCommands
|
||
|
||
[every machine — lock-step sim] OnMessage case 0x29 SNMUpdate ("Processing turn update, but the local player is not done" guard)
|
||
→ StrategyServer::BeginProcessTurn 0x007d98e0 Frame++, bProcessingTurn=1, "Begin processing turn %d", reset fleet transients, BroadcastEvent(0x24 SEProcessTurn)
|
||
→ StrategyServer::ApplyTurnCommands 0x007b18b0 "set for turn processing": fleet orders, builds, research, alliances (EVENT_ALLIANCE_*)
|
||
→ FUN_007ad0f0, FUN_0078f6a0, FUN_0081ff40 (small, unnamed)
|
||
→ StrategyServer::ProcessTurn 0x007dc6c0 (dt = 1.0f) ← §2.4
|
||
→ FUN_008cfe20(server->GameID), FUN_007cf540; if local clients: StrategyApp::SyncLocalClients(1) 0x00815fd0 → StrategyServer::SynchronizePlayer 0x007c6220 per local client
|
||
→ client state (+0x58) = 5; SNMUpdateComplete (0x40) to host
|
||
|
||
[querying / combat round — host] StrategyNetworkServer::RunCombatRound 0x007cbe80
|
||
→ per encounter: SendEncounterQuery 0x007bfe60 → SNMDoEncounterQuery (0x3c) → clients: state 4 (C1 query UI, FUN_00847320)
|
||
→ SNMEncounterQueryResults (0x30) → "Notifying %s to host encounter %d" SNMHostCombat (0x2a) / Reply (0x2b) / SNMLaunchCombat (0x2c) / SNMJoinCombat (0x2d)
|
||
(client side: StrategyClient::Update logs "All(%d) clients connected to combat server", "Launching combat for encounter %d"; combat itself = DemoApp+0x158 object driven from DemoApp::OnTick)
|
||
→ SNMEncounterResults (0x31) … → all resolved: SNMAllCombatDone (0x2f) "All combat complete, waiting for clients to process results"
|
||
|
||
[every machine] OnMessage case 0x2f SNMAllCombatDone
|
||
→ FUN_00789330, FUN_007c0600 (1089 B, post-combat bookkeeping), StrategyServer::ApplyEncounterResults 0x007d4400 (EVENT_STATION_ENABLED/DISABLED …)
|
||
→ DispatchTurnResults 0x007cd2a0 → SendTurnResultsToPlayers 0x007c5850 → per player SETurnResults::Create 0x007a7ae0 + fill 0x007c24d0 + dispatch 0x0079ac10 [SETurnResults]
|
||
→ FUN_00761310, StrategyApp::SyncLocalClients(0)
|
||
→ StrategyServer::GenerateTurnEvents 0x007dc640 → BuildTurnEvents 0x007db780 (EventStorage::TurnEvents per player, SynchronizePlayer) [SETurnEvents]
|
||
→ autosave "(Autosave EndTurn)" (FUN_00895210 / "Auto saving of game failed."), FUN_0076c610, FUN_00769340, FUN_00781cf0
|
||
→ state = 6, "StrategyNetworkClient: All combat complete msg received. New turn begins..."
|
||
|
||
[resume] host StrategyNetworkServer::SendResumePlaying 0x00794770 → SNMResumePlaying (0x43)
|
||
→ OnMessage case 0x43 → StrategyServer::ResumePlaying 0x007ddc90: every player with Status(+0x164)==0 gets SEResumePlaying (0x26); reply SNMResumePlayingReceived (0x44); state = 1 (playing)
|
||
→ (SELastPlaying / SEResumePlaying / SETurnEvents drive the UI_LAST_PLAYING "new turn" sound and the S16 events list)
|
||
|
||
[AI] SNMRunAI (0x3d) → StrategyApp::RunAI 0x008706f0 ("RunAI: No StrategyServer created") → RaiseAIPrepareTurn 0x00815f20 → SEAIPrepareTurn; AI players' TurnCommands enter the same SNMEndTurn/SNMUpdate path.
|
||
```
|
||
Client state machine (`StrategyNetworkClient+0x58`): 1 playing → (End Turn) → 5 turn processed / waiting for combat →
|
||
4 querying (per encounter) → 6 all-combat-done → 1.
|
||
|
||
### 2.4 `Game::StrategyServer::ProcessTurn` 0x007dc6c0 — phases (decompiled with the new types, `ghidra-verify/verify_ProcessTurn.c`)
|
||
1. **Per-system pre-pass** over `Systems`: colony lookup (FUN_007437e0), morale-event push (FUN_00752a10), string-table text (FUN_00743530/FUN_008c97f0), `SESystemAbandoned` via FUN_007b9df0.
|
||
2. FUN_0086b300 + FUN_007adc80 (alliance/diplomacy upkeep; no strings).
|
||
3. Per-player pre-pass over `Players` (small).
|
||
4. **Movement**: fleet snapshot (FUN_00794ad0 / FUN_007b9b90), `ProcessNodeSpaceTravel` 0x007a0e20 (EVENT_FLEET_MULTIPOINT_NONODE, EVENT_LOSTINNODESPACE_NOBORE/ENGINES), **`ProcessFleetMovement` 0x007da9a0 → `MoveFleet` 0x007d9ee0 per fleet ("Destination of fleet doesn't exist. Stopping fleet.", cancels ship actions) → `SEFleetArrived`; `OnFleetArrived` 0x007ccb10 (EVENT_FLEET_ARRIVED)**.
|
||
5. Per-fleet → per-ship FUN_00814ea0 (ship upkeep).
|
||
6. **Per-system `ServerSystem::ProcessTurn` 0x007598e0**: infra decay if unowned, independence, pop/morale growth, `ProcessPlague` 0x00756a90 (EVENT_PLAGUE_OUTBREAK/CURED, COLONY_DESTROYEDBYPLAGUE), **`ProcessBuildQueue` 0x00752500 → `BuildQueue::ProcessTurn` 0x00890d50 → `SEBuildCompleted`** (with ShipBuildOrderDef), resources/terraforming (FUN_0074b230/FUN_00754220), `ProcessSlaves` 0x007537b0 (EVENT_SLAVES_DEAD), `ProcessRebellion` 0x007583b0 (EVENT_SYSTEM_REBELLION_CONTINUES).
|
||
7. FUN_0078a7c0.
|
||
8. **Per-player `ServerPlayer::ProcessTurn` 0x00891340 (dt)**: income/savings (FUN_00840fe0), **research**: if `ResT` set → `RollResearchAccident` 0x00889dc0 ("ACCIDENT!!"/"All okay.", EVENT_LABACCIDENT_*) then `TechTree::ProcessResearch` 0x005876c0 (EVENT_RESEARCH_OVERBUDGET, EVENT_TECHS_UNLOCKED), else EVENT_NO_RESEARCH; special projects FUN_00863cf0.
|
||
9. `ProcessMissions` 0x007999a0, `ProcessStations` 0x007ae480 (EVENT_STATIONS_SCUTTLED), `ProcessDefenceSats` 0x007af0b0 (EVENT_DEFSATS_SCUTTLED).
|
||
10. Per-ship flag pass: flag 4 → FUN_0080caf0(1); flag 0x400000 → FUN_00815230.
|
||
11. **Encounter detection**: second snapshot FUN_00794ad0 → `local_98` = pending encounters. **If none**: end-of-turn tail runs now — `ProcessAid` 0x007ad100 (EVENT_GIVE_SAVINGS/RESEARCH), `ProcessSpecialProjects` 0x007a3310 (EVENT_SPRJTECHOFFER_STARTED), `ProcessSurrenders` 0x007d0d10 (EVENT_PLAYER_SURRENDERED_, EVENT_SYSTEM_SURRENDERED), per-player FUN_00818530(0), script hooks `SvSctOb->vft+0x10(6)` / `(0x1c)`, FUN_0086a8d0, FUN_0078ab30, FUN_00799380, FUN_0078aa70, per-system FUN_00743ec0, FUN_007b4c00, FUN_007d7f70 (2.4 KB, per-team 0x74-byte records), and every player with `+0xf9==0 || +0xfa!=0` gets `Status(+0x164) = 1` (back to playing). **If encounters exist** the tail is deferred to the `SNMAllCombatDone` handler (§2.3), i.e. the turn's results/events are only produced after the combat round.
|
||
|
||
---
|
||
|
||
## 3. Threads (the three `CreateThread` sites)
|
||
|
||
| # | site | creator | thread proc | what it does |
|
||
|---|---|---|---|---|
|
||
| 1 | 0x0090242d | `Mars::NetworkManager::Create` 0x00902350 ← `Mars::Network::Startup` 0x00902470 ← `DemoApp::OnStartup` | `Mars::NetworkManager::WatchdogThreadProc` 0x00901f40 | `Sleep(10)` loop under `g_netCS` 0x00b2e5f8: pings host link (+0x20) and listener (+0x98), detects timeouts — "Network(%f): No response from host %s." / "...from client %s." — and drops the link. Pure liveness watchdog; never touches the sim. Globals `g_pNetworkManager` 0x00b2e61c, `g_hNetworkWatchdogThread` 0x00b2e620. |
|
||
| 2 | 0x008a14ef | `Mars::Application::Initialize` 0x008a0e50 ("Initializing Streaming sound update thread...") | `Mars::SoundSystem::StreamingUpdateThreadProc` 0x008ef1d0, **SetThreadPriority 15 = TIME_CRITICAL** | `WaitForMultipleObjects({app+0x84 wake, app+0x88 quit})`; under `g_musicCS` 0x00b2e4a8: if the current music stream (`g_pCurrentMusicStream` 0x00b2e4c4) has ended/faded (float +0x388 ≤ 0) → stop it and open the next queued track (`MusicPlayer::OpenMusicFile` 0x008ef040, "[%s] cannot open music file for playback"), else `StreamingSound::FillBuffer` 0x0091b5a0 (DirectSound GetCurrentPosition/Lock/decode; "Couldn't restore buffer"); then FUN_008b65d0 (sound-system update). Music/DirectSound streaming only — but it is the **only elevated-priority thread** in the process. |
|
||
| 3 | 0x00736e84 | `Game::BackgroundWorker::Start` 0x00736e30, first statement of `Game::StarMapPanelBase` ctor 0x00741cd0 (Render/StarMapBlobs_Solid.fx, StarMapBlobs_Glow.fx, POLMAP_* colours, Skysphere) ← `PoliticalMapPanel` ctor 0x007424b0 ← strategy-map screen ctor FUN_005e9780 ← … ← `StrategyApp::CreateGame` | `Game::BackgroundWorker::ThreadProc` 0x00735bb0 | `Sleep(10)` poll loop; struct `{CRITICAL_SECTION@0; HANDLE thread@0x18; job*@0x54; flags@0x58..0x5c (0x5b quit, 0x5c exited)}`; when a job is posted runs `StarMapBlobs::BuildBlobMesh_Job` 0x00732ab0 = political-map territory "blob" mesh (point cloud → centroid/spread → implicit-surface polygoniser FUN_008fc160 with callbacks FUN_00722010/FUN_0071ea60 → mesh FUN_008fa5b0), then sets done. **Strategic-map overlay only — not the battle loader** (prior recon's "prime suspect" is cleared). |
|
||
|
||
Consequences for the battle-load / many-core lead: the combat load and the whole sim run on the **main thread** (DemoApp::OnTick
|
||
state machine → combat object at `DemoApp+0x158`, `OnUpdate` → `+0x158->vft+4`). The only application threads are two 10 ms
|
||
pollers and a TIME_CRITICAL audio streamer; everything else multi-threaded is inside D3D9/DirectSound/driver. If the
|
||
slowdown is contention, the candidates inside the game's own code are the `g_musicCS` critical section (held by the
|
||
TIME_CRITICAL thread while decoding) and the `Sleep(10)` pollers — not a sim worker.
|
||
|
||
---
|
||
|
||
## 4. Caveats / open items
|
||
- Names are inferred from strings/callers (no symbols). `StrategyNetworkClient::OnMessage` (0x00784640) is the combined
|
||
host+client message handler (host branches test `+0x54 = StrategyServer*`); the "StrategyNetworkServer" functions live
|
||
on the host object driven from `DemoApp::OnUpdate`.
|
||
- `StrategyServer` struct is partial (41 fields); `ServerPlayer::Read/Write` show only the +0x3a0.. tail by name.
|
||
- Not named: FUN_007ad0f0/FUN_0078f6a0/FUN_0081ff40/FUN_007cf540 (between ApplyTurnCommands and ProcessTurn),
|
||
FUN_00789330/FUN_007c0600 (AllCombatDone pre-pass), FUN_0086b300/FUN_007adc80/FUN_0078a7c0/FUN_007d7f70 (turn phases
|
||
without strings), the client-side event sink behind `StrategyClient::RaiseEvent`, and the `Mars::Stream` vftable.
|
||
- The static-initialiser region 0x009be000–0x009c1400 is still undisassembled code in the project (only labels were
|
||
added); running "Disassemble" there would expose all 82 registrations as functions.
|
||
- reva-server was stopped for the headless runs and restarted at the end. No git commit was made.
|