board+roadmap: lane E1 income 25/25; correct roadmap item 1 - the turn path uses ComputeOutput, not ComputeMaxIncome
This commit is contained in:
parent
bebdee1ceb
commit
3f014a42eb
3 changed files with 97 additions and 3 deletions
|
|
@ -44,8 +44,7 @@ turn, after the autosave**. `Summary.Checksum` is fed by `ModCount`. So, two run
|
|||
|
||||
| # | engine deliverable | module | RE input (done unless noted) | closes |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **income chain**: `TradePointsToMoney` (`incomeModifier/14000`) + AI difficulty ×1.1 | `game/sim` | lane N/Y; 25-record `BnkEl` oracle, 6/25 match now; **multiplier not on the wire — one Ghidra read** | P01/P02/P03/P05/P06/T31, ~82 of 204 leaves, 2 RNG words |
|
||||
| 2 | **wire `ShipCensus` + alliance mask into `BuildTurnRecord`**, commit T36 | `app` | lanes D2/A2, 480+560 fields 0 mismatch | +24 turnstats leaves, clean |
|
||||
| 1 | ~~income chain~~ **DONE, and it did not unblock the phases** — `TradePointsToMoney` + difficulty ×1.1 landed at **25/25** on the `BnkEl` oracle (lane E1). But `ComputeBudget`'s **turn path** calls `ComputeOutput` with the system's own `Rts` sliders, **not** `ComputeMaxIncome` — a strictly larger function where the repair pass and the unspent-industry/terraform cascades are live. **The new item 1 is `ComputeOutput` on the turn path.** | `game/sim` | lane E1; `findings/subsystems/income-term.md` | P01/P02/P03/P05/P06, ~82 leaves, 2 RNG words |
|
||||
| 3 | **encounter-detection draws** in the generator model | `game/sim` + `app` | lane I bound; **lane H measuring now** | last 2 RNG words |
|
||||
| 4 | **`nve` visibility record** | `game/sim` | one mechanism ×8, unread | 32 leaves |
|
||||
| 5 | **post events into the save's turn bucket** (P11 + tail event phases) | `app` + `game/events` | model exists; wiring only | events subtree |
|
||||
|
|
|
|||
|
|
@ -245,3 +245,8 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| THE GATE IS AFlags - and the near-miss is the finding | verify | verified | high | 100% | 2026-09-08 | NVE is a std::map<playerIdx, {int16 ETS, int32 Eid}> - what this player LAST SAW at this system: turn of sighting + the encounter type that was there. Writer 0x00756300 (ETS = server->Frame), called from tail phase 17 under one gate, BYTE-DECODED at 0x007cf7a7..0x007cf7ce, not taken from the decompiler. **THE GATE IS `AFlags`** - the DERIVED, NON-STICKY union FFlags|GFlags|isOwner - NOT VFlags, NOT EFlags. That is load-bearing: **all three masks agree on nearly every system of every save, so a model on the wrong one LOOKS RIGHT**. zuul-turn23-fleet23.sav's Bismol separates them, and the engine reproduces the freeze on the save itself (ETS and ltis stay at 22 while every other system moves to 24) - with a host test that asserts BOTH the freeze AND what the wrong gate would have produced |
|
||||
| ltis writer NAMED (was an unnamed offset) | objects | verified | high | 90% | 2026-09-08 | Lane E3 names a writer this board recorded as unnamed: ltis's writer is 0x00743ec0, `if AFlags != 0: ltis = Frame` (decompiler-level, not byte-decoded). Also EFlags |= AFlags in tail phase 21. AND lane B5's flagged indirect edge is RESOLVED: SetExploredBy's vft[0x1c] is ServerSystem vtable slot 7 = 0x007480b0, and it writes only an UNSERIALISED runtime mask |
|
||||
| visibility: what remains and the workloads | verify | backlog | — | 0% | 2026-09-08 | (1) **Eid's source is a HYPOTHESIS** - the original reads StarSystem+0x184, set once at map generation and NOT ON THE WIRE; the engine recovers it from the encounter fleet's FtEnc and agrees on all six encounter fleets in the corpus. NEEDS: a save where an encounter fleet is DESTROYED at a system that stays visible. (2) **NVO.TShn** (10 leaves/pair) EVALUATED AND REPORTED, NOT WRITTEN - its gate is demonstrably NOT AFlags (Spica has AFlags==0 and TShn moves anyway); needs a watchpoint on the +0x274 map, with Spica vs Bismol as the discriminating pair. (3) **rcex** (6 leaves/pair) toggles 0 -> 1<<16 -> 0 with this cluster; UNEXPLAINED, UNASSIGNED. (4) NEVER EXECUTED BY ANY SAVE: the map's multi-entry ordering (every corpus system has a single-bit AFlags) and the alliance intel-sharing rule 0x00754d90 (no save has an alliance) - needs a two-empire-contact save |
|
||||
| income chain 25/25 - and the roadmap's item 1 was WRONG | subsystems | verified | high | 90% | 2026-09-08 | Lane E1 took the BnkEl oracle from **6/25 to 25/25**. THE MULTIPLIER RESOLVES FULLY: StrategyServer::IncomeDifficultyMod 0x0080f470 returns f32(f32(triple[1]) x f32(server->IncMod)) where IncMod is the Sim block's own tag, ON THE WIRE; DifficultyMods::Select 0x0059b490 returns the AI triple iff p->[0xf9] && !p->NPC; ServerPlayer+0x36c is an UNNAMED UNSAVED pointer filled by ServerPlayer::Read via LoadDifficultyRow. **THE TABLE IS BUILT IN CODE** (BuildDifficultyTable 0x005a3870) from .rdata float literals - no data file, no GlobalConst key, the same shape lane N found for the pop-type table; id 1 (which EVERY corpus save carries) is AI {3.0, 1.1, 1.5}. Q3 WAS WRONG about the third column: it is a RESEARCH multiplier, not a trade one, and the first is a fleet-maintenance DIVISOR. **BUT P01/P02/P03/P05/P06 ARE NOT UNBLOCKED AND MY ROADMAP ITEM 1 WAS WRONG**: ComputeBudget at 0x008631fd BRANCHES - projected mode uses ComputeMaxIncome, THE TURN PATH USES ComputeOutput 0x00751fb0 WITH THE SYSTEM'S OWN Rts SLIDERS, where the repair pass runs and the unspent-industry/terraform cascades into the money channel are live. A strictly larger function, and nothing in the corpus states its answer. The RNG reason narrowed from "a system's money is unmodelled" to "the PROJECTED-RATE money is" |
|
||||
| E1's own prediction was wrong, and recorded as wrong | verify | verified | high | 100% | 2026-09-08 | Lane E1 predicted the 12 remaining Zuul misses were the suitability cost. THEY WERE NOT - every corpus colony sits EXACTLY at its species' ideal, so CalcSuitMod is 0 and that whole term is UNEXERCISED. The misses were in the OUTPUT half: SpeciesDef +0x4c/+0x50 are PER SPECIES (0/10 Human & Tarkas, 10/40 Zuul) and the predictor carried them as one global pair; 400 output points = 2000 money per Zuul colony, and the observed 4400 and 5566 deltas fall out TO THE UNIT. FALSIFICATION ACTUALLY RUN: model 25/25; "nobody is AI" 14/25 (the 11 AI records break); "both real players AI" 14/25 (the 11 human records break, each by x1.1); human species pair forced on Zuul 0/2 |
|
||||
| T31 unblocked in substance, still listed blocked | verify | mapped | high | 90% | 2026-09-08 | closed 0 / regressed 0 - it self-checks every run against the save's own BnkEl (8/8, 7/7 ... on ALL 11 saves) and closes nothing because the limits move between turn1 and turn2 from CIVILIAN POPULATION GROWTH, which is not committed, so our value equals the input's. Still blocked on two named things, NEITHER THE FORMULA: ServerPlayer+0xf9 (is-AI) is a game-setup input not on the wire (--ai-player N supplies it), and BnkPr needs BANKRUPTCY_PROTECTION_LIMIT_FACTOR from the data files - committing it from a zero constant REGRESSED a leaf in the first measurement, so it is now gated on haveTuning |
|
||||
| NEW WIRE FACT: ISsp/ISsu is server->IdealSuit[] | objects | verified | high | 100% | 2026-09-08 | Lane E1: the Sim block's ISsp/ISsu IS server->IdealSuit[] (0x0080f4b0, raw base +0xf8), randomised per game by the map generator, cross-checking against every ServerPlayer.IdealSuit in all 11 saves. **So the suitability cost's ideal needs no data file.** struct-recovery.md 5 lists the tags without naming them |
|
||||
| income chain coverage - read before quoting 25/25 | verify | backlog | — | 0% | 2026-09-08 | Lane E1's own caveat list: UNTESTED are the suitability cost (x0 everywhere - every corpus colony sits at its species' ideal), slaves, addiction, morale, stations, the civilian capacity surplus, difficulty levels 0 and 2, and the maintenance/research columns of all three difficulty rows. `aidf` is 1 on all 25 records |
|
||||
|
|
|
|||
|
|
@ -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 @ 728348b, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ bebdee1, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -1319,6 +1319,52 @@ constexpr uint32_t StrategyNetworkClient_off_Server = 0x00000054;
|
|||
constexpr uint32_t StrategyServer_AbandonChaosCheck = 0x003b9df0;
|
||||
// thiscall void (StrategySim* this /* S+4 */, ...) // the fleet-move command handler; logs "StrategySim: Fleet not found.", "StrategySim: Waypoint %d(id) not found." and "StrategySim: (see above) cannot move fleet %d(id)." Bumps ModCount at 0x008657aa. THE ONLY ModCount WRITER OTHER THAN THE ABANDON CHECK THAT IS DIRECT-CALL REACHABLE FROM EITHER TURN DRIVER (from OnAllCombatDone_Tail's 1369-function closure; not from ProcessTurn's 1382) [verified]
|
||||
constexpr uint32_t StrategySim_MoveFleetCommand = 0x00465780;
|
||||
// thiscall void __thiscall Game::StrategyApp::RunAI(int playerNetId, const char* aiCustomDataName, unsigned char aiPersonality, unsigned int rngSeed) -- RET 0x10, four stack args. The ONE-SHOT AI construction path, reached only from StrategyNetworkClient::OnMessage 0x00784640+0x96e, case SNMRunAI (net msg id 0x3d). Resolves the player through the handle registry at server+0x84; refuses on a human (`p->IsAI(+0xf9) == 0` -> "RunAI: Cannot create a StrategyClient/AI for a human player."); logs "RunAI: Creating AI client for %s using %08x for random seed."; operator_new(0x708) + StrategyClient ctor 0x00782ed0 WITH rngSeed; StrategyServer::InitGame; StrategyClient::CreateAI 0x007653c0(aiPersonality) which builds the Game::StrategyAIAgent into StrategyClient+0x12c ("RunAI: Failed to create AI for %s (player %d)."); if a save-game blob was supplied, "Loading AI custom data from save game..." through agent vt[5]/vt[6]; finally RaiseAIPrepareTurn 0x00815f20. NOT a per-turn entry point [verified]
|
||||
constexpr uint32_t StrategyApp_RunAI = 0x004706f0;
|
||||
// cdecl void (Game::StrategyServer* srv) with EBX = Game::StrategyAIAgent* (register-passed; both call sites set EBX before the call). Builds a stack Game::SEAIPrepareTurn (vftable 0x00a23c00) and invokes agent->vt[1](9, &ev) -- i.e. StrategyAIAgent::OnEvent with client event type 9. Also fires the SVScriptObject hooks at srv+0x1b4 with ids 9 and 0xa. Exactly two call sites: RunAI 0x008706f0+0x26c and StrategyApp::CreateGame 0x00888e80+0x3f5 -- both are game/AI construction, so SEAIPrepareTurn is NOT raised once per turn despite the name [verified]
|
||||
constexpr uint32_t StrategyApp_RaiseAIPrepareTurn = 0x00415f20;
|
||||
// thiscall void __thiscall Game::StrategyAIAgent::OnEvent(int clientEventType, Game::StrategyEvent** ev) -- RET 8. IStrategyAIAgent vtable slot 1 (vftable 0x00a1b244). Pushes a log scope on this->+0x10, then calls StrategyAIContext::OnStrategyEvent 0x006c2b90 with ECX = this->+0x94 (the context) and args (type, ev, &thunk 0x006d0ab0, this). The thunk forwards to StrategyAIAgent::OnAIPacket 0x006cf8a0. Every StrategyClient event handler in 0x00773xxx-0x00777xxx forwards through this slot when StrategyClient+0x12c is non-null [verified]
|
||||
constexpr uint32_t StrategyAIAgent_OnEvent = 0x002d0ad0;
|
||||
// thiscall void __thiscall Game::StrategyAIContext::OnStrategyEvent(int clientEventType, Game::StrategyEvent** ev, void (*cb)(void*, void*), Game::StrategyAIAgent* agent) -- RET 0x10. Registers {cb, agent, seq} on the pending-callback deque at this+0x58 (ring deque, buf@+0x5c cap@+0x60 head@+0x64 size@+0x68, 0x0c-byte nodes, push helper 0x0069e470 under a critical section). Then `switch (type - 6)` over 0..0x20 through the byte index table at 0x006c33a4 and jump table at 0x006c3360 (17 distinct cases), updating the AI world model and emitting INTERNAL AI packets {int code; ...} through StrategyAIContext::Broadcast 0x006b3840. Client event 9 (SEAIPrepareTurn) emits codes 1 then 2; client event 0x26 (SEResumePlaying) emits code 3. Tail: if the pending deque size is 1 it drains a second, separate queue at this+0x38 via 0x006a8690 [verified]
|
||||
constexpr uint32_t StrategyAIContext_OnStrategyEvent = 0x002c2b90;
|
||||
// thiscall void __thiscall Game::StrategyAIContext::Broadcast(const AIPacket* pkt) -- walks the listener red-black tree at this+0xc (std::set/map nodes; `_Isnil` at node+0x15) and calls listener->vt[3](pkt) on each -- that is the Game::AIObject event slot, implemented by AIPlayer (0x00723ed0), AISystem (0x006b3ae0), AIFleet (0x006b3970), AIBuildOrder and StrategyAIAgent (0x0069de50). Then, if the pending-callback deque at this+0x68 is non-empty, iterates it (0x0069e510 / 0x006a4ee0) and delivers the same packet to the queued {cb, this} pairs -- the hop that reaches StrategyAIAgent::OnAIPacket 0x006cf8a0. 19 call sites, all inside OnStrategyEvent 0x006c2b90 and 0x006c29c0 [mapped]
|
||||
constexpr uint32_t StrategyAIContext_Broadcast = 0x002b3840;
|
||||
// thiscall void __thiscall Game::StrategyAIAgent::OnAIPacket(const AIPacket* pkt) -- 2008 bytes. `eax = pkt->code - 2; if (eax > 0xf) return; jmp [eax*4 + 0x006d0078]` -- a 16-entry jump table over internal packet codes 2..17. Code 2 = the PREPARE TURN body (0x006cf958, logs "====== AI Prepare Turn (%s) ======"); code 3 = the PROCESS TURN body (0x006cfabf, logs "====== AI Process Turn (%s) ======", ~30 phases, ends by calling cl_EndTurn 0x00579310). Codes 4/6/7/8/10/11/13 fall through to the no-op at 0x006d0058. this->+0x10 = the owning StrategyClient, this->+0x14 = the ClientPlayer (name std::string at +0x40), this->+0x94 = the StrategyAIContext. Reached only through the thunk at 0x006d0ab0 [verified]
|
||||
constexpr uint32_t StrategyAIAgent_OnAIPacket = 0x002cf8a0;
|
||||
// thiscall void __thiscall Game::StrategyClient::OnResumePlaying(Game::StrategyEvent** ev) -- the case-0x26 handler of StrategyClient::RaiseEvent 0x00783ee0 (jump table 0x00784200, 0x2c entries). At +0x9d: `if (this->+0x12c) agent->vt[1](0x26, ev)` -- THE per-turn AI trigger. SEResumePlaying is broadcast by StrategyServer::ResumePlaying 0x007ddc90 at the very start of a new turn, after OnAllCombatDone_Tail has written the pre-turn autosave [verified]
|
||||
constexpr uint32_t StrategyClient_OnResumePlaying = 0x00377480;
|
||||
// offset Game::StrategyAIAgent* -- non-null only on an AI client. Every StrategyClient event handler tests it before forwarding the event to the agent; SendEndTurn 0x00783980 reads agent->vt[8]() through it to fetch the AIEncounterFlags it appends to SNMEndTurn; cl_EndTurn 0x00579310 refuses to end the turn unless it is non-null [verified]
|
||||
constexpr uint32_t StrategyClient_off_AIAgent = 0x0000012c;
|
||||
// offset Mars::RNG* -- a PER-CLIENT generator, operator_new(0x9cc) + RNG_Seed(ctorArg) in the StrategyClient constructor 0x00782ed0+0x143..+0x181. For an AI client the seed is RunAI's 4th argument. THE ONLY GENERATOR THE STRATEGIC AI DRAWS FROM: all six direct NextInt sites in the AI module reach it, and so do the two façade helpers cl_Chance 0x00578cf0 and cl_RandRange 0x005798e0. It is distinct from the strategic generator at StrategyServer+0x16c and is NOT serialised anywhere in the save [verified]
|
||||
constexpr uint32_t StrategyClient_off_RNG = 0x00000134;
|
||||
// offset Game::TurnCommands -- the ACCUMULATING order queue. Every StrategyClient order method in 0x00762ca0..0x00763f60 does `lea ecx,[this+0x160]` and appends to one of its 27 lists (or sets one of its six gates). At End Turn, StrategyClient::BuildTurnCommands 0x00783780 does TurnCommands::operator=(this->+0x4d8, this->+0x160) and then overwrites the player id, research rate and fleet-move list from live state [verified]
|
||||
constexpr uint32_t StrategyClient_off_PendingTurnCommands = 0x00000160;
|
||||
// offset Game::TurnCommands -- the SEND buffer, the object that becomes the `Player.<id>.TurnCommands_v5` block on the wire. Written only by StrategyClient::BuildTurnCommands 0x00783780 (from +0x160) and read by SendEndTurn 0x00783980+0x96, which copies it into the SNMEndTurn message (vftable 0x00a229e0) with TurnCommands::operator= 0x007832b0 [verified]
|
||||
constexpr uint32_t StrategyClient_off_SendTurnCommands = 0x000004d8;
|
||||
// thiscall void __thiscall Game::StrategyClient::BuildTurnCommands(Game::TurnCommands* dst) -- called once from EndTurn 0x00783be0+0xee with dst = &this->+0x4d8. TurnCommands::Clear 0x00893f00(dst); TurnCommands::operator= 0x007832b0(dst, &this->+0x160); dst->playerId(+4) = this->+0x150->+4; TurnCommands::SetResearchRate 0x0080f2d0(dst, this->+0x150->+0xbc) -- which is why EVERY save's TurnCommands block has the research-rate gate set and the other five clear; then, if this->+0x6d0, walks the pending fleet-move vector at this->+0x6d4/+0x6d8 (stride 8) into the move list [verified]
|
||||
constexpr uint32_t StrategyClient_BuildTurnCommands = 0x00383780;
|
||||
// thiscall Game::TurnCommands& __thiscall Game::TurnCommands::operator=(const TurnCommands& src) -- member-by-member copy of the six gates and their payloads, then the 27 lists. 11 call sites, including SendEndTurn, StrategyServer::OnPlayerEndTurn 0x007d9af0+0x68 (the host storing an arriving block), BuildTurnEvents and LoadGame [verified]
|
||||
constexpr uint32_t TurnCommands_Assign = 0x003832b0;
|
||||
// cdecl void () -- the AI's turn-submission façade. `c = g_StrategyClients[g_CurrentClientIndex]; if (c && c->AIAgent(+0x12c) && !c->bTurnEnded(+0x15c)) StrategyClient::EndTurn(c, true);` -- note the +0x12c test: this entry point works ONLY for an AI client. Called from the AI Process Turn body at 0x006cfcd9. The other caller of StrategyClient::EndTurn is the human UI at 0x005e4f80+0x5f [verified]
|
||||
constexpr uint32_t cl_EndTurn = 0x00179310;
|
||||
// cdecl bool (float p) -- `c = g_StrategyClients[g_CurrentClientIndex]; return c ? RNG_Chance(c->RNG(+0x134), p) : false;`. One of the AI's two randomness façades; used by the AI Process Turn body at 0x006cfc24 for the surrender roll that follows the "Survival Outlook: Dead in %i turns (%5.2f%% chance to surrender this turn)" log line [verified]
|
||||
constexpr uint32_t cl_Chance = 0x00178cf0;
|
||||
// cdecl int (int lo, int hi) -- `c = g_StrategyClients[g_CurrentClientIndex]; if (!c) return 0; n = hi - lo; return lo + RNG_NextInt(&c->RNG(+0x134)->mt, &n);`. NOTE RNG_NextInt is INCLUSIVE of its bound (addresses.json), so the range is [lo, hi] inclusive. Eight AI-module call sites [verified]
|
||||
constexpr uint32_t cl_RandRange = 0x001798e0;
|
||||
// offset Game::StrategyClient* g_StrategyClients[] -- the client table the whole 0x00578cf0..0x005793xx façade family indexes with g_CurrentClientIndex (0x00ae4808). 40 functions reference it. The AI runs as the current client: everything it does goes through this indirection, which is how one process hosts the human client and N AI clients over the same API [mapped]
|
||||
constexpr uint32_t g_StrategyClients = 0x006e47e4;
|
||||
// offset int -- index into g_StrategyClients (0x00ae47e4). Selects which client the cl_* façade acts on. Not instrumented; who sets it, and when relative to the AI's turn, is open [mapped]
|
||||
constexpr uint32_t g_CurrentClientIndex = 0x006e4808;
|
||||
// offset Mars::RNG -- a STATIC generator in .data, 0x9cc bytes. Its only static initialiser (0x009dc6e0) writes the Mars::IStreamable vftable 0x009e22bc, NOT the Mars::RNG vftable 0x009e9aec that RNG_Seed installs: none of the six RNG_Seed call sites in the image targets it, so its mt[624] is the zero-initialised BSS array and `left` is 0. An all-zero MT19937 state is a fixed point of the twist, so EVERY draw from it returns 0. Five consumers: SNMRunAI (the AI client seed, OnMessage+0x955), RunCombatRound 0x007cbe80+0x60f, 0x007c2fa0+0xc84, 0x0079ea90+0x73 (an RNG_Chance) and 0x005b9f00+0xc0 [verified]
|
||||
constexpr uint32_t g_GlobalRNG = 0x006f6e58;
|
||||
// thiscall void __thiscall -- loads Data/Strategy/AI/aitechmode.csv, aitechpri.csv and aitechgrp.csv through the Mars::ICSVRowParser subclasses Game::AIUserTechModeRowParser (vftable 0x00a1ae8c), AIUserTechPriRowParser (0x00a1ae7c) and AIUserTechGrpRowParser (0x00a1ae6c). Diagnostics: "%s, %i: tech %s does not exist.", "%s, %i: priority %i (%s) is out of range 0..255.", "%s, %i: bad research mode character: %s", "%s, %i: bad group: %s". Tech group ids are the AITG_* strings emitted by 0x006920a0 (ARMOR BALWEAP BEAM BIOWEAP NRGWEAP SHIELDS TORPS WARHEAD) [mapped]
|
||||
constexpr uint32_t AIRulesDB_LoadTechTables = 0x002c7980;
|
||||
// thiscall void __thiscall -- loads data/strategy/ai/stock_design_names.csv, stock_diplomacy_messages.csv and stock_player_names.csv through Game::AIPersonaDB::StockDesignNameRowParser (vftable 0x00a1b034), StockDiplomacyMessageRowParser (0x00a1b024) and StockPlayerNameRowParser (0x00a1b014). Diagnostics key on species and on a diplomatic event id; the id vocabulary is the 53 AIDIP_* strings returned by 0x00690960 [mapped]
|
||||
constexpr uint32_t AIPersonaDB_LoadStockTables = 0x002c6250;
|
||||
// thiscall void __thiscall -- loads data/strategy/ai/affinity_weapon.csv and affinity_section.csv ("ToAISectionRule: %s ship section not found: %s"). These are the ship-design affinity weights consumed by the design composer 0x006ad700 ("AIComposeShipBlueprint: SectionBlueprint::MAX_OPTIONS", "While AI for %s was designing a ship: Did not find any weapon to match %s, %s, bNoTrackingWeapons=%i."). Section-class rule selection is 0x0069cc30 / 0x0069cdb0 ("AISelectSectionClassRules: maxout", "AISelectSectionClassRulesMergeFallback: maxout"); the generic selector is 0x00695140 ("AIRulesDB::SelectRules_T: maxout") [mapped]
|
||||
constexpr uint32_t AIRulesDB_LoadAffinityTables = 0x002c63c0;
|
||||
// thiscall void __thiscall -- loads Data/Strategy/AI/weapon_replacements.csv through Game::StrategyAIContext::WeaponReplacementsRowParser (vftable 0x00a1a62c). Consumed by 0x00694f80 ("StrategyAIContext::GetWeaponReplacement: maxReplacements (%i)") [mapped]
|
||||
constexpr uint32_t StrategyAIContext_LoadWeaponReplacements = 0x002b4dc0;
|
||||
// thiscall void (CombatResolveContext* this) // THE POST-BATTLE RETREAT PIPELINE. Exactly one caller: CombatResolver_Run 0x007d5af0, unconditionally, at 0x007d5be2. Real body 0x007d5a00..0x007d5abb; the only jcc in it is the operator-new null test whose false arm is a _CxxThrowException. It builds a ~0x2c-byte RetreatContext stack local from the resolver's ctx (rc->+0x00 = ctx->+0x00 = S; rc->+0x04 = ctx->+0x08 = enc; rc->+0x08 = ctx->+0x0c = res; a std::map<int,ServerSystem*> at rc->+0x0c with an operator_new(0x18) head node at rc->+0x10 and _Mysize rc->+0x14; a std::vector<RetreatGroup*> at rc->+0x1c/+0x20/+0x24) and runs SIX unconditional this-calls in a straight line: FUN_0079bb90 (per-player destinations), FUN_0079bcd0 (build groups), FUN_007b0320 (whole vs partial), FUN_00790790 (split partial fleets), FUN_007d5650 (execute; EVENT_FLEET_RETREATED_VIA_TELEPORT), FUN_007a7cd0 (destructor). CORRECTS combat-resolver.md's characterisation of this as 'the per-phase combat pipeline': it is ONE subsystem, retreat, not six combat phases. DRAW-FREE: a 327-function closure (E8 calls plus E9 tail-call thunks) contains zero calls to the four RNG primitives and zero inlined MT tempering immediates [verified]
|
||||
constexpr uint32_t CombatResolve_Retreat = 0x003d5a00;
|
||||
// thiscall void (RetreatContext* this) // RETREAT PHASE 1. One loop over enc->members (stride 0x44, magic 0x78787879 / sar 5). Per member: FUN_00787210(&enc->+0x1c, enc->+0x0c, member->+0x00 /*ServerPlayer*/, &r1, &r2, &r3), then this->dest[player->PlyrIdx(+0x28)] = the FIRST NON-NULL of (r1, r2, r3) via std::map<int,T*>::operator[] 0x0076bce0. So the per-player retreat destination is: nearest system you own, else nearest system with no hostile presence, else nearest system at all [verified]
|
||||
|
|
@ -1423,6 +1469,50 @@ constexpr uint32_t ShipDesign_off_Dtc = 0x00000134;
|
|||
constexpr uint32_t ShipSectionDef_off_SectionClass = 0x00000260;
|
||||
// offset unsigned int // low dword of the section's 64-bit role-flag word (high dword at +0x29c), one bit per boolean role key in the .shipsection file [verified]
|
||||
constexpr uint32_t ShipSectionDef_off_RoleFlagsLow = 0x00000298;
|
||||
// thiscall double (ServerSystem* sys, int groupType) // `ret 4`, real end 0x0074d8e1. The income analogue of PopOutput 0x0074d8f0, and NOT the same law: it sums, over species 0..6, `(double)ftol( (double)GroupIncome(groupType, count) x moraleMod x addictionMod )` -- so the value truncates TWICE per (group, species) row, once inside GroupIncome and once after both factors. moraleMod is 0x00746910 (the same helper the output term uses) and applies to groupType 1 only; addictionMod is the float behind slot 0x00aeca48 when the system's int[7] addiction table at +0x1e4 has a non-zero entry for that species. For groupType 1 and the owner's own species on a non-independent system the count first gains the capacity surplus from two calls to 0x0074a6d0 (out slot 4, then out slot 6), max(0, B - A) [verified]
|
||||
constexpr uint32_t ServerSystem_PopIncome = 0x0034d760;
|
||||
// thiscall double (ServerSystem* sys) // plain `ret`, real end 0x0074b793. groupType 2 of the same loop as PopIncome, over SlaveCount(species) (0x0074b610): no morale factor and no capacity surplus, but the addiction factor still applies. Unexercised: slave counts are 0 on every call in the corpus [verified]
|
||||
constexpr uint32_t ServerSystem_SlaveIncome = 0x0034b700;
|
||||
// thiscall int64 (ServerSystem* sys, int species) // the slave-group population of one species; the group-2 counterpart of GroupPopulation 0x00747ba0 [unverified]
|
||||
constexpr uint32_t ServerSystem_SlaveCount = 0x0034b610;
|
||||
// thiscall void (ServerSystem* sys, int out[12]) // zeroes `out`, returns immediately when the caller's pointer is null or the system has no owner (+0x100), else calls ComputeOutputFromRates(out, &sys->Rts /*+0x88*/) -- the system's OWN rate sliders, not a max-mods vector. THE DISTINCTION THAT MATTERS: ComputeBudget's real (non-projected) per-system money is this function's out[3], while its projected mode and UpdateBankruptcyLimits use ComputeMaxIncome 0x007521c0. They are different numbers: this path funds the science, construction and terraform channels, so the repair pass runs and the unspent-industry and unspent-terraforming cascades into the money channel are live [verified]
|
||||
constexpr uint32_t ServerSystem_ComputeOutput = 0x00351fb0;
|
||||
// thiscall float (StrategyServer* srv, ServerPlayer* p) // `ret 4`, real end 0x0080f49f. Returns float32( float32(DifficultyMods_Select(p->diffMods /*+0x36c*/, p)[1]) x float32(srv->IncMod /*raw base +0xbc, the Sim block's `IncMod` tag*/) ), or just the server modifier when p is null. Every step is stored back through a 4-byte float. `ecx` here is the RAW StrategyServer base (ServerSystem+0x10), four bytes above the base the class's own methods get. THIS IS THE MISSING x1.1: at the difficulty level every corpus save carries (aidf == 1) the AI column of the table is 1.1f [verified]
|
||||
constexpr uint32_t StrategyServer_IncomeDifficultyMod = 0x0040f470;
|
||||
// thiscall float* (DifficultyMods* rec, ServerPlayer* p) // `ret 4`, real end 0x0059b4b3. Returns &rec->f[0] (the AI triple, at +0x04) when p is non-null AND p->[0xf9] (is-AI) is set AND p->NPC (+0xfb) is clear; otherwise &rec->f[3] (the non-AI triple, at +0x10). The three consumers read offset +0 (fleet maintenance divisor, ComputeBudget 0x0086338b), +4 (system and trade-route money, 0x0080f470 and 0x00833938) and +8 (research points bought with money, 0x0080e229 and 0x00863618) [verified]
|
||||
constexpr uint32_t DifficultyMods_Select = 0x0019b490;
|
||||
// cdecl void (int level, DifficultyMods* out) // real end 0x005a3a53 (Ghidra size 193 stops 2 bytes short). memcpy's the default {id 1, 1.0f x6} into `out` FIRST, then builds the table with BuildDifficultyTable 0x005a3870 and linear-searches it for id == level (stride 0x1c, from the 0x92492493 magic divide), copying the six floats on a hit. An out-of-range level therefore yields all ones rather than failing. Called from ServerPlayer::Read 0x008804d0 at 0x00880fa3, gated on 0 <= aidf < 3, which is also where ServerPlayer+0x368 (`aidf`) is stored [verified]
|
||||
constexpr uint32_t LoadDifficultyRow = 0x001a3990;
|
||||
// thiscall vector<DifficultyMods>* (vector<DifficultyMods>* out) // real end 0x005a3989. THE TABLE IS BUILT IN CODE, from .rdata float literals -- no data-file key, no GlobalConst slot, same shape as lane N's pop-type table. Three rows of {int id; float ai[3]; float other[3]} (0x1c): id 0 = ai {1,1,1} / other {1.5,1.5,1.5} (0x00a1b000); id 1 = ai {3.0 (0x00a0451c), 1.1 (0x009f957c), 1.5} / other {1,1,1}; id 2 = ai {1e6 (0x009ebd7c), 1.7 (0x009f9580), 2.0 (0x00a04518)} / other {1,1,1}. Read as: level 0 gives the break to the human player, levels 1 and 2 give it to the AI, and on level 2 the AI's fleet maintenance is divided by a million [verified]
|
||||
constexpr uint32_t BuildDifficultyTable = 0x001a3870;
|
||||
// thiscall float (ServerPlayer* p) // seven bytes: `fld DWORD [ecx+0x30c]; ret`. The save's per-player `IncMod` [verified]
|
||||
constexpr uint32_t ServerPlayer_GetIncMod = 0x0040dd10;
|
||||
// thiscall float (ServerPlayer* p) // `SpeciesDef(p->Species /*+0x5c*/)->+0x24`, the multiplier on the suitability MONEY cost (Zuul 0.7). A data-file value [verified]
|
||||
constexpr uint32_t ServerPlayer_GetSpeciesCostFactor = 0x0040dd20;
|
||||
// thiscall float (StrategyServer* srv, int species) // `ret 4`; one instruction of work: `fld DWORD [ecx + species*4 + 0xf8]`. The per-species ideal-suitability array on the RAW server base. IT IS ON THE WIRE: the Sim block's `ISsp`/`ISsu` pairs are this float[7] in species-index order, and the array is randomised per game by the map generator -- verified against every ServerPlayer's own `IdealSuit` field in all 11 corpus saves. CalcSuitMod reads THIS, not the player's field [verified]
|
||||
constexpr uint32_t StrategyServer_IdealSuit = 0x0040f4b0;
|
||||
// thiscall double (ServerSystem* sys) // real end 0x0074690d. Returns 0 with no owner, else float32(|IdealSuit(species) - sys->Suit|) / |owner->TerraMod (+0x134) x [0x00a1f928] / 20000|. The sign term (-1.0 when the planet's suitability is STRICTLY above the ideal) is multiplied in BEFORE the fabs at 0x00746906 and therefore cancels: the result is always >= 0. That is why the unspent-terraform cascade into the money channel is provably zero under the max-income rate vector [verified]
|
||||
constexpr uint32_t ServerSystem_TerraformPointsNeeded = 0x00346890;
|
||||
// thiscall void (ServerSystem* this, ServerPlayer* p, int encounterId) // 75 B, ret 8. THE NVE WRITER. if (!p) return; s = (int16)this->owner(+0x10)->Frame(+0x8); rec = NVE_map_at(&this->NVE(+0x284), &p->PlyrIdx(+0x28)); rec[0] = (s<<16)|s; rec[1] = encounterId. The map value is 8 bytes at node+0x10: an UNSERIALISED int16 touch stamp at +0, the saved ETS int16 at +2, the saved Eid int32 at +4 -- so ETS and the touch stamp are both set to the frame here, and only the writer at 0x007536a0 makes them differ. Sole caller is the tail's PlayerView-rebuild phase 0x007cf560, under the gate (AFlags >> PlyrIdx) & 1 [mapped]
|
||||
constexpr uint32_t ServerSystem_RecordObservation = 0x00356300;
|
||||
// thiscall void (ServerSystem* this, ServerPlayer* p, NveValue* src) // 70 B, ret 8. Intel sharing. rec = NVE_map_at(&this->NVE, &p->PlyrIdx(+0x28)); rec[0] = (hi16(src[0]) << 16) | (int16)this->owner(+0x10)->Frame(+0x8); rec[1] = src[1]. i.e. the receiver gets the DONOR's sighting turn (ETS) and encounter id unchanged, and only the unserialised touch stamp becomes the current frame. Never executed by any save in the corpus: no save has two players in an alliance [mapped]
|
||||
constexpr uint32_t ServerSystem_CopyObservationTo = 0x003536a0;
|
||||
// cdecl bool (ServerSystem* sys, ServerPlayer* from, ServerPlayer* to) // 96 B. Null-guards all three and from != to; a = FindObservation(from); b = FindObservation(to); if (a && (!b || b->ETS(+2) < a->ETS(+2))) { CopyObservationTo(to, a); return true; } return false. NEWER SIGHTING WINS, compared as a signed int16 [mapped]
|
||||
constexpr uint32_t ServerSystem_ShareObservation = 0x00354d90;
|
||||
// thiscall NveValue* (ServerSystem* this, ServerPlayer* p) // 58 B, ret 4. Map find on p->PlyrIdx(+0x28); returns node+0x10 (the 8-byte value) or null when the search ended at this->NVE head (+0x284) [mapped]
|
||||
constexpr uint32_t ServerSystem_FindObservation = 0x0034d360;
|
||||
// thiscall int (ServerSystem* this, ServerPlayer* p) // 64 B, ret 4. Same map find; returns node+0x14 (Eid) or -1. The extra `!= -0x10` guard is the null-node case reached through node+0x10 [mapped]
|
||||
constexpr uint32_t ServerSystem_LastSeenEncounterId = 0x0034f830;
|
||||
// thiscall bool (ServerSystem* this, ServerPlayer* p) // 34 B, ret 4. return ((1 << (p->PlyrIdx(+0x28) & 0x1f)) & this->AFlags(+0xd4)) != 0. THE gate on the observation record, on the explored sweep and on the PlayerView rebuild -- AFlags, the DERIVED non-sticky union, not VFlags. 19 callers [verified]
|
||||
constexpr uint32_t ServerSystem_IsVisibleTo = 0x00343fb0;
|
||||
// fastcall void (ServerSystem* this) // 41 B, WHOLE BODY. if (this->AFlags(+0xd4) != 0) this->ltis(+0x2c8) = this->owner(+0x10)->Frame(+0x8). This is `ltis`'s writer, which board.md recorded as unnamed; it is driver phase 29 (0x007dcbd6, per system). TShn's writer is still unnamed and is demonstrably a DIFFERENT rule: Spica in turn1-state has AFlags == 0 and its TShn moves anyway [mapped]
|
||||
constexpr uint32_t ServerSystem_UpdateLastObservedTurn = 0x00343ec0;
|
||||
// thiscall void (ServerSystem* this, ServerPlayer* p, bool wasSet, bool on) // 71 B, WHOLE BODY. Game::ServerSystem primary vftable 0x00a2044c SLOT 7 -- this RESOLVES the indirect edge lane B5 flagged in SetExploredBy's tail (vft[0x1c]). if (!wasSet && on && this->owner(+0x10)->Frame(+0x8) > 1) { FUN_00747a20(p->PlyrIdx(+0x28), 1); if (p->PlyrIdx < 0xf) this->+0x2a4 |= 1 << (PlyrIdx & 0x1f); } -- +0x2a4 sits past NVs and is NOT in the serialised table, so this edge writes no save state. Note the Frame > 1 guard: turn 1 is special-cased [mapped]
|
||||
constexpr uint32_t ServerSystem_OnExploredChanged = 0x003480b0;
|
||||
// cdecl bool (StarSystem* sys, int encounterId) // 129 B. Refuses unless the system is unowned (FUN_007437e0 == 0), has no planets, sys->+0x184 == -1 and two further tests pass; then sys->+0x184 = encounterId and ORs a mask from the encounter def into sys->+0x19c. sys->+0x184 is the field ServerSystem_RecordObservation copies into Eid. It is constructed to -1 (StarSystem ctor 0x00752ea0, member index 0x61) and IS NOT ON THE WIRE, so a reimplementation has to recover it from the encounter fleet's FtEnc -- which agrees on all six encounter fleets in the corpus and which no save can separate from the real field [mapped]
|
||||
constexpr uint32_t StarSystem_PlaceEncounter = 0x003887c0;
|
||||
// fastcall void (StrategyServer* S) // 948 B, tail phase 17. Four passes: (1) walk the per-(system, player) view tree at S+0x228 and drop entries whose player can no longer see the system, then clear the tree and reset S+0x22c = 0; (2) per player x per system, if IsVisibleTo, FUN_0075f550; (3) per player x per system, if IsVisibleTo, ServerSystem_RecordObservation(sys, player, sys->+0x184) -- byte-decoded at 0x007cf7a7..0x007cf7ce, this is the ONLY caller of the NVE writer outside intel sharing; (4) per system x per player, if IsExploredBy and a colony exists, build a PlayerView via 0x00755ab0/0x007561d0 and apply it. Draw-free [mapped]
|
||||
constexpr uint32_t StrategyServer_RebuildPlayerViews = 0x003cf560;
|
||||
// cdecl void (const char* gamename) /* GameSpy SDK gsAvailable. sprintf("%s.available.gamespy.com"), inet_addr/gethostbyname, UDP socket, sendto port 27900 (htons 0x6cfc) with '\x09\0\0\0\0' + gamename + NUL, len = strlen(gamename)+6. Overridable hostname buffer at 0x00b085b0. Leaves socket = -1 on DNS failure. */ [verified]
|
||||
constexpr uint32_t GameSpy_GSIStartAvailableCheck = 0x0000a060;
|
||||
// cdecl int (void) /* returns 0=waiting 1=available 2=unavailable 3=temporarily-unavailable. Socket==-1 (start failed) => returns 1. Retries once after 2000 ms then returns 1. */ [verified]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue