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.
This commit is contained in:
parent
52581efbfb
commit
e30619630e
2 changed files with 1111 additions and 0 deletions
870
findings/subsystems/path-solver.md
Normal file
870
findings/subsystems/path-solver.md
Normal file
|
|
@ -0,0 +1,870 @@
|
|||
# `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.
|
||||
241
ghidra/addresses.d/lane-p2.json
Normal file
241
ghidra/addresses.d/lane-p2.json
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{ "entries": [
|
||||
|
||||
{ "name": "PathSolver",
|
||||
"addr": "0x007066c0",
|
||||
"convention": "cdecl",
|
||||
"prototype": "bool (StarFleet* fleet, MapObject* start, MapObject** dests, unsigned count, int* flagsOut, int* failIdxOut, int* typesOut, NodeRoute* routesOut) // 8 STACK ARGS, plain RET, esp cleaned by the caller (add esp,0x20 at both call sites) -- cdecl, NOT thiscall, even though ecx is loaded with the fleet for the range helpers. Real body 0x007066c0..0x00706907 then 8 int3 to the next start 0x00706910; Ghidra's 584 is correct. IT IS NOT A PATH FINDER: no frontier, no visited set, no relaxation, no recursion. It walks the caller's already-chosen destination list and calls ClassifyLeg 0x00703730 once per consecutive pair, accumulating flags (OR) and the index of the FIRST failing leg. RETURNS TRUE for every call with a non-null fleet and non-null start; all outcome information is in flagsOut/failIdxOut. Leading-destination drop at 0x00706722: if dests[0] is the fleet itself or the fleet's current SYSTEM (LocID with +0x14==0), dests is advanced and count decremented IN THE SOLVER'S OWN FRAME -- the caller's count is unchanged, so typesOut/routesOut end up shifted by one and the last element is never written. Three fuel figures: StarFleet_MinRange(fleet,0) for the pre-flight probe, FUN_00705d60(fleet,true) as the refuel reset, FUN_00705d60(fleet,false) as the running budget, clamped to >= 0 at the top of every leg. Per-leg draw-down uses an INLINED Mars_Vec3_Length (three f32 deltas, one narrowing on the sum of squares, one on the sqrt, one on the subtraction). The store of 0x009e22bc into the NodeRoute local on the back edge is the INLINED destructor (the Mars::IStreamable base vftable), not a branch and not a missing re-init -- the ctor runs again at the top of the next iteration. Two callers, both direct, none indirect: FUN_005e6d50 (UI, dry run: flags only, then a confirmation dialog if flags != 0) and StrategyServer_OrderFleetMove 0x008653c0",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_ClassifyLeg",
|
||||
"addr": "0x00703730",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (StarFleet* this, MapObject* from, MapObject* to, float* rangeInOut, int* flagsOut, NodeRoute* routeOut) // RET 0x14. Returns the WAYPOINT TYPE for one leg: 0 (no move possible), the owner's species drive type, 3 (node route), 4 or 5 (gate transit). Real body 0x00703730..0x00703bc9 then 6 int3 to 0x00703bd0. flagsOut may be null (a stack dummy is substituted and the whole flag block at 0x00703846 is skipped). Order of decision: (A) if `to` is a fleet, try to intercept it via FUN_00703650; (B) raise the flag bits; (C) if `to` is a deep-space point the player may not use, raise 0x400; (D) if the player has a gate at one end, check gate traffic against NGts*PrGtTrf and return 4 (gate->gate) or 5 (gate->gateless within CstR); (E) if the species drive type is not 3, return it unchanged -- every non-node race stops here with no range check and no route record; (F) otherwise solve the single node-line hop. Endpoint kinds from MapObject->+0x14: 0 system, 1 fleet, 2 deep-space point. The route record is written ONLY when the returned type is 3; for every other type it is left {nrp:-1, nrf:0, nrt:0}",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_ShipActionsWillCancel",
|
||||
"offset": "0x00000001",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x001, set at 0x00703897. Some ship in the fleet is performing a cancellable action; OrderFleetMove cancels them all and proceeds. A WARNING, not a refusal -- the UI's dry run (flags != 0) shows it, the server's mask (flags & 0x418) ignores it",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_NodeLegOutOfRange",
|
||||
"offset": "0x00000002",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x002, ORed at 0x00703b10 from the errBits local seeded at 0x00703a0d. The leg's EXISTING node line is beyond the fleet's remaining fuel (LegInRange FUN_006ffa00 false). Not a refusal: OrderFleetMove installs the plan anyway",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_GateTrafficExceeded",
|
||||
"offset": "0x00000004",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x004, set at 0x007039c5 when owner->GTraf(+0x14c) + fleet->+0xc0 would exceed owner->NGts(+0x144) * owner->PrGtTrf(+0x148). The leg then returns type 0. NOT one of OrderFleetMove's refusal bits, so a plan can be installed over gate capacity with a type-0 first waypoint. The fleet's own cost is zeroed first if its CURRENT waypoint is already a gate transit (it is already counted)",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_CannotInterceptFleet",
|
||||
"offset": "0x00000008",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x008, set at 0x00703859. THE FIRST OF OrderFleetMove's THREE REFUSAL BITS. The destination is a FLEET that is itself traversing a node route, and no interception point could be computed -- FUN_00703650 requires the mover to be sitting at one of the two ends of the target's node line and the whole line to be in range. Not raised when the target fleet is not node-travelling at all",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_FleetGrounded",
|
||||
"offset": "0x00000010",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x010, set at 0x0070386d when ANY ship in the fleet satisfies StarShip_IsGroundedByDamage (destroyed drive). THE SECOND OF OrderFleetMove's THREE REFUSAL BITS. CLEARED again at 0x007039d3 on the successful gate-transit path -- a Hiver gate throw ignores dead drives, the same rule the retreat pipeline reaches from the other side via its species-1 bypass",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_NoNodeLineAndCannotBore",
|
||||
"offset": "0x00000020",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x020, set at 0x00703b8b. No node line joins the two systems for this player and the fleet lacks the node-bore capability (StarFleet_HasFlagShips(fleet, 0x20000, 0) is false). Returns type 0. Not a refusal bit",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_BoredLineOutOfRange",
|
||||
"offset": "0x00000040",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x040, ORed at 0x00703b10 from errBits after it is re-seeded at 0x00703b63. A node line was successfully bored but the leg is still out of fuel range. Distinguishes 'ran out of fuel on a line that already existed' (0x002) from 'ran out of fuel on a line we just made' (0x040)",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_NodeBoreFailed",
|
||||
"offset": "0x00000080",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x080, set at 0x00703b7d when FUN_006e4de0 (bore a node line between two systems) returns false. Not a refusal bit",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_DestSystemNotFriendly",
|
||||
"offset": "0x00000100",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x100, set at 0x00703a42. A node-drive leg from a deep-space POINT to a SYSTEM whose owner is neither the player nor a player with a positive relation (FUN_00817890). Not a refusal bit",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_SourceSystemNotFriendly",
|
||||
"offset": "0x00000200",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x200, set at 0x00703a6a. The mirror of 0x100: a node-drive leg from a SYSTEM that is not friendly-owned to a deep-space POINT. Not a refusal bit",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_DestPointNotPermitted",
|
||||
"offset": "0x00000400",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x400. THE THIRD OF OrderFleetMove's THREE REFUSAL BITS. Two sites: 0x007038c5 (the destination point is in neither of the player's two per-point masks at point+0x8c and point+0x90, and the player is not species 4) and 0x00703ad3 (a node-drive leg to a point the player may not use). At the first site the leg then returns 0 for a gate or node drive and the plain drive type otherwise",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_ShipActionEight",
|
||||
"offset": "0x00000800",
|
||||
"convention": "constant",
|
||||
"prototype": "int // ClassifyLeg flag bit 0x800, set at 0x0070388a. The fleet contains a ship whose current action (ship+0x4c) is exactly 8. Singled out of the general 0x001 warning by masking bit 8 out of the action bitmask before the 0x001 test. A WARNING, not a refusal",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "PathFlag_OrderRefusalMask",
|
||||
"offset": "0x00000418",
|
||||
"convention": "constant",
|
||||
"prototype": "int // 0x400|0x010|0x008. The literal in `test DWORD PTR [ebp-0x10],0x418` at 0x00865499 -- the only bits that make StrategyServer_OrderFleetMove refuse. On a hit it logs level 2 with the .rdata format at 0x00a31e44, \"StrategySim: %s (%s) move not permitted at this time.\", with the fleet's FtName(+0x5c) and the owner's name string (owner+0x40), both read through the MSVC std::string SSO test. Every other bit is either advisory or a route-quality complaint the server commits anyway. The UI dry run at 0x005e6da0 instead tests flags != 0, which is what surfaces the whole word to the player",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "DriveTypeOfSpecies",
|
||||
"addr": "0x0080c7d0",
|
||||
"convention": "cdecl",
|
||||
"prototype": "int (int species) // 50 B. A 7-ENTRY JUMP TABLE at 0x0080c804, resolved byte by byte: Human(0)->3, Hiver(1)->0, Tarkas(2)->1, Liir(3)->2, _NPC(4)->0, Zuul(5)->3, Morrigi(6)->6; anything above 6 -> 0. THE ANSWER TO THE TYPE-2 QUESTION: waypoint type 2 is the LIIR drive, and it is unreachable for any node-drive race by construction. The value it returns IS the waypoint type for every leg the gate block and the node-route block decline, so a fleet's default waypoint type is a pure function of its owner's species -- no ship data, no terrain, no tech",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_GetDriveType",
|
||||
"addr": "0x006ff810",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (StarFleet* this) // 118 B, no stack args. Returns 0 for an empty fleet, else DriveTypeOfSpecies(this->PID(+0x58)->Species(+0x5c)), else 0 if the fleet has more than one ship and any ship disagrees. ORIGINAL DEFECT: the disagreement loop at 0x006ff853 re-reads the FLEET's owner species on every iteration instead of indexing ship i, so the compared value is loop-invariant and the loop can never fail. As shipped it is dead code; reproduce it as written rather than 'fixing' it to read per-ship data",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "ServerSystem_HasGate",
|
||||
"addr": "0x00744010",
|
||||
"convention": "thiscall",
|
||||
"prototype": "bool (ServerSystem* this, ServerPlayer* p) // 34 B, RET 4. return (this->GFlags(+0xdc) >> p->PlyrIdx(+0x28)) & 1. IDENTIFIES GFlags: combat-retreat-pipeline.md lists +0xdc as 'a second presence source (not read here)' -- it is the per-player GATE mask, and the whole waypoint-type-4/5 branch of ClassifyLeg is built on it",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "ServerPlayer_GateProjectionReaches",
|
||||
"addr": "0x00818040",
|
||||
"convention": "thiscall",
|
||||
"prototype": "bool (ServerPlayer* this, ServerSystem* from, ServerSystem* to) // 150 B, RET 8. return from && to && 0.0f < this->CstR(+0x150) && ServerSystem_HasGate(from,this) && !ServerSystem_HasGate(to,this) && Mars_Vec3_Length(from->Pos - to->Pos) <= this->CstR. GIVES CstR A READER: strategic-turn-internals.md records it as unused; it is the GATE PROJECTION RADIUS -- how far past a gate a fleet can be thrown when the far end has no receiving gate. Its result is exactly the 4-vs-5 choice in ClassifyLeg (`add eax,4` after `setne`), so waypoint type 5 is a Hiver gate throw at a GATELESS system, not the Zuul node bore or Morrigi gravity casting. Length via Mars_Vec3_Length, so two float32 narrowings; the comparison is non-strict",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "ServerPlayer_GateTrafficCapacity",
|
||||
"addr": "0x0080dc50",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (ServerPlayer* this) // 14 B, no frame: `mov eax,[ecx+0x148]; imul eax,[ecx+0x144]` = this->PrGtTrf(+0x148) * this->NGts(+0x144) -- per-gate traffic times gate count, both saved ints. Compared against GTraf(+0x14c) + the fleet's own int16 cost at fleet+0xc0",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_LegInRange",
|
||||
"addr": "0x006ffa00",
|
||||
"convention": "thiscall",
|
||||
"prototype": "float-free bool (StarFleet* this, MapObject* a, MapObject* b, float* rangeOpt) // 170 B, RET 0xc. THE ONLY FLOAT IN THIS SUBSYSTEM THAT DECIDES A FAILURE. dx,dy,dz each stored to a float32 slot; the sum of squares accumulated on the x87 stack and narrowed to float32 ONCE at 0x006ffa46; r = rangeOpt ? *rangeOpt : StarFleet_MinRange(this,0.0f); r = min(r, FUN_006ff710(this)) with both candidates read back from float32 slots; then `fmul st(0),st` computes r*r AND LEAVES IT IN THE REGISTER -- it is never stored. So the comparison is f32(sumsq) <= (double)r*(double)r, NOT f32(sumsq) <= f32(r*r). A reimplementation that narrows the square disagrees exactly at the boundary. Non-strict: equality returns true (test ah,0x41 then jp)",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_MinTankCapacity",
|
||||
"addr": "0x006ff710",
|
||||
"convention": "thiscall",
|
||||
"prototype": "float (StarFleet* this) // 155 B, no stack args. Min over the fleet's ships of Ship_MaxRange 0x0080c820, seeded FLT_MAX from the .rdata word at 0x009e23a8, EXCEPT that it returns 0.0f (not FLT_MAX) for a fleet with no ships and EXITS EARLY the moment the running minimum is <= 0 -- so it is not a pure min if a zero-range ship precedes a negative one. Used only to cap the range in StarFleet_LegInRange",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "NodeGraph_FindNodeLine",
|
||||
"addr": "0x006e4eb0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (NodeGraph* this, ServerPlayer* p, ServerSystem* a, ServerSystem* b) // 303 B, RET 0xc. THE ONLY GRAPH STRUCTURE IN THE PATH SUBTREE, and it is a SINGLE-HOP ADJACENCY QUERY, never a search: it returns the path index of a node line joining a and b that player p has discovered, or -1. Rejects null args and a==b by system index (+0x5c). Gate: a TRIANGULAR adjacency array at this->+0x24 indexed (hi-1)*hi/2 + lo with one bit per player, so a pair the player has not discovered short-circuits to -1. Then it walks a hash bucket, accepting entries whose {+0xc,+0x10} pair matches in either order and whose +0x2c mask carries the player's bit, and returns entry->+0x8. ORIGINAL DEFECT: the ranking term FUN_006e2130((this->+0x4)->+0x8) depends only on `this`, so it is identical for every candidate; with best seeded at -1 the FIRST matching bucket entry always wins and every later one is dropped on the non-strict `score > best`. As shipped the tie-break is hash-bucket order",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_CanRefuelAt",
|
||||
"addr": "0x00703c90",
|
||||
"convention": "cdecl",
|
||||
"prototype": "bool (MapObject* node, StarFleet* fleet) // 85 B. owner = MapObject_GetOwner(fleet); returns true if any fleet parked at the node and owned by that player carries a ship with capability mask 2 (the tanker bit), OR if the node has an owner whose relation to the fleet's owner is >= 3. NOTE the relation scale: strategic-turn-internals.md 5.2 records FUN_0080e050 as '1 ally, 2 NAP, 3 cease-fire', which would make this 'refuel at a cease-fire system but not at an ally's'; FUN_006d2050 was NOT read, and two call sites use the same scale with different thresholds (>= 3 here, > 0 in FUN_00817890), so 5.2's ordering should be re-checked. Called by PathSolver only when the destination's kind tag (+0x14) is 0, i.e. a system -- reaching one resets the running fuel budget to full tanks",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "MapObject_GetOwner",
|
||||
"addr": "0x0071e280",
|
||||
"convention": "thiscall",
|
||||
"prototype": "ServerPlayer* (MapObject* this) // 26 B, no frame. switch on this->+0x14: 0 (system) -> this->PID(+0x100); 1 (fleet) -> this->PID(+0x58); anything else (2 = deep-space point) -> 0. Confirms the kind tag's three values from a third, independent site",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "MapObject_AsSystem",
|
||||
"addr": "0x0071e340",
|
||||
"convention": "thiscall",
|
||||
"prototype": "MapObject* (MapObject* this) // 12 B, no frame: return (this->+0x14 != 0) ? 0 : this. A checked downcast to the kind-0 (system) case, written with the neg/sbb/not/and branchless idiom",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_SolveFleetIntercept",
|
||||
"addr": "0x00703650",
|
||||
"convention": "register-live-in",
|
||||
"prototype": "bool (/* ebx = StarFleet* mover, esi = StarFleet* target -- BOTH LIVE-IN, NEITHER WRITTEN */ float* rangeIn, MapObject** systemOut) // 214 B, cdecl stack frame but it TESTS ebx AND esi WITHOUT EVER WRITING THEM. Reading it as a plain two-argument cdecl function produces nonsense; its one caller (StarFleet_ClassifyLeg at 0x00703831) supplies both registers. Returns false unless the target has waypoints, its front waypoint is type 3, and FUN_00703520 accepts the geometry. On success *systemOut is the system to aim at: if the mover sits at the target's destination, aim at the target's node-transit ORIGIN; if it sits at the origin, aim at the destination; otherwise return true with *systemOut left 0. The transit origin is FUN_006ffab0, which resolves FlightPlan.pnd(+0xf8) through the entity hash at (fleet->galaxy(+0x10))+0x80 -- SO pnd IS THE NETWORK ID OF THE NODE TRANSIT'S ORIGIN OBJECT",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_GetNodeTransitOrigin",
|
||||
"addr": "0x006ffab0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "MapObject* (StarFleet* this) // 43 B, no frame. Returns 0 when the waypoint vector is empty, else IDMap resolve of this->FPlan.pnd(+0xf8) through (this->galaxy(+0x10))+0x80. Pairs with StarFleet_ResolveWaypoint 0x00701390, which resolves the front waypoint's Wpt id through the same map: origin and destination of the current node transit",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_PendingShipActionMask",
|
||||
"addr": "0x006ff990",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (StarFleet* this) // 101 B, no stack args. OR of (1 << ship->+0x4c) over every ship satisfying FUN_0081f880, i.e. every ship whose current action is neither 0 nor 6 and is not action 8 with bit 3 of ship->+0x1c set. ship+0x4c IS THE SHIP'S CURRENT ACTION: OrderFleetMove open-codes the identical three-way predicate at 0x008655ed and calls the 'Ship leaving %s is still doing %s. Cancelling action.' cancel FUN_00849280 on every ship that passes it. The mask feeds ClassifyLeg's warning bits: bit 8 becomes 0x800, anything else becomes 0x001. Nothing bounds-checks the shift, so an action enum >= 32 would be UB",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_AnyShipGroundedByDamage",
|
||||
"addr": "0x00700240",
|
||||
"convention": "thiscall",
|
||||
"prototype": "bool (StarFleet* this) // 80 B, no stack args. True if ANY ship in the fleet satisfies StarShip_IsGroundedByDamage 0x00815090 (a destroyed drive, tested with FLT_EPSILON rather than zero). Sole producer of ClassifyLeg's 0x010 refusal bit",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_SetFlightPlan",
|
||||
"addr": "0x00707080",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (StarFleet* this, Waypoint* wpts, int count, int originId) // 514 B, RET 0xc. Real body 0x00707080..0x00707281 then 14 int3 to 0x00707290. EVERYTHING IT WRITES IS SAVED STATE: GTraf(+0x14c) debited by the int16 at fleet+0xc0 if the OLD front waypoint was a gate transit; FPlan.wpts assigned from a zeroed temp then the new list inserted; FPsp2(+0xd8) 0.0f then recomputed by FUN_00705c70; FPeta2(+0xdc) 0; FPogn2(+0xe0) zeroed then set to the fleet's Pos -- the position the order was given from; FPdpos(+0xec) zeroed then set to the FIRST waypoint target's Pos, resolved through the IDMap at (fleet->galaxy)+0x80 and left zero if it does not resolve; pnd(+0xf8) 0 then originId; FtTrans(+0xfc) = wpts[0].Tp, A SECOND SAVED COPY OF THE FIRST LEG'S WAYPOINT TYPE; FtOrig(+0x100) = the fleet's Pos; then GTraf re-credited if the NEW front waypoint is a gate transit. Checked on all 11 curated saves: FtTrans == wpts[0].Tp on 46 of 46 flight plans",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "FlightPlan_Waypoint_Set",
|
||||
"addr": "0x007006e0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (Waypoint* this, int Tp, const NodeRoute* r) // 34 B, RET 8: this->Tp(+0x8) = Tp; this->nrt.nrp(+0x10) = r->nrp(+0x4); this->nrt.nrf(+0x14) = r->nrf(+0x8); this->nrt.nrt(+0x18) = r->nrt(+0xc). Pins Waypoint = {vptr, int Wpt@+4, int Tp@+8, NodeRoute nrt@+0xc} at 0x1c bytes, cross-checked by the 0x92492493 divide-by-28 at 0x00865594 and the add edi,0x1c stride. The vptr of the destination is not touched",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "NodeRoute_Construct",
|
||||
"addr": "0x006e1b20",
|
||||
"convention": "thiscall",
|
||||
"prototype": "NodeRoute* (NodeRoute* this) // 24 B, no frame, returns this in eax: vptr = 0x00a1cbdc (the Game::NodeRoute vftable), nrp = -1, nrf = 0, nrt = 0. THE DEFAULT nrp IS -1, NOT 0 -- and -1 is also what ClassifyLeg writes for a freshly bored node line, which is why the Zuul saves carry a mix of -1 and real path indices while the Human save carries only non-negative ones",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "StarFleet_PosDiffersFromPointLocation",
|
||||
"addr": "0x0080ec50",
|
||||
"convention": "cdecl",
|
||||
"prototype": "bool (StarFleet* f) // 90 B. FUN_006fe320(f) returns f->LocID(+0xa0) ONLY when the location's kind tag (+0x14) is 2, a DEEP-SPACE POINT -- never a system. Returns true iff any of the three position components differs by exact IEEE comparison (fucompp, test ah,0x44, jp), no epsilon. OrderFleetMove's opening snap is therefore point-only; combat-retreat-pipeline.md 2.5's 'snaps the fleet's position onto its current system' is corrected here -- a fleet parked at a system is never snapped",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" }
|
||||
|
||||
] }
|
||||
Loading…
Add table
Reference in a new issue