FUN_007066c0 is not a path finder. It performs no graph search: it walks the caller's already-chosen destination list and classifies each consecutive pair through FUN_00703730, accumulating flags and the index of the first failing leg. The only graph structure in the subtree is a single-hop adjacency query. A multi-hop node route in a save is n waypoints, one per hop. OrderFleetMove's three failure bits, from the 0x418 literal: 0x008 the destination fleet is on a node route and no intercept could be solved, 0x010 a ship's drive is destroyed, 0x400 the destination point is not one the player may use. The other nine bits are advisory or route-quality complaints the server commits anyway -- the UI's dry run tests the whole word, which is what separates the two groups. Waypoint type 2 is the Liir drive, not a node line. The species-to-drive jump table maps Human and Zuul (the two node races) to 3 and Liir to 2, so lane O's 20+ all-type-3 observations were forced by the table. Nothing is unreachable and nothing needs fixing; exercising type 2 needs a Liir fleet, not a node line. Also: GFlags(+0xdc) is the per-player gate mask; CstR is the gate-projection radius and type 5 is a gate throw at a gateless system, not the Zuul bore; pnd is the node transit's origin id; FtTrans is a second saved copy of the first waypoint's type. Two original defects recorded as shipped (a loop-invariant drive comparison, a loop-invariant node-line ranking term) and one predicted (the leading-destination drop shifting the output arrays). P1/P2/P5 written before the run and checked offline against all 11 saves: 58 waypoints, 46 flight plans, 0 failures. 37 addresses filed; merge generates 912, validated to a scratch path.
870 lines
51 KiB
Markdown
870 lines
51 KiB
Markdown
# `FUN_007066c0` and `FUN_00703730` — the "path solver", read from the instruction stream
|
||
|
||
Lane P2, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs.
|
||
|
||
**Method.** Every function below was disassembled with `objdump -D -b binary -m i386 -M intel` over the raw
|
||
`.text` image (file offset `VA - 0x400C00`), **each to the next function start** (rule 17), and the padding
|
||
inspected. Jump tables were resolved byte by byte from `.text`. No claim about control flow here comes from a
|
||
decompiler. Everything is marked **[V]** instruction-verified by me or **[I]** inferred; there are no delegated
|
||
reads in this lane.
|
||
|
||
Closes `combat-retreat-pipeline.md` §6's *"The three failure bits `0x008 / 0x010 / 0x400` of `OrderFleetMove`"*
|
||
and §8's *"the single biggest hole"*. It also closes the type-2 question that `movefleet-position-rounding.md`
|
||
§6/§7, `strategic-turn-internals.md` §4.3 and lane O's behavioural sweep have each circled from a different
|
||
side, and it **corrects the name of waypoint type 2**.
|
||
|
||
---
|
||
|
||
## 0. Lead: it is not a path *finder*
|
||
|
||
**`FUN_007066c0` performs no graph search over systems.** There is no frontier, no visited set, no
|
||
relaxation, no priority queue, and no recursion. It is a **per-leg classifier and validator** over a waypoint
|
||
list its caller already chose:
|
||
|
||
```
|
||
solver(fleet, start, dests[], n) :
|
||
drop dests[0] if it is the fleet itself or the fleet's current system
|
||
probe leg (start -> dests[0])
|
||
prev = start
|
||
for i in 0 .. n-1:
|
||
types[i] = ClassifyLeg(fleet, prev, dests[i], &rangeLeft, &legFlags, &routes[i])
|
||
flags |= legFlags ; failIndex = first i with legFlags != 0
|
||
rangeLeft = f32(rangeLeft - f32(sqrt(f32(|prev.pos - dests[i].pos|^2))))
|
||
if dests[i] is a system where the fleet may refuel: rangeLeft = fullTank
|
||
prev = dests[i]
|
||
return true // ALWAYS true past the argument checks
|
||
```
|
||
|
||
The only graph lookup in the whole subtree is `FUN_006e4eb0`, and it is a **single-hop adjacency query**:
|
||
"is there a node line between system A and system B that this player has discovered?" It answers with the
|
||
line's path index or −1. It never looks at a third system. **A multi-hop node route in a save is therefore
|
||
`n` waypoints, one per hop, each classified independently** — the routing decision was made by whoever built
|
||
`dests[]` (the UI's map click chain, or the AI), never here. [V]
|
||
|
||
The function's **return value is `true` for every call that has a non-null fleet and a non-null start
|
||
object**; all outcome information travels in the two out-parameters. [V]
|
||
|
||
`ret` (no `ret N`) with eight stack arguments — **cdecl, not thiscall**, despite `ecx` being loaded with the
|
||
fleet at three call sites. The fleet is passed as argument 1 *and* used as `this` for the range helpers. [V]
|
||
|
||
### 0.1 Real boundaries (rule 17)
|
||
|
||
| function | Ghidra size | real body | verdict |
|
||
|---|---|---|---|
|
||
| `FUN_007066c0` | 584 | `0x7066c0..0x706907`, then 8 × `int3` to `0x706910` | **correct** |
|
||
| `FUN_00703730` | 1178 | `0x703730..0x703bc9`, then 6 × `int3` to `0x703bd0` | **correct** |
|
||
| `FUN_008653c0` | 801 | `0x8653c0..0x8656ea`, then 5 × `int3` to `0x8656f0` | **correct** |
|
||
| `FUN_00707080` | 514 | `0x707080..0x707281`, then 14 × `int3` to `0x707290` | **correct** |
|
||
|
||
Four for four. Recorded because the rule exists — but this lane found the sizes right, and the disassembly to
|
||
the next start is what establishes that rather than assuming it.
|
||
|
||
### 0.2 Two functions that read live-in registers
|
||
|
||
`FUN_00703650` and `FUN_00703bd0` each **test a register they never write**:
|
||
|
||
```
|
||
703650 push ebp; mov ebp,esp
|
||
703653 mov eax,[ebp+0xc] ; test eax,eax ; je ... ; mov DWORD PTR [eax],0x0
|
||
703660 test ebx,ebx ; <-- ebx is live-in
|
||
703668 test esi,esi ; <-- esi is live-in
|
||
```
|
||
|
||
At `FUN_00703730`'s call site 0x703831, `ebx` = the moving fleet and `esi` = the target fleet; at
|
||
`FUN_00703c90`'s call site 0x703caa, `esi` = the system. Both have exactly one caller. **Treating either as a
|
||
plain cdecl function and reading only its stack arguments produces nonsense** — I nearly did. This is a new
|
||
failure mode for the rules file, adjacent to §7.3 of `combat-retreat-pipeline.md`: *check whether a callee
|
||
reads a callee-saved register before it writes it.* [V]
|
||
|
||
---
|
||
|
||
## 1. `FUN_007066c0` — the driver
|
||
|
||
### 1.1 Signature, from the two call sites and the frame
|
||
|
||
```c
|
||
// __cdecl, 8 args, always returns true past the guards.
|
||
bool PathSolver(StarFleet* fleet, // +0x08 the moving fleet ("this" for the range helpers)
|
||
MapObject* start, // +0x0c where leg 0 begins (OrderFleetMove passes the fleet)
|
||
MapObject** dests, // +0x10 the ordered destination list
|
||
unsigned count, // +0x14 its length (DECREMENTED IN PLACE, see 1.2)
|
||
int* flagsOut, // +0x18 OR of every leg's flags (optional)
|
||
int* failIdxOut, // +0x1c index of the FIRST failing leg (optional, -1 = none)
|
||
int* typesOut, // +0x20 one waypoint type per dest, stride 4 (optional)
|
||
NodeRoute* routesOut); // +0x24 one NodeRoute per dest, stride 0x10 (optional)
|
||
```
|
||
|
||
`typesOut` and `routesOut` are written **only when non-null**, and each is guarded separately
|
||
(0x706808 / 0x706814), so a caller may ask for flags alone. Both callers do exactly that or ask for
|
||
everything:
|
||
|
||
| caller | call | what it wants |
|
||
|---|---|---|
|
||
| `FUN_005e6d50` (UI move command) | `(f, f, dests, n, &flags, 0, 0, 0)` | **dry run.** If `flags != 0` it allocates a 0x8f8-byte dialog and hands it `(f, dests, n, flags)` — the move-confirmation prompt. If `flags == 0` it sends the command. [V] |
|
||
| `FUN_008653c0` `OrderFleetMove` | `(f, f, dests, n, &flags, 0, types._Myfirst, routes._Myfirst)` | the real thing |
|
||
|
||
**The UI tests `flags != 0`; `OrderFleetMove` tests `flags & 0x418`.** That split is what separates the
|
||
warning bits from the refusal bits, and it is the cleanest evidence for the reading in §3. [V]
|
||
|
||
### 1.2 The leading-destination drop
|
||
|
||
```
|
||
706722 ecx = dests[0]
|
||
706724 if (fleet == ecx) goto drop
|
||
706728 eax = fleet->LocID(+0xa0)
|
||
70672e if (!eax || eax->+0x14 != 0) eax = 0 ; inlined StarFleet_GetLocationIfNode
|
||
706739 if (eax == ecx) goto drop
|
||
goto keep
|
||
drop: dests += 4 ; --count ; 0x70673d
|
||
```
|
||
|
||
So a destination list whose first entry is the fleet itself, or the *system* the fleet is already at, has that
|
||
entry removed — `dests` and `count` are advanced/decremented **in the solver's own stack copies only**
|
||
(`count` lives at `[ebp+0x14]`, a by-value argument), so `OrderFleetMove`'s `count` is untouched and it still
|
||
builds `count` waypoints from its **un-dropped** list. **The `types[]` and `routes[]` the solver writes are
|
||
therefore shifted by one relative to the waypoints `OrderFleetMove` builds whenever the drop fires**, and
|
||
`types[count-1]` / `routes[count-1]` are never written at all — they keep the value the vector was
|
||
constructed with. That is an original defect and it is testable: §7, P4. [V]
|
||
|
||
`FUN_00459f70`, `FUN_0085bf00` and `FUN_00703e90` are all MSVC `vector::resize(n)` — shrink-by-erase on one
|
||
side, grow-and-value-initialise on the other (`rep stos eax=0` for the `int` vector at 0x459fd8) — **not
|
||
`reserve`**, so `_Myfirst != _Mylast` and the two out-pointers are non-null whenever `count > 0`, and the
|
||
untouched tail element is a genuine zero rather than uninitialised memory. Checked precisely because the whole
|
||
paragraph above collapses if any of the three were a `reserve`. [V]
|
||
|
||
The `+0x14 != 0` guard means the drop only fires for a **system** location, never a point or a fleet. [V]
|
||
|
||
### 1.3 The three fuel figures
|
||
|
||
```
|
||
70674b [ebp-0x24] = StarFleet_MinRange(fleet, 0.0f) ; 0x006ff6a0
|
||
706757 [ebp-0x38] = FUN_00705d60(fleet, true) ; full tanks
|
||
706762 [ebp-0x10] = FUN_00705d60(fleet, false) ; fuel remaining <- the running budget
|
||
```
|
||
|
||
`StarFleet_MinRange` is already `verified` in `addresses.json` with the identical body I read here
|
||
(min over ships of `ship->Range(+0x20)`, seeded `FLT_MAX` from the `.rdata` word at 0x009e23a8 = `0x7f7fffff`,
|
||
strictly-greater update, then `f32(best + bias)`); **agreement recorded, no duplicate entry filed.** [V]
|
||
|
||
`FUN_00705d60(fleet, bool useMaxRange)` builds a `vector<float>` of per-ship ranges — `ship->Range(+0x20)`
|
||
when the flag is false, `Ship_MaxRange` (0x0080c820) when true — alongside a per-design list keyed on
|
||
`design->+0x12c`, and folds them. **I read only its first ~0x90 bytes and its two call sites**; the fold
|
||
itself (tanker redistribution) is *not* read. What is established is the argument's polarity and that the
|
||
result is a float. [V for the polarity, **not read** for the fold]
|
||
|
||
The **pre-flight probe** at 0x706795 passes `&[ebp-0x24]` — the plain `MinRange`, not the effective range — so
|
||
the first leg is evaluated **twice**, once against the pessimistic budget and once for real. Its
|
||
`flagsOut` is *assigned* (not OR'd) and its `failIdx` set to 0; its return value and its route are discarded
|
||
(`routeOut = NULL`). [V]
|
||
|
||
### 1.4 The per-leg loop, exactly
|
||
|
||
```
|
||
7067c3 rangeLeft = max(rangeLeft, 0.0f) ; fcomp 0.0 vs rangeLeft, strict
|
||
7067df legFlags = 0
|
||
7067e6 NodeRoute tmp; ctor 0x006e1b20 -> {vptr=0x00a1cbdc, nrp=-1, nrf=0, nrt=0}
|
||
706803 t = ClassifyLeg(fleet, prev, cur, &rangeLeft, &legFlags, &tmp) ; FUN_00703730
|
||
706808 if (typesOut) typesOut[i] = t
|
||
706814 if (routesOut) { routesOut[i].nrp = tmp.nrp; .nrf = tmp.nrf; .nrt = tmp.nrt } ; vptr NOT copied
|
||
70682e if (legFlags) { if (failIdx == -1) failIdx = i; flags |= legFlags }
|
||
706844 rangeLeft -= legLength(prev, cur) ; see 1.5
|
||
706886 if (cur->+0x14 == 0 && CanRefuelAt(cur, fleet)) rangeLeft = fullTank
|
||
7068b2 ++i; dests += 4; routesOut += 0x10; prev = cur
|
||
7068ce tmp.vptr = 0x009e22bc ; the INLINED NodeRoute destructor -- see below
|
||
```
|
||
|
||
**Rule 4 trap, and it is a live one here.** `mov DWORD PTR [ebp-0x48],0x9e22bc` on the back edge looks like a
|
||
second object being installed. It is not: 0x009e22bc is the `Mars::IStreamable` vftable (rtti), and this store
|
||
is the whole of the inlined `~NodeRoute()` — MSVC's base-class-vptr reset with nothing left to free. The
|
||
*constructor* runs again at 0x7067e6 at the top of the next iteration, so `nrp` really is reset to −1 every
|
||
leg. Reading the store as a branch or as a missing re-init would have produced a false "stale node route
|
||
carried between legs" bug report. [V]
|
||
|
||
The `[ebp-0x4]` writes (0 before the call, −1 on the back edge) are the SEH try-level for that same local, not
|
||
control flow. [V]
|
||
|
||
### 1.5 The leg length — and its precision
|
||
|
||
```
|
||
706844 fld [edi+0x18] ; fsub [esi+0x18] ; fstp DWORD [ebp-0x30] ; dx -> FLOAT32
|
||
70684d fld [edi+0x1c] ; fsub [esi+0x1c] ; fstp DWORD [ebp-0x24] ; dy -> FLOAT32
|
||
706856 fld [edi+0x20] ; fsub [esi+0x20] ; fstp DWORD [ebp-0x34] ; dz -> FLOAT32
|
||
70685f..706876 dx*dx + dy*dy + dz*dz entirely on the x87 stack (53-bit)
|
||
706878 fstp DWORD [ebp-0x34] ; sumsq -> FLOAT32
|
||
70687b fld [ebp-0x34] ; call 0x924f52 (sqrt) ; fstp DWORD [ebp-0x34] ; len -> FLOAT32
|
||
70688a fld [ebp-0x34] ; fstp DWORD [ebp-0x34] ; a no-op reload/store
|
||
706890 fld [ebp-0x10] ; fsub [ebp-0x34] ; fstp DWORD [ebp-0x10] ; rangeLeft -> FLOAT32
|
||
```
|
||
|
||
This is `Mars_Vec3_Length` (lane M §1) **inlined**, with the same two narrowings, plus a third on the
|
||
subtraction. Lane M's rule holds verbatim: the delta a reimplementation subtracts is `f32(a.c − b.c)`, not the
|
||
exact difference, and the sum of squares is narrowed **once**, after a 53-bit accumulation. 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** here. `fpu_cw = 0x127f`. [V]
|
||
|
||
`prev` is the **previous destination object**, not the fleet's position, from leg 1 onward
|
||
(`edi = esi` at 0x7068c5) — so the budget is drawn down along the *waypoint chain*, not from where the fleet
|
||
actually is. On leg 0, `prev` is the `start` argument, which `OrderFleetMove` sets to the fleet. [V]
|
||
|
||
### 1.6 Refuelling
|
||
|
||
`FUN_00703c90(node, fleet)` — 85 B [V]:
|
||
|
||
```
|
||
owner = MapObject_GetOwner(fleet) ; FUN_0071e280, switch on +0x14:
|
||
; 0 -> sys->PID(+0x100), 1 -> fleet->PID(+0x58), else 0
|
||
a = FUN_00703bd0(owner) ; esi = node LIVE-IN. For each fleet at the node (vft[2] count, vft[3] index),
|
||
; skipping any whose owner differs when owner != 0:
|
||
; true if StarFleet_HasFlagShips(f, 2, 0) -- capability mask 2, the tanker bit
|
||
b = node && owner && node->PID(+0x100) && Relation(node->PID, owner) >= 3 ; FUN_0080e050
|
||
return a || b
|
||
```
|
||
|
||
`FUN_0080e050` is `FUN_006d2050(this->PlyrIdx, &this->Team(+0x168), other->PlyrIdx)`. `strategic-turn-internals`
|
||
§5.2 records that function's result as *"1 ally, 2 NAP, 3 cease-fire"*; if that ordering is right, `>= 3`
|
||
would mean *"refuel at a cease-fire system but not at an ally's"*, which is almost certainly backwards.
|
||
**I did not read `FUN_006d2050`.** The `>= 3` test is [V]; the meaning of 3 is **open**, and §5.2's ordering
|
||
should be re-checked by whoever needs it. Two independent call sites use the same scale with different
|
||
thresholds (`>= 3` here, `> 0` in `FUN_00817890`), which is itself a hint that the scale is monotone in
|
||
friendliness and §5.2 has it inverted. [I]
|
||
|
||
Note the refuel test is gated on `cur->+0x14 == 0` — **systems only**, never a point or a fleet. [V]
|
||
|
||
---
|
||
|
||
## 2. `FUN_00703730` — `ClassifyLeg`, the actual rule
|
||
|
||
```c
|
||
// __thiscall, ret 0x14. Returns the WAYPOINT TYPE for this leg (0 = "no move possible").
|
||
int ClassifyLeg(StarFleet* this, MapObject* from, MapObject* to,
|
||
float* rangeInOut, int* flagsOut, NodeRoute* routeOut);
|
||
```
|
||
|
||
`flagsOut` may be null — the function substitutes a stack dummy at `[ebp-0x18]` so the `or` sites never need a
|
||
null test, and then **skips the whole flag block** at 0x703846 when the caller passed null. So a caller that
|
||
wants only the type gets no flag work done at all. [V]
|
||
|
||
### 2.1 Endpoint classification
|
||
|
||
`MapObject->+0x14` is the kind tag, already established by `MoveFleet`'s arrival switch
|
||
(`strategic-turn-internals` §4.3): **0 = system, 1 = fleet, 2 = deep-space point**. [V]
|
||
|
||
```
|
||
from: kind 2 -> fromPoint = from
|
||
kind 0 -> fromSystem = from
|
||
kind 1 -> loc = from->LocID(+0xa0); loc kind 0 -> fromSystem = loc; loc kind 2 -> fromPoint = loc
|
||
to: toSystem = (kind==0) ? to : 0 (the neg/sbb/not/and idiom, 0x7037bd)
|
||
toFleet = (kind==1) ? to : 0
|
||
toPoint = (kind==2) ? to : 0
|
||
```
|
||
[V]
|
||
|
||
### 2.2 The default: species decides the waypoint type
|
||
|
||
```
|
||
703770 driveType = FUN_006ff810(fleet)
|
||
```
|
||
|
||
`FUN_006ff810` (118 B) [V]:
|
||
|
||
```
|
||
if (fleet has no ships) return 0
|
||
t = DriveTypeOfSpecies(fleet->PID(+0x58)->Species(+0x5c)) ; FUN_0080c7d0
|
||
if (t == 0) return 0
|
||
if (fleet has exactly one ship) return t
|
||
for i in 1 .. nShips-1:
|
||
if (DriveTypeOfSpecies(fleet->PID->Species) != t) return 0 ; <-- LOOP-INVARIANT, see below
|
||
return t
|
||
```
|
||
|
||
**Original defect.** The loop body re-reads `fleet->PID->Species` — the *fleet's* owner — on every iteration
|
||
rather than indexing ship `i`. The compared value is therefore constant and the loop can never fail. The
|
||
intent was evidently "every ship in this fleet must have the same drive"; as shipped it is dead code. It costs
|
||
nothing behaviourally (a fleet's ships all belong to one player) but a reimplementation that "fixes" it to
|
||
read per-ship data would diverge the moment a mixed fleet exists. Recorded as-shipped. [V]
|
||
|
||
`FUN_0080c7d0(species)` (50 B) is a **7-entry jump table** at 0x0080c804, resolved byte by byte [V]:
|
||
|
||
| species | index | jump target | drive type |
|
||
|---|---|---|---|
|
||
| Human | 0 | 0x0080c7e2 | **3** |
|
||
| Hiver | 1 | 0x0080c7fe | **0** |
|
||
| Tarkas | 2 | 0x0080c7e9 | **1** |
|
||
| **Liir** | **3** | 0x0080c7f0 | **2** |
|
||
| _NPC | 4 | 0x0080c7fe | **0** |
|
||
| Zuul | 5 | 0x0080c7e2 | **3** |
|
||
| Morrigi | 6 | 0x0080c7f7 | **6** |
|
||
|
||
(species enum from `strategic-turn-internals` §0; index out of 0..6 returns 0.)
|
||
|
||
### 2.3 The decision, in order
|
||
|
||
```
|
||
A. if (to is a FLEET):
|
||
targetOnNodeRoute = to->wpts non-empty && IsNodeWaypoint(to->wpts[0].Tp)
|
||
ok = SolveIntercept(...) ; FUN_00703650, ebx=fleet esi=to live-in
|
||
if (ok) toSystem = the resolved system
|
||
B. if (flagsOut): ; the whole block is skipped when null
|
||
if (to is a fleet && !ok && targetOnNodeRoute) flags |= 0x008
|
||
if (AnyShipGroundedByDamage(fleet)) flags |= 0x010 ; FUN_00700240
|
||
m = PendingShipActionMask(fleet) ; FUN_006ff990
|
||
if (m & 0x100) { m &= ~0x100; flags |= 0x800 }
|
||
if (m) flags |= 0x001
|
||
C. if (to is a POINT):
|
||
if (!PointVisibleTo(to, owner) && !PointKnownTo(to, owner)) ; +0x8c / +0x90 masks
|
||
flags |= 0x400
|
||
return IsGateTransitWaypoint(driveType) || IsNodeWaypoint(driveType) ? 0 : driveType
|
||
D. fromHasGate = fromSystem && ServerSystem_HasGate(fromSystem, owner) ; FUN_00744010, GFlags(+0xdc)
|
||
toHasGate = toSystem && ServerSystem_HasGate(toSystem, owner)
|
||
canProject = GateProjection(owner, fromSystem, toSystem) ; FUN_00818040
|
||
if ( (fromHasGate && (toHasGate || canProject ||
|
||
(toPoint && PointUsableBy(toPoint, owner)))) ; FUN_0080ea30
|
||
|| (toHasGate && fromPoint) ):
|
||
capacity = owner->NGts(+0x144) * owner->PrGtTrf(+0x148) ; FUN_0080dc50
|
||
cost = (int16)fleet->+0xc0
|
||
if (fleet's CURRENT waypoint is already a gate transit) cost = 0
|
||
if (owner->GTraf(+0x14c) + cost > capacity) { flags |= 0x004; return 0 }
|
||
flags &= ~0x010 ; <-- clears the grounded bit
|
||
return canProject ? 5 : 4
|
||
E. if (driveType != 3) return driveType ; every non-node race stops here
|
||
F. ... the node-route branch, §2.5 ...
|
||
```
|
||
[V] throughout.
|
||
|
||
Step **E** is the whole story for Hiver (0), Tarkas (1), Liir (2), NPC (0) and Morrigi (6): once the gate block
|
||
declines, the leg's waypoint type is the species' drive type and **nothing else is checked** — no range, no
|
||
node line, no route record. Type 0 for Hiver/NPC is not a failure; `MoveFleet`'s switch treats every type it
|
||
does not name as `step = FPsp2 × dt`.
|
||
|
||
### 2.4 The gate block, decoded
|
||
|
||
`FUN_00744010(sys, player)` (34 B) is `(sys->GFlags(+0xdc) >> player->PlyrIdx(+0x28)) & 1`. `GFlags` is the
|
||
one `ServerSystem` presence mask `combat-retreat-pipeline.md` §5 left as *"a second presence source (not read
|
||
here)"*. **It is the per-player gate mask**, and the whole of §2.3 D is built on it. [V]
|
||
|
||
`FUN_00818040(player, A, B)` (150 B) [V]:
|
||
|
||
```
|
||
if (!A || !B) return false
|
||
if (!(0.0f < player->CstR(+0x150))) return false
|
||
if (!HasGate(A, player)) return false
|
||
if ( HasGate(B, player)) return false ; B must NOT have one
|
||
return Mars_Vec3_Length(A.pos - B.pos) <= player->CstR ; non-strict; 0x004224b0
|
||
```
|
||
|
||
**`CstR` now has a reader.** `strategic-turn-internals` §4.3 records it as *"`CstR` unused here"*; it is the
|
||
**gate-projection radius** — how far past a gate a fleet can be thrown when the far end has no receiving gate.
|
||
The distance is `Mars_Vec3_Length`, i.e. two float32 narrowings (lane M §1), compared **non-strictly**. [V]
|
||
|
||
So types 4 and 5 are both Hiver gate transits and the distinction is precise:
|
||
|
||
* **type 4** = gate → gate. Both ends carry the player's gate.
|
||
* **type 5** = gate → **gateless** system within `CstR`. `MoveFleet`'s case 5 is
|
||
*"roll = rand01() × CstE; if roll > CstT the fleet scatters around the destination"* (B4's correction) —
|
||
which is exactly the risk of throwing a fleet at a system with nothing to catch it.
|
||
|
||
This **corrects** `strategic-turn-internals` §4.3's medium-confidence guess that type 5 is *"the Zuul
|
||
node-bore / Morrigi gravity casting"*. It is neither: Zuul and Morrigi can never reach step D unless they
|
||
somehow hold a gate, and the node bore lives in step F. `CstE`/`CstT`/`CstR` are one coherent trio of
|
||
**gate-projection** parameters. [V for the classification, [I] for the `CstE`/`CstT` reading, which is B4's]
|
||
|
||
The `flags &= ~0x010` at 0x7039d3 is worth stating on its own: **a gate transit ignores dead drives.** That is
|
||
the same rule `combat-retreat-pipeline.md` §2.2 found from the other end (the species-1 bypass of
|
||
`IsGroundedByDamage` on the retreat gate arm), reached independently. [V]
|
||
|
||
### 2.5 The node-route branch (`driveType == 3`: Human and Zuul only)
|
||
|
||
```
|
||
7039f6 graph = (fleet->galaxy(+0x10))->vft[1]() ; INDIRECT EDGE -- not resolved
|
||
7039fb nrf = 0 ; nrp = -1 ; errBits = 0x002
|
||
case fromPoint && toSystem:
|
||
if (SystemIsFriendly(owner, toSystem)) { nrp = -1; origin = fromPoint; goto CHECK }
|
||
else { flags |= 0x100; goto FAIL }
|
||
case toPoint:
|
||
if (!fromSystem) goto FAIL
|
||
if (!SystemIsFriendly(owner, fromSystem)) { flags |= 0x200; goto FAIL }
|
||
if (!PointUsableBy(toPoint, owner)) { flags |= 0x400; goto FAIL }
|
||
nrp = -1; origin = fromSystem; dest = toPoint; goto CHECK
|
||
default (system -> system):
|
||
if (!fromSystem || !toSystem || fromSystem == toSystem) goto FAIL
|
||
p = FindNodeLine(graph, owner, fromSystem, toSystem) ; FUN_006e4eb0
|
||
if (p != -1) { nrp = p; origin = fromSystem; goto CHECK }
|
||
if (!StarFleet_HasFlagShips(fleet, 0x20000, 0)) { flags |= 0x020; goto FAIL }
|
||
errBits = 0x040
|
||
if (!BoreNodeLine(owner, fromSystem, toSystem, 0, fleet)) ; FUN_006e4de0
|
||
{ flags |= 0x080; goto FAIL }
|
||
nrp = -1; origin = fromSystem; goto CHECK
|
||
CHECK: if (origin && dest && !InRange(fleet, origin, dest, rangeInOut)) { flags |= errBits; goto FAIL }
|
||
return 3 ; and write the route out
|
||
FAIL: return 0 ; and write an EMPTY route out
|
||
```
|
||
[V]
|
||
|
||
* **`0x20000` is the node-bore capability bit** on the fleet's cached capability mask `+0xb8`
|
||
(`StarFleet_HasFlagShips` 0x00703500, already `verified` from lane Z, delegating to
|
||
`FUN_00702d70(this, maskA, maskB, out)`). It is the same bit `AddShip` special-cases into
|
||
`(fleet->galaxy)->+0x114` (`combat-retreat-pipeline.md` §2.4). Zuul node cruisers. [V]
|
||
* **`FUN_006e4eb0`** (303 B) is the only graph structure in the subtree [V]:
|
||
|
||
```
|
||
if (!player || !A || !B || A->Idx(+0x5c) == B->Idx(+0x5c)) return -1
|
||
hi = max(idxA, idxB); lo = min(idxA, idxB)
|
||
bit = 1 << player->PlyrIdx(+0x28)
|
||
if (!(graph->+0x24[ (hi-1)*hi/2 + lo ] & bit)) return -1 ; TRIANGULAR adjacency, one dword per pair
|
||
walk the hash bucket (0x006905a0):
|
||
for each entry whose {+0xc,+0x10} pair matches {idxA,idxB} in either order
|
||
and whose +0x2c mask contains the player's bit:
|
||
score = FUN_006e2130( (graph->+0x4)->+0x8 ) ; <-- LOOP-INVARIANT
|
||
if (score > best) { best = score; result = entry->+0x8 }
|
||
return result (initialised to -1)
|
||
```
|
||
|
||
**Original defect #2, and this one is the tie-break.** `score` depends only on `graph`, so it is identical
|
||
for every candidate. `best` starts at −1, so the **first** matching entry in the bucket wins and every later
|
||
one is discarded by `score > best` being false on equality. Whatever the ranking was meant to be (line
|
||
length? a per-line discovery level?), as shipped **`FindNodeLine` is "first match in hash-bucket order"**.
|
||
A reimplementation must reproduce the bucket order to reproduce the chosen `nrp`, or accept that `nrp` is
|
||
unmodelled. I did not read `FUN_006e2130` or the hash function 0x006905a0. [V for the invariance, **not
|
||
read** for the two callees]
|
||
|
||
* **`InRange`** is `FUN_006ffa00` (170 B), and it is the only place a *distance* decides a *failure*. §4 below.
|
||
|
||
### 2.6 Intercepting a moving fleet
|
||
|
||
`FUN_00703650` (214 B, `ebx` = mover, `esi` = target, both live-in) [V]:
|
||
|
||
```
|
||
if (!mover || !target) return false
|
||
if (target->wpts empty) return false
|
||
if (!IsNodeWaypoint(target->wpts[0].Tp)) return false ; target must be on a type-3 leg
|
||
if (!CanIntercept(mover, target, rangeIn)) return false ; FUN_00703520
|
||
myLoc = (mover->LocID && mover->LocID->+0x14 == 0) ? mover->LocID : 0
|
||
if (myLoc == StarFleet_ResolveWaypoint(target)) ; I'm at the target's destination
|
||
*out = AsSystem(NodeTransitOrigin(target)); return true ; -> aim at where it came FROM
|
||
if (StarFleet_GetLocationIfNode(mover) == NodeTransitOrigin(target))
|
||
*out = AsSystem(StarFleet_ResolveWaypoint(target)); return true
|
||
return true ; *out left 0
|
||
```
|
||
|
||
`NodeTransitOrigin` is `FUN_006ffab0`: it resolves `fleet->FPlan.pnd(+0xf8)` through the entity hash at
|
||
`(fleet->galaxy(+0x10)) + 0x80` — the same `IDMap` `combat-retreat-pipeline.md` §5 found at `(S+4)+0x80`.
|
||
**`pnd` is the network id of the node transit's origin object**, and §6.1 shows where it is written. That is a
|
||
new name for a field the save format carried unexplained. [V]
|
||
|
||
`AsSystem` is `FUN_0071e340`: `return (x->+0x14 != 0) ? 0 : x`. [V]
|
||
|
||
`FUN_00703520` (292 B) [V] returns **true** when the target is not node-travelling at all (three early outs),
|
||
and otherwise demands that the mover sit at one end of the target's node line *and* that the whole line be
|
||
`InRange`. So the geometry is: **you can only cut a node-travelling fleet off at one of the two ends of the
|
||
line it is on.**
|
||
|
||
Note the third `return true` at 0x70371d with `*out` still 0: an intercept can succeed while resolving no
|
||
system, in which case `toSystem` stays whatever it was (0 for a fleet target) and the leg falls through to
|
||
step D/E with no system at either end.
|
||
|
||
---
|
||
|
||
## 3. The flag word — and `OrderFleetMove`'s three failure bits
|
||
|
||
Every bit, its site, and its meaning. All [V].
|
||
|
||
| bit | set at | meaning | fails `OrderFleetMove`? |
|
||
|---|---|---|---|
|
||
| **0x001** | 0x703897 | some ship in the fleet is performing a cancellable action | no — **warning** |
|
||
| 0x002 | 0x703b10 (errBits) | the leg's existing node line is out of fuel range | no |
|
||
| 0x004 | 0x7039c5 | **gate traffic would exceed `NGts × PrGtTrf`** | no |
|
||
| **0x008** | 0x703859 | destination is a fleet on a node route and **no intercept could be solved** | **YES** |
|
||
| **0x010** | 0x70386d | some ship's **drive is destroyed** (`StarShip_IsGroundedByDamage`) — *cleared* at 0x7039d3 on a gate leg | **YES** |
|
||
| 0x020 | 0x703b8b | no node line between the two systems and the fleet **cannot bore one** | no |
|
||
| 0x040 | 0x703b63 (errBits) | a line was bored, but the leg is still out of fuel range | no |
|
||
| 0x080 | 0x703b7d | the **node-bore attempt failed** | no |
|
||
| 0x100 | 0x703a42 | point → system, and the system is not friendly-owned | no |
|
||
| 0x200 | 0x703a6a | system → point, and the source system is not friendly-owned | no |
|
||
| **0x400** | 0x7038c5, 0x703ad3 | the destination **point** is not one this player may move to | **YES** |
|
||
| **0x800** | 0x70388a | the fleet contains a ship performing action **8** | no — **warning** |
|
||
|
||
`OrderFleetMove` tests `test DWORD PTR [ebp-0x10], 0x418` at 0x865499 — **0x400 | 0x010 | 0x008**, exactly
|
||
B5's three — and on a hit logs at level 2 with the `.rdata` string at 0x00a31e44:
|
||
|
||
```
|
||
StrategySim: %s (%s) move not permitted at this time.
|
||
```
|
||
|
||
with `%s %s` = the fleet's `FtName(+0x5c)` and the owner's name string (`owner+0x40`), both read through the
|
||
MSVC `std::string` SSO test (`capacity >= 0x10 → indirect`). [V]
|
||
|
||
**So the three "failure" bits are the three that cannot be repaired by the player pressing OK.** Every other
|
||
bit is either advisory (0x001, 0x800: "this cancels orders") or a *route-quality* complaint the engine is
|
||
willing to commit anyway (0x002/0x004/0x020/0x040/0x080/0x100/0x200). The UI's `flags != 0` test is what shows
|
||
the whole word to the player; the server's `& 0x418` is what refuses.
|
||
|
||
`0x004` **not** being a refusal is the surprising one: a Hiver player can be ordered over gate capacity, the
|
||
flag is raised, `ClassifyLeg` returns 0 for that leg, and `OrderFleetMove` **still installs the plan** with a
|
||
type-0 waypoint. That is a testable consequence — §7.
|
||
|
||
### 3.1 The two per-ship masks
|
||
|
||
`FUN_00700240(fleet)` (80 B): true if **any** ship satisfies `StarShip_IsGroundedByDamage` (0x00815090,
|
||
already `verified` from lane B5 — agreement recorded). → bit 0x010. [V]
|
||
|
||
`FUN_006ff990(fleet)` (101 B): `OR of (1 << ship->+0x4c)` over ships satisfying `FUN_0081f880(ship)`, where
|
||
|
||
```
|
||
FUN_0081f880(ship) = ship && ship->+0x4c != 0 && ship->+0x4c != 6
|
||
&& !(ship->+0x4c == 8 && (ship->+0x1c & 8) == 8)
|
||
```
|
||
|
||
`ship->+0x4c` is the ship's **current action**, and the proof is in `OrderFleetMove` itself: at 0x8655ed it
|
||
open-codes the *identical* three-way predicate and calls `FUN_00849280(S+4, ship, 0)` — the "Ship leaving %s
|
||
is still doing %s. Cancelling action." cancel that `strategic-turn-internals` §4.3 already names — on every
|
||
ship that passes it. So bit 0x001 means "this order will cancel N ship actions" and bit 0x800 singles out
|
||
action **8**. Both are prompts, not refusals. [V]
|
||
|
||
*(A shift by `ship->+0x4c` means the action enum must stay under 32; nothing bounds-checks it.)*
|
||
|
||
---
|
||
|
||
## 4. Precision profile
|
||
|
||
Ordered by how much a reimplementation would suffer from getting it wrong.
|
||
|
||
**1. `InRange` — `FUN_006ffa00`, the only float that decides a failure.** [V]
|
||
|
||
```
|
||
6ffa0f d.x = f32(B.pos.x - A.pos.x) ... d.y, d.z ; each stored to a float32 slot
|
||
6ffa2d..6ffa44 dx*dx + dy*dy + dz*dz on the x87 stack (53-bit)
|
||
6ffa46 fstp DWORD [ebp-0x4] ; sumsq -> FLOAT32
|
||
6ffa4d r = rangePtr ? *rangePtr : StarFleet_MinRange(fleet, 0.0f)
|
||
6ffa63 cap = FUN_006ff710(fleet) ; min over ships of Ship_MaxRange
|
||
6ffa6b..6ffa83 r = (r > cap) ? cap : r ; both read back from FLOAT32 slots
|
||
6ffa86 fld [ebp+0x8] ; fmul st(0),st ; r*r -- LEFT IN THE REGISTER
|
||
6ffa8b fld [ebp-0x4] ; fcompp ; compares f32(sumsq) with the 53-bit r*r
|
||
6ffa95 jp -> false ; true iff sumsq <= r*r
|
||
```
|
||
|
||
**`r*r` is never stored.** The comparison is `f32(sumsq) <= (double)r × (double)r`. A reimplementation that
|
||
writes `sumsq <= f32(r*r)` — the natural mirror of every other narrowing in this engine — will disagree
|
||
exactly at the boundary, which is precisely where a fuel check lives. This is the same class of finding as
|
||
lane M's rounded reciprocal, and it is the one number in this subsystem that flips a decision.
|
||
|
||
Also: the sense is **`<=`**, non-strict, established from `test ah,0x41` + `jp` (equal ⇒ C3 alone ⇒ odd parity
|
||
⇒ `jp` not taken ⇒ true).
|
||
|
||
**2. `FUN_006ff710`** (155 B) — the tank cap. Min over ships of `Ship_MaxRange(ship)` (0x0080c820), seeded
|
||
`FLT_MAX`, **with an early exit the moment the running min is `<= 0`** (`fldz; fld best; fcom st(1); test
|
||
ah,0x41; jnp exit`) — so it is *not* a pure min when a zero-range ship appears before a negative one. Returns
|
||
`0.0f`, not `FLT_MAX`, for a fleet with no ships. [V]
|
||
|
||
**3. The leg length** — §1.5. Three f32 deltas, one narrowing on the sum, one on the sqrt, one on the
|
||
subtraction. Bit-identical to a double accumulator with a single final narrowing.
|
||
|
||
**4. `Mars_Vec3_Length` in the gate-projection test** — lane M §1's two narrowings, compared `<=` against
|
||
`CstR` read from a float32 field.
|
||
|
||
**5. `rangeLeft = max(rangeLeft, 0.0f)`** at the top of every leg (0x7067c3), via `fcomp` of the constant 0.0
|
||
against the slot: `!(0.0 <= rangeLeft) ⇒ 0`. NaN would clamp to 0. [V]
|
||
|
||
**6. `MinRange`'s seed and update** — `FLT_MAX` (0x009e23a8 = `0x7f7fffff`), strict `best > range` update, so
|
||
on an exact tie the **earlier** ship wins and an empty fleet returns `FLT_MAX + bias`. Same shape as the
|
||
retreat destination search's tie-break (`combat-retreat-pipeline.md` §2.1). [V]
|
||
|
||
`fpu_cw = 0x127f` throughout — 53-bit precision, round-to-nearest — as lane F measured. Nothing in this
|
||
subtree changes it.
|
||
|
||
---
|
||
|
||
## 5. Why waypoint type 2 is never produced — and what it actually is
|
||
|
||
**Answer: type 2 is the Liir drive.** `FUN_0080c7d0` maps species 3 → 2 and nothing else does. Lane O's 20+
|
||
observations were taken on *"both node-drive races"* — Human (0) and Zuul (5) — and `FUN_0080c7d0` maps both
|
||
of those to **3**. Every one of those observations was forced by the table, and no amount of single- vs
|
||
multi-hop, natural vs rip-bored, planned vs in-transit variation could have produced a 2. [V]
|
||
|
||
The reachability argument, in full:
|
||
|
||
* The classifier can only return `0`, `driveType`, `3`, `4` or `5` (five `ret` sites, §2.3). [V]
|
||
* `driveType` is a **pure function of the owning player's species** — no ship data, no terrain, no tech
|
||
(§2.2; the one loop that reads ship data is the invariant no-op). [V]
|
||
* The node-route branch (§2.5) is entered **only** when `driveType == 3` and returns only `3` or `0`. [V]
|
||
* The gate branch returns only `4` or `5`, and its guard is `GFlags` — the gate mask. [V]
|
||
|
||
So a **Human or Zuul fleet can never carry a type-2 waypoint**, and a **Liir fleet's every straight leg is
|
||
type 2**. There is no bug, no unreachable path, and nothing to fix.
|
||
|
||
### 5.1 Correction: "node line" is the wrong name for type 2
|
||
|
||
`strategic-turn-internals` §4.1/§4.3 names 0x00705510 `NodeLine::Step` and 0x00702e20 `FindNodeLines`, and
|
||
`movement.h` in the engine calls kind 2 `NodeLine`. On this evidence that naming is **wrong**, and it is what
|
||
made B4's *"the type-2 node-line step is wrong by construction"* and lane O's *"type 2 never appears"* look
|
||
like the same puzzle when they are not:
|
||
|
||
* type 2 belongs to **Liir**, the one race with no node drive and no gates;
|
||
* its speed profile is built from `STUTTER_SYSTEM_INFLUENCE_RADIUS`, `STUTTER_MIN_SPEED`, `STUTTER_MAX_SPEED`
|
||
and is **slowest at a system and fastest in deep space** (`v = speed × ((MAX−MIN)×(dist/RADIUS) + MIN)`,
|
||
`formula-gaps.md`);
|
||
* the image carries `SHIP_STUTTERWARP_DEEPSPACE_MANEUVER_FACTOR` as a ship key;
|
||
* `IsNodeWaypoint` deliberately excludes 2 and accepts only 3 — which is now simply consistent rather than
|
||
anomalous.
|
||
|
||
Type 2 is the **Liir stutterwarp**. `FUN_00702e20` finds *systems whose influence spheres the leg crosses*
|
||
(`formula-gaps.md` already reads it as a ray/sphere test against systems), not node lines. Renaming is
|
||
proposed in `addresses.d/lane-p2.json`: `Stutter_Step` / `FindStutterInfluenceSystems`, with the old names
|
||
kept as aliases in the comments so the earlier findings remain searchable. [V for the species mapping and the
|
||
predicate tables; **[I]** for the *name* "stutterwarp", which rests on the constant names and the shape of the
|
||
speed curve, not on a string that says so.]
|
||
|
||
### 5.2 What this predicts for lane M and B4
|
||
|
||
Lane M could not build a type-2 workload and correctly said so. The reason is now concrete: **`ref-turn2` has
|
||
no Liir player.** Exercising type 2 does not require a node line at all — it requires a **Liir fleet moving
|
||
anywhere**. That is a far cheaper save to author than the one lane M was contemplating, and B4's untested
|
||
`NodeLineStep`/`BuildStutterSegments` become reachable the moment such a save exists.
|
||
|
||
---
|
||
|
||
## 6. What reaches saved state
|
||
|
||
`OrderFleetMove` (0x008653c0) is the writer. Read in full to the next function start. [V]
|
||
|
||
```c
|
||
bool StrategyServer::OrderFleetMove(StarFleet* f, MapObject** dests, unsigned count) {
|
||
if (!f) return false;
|
||
if (PosDiffersFromPointLocation(f)) { // FUN_0080ec50: exact fucompp on all three components,
|
||
this->vft[2](); // and only when LocID->+0x14 == 2 (a POINT)
|
||
if (PosDiffersFromPointLocation(f)) f->Pos = f->LocID->Pos;
|
||
}
|
||
vector<int> types(count); // 0x00459f70
|
||
vector<NodeRoute> routes(count); // 0x0085bf00
|
||
int flags = 0;
|
||
PathSolver(f, f, dests, count, &flags, nullptr, types.data(), routes.data());
|
||
if (flags & 0x418) { Log(2, "StrategySim: %s (%s) move not permitted at this time.\n", ...); return false; }
|
||
|
||
vector<Waypoint> wpts(count); // 0x00703e90; stride 0x1c
|
||
for (i = 0; i < count; ++i) {
|
||
wpts[i].Wpt(+0x4) = dests[i] ? dests[i]->id(+0x4) : 0;
|
||
Waypoint_Set(&wpts[i], types[i], &routes[i]); // FUN_007006e0
|
||
}
|
||
StarFleet::SetFlightPlan(f, wpts.data(), wpts.size(), f->LocID ? f->LocID->id(+0x4) : 0); // 0x00707080
|
||
for (ship in f->NShips) if (ship is acting) CancelShipAction(this, ship, 0); // 0x00849280
|
||
return true;
|
||
}
|
||
```
|
||
|
||
`FUN_007006e0` is `Waypoint::Set(int Tp, const NodeRoute* r)`: `+0x8 = Tp`, `+0x10 = r->nrp`,
|
||
`+0x14 = r->nrf`, `+0x18 = r->nrt` — which pins `Waypoint = {vptr, Wpt@4, Tp@8, NodeRoute nrt@0xc}` at
|
||
**0x1c** bytes (confirmed independently by the `0x92492493` divide-by-28 at 0x865594 and the `add edi,0x1c`
|
||
stride). Exactly `struct-recovery.md` §3.1. [V]
|
||
|
||
### 6.1 `SetFlightPlan` — `FUN_00707080`, every write
|
||
|
||
| write | field | value |
|
||
|---|---|---|
|
||
| `owner->+0x14c` −= `f->+0xc0` | **`ServerPlayer.GTraf`** | debited **before** the change if the *old* first waypoint was a gate transit |
|
||
| `f->+0xc8..0xd7` | **`FlightPlan.wpts`** | assigned from a zeroed temp (`FUN_00703cf0`), then the new list inserted (`FUN_00706c90`) |
|
||
| `f->+0xd8` | **`FPsp2`** | 0.0f from the temp, then recomputed by `FUN_00705c70` |
|
||
| `f->+0xdc` | **`FPeta2`** | 0 |
|
||
| `f->+0xe0..0xeb` | **`FPogn2`** | 0,0,0 then **`f->Pos`** — the position the order was given from |
|
||
| `f->+0xec..0xf7` | **`FPdpos`** | 0,0,0 then the **first waypoint target's `Pos`**, resolved through the IDMap at `(f->galaxy)+0x80`; left zero if it does not resolve |
|
||
| `f->+0xf8` | **`pnd`** | 0 then the **origin location's network id** (`f->LocID->+0x4`, or 0) |
|
||
| `f->+0xfc` | **`FtTrans`** | **`wpts[0].Tp`** — a second, saved copy of the first leg's waypoint type |
|
||
| `f->+0x100..0x10b` | **`FtOrig`** | `f->Pos` |
|
||
| `owner->+0x14c` += `f->+0xc0` | **`GTraf`** | re-credited if the *new* first waypoint is a gate transit |
|
||
|
||
Field names from `objects/layouts.md`; every offset above is [V] from the instruction stream and every one of
|
||
them is a **saved** field the autosave oracle sees. `FtTrans = wpts[0].Tp` is new — the save carries the first
|
||
waypoint's type twice, once inside `FPlan.wpts` and once as a top-level `StarFleet` int. [V]
|
||
|
||
Also reaching saved state on this path, but outside `SetFlightPlan`:
|
||
|
||
* **`StarFleet.Pos`** (+0x18) — the snap, but *only* when the fleet's location is a deep-space point.
|
||
`combat-retreat-pipeline.md` §2.5 records the exact-IEEE comparison correctly and misses the `+0x14 == 2`
|
||
guard; corrected here. [V]
|
||
* **every acting ship's state**, via `FUN_00849280`.
|
||
* **`ServerPlayer.GTraf`**, above.
|
||
|
||
Against B5's seven-container template: this path writes **four** of them (`FlightPlan`/`wpts`, `GTraf`,
|
||
`StarFleet.Pos`, and the new `FtTrans`), and adds the `nrt{nrp,nrf,nrt}` records, whose provenance is now
|
||
known:
|
||
|
||
* **`nrp`** = the node line's path index from `FindNodeLine` (`entry->+0x8`), or **−1**;
|
||
* **`nrf`** = the leg's **origin** object's network id (`origin->+0x4`), or 0;
|
||
* **`nrt`** = the leg's **destination** object's network id, or 0;
|
||
* and **all three are written only when the leg's type is 3.** For every other type the saved record is
|
||
`{-1, 0, 0}` — `FUN_00703730` resets it at 0x703a85 and only then fills it in (0x703aa8..0x703bb1). [V]
|
||
|
||
That last point is a save-visible invariant: **`Tp != 3 ⟹ nrt == {-1,0,0}`**, on every waypoint of every
|
||
fleet. It is checked in §7.1 — 58 waypoints, 0 failures.
|
||
|
||
---
|
||
|
||
## 7. A falsifiable prediction (rule 2)
|
||
|
||
Written before any run.
|
||
|
||
> **P1.** In every save in `verify/results/saves/`, and in every autosave any later lane takes, a waypoint
|
||
> whose `Tp` is not 3 has `nrt = {nrp: -1, nrf: 0, nrt: 0}`, and a waypoint whose `Tp` is 3 has
|
||
> `nrf` and `nrt` equal to network ids that resolve, with `nrp` either −1 (a freshly bored line, or a
|
||
> point endpoint) or a non-negative path index.
|
||
>
|
||
> **P2.** `StarFleet.FtTrans` equals `FPlan.wpts[0].Tp` for every fleet with a non-empty flight plan, and is
|
||
> unchanged from its previous value for every fleet with an empty one.
|
||
>
|
||
> **P3.** No fleet owned by a Human or Zuul player ever carries `Tp == 2`; no fleet owned by a Liir player
|
||
> ever carries `Tp == 3`; `Tp` 4 or 5 appears only for a player whose `NGts(+0x144)` is non-zero.
|
||
>
|
||
> **P4 (the cheap VM test, and the one worth doing).** Give a fleet a **multi-hop** move order whose **first
|
||
> click is the system the fleet is already parked at** — click your own system, then one or two more, then
|
||
> confirm. The resulting `FPlan.wpts` will have `n` entries, and the **types will be shifted by one**:
|
||
> `wpts[0].Tp` will be the type computed for `wpts[1]`'s destination, and **`wpts[n-1].Tp` will be 0**
|
||
> whatever the race is. For a Human or Zuul fleet the tell is unmistakable — the last waypoint of a node
|
||
> route saves as `Tp 0` with `nrt {-1,0,0}` instead of `Tp 3` with a live `nrp/nrf/nrt`. Set `n = 2` for the
|
||
> smallest case: `wpts[0].Tp` = the type of the leg to the second click, `wpts[1].Tp` = 0.
|
||
|
||
P1 and P2 are checkable **right now, offline**, against the 11 existing saves with `save_reader.py` — no VM,
|
||
no build. P3 needs a Liir save, which is the workload §5.2 argues for anyway. P4 needs the VM, one save with
|
||
a movable fleet, and about a minute; it is a **predicted bug in the original**, so a clean result falsifies
|
||
§1.2's reading of the drop rather than merely failing to confirm it.
|
||
|
||
P4's most likely way to be wrong: the UI may refuse to place a waypoint on the fleet's own system, in which
|
||
case the drop is only reachable from the AI's `OrderFleetMove` call sites (`FUN_007a4ff0`, `FUN_00865780`) and
|
||
the test must be run against an AI turn instead. The retreat pipeline cannot reach it — it passes
|
||
`count = 1` with a destination the phase-1 search explicitly excludes the battle system from, and the fleet is
|
||
at the battle system.
|
||
|
||
### 7.1 P1, P2 and a third check — **run, and green**
|
||
|
||
P1 and P2 were written above from the disassembly alone, then run against all 11 curated saves with
|
||
`verify/save-reader/save_reader.py`. A third check went in at the same time, because reading §2.5 makes it
|
||
obvious and it is free:
|
||
|
||
> **P5.** For a chain of type-3 waypoints, `nrf[0] == pnd`, `nrt[i] == Wpt[i]`, and `nrf[i+1] == nrt[i]` —
|
||
> because `ClassifyLeg` is handed `(prev, cur)` and writes `origin->id` / `dest->id`, and §1.4 sets
|
||
> `prev = cur` on the back edge.
|
||
|
||
| | plans | waypoints | pass | fail |
|
||
|---|---|---|---|---|
|
||
| **P1** `Tp != 3 ⟹ nrt == {-1,0,0}`; `Tp == 3 ⟹ nrt == Wpt && nrf != 0` | 46 | **58** | 58 | **0** |
|
||
| **P2** `FtTrans == wpts[0].Tp` | **46** | — | 46 | **0** |
|
||
| **P5** `nrf[0] == pnd` and `nrf[i+1] == nrt[i]` | **46** | — | 46 | **0** |
|
||
|
||
`Tp` histogram over all 58: **{3: 57, 1: 1}**. Zero type 2, zero type 0, zero 4, zero 5.
|
||
|
||
What that buys, and what it does not (rule 15). The coverage is thin in exactly the way lane O's was:
|
||
nine of the eleven saves are the same Zuul game at successive turns, so the 57 type-3 waypoints are far
|
||
from 57 independent observations. **But three things here could not have been guessed:**
|
||
|
||
1. **`nrt == Wpt` on all 57.** Two fields the save format carries separately, that no earlier lane connected,
|
||
and the reason they are equal is `ClassifyLeg` writing `dest->+0x4` into `nrt` while `OrderFleetMove`
|
||
writes the same `dests[i]->+0x4` into `Wpt`. Predicted from the instruction stream, confirmed.
|
||
2. **The `nrf` chain closes through `pnd`** across the three-hop plans in `zuul-turn23-fleet23.sav`
|
||
(`80→272`, `272→368`, `368→288`, with `pnd = 80`). That is §1.4's `prev = cur` back edge, visible in a
|
||
save.
|
||
3. **`nrp` splits exactly where §2.5 says it should.** `human-turn3-noderoute.sav` — Human, cannot bore —
|
||
has `nrp` = 37, 16, 9: all **non-negative**, all from `FindNodeLine`. The Zuul saves carry a **mix**:
|
||
52/53/54/56 where a line already existed, and **−1** where one did not. −1 is what 0x703adb writes after
|
||
a successful `BoreNodeLine`, and Zuul are the rip-borers. Nobody looked for this; the split falls out of
|
||
the branch structure and it is there.
|
||
|
||
**The one non-Zuul, non-Human data point is the best of all.** `turn3-state.sav`'s single flight plan is
|
||
**`Tp = 1`** with `nrt = {-1, 0, 0}`. That save's players are species **{0 Human ×2, 2 Tarkas ×2, 4 NPC ×4}**,
|
||
and `FUN_0080c7d0` maps Human→3, Tarkas→**1**, NPC→0. **Type 1 can only have come from a Tarka fleet**, and
|
||
the empty route record is P1 on the only non-node waypoint the campaign has ever recorded. That is a live
|
||
confirmation of a *second* row of the drive-type table, from a save that predates this lane, on a race nobody
|
||
was looking at. (The save does not carry a player *handle*, only `PlyrIdx`, so the fleet→player link is by
|
||
elimination over the species present, not by direct lookup.)
|
||
|
||
**Still at zero observations: types 0, 2, 4, 5** — Hiver/NPC, Liir, and both gate transits. P3 and P4 stand
|
||
unrun.
|
||
|
||
**How each could be wrong, and the symptom:**
|
||
|
||
1. `nrt` is written by more than one producer. `MoveFleet`'s multi-waypoint continuation calls
|
||
`FUN_00703730` twice (0x7da5c6, 0x7da5da) and I did **not** read those call sites. If either writes a
|
||
route into an existing waypoint, P1 breaks with a `Tp != 3` waypoint carrying a live `nrf`. That would not
|
||
falsify §2's reading of the classifier — it would mean the classifier has a second consumer that re-types
|
||
a waypoint after the fact.
|
||
2. `FtTrans` may have another writer. I found exactly one (`SetFlightPlan`), by reading, not by an
|
||
image-wide displacement scan for `+0xfc`. Symptom: P2 fails on a fleet whose plan was not installed by
|
||
`OrderFleetMove`.
|
||
3. P3's `Tp == 2` half rests on `FUN_0080c7d0` being the **only** producer of a drive type. It is the only
|
||
caller-visible one in this subtree, but `FUN_0080c7d0` has other callers I did not enumerate. Symptom: a
|
||
type-2 waypoint on a non-Liir fleet — which would be the more interesting result and should be chased,
|
||
not explained away.
|
||
4. A Hiver player with zero gates: P3's last clause predicts no type 4/5. If one appears, `GFlags` is set by
|
||
something other than gate construction and §2.4's identification of `+0xdc` is wrong.
|
||
|
||
---
|
||
|
||
## 8. Corrections to earlier findings
|
||
|
||
**8.1 To `strategic-turn-internals.md` §4.3 — waypoint type 5.** *"the identity of waypoint type 5 (a
|
||
probabilistic jump using the player's `CstE/CstT` — consistent with the Zuul node-bore / Morrigi gravity
|
||
casting; `CstR` unused here)"*. Type 5 is a **Hiver gate throw to a system with no receiving gate**, chosen at
|
||
0x7039e4 by `FUN_00818040`, whose radius **is `CstR`**. `CstR` is not unused; it is the gate-projection
|
||
radius. Zuul and Morrigi cannot reach the site.
|
||
|
||
**8.2 To `strategic-turn-internals.md` §4.1/§4.3 and the engine's `WaypointKind::NodeLine` — type 2.**
|
||
Type 2 is the **Liir** drive, not a node line. §5.1.
|
||
|
||
**8.3 To `combat-retreat-pipeline.md` §2.5's `OrderFleetMove` line.** *"It snaps the fleet's position onto its
|
||
current system when they differ"* — it snaps onto its current **point** (`FUN_006fe320` requires
|
||
`LocID->+0x14 == 2`); a fleet sitting at a system is never snapped. The exact-`fucompp` observation stands.
|
||
|
||
**8.4 To `combat-retreat-pipeline.md` §5's `ServerSystem` mask table.** `GFlags(+0xdc)`, listed as *"a second
|
||
presence source (not read here)"*, is the **per-player gate mask**, read by `FUN_00744010`.
|
||
|
||
**8.5 Agreements recorded, entries dropped (rule 14).** `StarFleet_MinRange` 0x006ff6a0,
|
||
`StarFleet_ResolveWaypoint` 0x00701390, `IsGateTransitWaypoint` 0x0056e6e0, `IsNodeWaypoint` 0x0056e720,
|
||
`StarShip_IsGroundedByDamage` 0x00815090, `StarFleet_GetLocationIfNode` 0x006fe300,
|
||
`StarFleet_HasFlagShips` 0x00703500 and `StrategyServer_OrderFleetMove` 0x008653c0 all already exist with
|
||
prototypes matching what I read. **Eight independent re-derivations agreeing; no duplicate rows filed.**
|
||
`StrategyServer_OrderFleetMove`'s existing prototype says the meaning of the three bits *"is not known"* — the
|
||
new `lane-p2.json` does not re-file the address; the meaning goes in this document and in the
|
||
`PathSolver` entry that names it.
|
||
|
||
---
|
||
|
||
## 9. What this lane did **not** read
|
||
|
||
* **`FUN_00705d60`'s fold** — past its first ~0x90 bytes. The polarity of its bool and the type of its result
|
||
are established; the tanker redistribution that turns per-ship ranges into a fleet range is not. It is the
|
||
input to every range decision in §4, so this is the largest remaining hole in the *numbers*.
|
||
* **`FUN_006e4de0`** — the node **bore**. Read only as a call shape (`(owner, from, to, 0, fleet)`, cdecl,
|
||
5 args) and by its two outcomes. It creates a node line and therefore almost certainly writes saved state
|
||
that this document does not list.
|
||
* **`FUN_006e2130`** and the bucket walk `FUN_006905a0` in `FindNodeLine`. The invariance of the ranking term
|
||
is [V]; what it computes is not read.
|
||
* **`FUN_006d2050`** — the relation scale. §1.6.
|
||
* **`FUN_0080c820`** (`Ship_MaxRange`), **`FUN_00705c70`** (the speed recompute that sets `FPsp2`),
|
||
**`FUN_00703cf0`**, **`FUN_00706c90`**, **`FUN_00849280`** past B5's first ~60 bytes.
|
||
* **`MoveFleet`'s two `FUN_00703730` call sites** (0x7da5c6, 0x7da5da) — the multi-waypoint continuation.
|
||
Prediction P1's first falsification route.
|
||
* **Indirect edges.** Three on the main line, none resolved: `(fleet->galaxy(+0x10))->vft[1]()` at 0x703a14
|
||
(the node graph getter — the single most important one, since the whole of §2.5 hangs off its result),
|
||
`(S+4)->vft[2]()` at 0x86540e in `OrderFleetMove`, and the `vft[2]`/`vft[3]` pair inside `FUN_00703bd0`
|
||
that enumerates a node's fleets. `tools/vtable_map.py` and `ghidra/vtable-owners.json` were consulted for
|
||
callers of `FUN_007066c0` (two direct, both named in §1.1; the E8/E9 sweep found no tail-call thunk into
|
||
it), but the three sites above are **outbound** edges and lane V2's inversion does not reach them.
|
||
* **The class of the kind-2 "point" object.** It has `Pos` at +0x18, a kind tag at +0x14 and two per-player
|
||
bitmasks at +0x8c / +0x90. It is not `Game::StarMapNode` (too small). Unresolved.
|
||
|
||
**The strongest and weakest sentence in this document are the same one:** the classifier's rules are read
|
||
instruction by instruction and are internally consistent with `FUN_0080c7d0`, the two waypoint jump tables,
|
||
`OrderFleetMove`'s `0x418` mask, the UI's `!= 0` mask and five verified object layouts — and **not one leg of
|
||
it has ever been observed executing under an instrument.**
|
||
|
||
---
|
||
|
||
## 10. The reimplementation
|
||
|
||
`sots-engine` branch **`wip/pathing`**, module `src/game/nav/` (a new directory; lane N owns `src/game/sim/`,
|
||
A2 `src/app`, D2 `src/game/data`). Clean-room: no addresses, no raw identifiers, no game data.
|
||
|
||
Modelled as pure functions returning a plan, not a mutation — the same split `game/combat` uses, because
|
||
installing a flight plan touches saved state the module does not own:
|
||
|
||
`DriveTypeOfSpecies`, `IsGateTransitWaypoint`, `IsNodeWaypoint`, `FleetDriveType`, `GateProjectionReaches`,
|
||
`GateTransitAllowed`, `LegInRange` (with the unstored `r*r`), `LegLength` (the five narrowings),
|
||
`ClassifyLeg`, `SolvePath`, and the flag constants with `kOrderRefusalMask`.
|
||
|
||
Gates run as separate commands; results in the lane report.
|