sots-re/ghidra/addresses.d/lane-b5.json
alex ab1c2296a1 B5: the combat resolver's sub-dispatcher is RETREAT, and it is the largest
saved-state writer under combat

FUN_007d5a00's six callees, read from the instruction stream to the next
function start, plus 18 helpers. Lane J named this "the real per-phase combat
pipeline"; the identification of it as the most tractable target was right and
the characterisation was not. It is one subsystem -- destinations, group,
classify, split, execute, destroy -- and it is StarFleet retreat.

Seven saved fields or containers move on this path: ship->fleet membership,
StarFleet objects created and destroyed in the master list, the system fleet
lists, fleet position/location/flight plan/flags, the ServerSystem presence and
explored masks, the per-player PlayerView, and ServerPlayer.GTraf.

Closes struct-recovery.md section 7's open "OID allocation (R2's x16)": the
allocator is at 0x008b8ae0, the counter lives per network node at
IDMap->+0x08 + node*0x14 + 0x10, it is pre-incremented and never issues 0, and
an id is (counter << 4) | (node & 0xF). R2's x16 is the shl 4. A partial
retreat mints one id per group, and that counter is almost certainly the save's
NMnx tag -- labelled hypothesis, with a falsifiable prediction written down
before any run.

New rule: retreating from a system you had not explored marks it explored for
you, writing EFlags and refreshing the PlayerView. New diplomacy rule: a system
whose owner captured it on the current turn loses that owner's ceasefire cover
in the hostile-presence test.

RNG recomputed independently from a different root and with a different tool:
327 functions in the closure once E9 tail-call thunks are followed (the first
pass missed an 8-byte jmp thunk and with it the whole id-allocator chain), zero
calls to the four primitives, zero inlined MT tempering immediates, and one
false-positive 0x11c stride that is StarFleet.FtMS being initialised to -1.
Bound, not proof: 152 indirect call sites in 91 of the 327, four of them on the
main line and flagged for the vtable lane.

Corrects my own first reading of the fleet-creation call: a ret-N tail-call
thunk in the middle of an argument list makes three pushes look like they
belong to the wrong call. Checking ret N against the push count catches it.

The honest limit stands: none of this has ever executed under an instrument.
combat-resolver.md section 10.3's workload now needs a second condition -- the
battle must produce a retreat, or all six phases are no-ops.
2026-09-08 12:00:14 -04:00

268 lines
27 KiB
JSON

