sots-re/findings/subsystems/path-solver.md
alex e30619630e lane P2: the path solver read from the instruction stream, and OrderFleetMove's three failure bits
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.
2026-09-08 12:43:34 -04:00

870 lines
51 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `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.