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.
51 KiB
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
// __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
// __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]
-
0x20000is the node-bore capability bit on the fleet's cached capability mask+0xb8(StarFleet_HasFlagShips0x00703500, alreadyverifiedfrom lane Z, delegating toFUN_00702d70(this, maskA, maskB, out)). It is the same bitAddShipspecial-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.
scoredepends only ongraph, so it is identical for every candidate.beststarts at −1, so the first matching entry in the bucket wins and every later one is discarded byscore > bestbeing false on equality. Whatever the ranking was meant to be (line length? a per-line discovery level?), as shippedFindNodeLineis "first match in hash-bucket order". A reimplementation must reproduce the bucket order to reproduce the chosennrp, or accept thatnrpis unmodelled. I did not readFUN_006e2130or the hash function 0x006905a0. [V for the invariance, not read for the two callees] -
InRangeisFUN_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,4or5(fiveretsites, §2.3). [V] driveTypeis 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 == 3and returns only3or0. [V] - The gate branch returns only
4or5, and its guard isGFlags— 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_SPEEDand 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_FACTORas a ship key; IsNodeWaypointdeliberately 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]
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 == 2guard; 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 fromFindNodeLine(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_00703730resets 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 whoseTpis not 3 hasnrt = {nrp: -1, nrf: 0, nrt: 0}, and a waypoint whoseTpis 3 hasnrfandnrtequal to network ids that resolve, withnrpeither −1 (a freshly bored line, or a point endpoint) or a non-negative path index.P2.
StarFleet.FtTransequalsFPlan.wpts[0].Tpfor 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 carriesTp == 3;Tp4 or 5 appears only for a player whoseNGts(+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.wptswill havenentries, and the types will be shifted by one:wpts[0].Tpwill be the type computed forwpts[1]'s destination, andwpts[n-1].Tpwill be 0 whatever the race is. For a Human or Zuul fleet the tell is unmistakable — the last waypoint of a node route saves asTp 0withnrt {-1,0,0}instead ofTp 3with a livenrp/nrf/nrt. Setn = 2for 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], andnrf[i+1] == nrt[i]— becauseClassifyLegis handed(prev, cur)and writesorigin->id/dest->id, and §1.4 setsprev = curon 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:
nrt == Wpton all 57. Two fields the save format carries separately, that no earlier lane connected, and the reason they are equal isClassifyLegwritingdest->+0x4intonrtwhileOrderFleetMovewrites the samedests[i]->+0x4intoWpt. Predicted from the instruction stream, confirmed.- The
nrfchain closes throughpndacross the three-hop plans inzuul-turn23-fleet23.sav(80→272,272→368,368→288, withpnd = 80). That is §1.4'sprev = curback edge, visible in a save. nrpsplits exactly where §2.5 says it should.human-turn3-noderoute.sav— Human, cannot bore — hasnrp= 37, 16, 9: all non-negative, all fromFindNodeLine. 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 successfulBoreNodeLine, 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:
nrtis written by more than one producer.MoveFleet's multi-waypoint continuation callsFUN_00703730twice (0x7da5c6, 0x7da5da) and I did not read those call sites. If either writes a route into an existing waypoint, P1 breaks with aTp != 3waypoint carrying a livenrf. 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.FtTransmay 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 byOrderFleetMove.- P3's
Tp == 2half rests onFUN_0080c7d0being the only producer of a drive type. It is the only caller-visible one in this subtree, butFUN_0080c7d0has 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. - A Hiver player with zero gates: P3's last clause predicts no type 4/5. If one appears,
GFlagsis set by something other than gate construction and §2.4's identification of+0xdcis 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_006e2130and the bucket walkFUN_006905a0inFindNodeLine. 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 setsFPsp2),FUN_00703cf0,FUN_00706c90,FUN_00849280past B5's first ~60 bytes.MoveFleet's twoFUN_00703730call 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 inOrderFleetMove, and thevft[2]/vft[3]pair insideFUN_00703bd0that enumerates a node's fleets.tools/vtable_map.pyandghidra/vtable-owners.jsonwere consulted for callers ofFUN_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
Posat +0x18, a kind tag at +0x14 and two per-player bitmasks at +0x8c / +0x90. It is notGame::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.