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.
This commit is contained in:
alex 2026-09-08 12:00:14 -04:00
parent 0253941d16
commit ab1c2296a1
2 changed files with 874 additions and 0 deletions

View file

@ -0,0 +1,606 @@
# `FUN_007d5a00` and its six callees, read from the instruction stream — the post-battle **retreat** pipeline
Lane B5, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs.
**Method.** Every one of the six callees, the dispatcher itself and 18 further helpers were disassembled byte
for byte with `objdump -b binary -m i386 -M intel` over the raw image, **each to the next function start**, not
to Ghidra's reported size (rule 17). Call targets from `dumps/functions.json`, immediates resolved against
`.rdata`/`.data`. No claim about control flow below comes from a decompiler. Two callee subtrees
(`FUN_0088b980`, `FUN_008653c0`) and the object-id allocator chain were read by delegated sweeps working from
the same instruction stream; **every such claim is marked**.
Closes the **tier-2 #1** gap of `findings/control-flow/combat-resolver.md` (lane J §8), which named these six
as *"the real per-phase combat pipeline … the best reimplementation target in the whole combat path"*.
It **corrects** that characterisation — see §7.
---
## 0. Lead: what reaches saved state
`FUN_007d5a00` is not a general combat pipeline. It is **retreat**, and it is the single largest writer of
saved state anywhere under the combat resolver. Lane J's headline — *"the resolver's own body writes nothing
outside its stack context"* — stands, and this is where a large part of the "its callees write plenty" lives.
Seven saved fields or containers are written on the retreat path. Every one is `verified` in
`findings/objects/struct-recovery.md`, so the autosave oracle can see all of them.
| # | what is written | where, in save terms | by | how it moves |
|---|---|---|---|---|
| **1** | **`StarShip.FltID`** (`ship+0x64`) | every retreating ship's fleet handle | `FUN_007063a0` / `FUN_007062e0` | a partially-retreating fleet's ships are re-parented into a brand-new fleet |
| **2** | **`StarFleet` objects themselves** — the `NumFlts / FltID / Flt` list off `StrategyServer` | `(S+4)->+0x60` = **`S+0x64`** | `FUN_0085b340` appends; `FUN_0088b980` erases | one fleet **created** per partially-retreating group; every fleet left with zero ships **destroyed** |
| **3** | **`NMnx` — the object-id counter** | the `StrategyServer` block's 4th tag, right after `NMSz`/`NMLc` | `IDMap::AllocateID` 0x008b8ae0 | **`inc DWORD PTR [ecx+eax*4+0x10]` at 0x008b8b16**, once per created fleet. See §5 |
| **4** | **`StarFleet.NShips`** (`fleet+0xa4/+0xa8`) and **`FtFlg`** (`+0x10c`) | per fleet | `FUN_007062e0`, `FUN_006fe1a0` | membership; the new fleet is created with flag **0x400** set, and a retreating fleet gets flag **2** OR'd in |
| **5** | **`StarFleet.LocID`** (`+0xa0`), **`Pos`** (`+0x18`), **`PrvPos`** (`+0x4c`) and the inline **`FlightPlan`** (`+0xc4..0xfb`) | per fleet | `FUN_006fe2f0`, `FUN_0074f240`, `FUN_0074d1c0`, `FUN_008653c0`→`FUN_00707080` | gate retreat teleports the fleet outright; a normal retreat installs a flight plan and detaches from the system |
| **6** | **`ServerSystem` presence and knowledge masks** — `VFlags(+0xcc)`, **`EFlags(+0xd0)`**, `AFlags(+0xd4)`, `FFlags(+0xd8)` — and **`NumFlts/Flt`** (`sys+0x16c`) | per system | `FUN_0074f240` / `FUN_0074d1c0` / `FUN_007438b0` | fleets entering and leaving; and **retreating from a system you had not explored marks it explored for you** |
| **7** | **`ServerPlayer.GTraf`** (`+0x14c`) and the per-system **`NVs` / `StarSystem::PlayerView`** map | per player / per (system, player) | `FUN_007063a0`, `FUN_00707080`, `FUN_007d5080` | gate traffic is debited and re-credited around every membership and flight-plan change |
**Three of those are new to the campaign's picture of combat**, and one closes a long-open question:
* **`NMnx` moves because a battle happened.** `struct-recovery.md` §7 lists *"`OID` allocation (R2's ×16)"* as
open. R2's "×16" is the `shl esi,0x4` at 0x008b8b38: an object id is `(counter << 4) | (nodeIndex & 0xF)`.
The counter is per network node, at `IDMap->+0x08 + node*0x14 + 0x10`, pre-incremented, and value 0 is
skipped with a `"IDMap: NextID wrapping for node %d."` log. **A partial retreat mints one id per group, and
that counter is serialized.** *(Delegated read, instruction-level, of `FUN_008b8ae0` / `FUN_008b8a70` /
`FUN_008b9350` / `FUN_008b8b70`; the call site and the thunk I re-read myself — see §7.3, where I correct my
own first reading of the argument list.)*
* **Retreat grants system knowledge.** `FUN_007d5650` tests `enc->system->EFlags & (1 << PlyrIdx)` and, when
clear, runs `FUN_007d5080` → `ServerSystem::SetExplored(player, true)` **plus** a `PlayerView` refresh
through the map at `S+0x224`. Both are saved.
* **Retreat rewrites the fleet graph.** Not a stat, not an event — the actual object graph the save's
`NumFlts` / `NShips` / `FltID` chain encodes.
**Nothing here writes the turn-results accumulator.** A whole-closure scan for the `SETurnResults` stride
(`imul r32,r32,0x11c` / `add r32,0x11c` at real instruction boundaries) over all 327 reachable functions
returns exactly one hit, 0x00706290, and it is a **false positive**: `mov DWORD PTR [esi+0x11c],0xffffffff` in
the `StarFleet` constructor, initialising `FtMS`. So lane J's `+0x90` writer and lane K's `+0x24` writer remain
the only two on the combat path.
### 0.1 A falsifiable prediction (rule 2)
Written before any run, for the workload `combat-resolver.md` §10.3 specifies:
> **On the first turn a real battle produces a retreat in which a fleet only partly runs, the post-turn
> autosave's `Summary` will differ from the pre-turn one in `NMnx` by exactly the number of retreat groups
> that had at least one split fleet — one per group, not one per ship and not one per fleet.**
> Additionally `NumFlts` rises by that same count and falls by the number of source fleets emptied.
How it could be wrong, and the symptom of each way:
1. `NMnx` might not be the `IDMap` counter at all (it is identified from the tag order `NMSz, NMLc, NMnx`
plus the counter's location, **not** from reading `StrategyServer::Write`). Symptom: `NMnx` does not move
at all, or moves on turns with no fleet creation.
2. The counter may advance for other reasons in the same turn (any object creation uses the same allocator).
Symptom: the delta exceeds the group count. That does **not** falsify the mechanism; it means the
experiment needs a turn whose only creation is the retreat.
3. A group can be flagged partial and still move no ship (every candidate's source fleet already retreating
whole). The fleet is created anyway — `FUN_00790790` creates it **before** the ship loop. Symptom: a fleet
with zero ships in the save. If that never appears, my reading of the creation order is wrong.
Prediction 3 is the interesting one: it is a **predicted bug in the original**, and it is cheap to look for in
any save taken after a battle.
---
## 1. The dispatcher — `FUN_007d5a00`, and its `this`
Called unconditionally by `CombatResolver_Run` at 0x007d5be2 (lane J block A), one caller image-wide.
**Real body 0x007d5a00..0x007d5abb; Ghidra's 225 bytes is right this time** (the range to the next start at
0x007d5af0 is 15 bytes of `int3` padding plus the `_CxxThrowException` tail).
It builds a **second, much smaller stack context** from the resolver's, and runs six methods on it. Fully
instruction-verified:
```
7d5a26 mov eax,[ecx+0xc] ; resolver ctx->+0x0c = Game::EncounterResults* res
7d5a29 mov edx,[ecx+0x8] ; +0x08 = Encounter* enc
7d5a2c mov ecx,[ecx] ; +0x00 = StrategyServer* S (the S frame)
7d5a32 mov [ebp-0x48],ecx ; rc->+0x00 = S
7d5a35 mov [ebp-0x44],edx ; rc->+0x04 = enc
7d5a38 mov [ebp-0x40],eax ; rc->+0x08 = res
7d5a3b mov [ebp-0x34],esi ; rc->+0x14 = 0 (map _Mysize)
7d5a3e call operator_new(0x18) ; the map's head node
7d5a4d..7d5a66 head->+0x00 = head->+0x04 = head->+0x08 = head; head->+0x14 = head->+0x15 = 1
7d5a69..7d5a6f rc->+0x1c = rc->+0x20 = rc->+0x24 = 0 (vector _Myfirst/_Mylast/_Myend)
7d5a78 call FUN_0079bb90 ; 1. per-player retreat destinations
7d5a80 call FUN_0079bcd0 ; 2. build the retreat groups
7d5a88 call FUN_007b0320 ; 3. whole-fleet vs partial
7d5a90 call FUN_00790790 ; 4. split the partial fleets
7d5a98 call FUN_007d5650 ; 5. execute the retreat (EVENT_FLEET_RETREATED_VIA_TELEPORT)
7d5aa7 call FUN_007a7cd0 ; 6. destructor
```
**All six are unconditional and in a straight line.** The only `jcc` in the function is the
`operator new` null test at 0x007d5a48, whose false arm is the `"vector<T> too long"` throw. There is no phase
gate.
### 1.1 `RetreatContext` — enumerated, not touched-scanned
```c
// A ~0x2c-byte STACK local in FUN_007d5a00. Never heap, never serialized.
struct RetreatContext {
StrategyServer* S; // +0x00 the S frame (verified: ->+0x44 is Systems, ->+0x84 the handle map)
Encounter* enc; // +0x04 (verified: ->+0x0c the system, ->+0x1c..+0x24 the position)
Game::EncounterResults* res; // +0x08 (verified: ->+0x68/+0x6c is the retreat request list)
std::map<int, ServerSystem*> dest; // +0x0c key = PlyrIdx. _Kcompare/_Alnod empty at +0x0c,
// _Myhead +0x10, _Mysize +0x14, _Alval +0x18
std::vector<RetreatGroup*> groups; // +0x1c _Myfirst/_Mylast/_Myend, _Alval +0x28
};
```
The map's element size is settled by its **node**, not by what the code touches: `_Buynode` is
`operator new(0x18)` writing `_Left/+0x00`, `_Parent/+0x04`, `_Right/+0x08`, `pair<int,void*>` at `+0x0c`,
`_Color/+0x14` and `_Isnil/+0x15` **as one 16-bit store**. 3 pointers + 8-byte pair + 2 bytes + 2 padding =
0x18 exactly. `FUN_0076bce0` is `std::map<int,T*>::operator[]`: `_Lbound`, then either `&node->_Myval.second`
(`node+0x10`) or a default-insert of **0**. *(Node and `operator[]` are a delegated read; the head-node writes
and the 0x18 allocation I read myself in the dispatcher.)*
### 1.2 `RetreatGroup` — 0x34 bytes, enumerated from its constructor
`FUN_0078f1c0` (41 B, instruction-verified by me) writes every field, which is what settles the layout —
including the two allocator words it does **not** write (rule 5):
```c
struct RetreatGroup { // operator new(0x34), ctor FUN_0078f1c0
ServerPlayer* owner; // +0x00 = ship->+0x10
ServerSystem* dest; // +0x04 the retreat destination
std::vector<StarShip*> ships; // +0x08/+0x0c/+0x10, _Alval +0x14
std::vector<StarFleet*> fleets; // +0x18/+0x1c/+0x20, _Alval +0x24
bool partial; // +0x28 set when a source fleet only partly ran
int mode; // +0x2c = request->+0x08 (1 selects the gate path)
int variant; // +0x30 = request->+0x10 (ctor default -1)
}; // 0x34
```
Groups are keyed on **all four** of `(owner, dest, mode, variant)` — `FUN_007871c0` (79 B, `ret 0x10`,
instruction-verified) is a linear scan comparing exactly those four words.
---
## 2. The phase map
| # | addr | size (Ghidra / real) | what it does | evidence |
|---|---|---|---|---|
| **1** | `FUN_0079bb90` | 184 / 184 (`ret` at 0x0079bc47) | **Per-player retreat destination.** One loop over `enc->members` (stride 0x44, magic `0x78787879 / sar 5`): `FUN_00787210(&enc->+0x1c, enc->+0x0c, member->player, &r1, &r2, &r3)`, then `dest[player->PlyrIdx] = first non-null of (r1, r2, r3)` | verified by me |
| **2** | `FUN_0079bcd0` | 422 / 422 (`ret` at 0x0079be75) | **Build the groups.** One loop over `res->+0x68/+0x6c`, **stride 0x14** (`0x66666667 / sar 3`) — a list of retreat requests. Resolve, filter, find the destination, find-or-create the group, append the ship | verified by me |
| **3** | `FUN_007b0320` | 808 / 808 (`ret` at 0x007b063f) | **Whole fleet or partial?** Per group, tally its ships by `ship->FltID`; a fleet whose tally equals `(fleet->+0xa8 − fleet->+0xa4)/4` goes into `group->fleets`, otherwise `group->partial = 1` | verified by me |
| **4** | `FUN_00790790` | 500 / 500 (`ret` at 0x00790978) | **Split.** Only for groups with `partial != 0`: mint an id, create a fleet, append it to `group->fleets`, then move every ship whose old fleet is *not* in that list; destroy any old fleet left empty; attach the new fleet to `enc->system` | verified by me |
| **5** | `FUN_007d5650` | 927 / 927 (`ret` at 0x007d59f4) | **Execute.** Per group, per fleet: grant system knowledge if unexplored; then either **gate-teleport** it (and post `EVENT_FLEET_RETREATED_VIA_TELEPORT`) or give it a **move order** and detach it from the system | verified by me |
| **6** | `FUN_007a7cd0` | 190 / 190 (`ret` at 0x007a7da5) | **Destructor.** `FUN_00793090(first, last)` deletes every group, then `erase(begin,end)` + `operator delete` on the vector buffer, then `_Tree::erase(head->_Left, head)` + `operator delete` on the map head. Writes no game state | verified by me |
Ghidra's size was correct for all six — but each was dumped to the next function start anyway, and the padding
was inspected. `FUN_007a7cd0` is the one where it would have mattered: its body ends only 34 bytes before the
next start.
### 2.1 Phase 1 in full — the retreat destination rule
`FUN_00787210` (318 B, `ret 0x18`, thiscall, **instruction-verified by me**) is the formula. It is three
independent nearest-system searches sharing one pass over `S->Systems` (`S->+0x44/+0x48`, 4-byte stride):
```
best1 = best2 = best3 = FLT_MAX ; the .rdata float at 0x009e23a8 is 0x7f7fffff
r1 = r2 = r3 = NULL
for sys in S->Systems:
if sys == enc->+0x0c: continue ; never the battle's own system
if FUN_007469e0(sys): continue ; unusable, see below
d = |sys->Pos - enc->pos|^2 ; SQUARED, float32
if d < best1 and FUN_007437e0(sys) == player: best1 = d; r1 = sys
if d < best2 and not FUN_00744070(sys, player): best2 = d; r2 = sys
if d < best3: best3 = d; r3 = sys
*out1 = r1; *out2 = r2; *out3 = r3
```
and the caller takes the **first non-null of r1, r2, r3**. So: *retreat to the nearest system you own; failing
that to the nearest system with no hostile presence; failing that to the nearest system, full stop.*
Two details a reimplementation gets wrong by default:
* **The three best-so-fars are genuinely independent.** A nearer system that fails a predicate does not spoil
that search — the `best` store lives *inside* the predicate arm at 0x007872c6 / 0x007872ec, not before it.
A single-loop "find nearest, then filter" version produces a different system.
* **The comparison is a strict `<`** (`fld best; fcomp d; test ah,0x41; jne skip` — the jump is taken on
*less-or-equal*, so only `best > d` proceeds). On an exact tie the **earlier** system in the vector wins,
which makes the answer depend on `Systems` order.
The distance arithmetic, read instruction by instruction at 0x0078726d–0x007872a3: the three component
differences are each `fstp`-ed to **float32** first; the products and their sum are accumulated in the x87
stack; the total is narrowed to float32 once, at 0x007872a3. Because a float32 delta has 24 significand bits,
each square is exact in double and so is the three-term sum — so a `double` accumulator with a single final
narrowing is **bit-identical** to the original here, not an approximation.
The three predicates, all instruction-verified by me:
| addr | what it is | body |
|---|---|---|
| `FUN_007469e0` (33 B) | **`ServerSystem::IsUnusableRetreatTarget`** | `return sys->Dstyd(+0xc5) != 0 \|\| (sys->vnh(+0xc6) != 0 && sys->vnex3(+0xc8) == 0)` — a destroyed system, or one hosting a Von Neumann hive that has not been cleared. All three are **saved bools**, which is what identifies the class as `Game::ServerSystem` |
| `FUN_007437e0` (7 B) | **`ServerSystem::GetOwner`** | `return sys->PID(+0x100)` |
| `FUN_00744070` (32 B) | **`ServerSystem::HasHostilePresence(player)`** | `return (FUN_0080dff0(player, sys) & sys->AFlags(+0xd4)) != 0` |
`FUN_0080dff0` (96 B, instruction-verified by me) is the hostility mask, and it carries a diplomacy rule that
is not in `findings/subsystems/` anywhere:
```
ownerBit = (sys && sys->PID && sys->TAcq(+0x80) == S->Frame) ? (1 << sys->PID->PlyrIdx) : 0
return ~( (1 << P->PlyrIdx) | (P->CF(+0x174) & ~ownerBit) | P->NA(+0x170) | P->AL(+0x16c) )
```
**A system whose owner captured it on the current turn loses that owner's ceasefire cover.** `TAcq` is the
saved "turn acquired" int and `S->+0xc` is `Frame` (lane Z's naming), reached as `P->+0x8->+0x8` because
`ServerPlayer+0x8 == S+4` (lane K §7.3). The complement is taken over the full 32 bits; that is harmless
only because it is immediately ANDed with a presence mask.
### 2.2 Phase 2 in full — the request list and the filters
`res->+0x68/+0x6c` is a **0x14-stride vector of retreat requests**, a member of `Game::EncounterResults`
lane J did not have. Fields used: `+0x04` the ship's handle, `+0x08` the mode word, `+0x0c` a requested
destination handle, `+0x10` the second key word.
Per request (all instruction-verified by me):
```
ship = HandleMap::Resolve(S->+0x84, req->+0x04); if (!ship) skip
if (ship->Dep(+0x50) != 0) skip ; already departed
if (ship->EncID(+0x7c) == 0x15) skip
if (FUN_004f47f0(ship->EncID)) skip ; DB-driven encounter-type mask
if (ship->PlrID(+0x10) == S->Players[S->+0x1c0]) skip ; the locally-controlled player
dest = FUN_0079bc50(ctx, req, ship); if (!dest) log level 2 and skip
if (!(req->+0x08 == 1 && ship->PlrID && ship->PlrID->Species(+0x5c) == 1))
if (FUN_00815090(ship)) skip ; grounded by damage
g = FUN_007871c0(ctx, ship, dest, req->+0x08, req->+0x10)
if (!g) { g = new RetreatGroup(); g->owner = ship->PlrID; g->dest = dest;
g->mode = req->+0x08; g->variant = req->+0x10; ctx->groups.push_back(g); }
g->ships.push_back(ship)
```
* **`FUN_0079bc50` (122 B, `ret 8`)** is the destination lookup: if `req->+0x0c` resolves through the handle
map, that is the answer (an explicitly ordered destination); if it does not, it logs
`"Retreat: %s cannot retreat to specified location. Location not found.\n"` and falls through to
`ctx->dest[ship->PlrID->PlyrIdx]` — the default from phase 1. It also has a guard returning 0 when the
ship carries no handle id.
* **`FUN_00815090` (46 B)** is the damage gate: `design = ship->DesID(+0x14)`; true (⇒ cannot retreat) iff
`(design->+0x74 || design->+0x78)` **and** `design->+0x12c < 2` **and** `FLT_EPSILON > ship->+0x30`. The
`.rdata` float at 0x009e1ef8 is `0x34000000` = **1.1920929e-07, `FLT_EPSILON`**, and `ship+0x30` is the
third float of the inline `ShipHealth` at `0x24..0x33` — the one R1 guesses is the drive. So: *a ship whose
drive is dead cannot run.*
* **The species-1 bypass.** The gate is skipped entirely when `mode == 1` and the owner's `Species(+0x5c)` is
**1**. That is the same species/mode pair phase 5 requires for the teleport arm — the gate species does not
fly out, so a dead drive does not stop it.
* **`FUN_004f47f0` (67 B)** is a **DB-sourced** predicate: it lazily ORs `1 << table[i]` over a count at
0x00ae04e0 and an int array at 0x00adfe88 (both `.bss`, filled from the data files), caches the mask at
0x00b0e984 behind a flag at 0x00b0e988, and returns `(mask >> t) & 1` for `t <= 0x17`, false above. The
bound 0x17 is the same `EncounterType` range lane K found `PickDominantEncounterType` returning over. **Do
not hard-code this mask.**
### 2.3 Phase 3 in full — whole versus partial
`FUN_007b0320` (instruction-verified by me). Per group:
```
tally = {} ; a local vector<{StarFleet*, int}>, 8-byte stride
for ship in group->ships:
f = ship->FltID(+0x64)
linear-search tally for f; if absent push {f, 0}; then ++entry.count
for (f, n) in tally:
if n == (f->+0xa8 - f->+0xa4)/4: group->fleets.push_back(f) ; the whole fleet ran
else: group->partial = 1
```
The `jb`/`jae` pair around 0x007b0504 / 0x007b0570 is **not** a phase gate: it is MSVC's inlined
`vector::push_back` aliasing check (`_Inside(&_Val)`), and both arms converge on
`*_Mylast = value; _Mylast += 4` at 0x007b05ce. Rule 4, same family as the inlined string destructor.
`fleet->+0xa4/+0xa8` is `StarFleet.NShips` (`struct-recovery.md` §3), so *whole* means literally "every ship
this fleet has is in this group".
### 2.4 Phase 4 in full — the split, and a predicted bug
`FUN_00790790` (instruction-verified by me). Gated on `group->partial != 0`; groups without it are untouched.
```
newFleet = FUN_0085b340(S+4, IDMap::AllocateLocalID(), group->owner, &enc->+0x1c, NULL)
group->fleets.push_back(newFleet) ; BEFORE any ship moves
for ship in group->ships:
old = ship->FltID
if old is already in group->fleets: continue ; its whole fleet is retreating; leave it
FUN_007063a0(old, ship) ; StarFleet::RemoveShip
if ((old->+0xa8 - old->+0xa4) & ~3) == 0:
FUN_0088b980(S+4, old, 0, 0) ; StrategyServer::DestroyFleet
FUN_007062e0(newFleet, ship) ; StarFleet::AddShip
if (enc->+0x0c) FUN_0074f240(enc->+0x0c, newFleet) ; StarSystem::FleetArrives
```
The `group->fleets.push_back(newFleet)` at 0x0079083c–0x007908af happens **before** the ship loop, and the
"is my old fleet already in the list" test at 0x007908e0 is what keeps a wholly-retreating fleet's ships where
they are. **A group flagged partial therefore always creates a fleet, even in the case where every ship turns
out to belong to a wholly-retreating fleet** — which is reachable when one fleet ran whole and a second
contributed a ship that was itself filtered out before phase 3's tally. That is prediction 3 of §0.1.
`FUN_007062e0` / `FUN_007063a0` (186 / 386 B, both instruction-verified by me) are the membership pair:
* **AddShip**: `fleet->NShips.push_back(ship)`; **`ship->FltID = fleet`**; `fleet->+0xb8 |= design->+0xb8`
and `+0xbc |= design->+0xbc` (capability masks); `fleet->+0xc0 (int16) += FUN_0056f430(design->+0x12c)`;
if flag `0x20000` on `+0xb8` changed, register in `(fleet->+0x10)->+0x114`; `FUN_00705c70(fleet)`.
* **RemoveShip**: if the fleet's current waypoint is a gate transit (`IsGateTransitWaypoint`, already named in
`strategic-turn-internals.md` §4.1), **`owner->GTraf(+0x14c) -= fleet->+0xc0`** — then `ship->FltID = 0`,
erase, recompute, and **re-add** `+0xc0` afterwards. `GTraf` is a saved player field.
**`FUN_0088b980` = `StrategyServer::DestroyFleet(fleet, killShips, arg)`**, called here with `(fleet, 0, 0)`
so the ship-massacre block is dead on this path. *(Delegated read, in full, to the next function start.)* It
is a real `delete`, and it unhooks the fleet from **six** places: the owner's fleet map at `owner+0x2b4`, the
server master vector at `(S+4)+0x60`, the location's fleet vector, a second server list at `(S+4)+0x114`,
`FUN_008b92a0(S+4+0x80, fleet)` (the id map), and every player's queued events via
`FUN_00812110(player+0x29c, fleetId, -1)` — the same `EventStorage` offset the campaign already models.
Before all that it sweeps the master fleet list for any fleet whose flight plan targets the dying one and
posts **`EVENT_FLEET_INTERCEPT_ABORTED`** to that fleet's owner, inlining `player+0x29c` rather than calling
`ServerPlayer::GetEventStorage`.
### 2.5 Phase 5 in full — executing it
`FUN_007d5650` (instruction-verified by me). Two nested loops: over `ctx->groups`, then over
`group->fleets` (which by now holds the whole fleets from phase 3 *and* any new fleet from phase 4).
```
if (enc->+0x0c && !FUN_00743880(enc->+0x0c, fleet->PID(+0x58)))
FUN_007d5080(S, S->+0x44[enc->+0x0c ->Idx(+0x5c)], fleet->PID) ; grant knowledge
if (FUN_007859f0(group) && group->dest != 0) { ; --- GATE ARM ---
if ((n = FUN_006fe300(fleet))) FUN_0074d1c0(n, fleet) ; leave the old system
FUN_0074f240(group->dest, fleet) ; arrive at the new one
... build two strings from data-driven format strings ...
EventStorage::PostEvent(GetEventStorage(fleet->PID),
{ msg, summary, fleet, 0, S->Frame, "EVENT_FLEET_RETREATED_VIA_TELEPORT", 0 })
} else { ; --- MOVE ARM ---
if (!FUN_008653c0(S+4, fleet, &group->dest, 1))
Log(2, "Retreat: Unable to set destination for retreat for %s.\n", fleet->PID->+0x40)
else {
FUN_006fe1a0(fleet, 2, 1) ; FtFlg |= 2
if ((n = FUN_006fe300(fleet))) FUN_0074d1c0(n, fleet) ; detach; it is in transit
}
}
```
* **`FUN_007859f0` (48 B, `ret 4`, instruction-verified)** is the gate test, and it is three conditions:
`group->mode(+0x2c) == 1` **and** `group->owner->Species(+0x5c) == 1` **and** `group->dest(+0x04) != 0`.
Species 1 is the gate-building species; this is the Hiver gate mechanic, and `EVENT_FLEET_RETREATED_VIA_TELEPORT`
fires nowhere else.
* **`FUN_00743880` (34 B)** is `ServerSystem::IsExploredBy(player)` = `sys->EFlags(+0xd0) & (1 << PlyrIdx)`,
and **`FUN_007438b0` (82 B)** is its setter — both instruction-verified by me. `FUN_007d5080` (195 B, also
mine) calls the setter with `true`, registers with the node graph at `S->+0x154`, and refreshes the
per-(system, player) view through the map at `S+0x224` (`FUN_007d1f00` → `FUN_0075cf50` → `FUN_00753430`).
The last leg of that chain is **not** read: I have the call shape, not the body. Lane K's phase 17 says
`S+0x228` is the `PlayerView` tree's head, which puts the map object at `S+0x224` — consistent, but the
exact field written is **inferred, not verified**.
* **`FUN_006fe300` / `FUN_006fe2f0` / `FUN_006fe1a0`** (19 / 16 / 37 B, all mine) are
`StarFleet::GetLocation` (returns `+0xa0` only when `loc->+0x14 == 0`), `SetLocation` and
`SetFlag(mask, on)` on `FtFlg(+0x10c)`.
* **`FUN_0074f240` / `FUN_0074d1c0`** (162 / 273 B, mine) are `StarSystem::FleetArrives` / `FleetDeparts`.
Arrives copies the system's `Pos` into the fleet's `+0x18/+0x1c/+0x20`, sets `SetLocation`, ORs the player's
bit into `FFlags(+0xd8)`, pushes the fleet into `sys->+0x16c` (the saved `NumFlts`/`Flt` list) and then
recomputes `AFlags(+0xd4)` and `VFlags(+0xcc)` as
`if (FFlags & bit || GFlags(+0xdc) & bit || PID == player) { AFlags |= bit; VFlags |= bit; } else AFlags &= ~bit`.
Departs is the mirror, and clears the `FFlags` bit only when the departing fleet was the player's **last**
at that system (`cmp [ebp-0x4],1; jg` at 0x0074d250 — it counts same-owner fleets during the erase scan).
* **`FUN_008653c0` = `StrategyServer::OrderFleetMove(fleet, dests[], count) -> bool`** *(delegated read, in
full)*. It snaps the fleet's position onto its current system when they differ — by **exact IEEE equality**,
`fucompp` with no epsilon — solves a path, and commits a flight plan through `FUN_00707080`, which debits
and re-credits `owner->GTraf(+0x14c)` around the change and writes `fleet+0xd8..0xf8` and `+0x100..+0x108`.
It returns false when the fleet is null or when the solver sets any of bits `0x008 / 0x010 / 0x400` in its
flag word; **the meaning of those three bits is not known** — `FUN_007066c0` was not opened.
---
## 3. RNG: confirmed draw-free, computed independently
Lane J's closure scan said this subtree draws nothing. Recomputed here from a different root and with a
different tool:
* **Closure**: BFS from 0x007d5a00 over `E8` call edges **plus `E9` tail-call thunks** (the first pass missed
`FUN_0080f710`, an 8-byte `jmp` thunk, and with it the entire id-allocator chain — worth recording, because
a call-graph that ignores tail calls silently loses whole subsystems). **327 functions.**
* **Direct calls to `RNG_NextInt` 0x004271c0, `RNG_NextFloat` 0x0047d830, `RNG_Chance` 0x008e6dd0,
`RNG_Twist` 0x00426e00: zero.**
* **Inlined Mersenne draws**: an instruction-boundary scan of all 327 bodies for the tempering immediates
`0xff3a58ad`, `0xffffdf8c`, `0x9d2c5680`, `0xefc60000` — **zero hits**. This is the check lane K's sweep
did not do and lane J's rule 16 exists for.
* Both delegated sweeps ran the same scan over `FUN_0088b980` and `FUN_008653c0` and their subtrees and
reported the same.
**The fleet-name generator is clean too.** `FUN_0085b340` names a new fleet through
`FUN_00856310(owner)` → `FUN_00855f80(owner+0x298)`, which is the one plausible place a draw could hide. It
**is** inside the 327 and carries no draw. The only non-constant input to a created fleet is the monotonic
id counter.
**The honest bound.** 91 of the 327 functions contain 152 indirect call sites, and none of them is resolved
here. This is a bound, not a proof, in exactly lane J §3.3's sense. Two of those indirect edges are on the
main line and matter: `FUN_007438b0`'s `sys->vft[0x1c](player, wasSet, on)` notify and
`FUN_0074d1c0`'s `sys->vft[0x14](fleet)` — **flagged for lane V2's vtable inversion**, along with
`FUN_0085b340`'s `(S+4)->vft[0x10](fleet)` and `FUN_008653c0`'s `(S+4)->vft[0x08]()`.
---
## 4. Events
Two `EVENT_*` keys in the entire 327-function closure, found by scanning every reachable body for `.rdata`
string immediates at real instruction boundaries:
| key | posted by | depth | condition |
|---|---|---|---|
| `EVENT_FLEET_RETREATED_VIA_TELEPORT` | `FUN_007d5650` | 1 | per retreating fleet, only on the gate arm: `group->mode == 1` **and** owner species **1** **and** a destination resolved |
| `EVENT_FLEET_INTERCEPT_ABORTED` | `FUN_0088b980` | 3 | per *other* fleet whose flight plan targets a fleet being destroyed, when a split empties that fleet. Posted to the **interceptor's** owner, not the retreater's |
Against lane J's table of 23: both were already listed, at the right depths. What is new is the **condition**
for each, and the fact that these two are the **only** ones the pipeline can post — the other 21 come from the
resolver's own body or from its other callees, never from here.
Neither goes through the string-table lookup (`FUN_008c9690`) the resolver uses for its composed keys; both are
literal `push imm32` of a `.rdata` string, so there is no data-file dependency on the **key**. The two
human-readable strings that ride with the teleport event *are* data-sourced: the format strings are
`*(char**)0x00aedf0c` and `*(char**)0x00aedf14`, `.bss` pointers filled at load, rendered through
`FUN_008c8eb0` (`_snprintf` into a 0x1ff buffer) with the destination system's `Name(+0xa8)` and the fleet's
`FtName(+0x5c)`.
The post itself, read from the bytes at 0x007d5859–0x007d58c5, is the campaign's known shape:
`EventStorage::PostEvent` 0x008862b0 with `this = ServerPlayer::GetEventStorage(fleet->PID)` 0x0080db00, the
two `std::string`s passed **by value** through the `sub esp,0x1c` + `FUN_00425430` copy-construct idiom, and
`S->Frame` as the turn. Dedup per turn bucket applies as it does everywhere else.
---
## 5. Object facts recovered on the way
**`sizeof(Game::StarFleet) = 0x120` (288).** From `operator new(0x120)` in `FUN_0085b340` *(delegated read)*.
That agrees exactly with `struct-recovery.md` §3, whose last named member is `FtMS` at `+0x11c` — an
independent enumeration closing a size the table left implicit.
**The object-id allocator, and `NMnx`.** `IDMap` lives at `(S+4) + 0x80`. Its node table is a
`vector<NodeEntry>` at `+0x08/+0x0c`, **stride 0x14** — pinned twice, by `FUN_008b8a70`'s `0x66666667 / sar 3`
divide-by-20 and by the `lea eax,[edi+edi*4]` + scale-4 index arithmetic. Each `NodeEntry` is a
`std::map<int, void*>` (0x10) followed by the counter at `+0x10`. `IDMap::AllocateID`:
```
008b8b16 inc DWORD PTR [ecx+eax*4+0x10] ; pre-increment the per-node counter
008b8b20 jne 0x8b8b34 ; on wrap to 0, log and increment again (0x8b8b30)
008b8b38 shl esi,0x4
008b8b3e or esi,edx ; id = (counter << 4) | (node & 0xF)
```
so **id 0 is `INVALID_NETWORK_ID`**, the low nibble is the network node index (max 16 nodes), and the upper
28 bits are the sequence. `IDMap::Insert` 0x008b9350 writes the id into the object at **`obj+0x04`** — the
same `+0x04` handle slot `StarShip` and every `NetworkObject` uses. *(All of §5's allocator content is a
delegated instruction-level read; I re-read only the call site and the thunk.)*
`struct-recovery.md` §7's open item *"`OID` allocation (R2's ×16)"* is therefore **closed**: R2's ×16 is the
`shl 4`. What is **not** closed is where `NodeEntry.nextId` is initialised and whether the save's `NMnx` tag
is exactly this counter. The tag order `KeyPath, NMSz, NMLc, NMnx, ModCount, Frame` is highly suggestive
(size, local node, next id) but `StrategyServer::Write` 0x0079fa70 was **not** read for it. Labelled
hypothesis, and §0.1's prediction is the cheap way to test it.
**`Game::EncounterResults +0x68/+0x6c`** — a 0x14-stride vector of retreat requests, with `+0x04` a ship
handle, `+0x08` a mode word, `+0x0c` a destination handle and `+0x10` a second key word. New; lane J §8 tier 3
lists 16 of the 0x178 bytes as named, and this adds one member.
**`ServerSystem` mask semantics**, from the two writers that maintain them:
`EFlags(+0xd0)` = explored-by; `FFlags(+0xd8)` = has-a-fleet-here; `GFlags(+0xdc)` = a second presence source
(not read here); `AFlags(+0xd4)` = the derived union `FFlags | GFlags | isOwner`, recomputed on every arrival
and departure; `VFlags(+0xcc)` = the same union but **sticky** (only ever OR'd). All five are saved ints.
---
## 6. What could not be settled statically
Named with the workload that would settle each, per the brief.
| open item | why static reading cannot close it | what would |
|---|---|---|
| **Whether `NMnx` is the `IDMap` counter** | the tag is emitted by `StrategyServer::Write` 0x0079fa70, which was not read | read the 3 relevant lines of 0x0079fa70 (cheap), **or** run §0.1's prediction |
| **The three failure bits `0x008 / 0x010 / 0x400`** of `OrderFleetMove` | they are set inside `FUN_007066c0`, the 8-argument path solver, unread | a hook on `FUN_008653c0`'s return with the flag word logged; or read `FUN_007066c0` |
| **Whether `S+0x224` is the `PlayerView` map** and which field `FUN_00753430` writes | the last leg of the chain was not read | lane K's phase 17 already touches `S+0x228`; one `find-variable-accesses` on `S+0x224` would settle it |
| **The 152 indirect call sites** in 91 of the 327 functions | closure is direct-call only | lane V2's vtable inversion. Four are on the main line and named in §3 |
| **Whether any of this ever runs** | **the resolver has never executed under an instrument.** Lane Z measured `res->+0x4 != 0` on every encounter of every run, which makes `ApplyEncounterResult` a whole-function no-op | `combat-resolver.md` §10.3's workload. This lane adds a second requirement to it: the battle must produce a **retreat**, i.e. `res->+0x68 != +0x6c`, or the whole pipeline is six no-ops |
| **`FUN_0056f430`**, the per-design gate-traffic cost | not read; it is the term `AddShip` accumulates into `fleet+0xc0` | 1 function, ~small; or a hook on `GTraf` |
| **`ServerPlayer+0x298`**, the fleet-name source | delegated read stopped at `FUN_00855f80` | read 1 function; it is confirmed draw-free by the immediate scan, so this is cosmetic |
**The strongest thing this lane can say and the weakest are the same sentence:** the rules above are read from
the instruction stream and are internally consistent with five independently-verified object layouts, and
**not one of them has ever been observed executing.**
---
## 7. Corrections
### 7.1 To `combat-resolver.md` §6 and §8 — `FUN_007d5a00` is retreat, not "the combat pipeline"
Lane J: *"a six-stage sub-dispatcher … **The real per-phase combat pipeline is under here**"* and, in tier 2,
*"this is where the per-phase combat pipeline lives. The resolver is a reporting layer; the sub-dispatcher is
the machine."*
The identification of it as the most tractable target was right and this lane is the proof of it. The
**characterisation** is not: the six stages are one subsystem, not six phases of combat. They are
`destinations → group → classify → split → execute → destroy`, and the whole thing is `StarFleet` retreat.
Combat damage, kills, boarding and prize resolution are **not** here — lane J's own tier-2 list has them under
`FUN_0079c740` (prizes), `FUN_0079b980` (stats) and `FUN_007c9e00` (interception geometry).
Proposed names, filed in `ghidra/addresses.d/lane-b5.json`: `CombatResolve_Retreat` for 0x007d5a00 and
`CombatRetreat_*` for the six.
**One agreement worth recording.** `StarFleet_SetFlag` 0x006fe1a0 was already in `addresses.json` from
`handoff/b4-colony-movement.md`, with the same address, the same name and the same prototype, derived from a
completely different path (colony/movement). My draft entry was **dropped**, not merged: the fragment
generator's duplicate-name check caught it, which is the check working as intended. Two independent readings
agreeing is worth more than a second copy of the row. 33 entries filed; the merge generates 861 total, written
to a scratch path.
### 7.2 To `combat-resolver.md` §6's callee table
The row for 0x007d5a00 says it *"builds a 0x18-byte temp from `ctx->+0x00/+0x08/+0x0c`"*. The temp is
**~0x2c bytes**: the three pointers, then a `std::map` at `+0x0c` (whose *node* is 0x18 — that is where the
number came from) and a `std::vector` at `+0x1c`. Nothing depends on the figure; corrected so it stops
propagating.
### 7.3 To my own first reading of `FUN_00790790`'s fleet creation
I first read 0x007907ce–0x007907d8 as `FUN_0080f710(this = S+4, owner, &enc->pos, 0)` — a four-argument
factory — because three `push`es sit immediately before the call. They do not belong to it. `FUN_0080f710` is
`ecx += 0x80; jmp FUN_008b8b70`, and `FUN_008b8b70` is `push [ecx+0x18]; call FUN_008b8ae0` — it supplies its
own single argument and consumes no stack. The three pushes plus the `push eax` of its **return value** are
the four arguments of `FUN_0085b340` (`ret 0x10`), whose call is 9 bytes later:
`CreateFleet(S+4, newOid, owner, &enc->pos, NULL)`. The id mint is evaluated *inside* the argument list.
Recorded rather than quietly fixed, because the failure mode is a new one for the rules file: **a `ret`-N thunk
in the middle of an argument list makes the arguments look like they belong to the wrong call.** The delegated
sweep caught it by reading `ret 4` off `FUN_008b8ae0`'s epilogue and counting; I had not checked the epilogue.
Checking `ret N` against the push count is cheap and would have caught it immediately.
---
## 8. What this lane did **not** read
* **`FUN_007066c0`**, the 8-argument path solver behind `OrderFleetMove`, and with it the meaning of its
failure bits. The single biggest hole.
* **`FUN_00707080`**'s call to `FUN_00703cf0`, which produces the 0x24-byte leg record copied into
`fleet+0xdc..+0xf8`. So the *fields* a retreat's flight plan writes are known; their *values* are not.
* **`FUN_00753430` / `FUN_0075cf50` / `FUN_007d1f00`** — the last three legs of the `PlayerView` refresh.
* **`FUN_00706140`**, the `StarFleet` constructor, beyond its first 0x7a bytes (delegated, marked SKIMMED by
the sweep). The initial value of `FtFlg` is therefore unknown; only that `CreateFleet` then ORs in `0x400`.
* **`FUN_00849280`** (cancel ship action) past its first ~60 bytes, and `FUN_00702ed0` / `FUN_007065d0` /
`FUN_00705c70` / `FUN_00700090` — the flight-plan recompute helpers.
* **`FUN_0056f430`**, `FUN_00855f80`, `FUN_008b9240`, `FUN_0072b400`.
* **The 152 indirect call sites.**
* Whether `enc->+0x0c` and `S->+0x44[idx]` are the same object or two halves of a `StarSystem` /
`ServerSystem` pair. `FUN_007d5650` uses both, one as a `this` for `ServerSystem` methods and one as an
index lookup, which is exactly the ambiguity `struct-recovery.md` §7 flags as medium-confidence. **Not
resolved.**
---
## 9. The reimplementation
`sots-engine` branch **`wip/battle`**, commit `ae170ec`, module `src/game/combat/` (a new directory — lane N
owns `src/game/sim/`). Clean-room: no addresses, no raw identifiers, no game data.
It models the **decision** and stops at the mutation. `BuildRetreatPlan` is a pure function returning a plan —
groups, splits, fleets to create, fleets left empty, players who learn the system — and applying it is left to
whatever owns the object store, because creating a fleet mints an id and appends to the saved master list. The
split keeps the two apart honestly and makes the whole thing host-testable.
Modelled: `IsUsableRetreatTarget`, `HostileMask` (including the captured-this-turn ceasefire lapse),
`ChooseRetreatDestination` (three independent nearest-searches, float32 squared distance, strict `<`),
`IsGroundedByDamage` (epsilon, not zero), `ShipMayRetreat`, `GroupRetreats` (all four key words),
`ClassifyFleets`, `UsesGateRetreat`, `BuildRetreatPlan`.
**53 hand-computed checks**, including the case a single-loop destination search gets wrong, the exact-tie
case, the epsilon-boundary case, the whole-versus-partial split and the cross-group fleet emptying.
Gates, run as separate commands: `tools/clean_room_check.sh` → **OK**; host `ctest` → **42/42**. No `src/shim/`
change, so no CT111 cross-build was required. (Engine `main` has moved to `8c23f6b` since the brief's
`b48d860`; 41 tests before this lane, 42 after.)
### 9.1 What is worth writing next, ranked
1. **`FUN_0079c740` (2270 B) — prize and reward resolution**, the resolver's last act. Draw-free by lane J's
scan, and it is the *other* half of a battle's strategic consequence.
2. **`FUN_00790990` (1040 B) — the 24 elimination conditions.** Player elimination is the largest possible
change to saved state and it has one well-defined output.
3. **`FUN_007066c0` — the path solver.** Not for its own sake: it is the last unknown on the retreat path and
it is shared with normal fleet movement, which lane B4 has already compared behaviourally.
4. **`FUN_0079c270` + `FUN_0078ba10` — plague.** Lane J's point stands: this may be the only place plague
starts, and `ServerSystem::ProcessPlague` is separately known to be draw-free.

View file

@ -0,0 +1,268 @@
{
"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"
}
]
}