{
"entries": [
{
"name": "CombatResolve_Retreat",
"addr": "0x007d5a00",
"convention": "thiscall",
"prototype": "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",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_ChooseDestinations",
"addr": "0x0079bb90",
"convention": "thiscall",
"prototype": "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",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_FindDestination",
"addr": "0x00787210",
"convention": "thiscall",
"prototype": "void (RetreatContext* this, Mars::Vector3* battlePos, ServerSystem* excludeSys, ServerPlayer* p, ServerSystem** out1, ServerSystem** out2, ServerSystem** out3) // ret 0x18. THE RETREAT DESTINATION FORMULA. THREE INDEPENDENT nearest-system searches sharing one pass over S->Systems (S->+0x44/+0x48, 4-byte stride), each with its own best-so-far seeded to the .rdata FLT_MAX at 0x009e23a8 (0x7f7fffff): out1 = nearest sys with FUN_007437e0(sys) == p (owner); out2 = nearest sys with FUN_00744070(sys, p) FALSE (no hostile presence); out3 = nearest sys, unconditional. Both skips: sys == excludeSys, and FUN_007469e0(sys). THE INDEPENDENCE IS LOAD-BEARING -- each 'best' store is INSIDE its predicate arm (0x007872c6 / 0x007872ec), so a nearer rejected system does not spoil that search; a single-loop 'find nearest then filter' gives a different answer. Distance is SQUARED: the three component deltas are each fstp'd to float32 first, the products and sum accumulate on the x87 stack, the total narrows to float32 once at 0x007872a3 (a double accumulator with one final narrowing is BIT-IDENTICAL, since a float32 delta squares exactly in double). Comparison is a strict < (fld best; fcomp d; test ah,0x41; jne skip), so an exact tie goes to the EARLIER system in the vector",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "ServerSystem_IsUnusableRetreatTarget",
"addr": "0x007469e0",
"convention": "thiscall",
"prototype": "bool (ServerSystem* this) // 33 B. return this->Dstyd(+0xc5) != 0 || (this->vnh(+0xc6) != 0 && this->vnex3(+0xc8) == 0). A destroyed system, or one hosting a Von Neumann hive that has not been cleared out. All three are SAVED bools (struct-recovery.md section 1), which is what identifies the class as Game::ServerSystem rather than a bare StarSystem. Note CombatResolve_NodeCannon 0x007bb530 uses only the +0xc5 half for its own destination filter",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "ServerSystem_GetOwner",
"addr": "0x007437e0",
"convention": "thiscall",
"prototype": "ServerPlayer* (ServerSystem* this) // 7 B: mov eax,[ecx+0x100]; ret. this->PID, the saved owner handle",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "ServerSystem_HasHostilePresence",
"addr": "0x00744070",
"convention": "thiscall",
"prototype": "bool (ServerSystem* this, ServerPlayer* p) // ret 4. return (ServerPlayer_HostileMaskAt(p, this) & this->AFlags(+0xd4)) != 0",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "ServerPlayer_HostileMaskAt",
"addr": "0x0080dff0",
"convention": "thiscall",
"prototype": "uint32 (ServerPlayer* this, ServerSystem* sys) // ret 4. THE HOSTILITY MASK, and it carries a diplomacy rule that is nowhere in findings/subsystems/: ownerBit = (sys && sys->PID(+0x100) && sys->TAcq(+0x80) == S->Frame) ? (1 << sys->PID->PlyrIdx) : 0; return ~( (1 << this->PlyrIdx(+0x28)) | (this->CF(+0x174) & ~ownerBit) | this->NA(+0x170) | this->AL(+0x16c) ). A SYSTEM WHOSE OWNER CAPTURED IT ON THE CURRENT TURN LOSES THAT OWNER'S CEASEFIRE COVER. The current turn is reached as this->+0x8->+0x8, which is S->+0xc = Frame, because ServerPlayer+0x8 == S+4. The complement is over all 32 bits, harmless only because it is immediately ANDed with a presence mask",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_BuildGroups",
"addr": "0x0079bcd0",
"convention": "thiscall",
"prototype": "void (RetreatContext* this) // RETREAT PHASE 2. One loop over res->+0x68/+0x6c -- a 0x14-STRIDE VECTOR OF RETREAT REQUESTS (magic 0x66666667 / sar 3), a Game::EncounterResults member not previously mapped: req->+0x04 ship handle, req->+0x08 mode word, req->+0x0c requested-destination handle, req->+0x10 second key word. Per request: ship = HandleMap::Resolve(S->+0x84, req->+0x04); skip if null, if ship->Dep(+0x50) != 0, if ship->EncID(+0x7c) == 0x15, if FUN_004f47f0(ship->EncID) (a DB-driven encounter-type mask), or if ship->PlrID(+0x10) == S->Players[S->+0x1c0] (the locally-controlled player). dest = CombatRetreat_ResolveShipDestination(ctx, req, ship); on 0 it logs 'Retreat: Could not find retreat destination for %s.' at level 2 and skips. Then, UNLESS (req->+0x08 == 1 && owner->Species(+0x5c) == 1), skip the ship if StarShip_IsGroundedByDamage(ship). Finally find-or-create the group via CombatRetreat_FindGroup and push_back the ship",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_ResolveShipDestination",
"addr": "0x0079bc50",
"convention": "thiscall",
"prototype": "ServerSystem* (RetreatContext* this, void* req, StarShip* ship) // ret 8. Returns 0 when ship is null or ship->+0x04 (its handle id) is 0. Else: if req->+0x0c != 0, HandleMap::Resolve(S->+0x84, req->+0x0c) and RETURN IT if found -- the explicitly ordered destination; if not found, log 'Retreat: %s cannot retreat to specified location. Location not found.' at level 2 with ship->PlrID->+0x40 (the player's name std::string) and fall through. Fall-through: return this->dest[ship->PlrID(+0x10)->PlyrIdx(+0x28)], the phase-1 default",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_FindGroup",
"addr": "0x007871c0",
"convention": "thiscall",
"prototype": "RetreatGroup* (RetreatContext* this, StarShip* ship, ServerSystem* dest, int mode, int variant) // ret 0x10. Linear scan of this->groups (+0x1c/+0x20) for the group matching ALL FOUR key words: g->+0x00 == ship->PlrID(+0x10), g->+0x04 == dest, g->+0x2c == mode, g->+0x30 == variant. Returns 0 when absent",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "RetreatGroup_Ctor",
"addr": "0x0078f1c0",
"convention": "thiscall",
"prototype": "RetreatGroup* (RetreatGroup* this) // 41 B; ENUMERATES the 0x34-byte heap record (operator new(0x34) at 0x0079bdee). Writes: +0x00 owner ServerPlayer* = 0; +0x04 dest ServerSystem* = 0; +0x08/+0x0c/+0x10 vector<StarShip*> = 0 (allocator +0x14 never written, per earned-rule 5); +0x18/+0x1c/+0x20 vector<StarFleet*> = 0 (allocator +0x24); +0x28 BYTE partial = 0; +0x2c int mode = 0; +0x30 int variant = -1. Total 0x34",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StarShip_IsGroundedByDamage",
"addr": "0x00815090",
"convention": "thiscall",
"prototype": "bool (StarShip* this) // 46 B. design = this->DesID(+0x14); return (design->+0x74 != 0 || design->+0x78 != 0) && design->+0x12c < 2 && (FLT_EPSILON > this->+0x30). The .rdata float at 0x009e1ef8 is 0x34000000 = 1.1920929e-07, FLT_EPSILON -- the test is against an epsilon, NOT against zero, and it is strict, so a health exactly AT the epsilon is not grounded. this->+0x30 is the third float of the inline ShipHealth at 0x24..0x33 (R1 guesses drive). True means the ship CANNOT retreat. The species-1 / mode-1 gate path skips this test entirely",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "IsBlockedEncounterType",
"addr": "0x004f47f0",
"convention": "cdecl",
"prototype": "bool (int encounterType) // 67 B. DB-SOURCED, DO NOT HARD-CODE. Returns false for encounterType > 0x17. Otherwise lazily builds a bitmask once (flag at 0x00b0e988, cache at 0x00b0e984) by FUN_004f47b0, which ORs 1 << table[i] over a count at 0x00ae04e0 and an int array at 0x00adfe88 -- both .bss, filled from the game data files at load -- and returns (mask >> encounterType) & 1. The 0x17 bound is the same EncounterType range PickDominantEncounterType 0x004f4c40 returns over",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_ClassifyFleets",
"addr": "0x007b0320",
"convention": "thiscall",
"prototype": "void (RetreatContext* this) // RETREAT PHASE 3. Per group in this->groups: tally the group's ships by ship->FltID(+0x64) into a local 8-byte-stride vector<{StarFleet*, int}> (linear search, push {f,0} then ++count); then per distinct fleet f, if count == (f->+0xa8 - f->+0xa4)/4 (i.e. EVERY ship the fleet has is in this group) push f into g->fleets(+0x18), else set g->partial(+0x28) = 1. WARNING: the jb/jae pairs at 0x007b0504 and 0x007b0570 are MSVC's inlined vector::push_back aliasing check (_Inside(&_Val)), NOT a phase gate -- both arms converge on *_Mylast = value; _Mylast += 4 at 0x007b05ce. Draw-free, event-free, writes only the group records",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_SplitFleets",
"addr": "0x00790790",
"convention": "thiscall",
"prototype": "void (RetreatContext* this) // RETREAT PHASE 4, and THE BIGGEST SAVED-STATE WRITER ON THE COMBAT PATH. Per group, gated on g->partial(+0x28) != 0: newFleet = StrategyServer_CreateFleet(S+4, IDMap_AllocateLocalID(S+4), g->owner, &enc->+0x1c, NULL) -- note the id mint is evaluated INSIDE the argument list, see IDMap_AllocateLocalID; push newFleet into g->fleets BEFORE the ship loop; then per ship in g->ships, skip it if ship->FltID(+0x64) is ALREADY in g->fleets (its whole fleet is retreating, leave it), else StarFleet_RemoveShip(old, ship), and if ((old->+0xa8 - old->+0xa4) & ~3) == 0 call StrategyServer_DestroyFleet(S+4, old, 0, 0), then StarFleet_AddShip(newFleet, ship). Finally if (enc->+0x0c) StarSystem_FleetArrives(enc->+0x0c, newFleet). PREDICTED ORIGINAL BUG: the fleet is created before the loop, so a group flagged partial creates a fleet even when every ship turns out to belong to a wholly-retreating fleet -- a zero-ship fleet in the save",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StarFleet_AddShip",
"addr": "0x007062e0",
"convention": "thiscall",
"prototype": "void (StarFleet* this, StarShip* ship) // ret 4. this->NShips(+0xa4).push_back(ship); SHIP->FltID(+0x64) = this (the saved handle); design = ship->DesID(+0x14); this->+0xb8 |= design->+0xb8 and this->+0xbc |= design->+0xbc (capability masks); this->+0xc0 (WORD, the fleet's gate-traffic cost) += FUN_0056f430(design->+0x12c); if bit 0x20000 of +0xb8 changed state, push_back this into (this->+0x10)->+0x114; FUN_00705c70(this) to recompute derived stats",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StarFleet_RemoveShip",
"addr": "0x007063a0",
"convention": "thiscall",
"prototype": "void (StarFleet* this, StarShip* ship) // ret 4. Linear-search this->NShips(+0xa4/+0xa8) for ship; return if absent. If the fleet's flight plan is non-empty (+0xc8 != +0xcc) and IsGateTransitWaypoint(wpt->+0x08) 0x0056e6e0, this->PID(+0x58)->GTraf(+0x14c) -= (int16)this->+0xc0. Then SHIP->FltID(+0x64) = 0; erase the slot (memmove + _Mylast -= 4); FUN_00700090(this); FUN_00705070(this, ship); re-add the gate-traffic term under the same test; if bit 0x20000 of +0xb8 changed, find-and-erase this from (this->+0x10)->+0x114; if any ship remains, FUN_00705c70(this). GTraf is a SAVED ServerPlayer field",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StarSystem_FleetArrives",
"addr": "0x0074f240",
"convention": "thiscall",
"prototype": "void (ServerSystem* this, StarFleet* f) // ret 4. f->Pos(+0x18/+0x1c/+0x20) = this->Pos (three raw dword copies); StarFleet_SetLocation(f, this); bit = 1 << f->PID(+0x58)->PlyrIdx(+0x28); this->FFlags(+0xd8) |= bit; this->+0x16c.push_back(f) (the SAVED NumFlts/Flt list); then recompute: if (FFlags & bit || GFlags(+0xdc) & bit || PID(+0x100) == owner) { AFlags(+0xd4) |= bit; VFlags(+0xcc) |= bit; } else AFlags &= ~bit -- on this path the else arm is unreachable because FFlags was just set",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StarFleet_SetLocation",
"addr": "0x006fe2f0",
"convention": "thiscall",
"prototype": "void (StarFleet* this, void* loc) // 16 B: this->LocID(+0xa0) = loc. Saved handle",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StarFleet_GetLocationIfNode",
"addr": "0x006fe300",
"convention": "thiscall",
"prototype": "void* (StarFleet* this) // 19 B: eax = this->LocID(+0xa0); return (eax && eax->+0x14 == 0) ? eax : 0. The +0x14 word is a location-class tag; the sibling FUN_006fe320 returns it only when the tag is 2. The retreat path uses this one, whose class carries the fleet list at +0x16c and the name std::string at +0xa8",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_Execute",
"addr": "0x007d5650",
"convention": "thiscall",
"prototype": "void (RetreatContext* this) // RETREAT PHASE 5. Two nested loops: over this->groups, then over g->fleets (whole fleets from phase 3 plus any new fleet from phase 4). Per fleet f: (a) if (enc->+0x0c && !ServerSystem_IsExploredBy(enc->+0x0c, f->PID(+0x58))) StrategyServer_GrantSystemIntel(S, S->+0x44[enc->+0x0c->Idx(+0x5c)], f->PID) -- RETREATING FROM AN UNEXPLORED SYSTEM REVEALS IT, writing the SAVED EFlags and PlayerView; (b) if CombatRetreat_UsesGate(g) && g->dest(+0x04) != 0, the GATE ARM: StarSystem_FleetDeparts(GetLocationIfNode(f), f), StarSystem_FleetArrives(g->dest, f), then two _snprintf'd strings from the .bss format-string pointers at 0x00aedf0c and 0x00aedf14 (destination Name(+0xa8), fleet FtName(+0x5c)) and EventStorage_PostEvent(ServerPlayer_GetEventStorage(f->PID), {msg, summary, f, 0, S->Frame, 'EVENT_FLEET_RETREATED_VIA_TELEPORT', 0}); (c) else the MOVE ARM: if (!StrategyServer_OrderFleetMove(S+4, f, &g->dest, 1)) log 'Retreat: Unable to set destination for retreat for %s.' at level 2 with f->PID->+0x40, else StarFleet_SetFlag(f, 2, true) and StarSystem_FleetDeparts(GetLocationIfNode(f), f)",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_UsesGate",
"addr": "0x007859f0",
"convention": "cdecl",
"prototype": "bool (RetreatGroup* g) // 48 B, ret 4 (ecx is set at the one call site but unused). return g != 0 && g->mode(+0x2c) == 1 && g->owner(+0x00) != 0 && g->owner->Species(+0x5c) == 1 && g->dest(+0x04) != 0. Species 1 is the gate-building species; this is the ONLY gate on EVENT_FLEET_RETREATED_VIA_TELEPORT, which is posted nowhere else in the image. The same (mode == 1 && species == 1) pair is what lets a ship with a dead drive past StarShip_IsGroundedByDamage in phase 2",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "ServerSystem_IsExploredBy",
"addr": "0x00743880",
"convention": "thiscall",
"prototype": "bool (ServerSystem* this, ServerPlayer* p) // 34 B, ret 4: return (this->EFlags(+0xd0) & (1 << p->PlyrIdx(+0x28))) != 0. EFlags is a SAVED int",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "ServerSystem_SetExploredBy",
"addr": "0x007438b0",
"convention": "thiscall",
"prototype": "void (ServerSystem* this, ServerPlayer* p, bool on) // 82 B, ret 8. bit = 1 << p->PlyrIdx(+0x28); wasSet = (this->EFlags(+0xd0) & bit) != 0; on ? EFlags |= bit : EFlags &= ~bit; then this->vft[0x1c](p, wasSet, on) -- AN INDIRECT EDGE, unresolved here, flagged for the vtable-inversion lane",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StrategyServer_GrantSystemIntel",
"addr": "0x007d5080",
"convention": "thiscall",
"prototype": "void (StrategyServer* this /*the S frame*/, ServerSystem* sys, ServerPlayer* p) // 195 B, ret 8. Returns immediately if either pointer is null. ServerSystem_SetExploredBy(sys, p, true); FUN_006e4620(this->+0x154 /*the node-line/graph object*/, sys, p, 1); then builds a stack temp via FUN_00755ab0, fills it with FUN_007561d0(sys, &tmp, p), and applies it through FUN_007d1f00(this->+0x224, &sys->Idx(+0x5c), &p->PlyrIdx(+0x28), &tmp) -> FUN_0075cf50 -> FUN_00753430, destroying the temp with FUN_006bba00. this->+0x224 is INFERRED to be the per-(system, player) StarSystem::PlayerView map -- combat-done-tail.md phase 17 has its tree head at S+0x228 and its size word at S+0x22c, which puts the map object at S+0x224 -- but the final leg FUN_00753430 was NOT read, so the exact field written is a labelled hypothesis",
"status": "hypothesis",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "CombatRetreat_ContextDtor",
"addr": "0x007a7cd0",
"convention": "thiscall",
"prototype": "void (RetreatContext* this) // RETREAT PHASE 6, and it is only a destructor -- it writes no game state. FUN_00793090(this->+0x1c, this->+0x20) deletes every RetreatGroup; then vector::erase(begin,end) (a degenerate memmove of 0 bytes plus _Mylast = _Myfirst) and operator delete on the buffer, zeroing +0x1c/+0x20/+0x24; then _Tree::erase(head->_Left, head) via FUN_006a4e40 on the map at this->+0x0c and operator delete on its head node",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StrategyServer_CreateFleet",
"addr": "0x0085b340",
"convention": "thiscall",
"prototype": "StarFleet* (StrategyServer* this /*the S+4 frame*/, int oid, ServerPlayer* owner, Mars::Vector3* pos, const char* nameOverride) // 347 B, ret 0x10. operator new(0x120) -- SIZEOF(Game::StarFleet) = 0x120 BY ENUMERATION, agreeing with struct-recovery.md section 3 whose last member FtMS sits at +0x11c -- then ctor FUN_00706140; IDMap_Insert(this+0x80, obj, oid) which writes the id into obj->+0x04; obj->+0x10 = this (the S+4 pointer, NOT a ServerPlayer as it is on StarShip); obj->PID(+0x58) = owner; the name from FUN_00856310(owner) (-> FUN_00855f80 on owner+0x298) assigned into obj->FtName(+0x5c) unless nameOverride is given; pos copied to BOTH +0x18/+0x1c/+0x20 (Pos) and +0x4c/+0x50/+0x54 (PrvPos); StarFleet_SetFlag(obj, 0x400, true); push_back into the vector at this->+0x60 (= S+0x64), THE SAVED NumFlts/FltID/Flt MASTER LIST; then the virtual (*this)->vft[0x10](this, obj). Draw-free, including the name generator. DELEGATED instruction-level read; the call site is mine",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "IDMap_AllocateLocalID",
"addr": "0x0080f710",
"convention": "thiscall",
"prototype": "int (StrategyServer* this /*the S+4 frame*/) // AN 8-BYTE TAIL-CALL THUNK: sub ecx,0xffffff80 (ecx += 0x80) then jmp FUN_008b8b70, which is push [ecx+0x18] (the local node index); call IDMap_AllocateID; ret. IT TAKES NO STACK ARGUMENTS. At the one retreat call site 0x007907d8 three pushes sit immediately before it and belong to the LATER call to StrategyServer_CreateFleet -- the id mint is evaluated inside that call's argument list. Checking ret N against the push count is what catches this; see combat-retreat-pipeline.md section 7.3. The IDMap subobject therefore lives at (S+4)+0x80",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "IDMap_AllocateID",
"addr": "0x008b8ae0",
"convention": "thiscall",
"prototype": "int (IDMap* this, int nodeIndex) // ret 4. THE OBJECT-ID ALLOCATOR. Closes struct-recovery.md section 7's open item 'OID allocation (R2's x16)': R2's x16 is the shl 4 below. Layout: this->+0x08/+0x0c is a vector<NodeEntry> of STRIDE 0x14 (pinned twice -- FUN_008b8a70's 0x66666667 / sar 3 divide-by-20, and the lea eax,[edi+edi*4] with scale 4 here); NodeEntry is a std::map<int,void*> (0x10) followed by the counter at +0x10; this->+0x18 is the local node index. Body: returns 0 if nodeIndex == -1 (logs 'IDMap: Map not initialized.') or the node does not exist; else 0x008b8b16 'inc DWORD PTR [ecx+eax*4+0x10]' PRE-INCREMENTS the per-node counter, and on wrap to 0 logs 'IDMap: NextID wrapping for node %d.' and increments AGAIN (0x008b8b30), so 0 is never issued -- 0 is INVALID_NETWORK_ID; then id = (counter << 4) | (nodeIndex & 0xF) at 0x008b8b38/0x008b8b3e, with an overflow log if the counter no longer round-trips. THE COUNTER IS ALMOST CERTAINLY THE SAVE'S 'NMnx' TAG (StrategyServer block order: KeyPath, NMSz, NMLc, NMnx, ModCount, Frame) -- LABELLED HYPOTHESIS: StrategyServer::Write 0x0079fa70 was not read for it. DELEGATED instruction-level read",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "IDMap_Insert",
"addr": "0x008b9350",
"convention": "thiscall",
"prototype": "void (IDMap* this, void* obj, int id) // ret 8. Logs 'IDMap: Object already exists with id %d.' and continues if the id is taken; refuses id 0 with 'IDMap: Object with INVALID_NETWORK_ID not inserted.'; node = id & 0xF, bounds-checked; WRITES THE ID INTO obj->+0x04 (the NetworkObject handle slot every serialized object uses); then _Buynode + _Insert into the map at _Myfirst + node*0x14. DELEGATED instruction-level read",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StrategyServer_DestroyFleet",
"addr": "0x0088b980",
"convention": "thiscall",
"prototype": "void (StrategyServer* this /*the S+4 frame*/, StarFleet* f, bool killRemainingShips, int shipDestroyArg) // 1352 B, ret 0xc. Called from the retreat split as (f, 0, 0), so the ship-massacre block 0x0088bbf1..0x0088bc82 is dead on that path. A REAL DELETE (vtable slot 0 with flag 1, the MSVC scalar deleting destructor, at 0x0088bea6). Order: (1) sweep the master fleet vector this->+0x60/+0x64 for any fleet whose flight plan (+0xc8/+0xcc, waypoint stride 0x1c) carries a waypoint whose field_0 is f's id -- for each, re-plan it via FUN_00707290 and POST 'EVENT_FLEET_INTERCEPT_ABORTED' to THAT fleet's owner, inlining player+0x29c rather than calling ServerPlayer::GetEventStorage; (2) optionally destroy remaining ships; (3) erase from the owner's fleet map at owner+0x2b4; (4) per player, FUN_00812110(player+0x29c, fleetId, -1) to purge queued events referencing it; (5) erase from this->+0x60 or log 'StrategySim: Can't delete %s (%i), not found in master list!'; (6) detach from its location; (7) vft[0x14](f), erase from this->+0x114, FUN_008b92a0(this+0x80, f); (8) delete. No RNG, no inlined MT draw, no SETurnResults stride. DELEGATED instruction-level read, full body to the next function start",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "StrategyServer_OrderFleetMove",
"addr": "0x008653c0",
"convention": "thiscall",
"prototype": "bool (StrategyServer* this /*the S+4 frame*/, StarFleet* f, ServerSystem** dests, unsigned count) // 801 B, ret 0xc. Returns false if f is null, or if the path solver FUN_007066c0 sets any of bits 0x008 / 0x010 / 0x400 in its flag word (then logging 'StrategySim: %s (%s) move not permitted at this time.' at level 2) -- THE MEANING OF THOSE THREE BITS IS UNKNOWN, FUN_007066c0 was not opened. Before that gate it SNAPS the fleet's position onto its current system when they differ, tested by FUN_0080ec50 with fucompp EXACT IEEE EQUALITY and no epsilon; that snap is committed even on the false path. On success it builds a 0x1c-stride waypoint vector and commits it through FUN_00707080, which debits owner->GTraf(+0x14c) by (int16)f->+0xc0, writes f->+0xd8..+0xf8 and +0x100..+0x108 (cached source/destination positions and the leg record), then re-credits GTraf; finally it cancels conflicting ship actions via FUN_00849280. No RNG and no x87 in the function itself. DELEGATED instruction-level read, full body to the next function start",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
},
{
"name": "Map_IntPtr_Subscript",
"addr": "0x0076bce0",
"convention": "thiscall",
"prototype": "void** (std::map<int, void*>* this, const int* key) // 125 B, ret 4. MSVC std::map<int,T*>::operator[]: _Lbound over the tree from this->_Myhead(+0x04)->_Parent, testing _Isnil at node+0x15 and the key at node+0x0c; if found returns &node->_Myval.second (node+0x10), else default-inserts the pair {key, 0} via _Buynode 0x008b91d0 + _Insert 0x0072b400 and returns the same. NODE IS 0x18 BY ENUMERATION from _Buynode's operator new(0x18): _Left +0x00, _Parent +0x04, _Right +0x08, pair<int,void*> at +0x0c/+0x10, and _Color/_Isnil written as ONE 16-bit store at +0x14/+0x15, plus 2 bytes padding. DELEGATED instruction-level read",
"status": "verified",
"source": "findings/control-flow/combat-retreat-pipeline.md"
}
]
}