nav: lane DN predictions for the route-classifier live compare, committed first
The classifier is a real callable unit (StarFleet::ClassifyLeg 0x00703730, thiscall, ret 0x14, two direct callers), so the hook goes on the classifier and not on a proxy for it. Nine predictions with their falsification symptoms, the workload that reaches both route classes, and the three input gaps that are counted per call rather than argued away. addresses.d/dn.json carries the eleven offsets the hook needs plus the vtable slot that resolves path-solver.md's node-graph indirect edge.
This commit is contained in:
parent
fdea0fde08
commit
3368e28091
2 changed files with 219 additions and 0 deletions
132
findings/subsystems/nav-classifier-live.md
Normal file
132
findings/subsystems/nav-classifier-live.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# The route classifier under a hook — `game/nav` from `modelled` to `compared`
|
||||
|
||||
Lane DN, 2026-09-09. Guest **VM146** (`sots-re-win10-146`), build box CT111, own build dir
|
||||
`/srv/re-lab/build/sots-engine-dn`, own worktree `sots-engine` branch `wip/dn`.
|
||||
|
||||
**Hold announced:** VM146 only, for the duration of this lane. VM140 is the exclusive reference
|
||||
guest and was not touched; VM141 (lane CZ) and VM145 (lane CR) are held by other lanes and were
|
||||
not touched. VM146 is released restored and hash-verified at the end of this document.
|
||||
|
||||
`game/nav` is the campaign's largest **`modelled`** module by span of behaviour and it had never
|
||||
executed under an instrument. `tools/displacement.py` records it as
|
||||
|
||||
> offline vs 58 waypoints / 46 flight plans across 11 saves, 0 failures
|
||||
> — *never instrumented; not one leg has executed under a hook*
|
||||
|
||||
and `path-solver.md` closes with the same sentence from the other side: *"the classifier's rules
|
||||
are read instruction by instruction … and **not one leg of it has ever been observed executing
|
||||
under an instrument**."* This lane executes it.
|
||||
|
||||
---
|
||||
|
||||
## 0. The entry point is a real function, so the hook is on the classifier itself
|
||||
|
||||
The brief allowed for the classifier being inlined into its caller, in which case the honest hook
|
||||
would have been on the enclosing function comparing its *output*. **It is not inlined.**
|
||||
`path-solver.md` §0.1/§1.1/§2 read it from the instruction stream:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `StarFleet_ClassifyLeg` | `0x00703730`, **thiscall**, `ret 0x14`, six arguments, real body `0x00703730..0x00703bc9` then 6 × `int3` to the next function start |
|
||||
| callers | `PathSolver` `0x007066c0` at two sites (the pre-flight probe and the per-leg loop), and `MoveFleet`'s multi-waypoint continuation at `0x7da5c6` / `0x7da5da` |
|
||||
| output surface | the **return value** (the waypoint kind), `*flagsOut` (the flag word), and the route record's three written words at `routeOut+4..+0xf`. Nothing else. |
|
||||
|
||||
So the comparison surface is the classifier's own three outputs plus its return value, and there
|
||||
is no unit boundary invented here that the binary does not have. `PathSolver` is hooked too, but
|
||||
**trace only** — see §1.3.
|
||||
|
||||
---
|
||||
|
||||
## 1. Predictions (committed before the compare run, rule 2)
|
||||
|
||||
This section is committed in its own commit, before the instrumented build ran once.
|
||||
|
||||
### 1.1 The workload, and why it reaches two route classes
|
||||
|
||||
Save `ar-oracle-A-pre.sav` (`15b99255…`), turn 38, players: **Human ×2** (net 16 = the human seat,
|
||||
496), **Tarkas ×2** (32, 512), **NPC ×4** (528–576). `DriveTypeOfSpecies` maps Human → 3,
|
||||
Tarkas → 1, NPC → 0, so *three* drive classes are present before the AI does anything.
|
||||
|
||||
Procedure, fixed and replayed identically in every run:
|
||||
|
||||
1. load (SavedGames reset to exactly one file, row 1), lobby, `Launch`;
|
||||
2. `Move`, order **Delta Fleet → Isis**. The game answers with its own prompt —
|
||||
*"Delta Fleet does not have the range to reach Isis using nodes. Do you want to plot a
|
||||
sublight course?"* — and `OK` commits it. **This is the sublight-crawl class.**
|
||||
3. `Move`, order **Delta Fleet → Venkman**. Hover reads *"Range: 6.0 / ETA: 2 Turns"*, there is no
|
||||
prompt, the order is committed straight away. **This is the node-route class.**
|
||||
4. `End Turn`.
|
||||
|
||||
The hover readout named the class before either click, exactly as the campaign's guest notes say:
|
||||
Venkman `ETA: 2 Turns` (node), Isis `ETA: 50 Turns` in red (sublight); Nu Ophiuchi `4 Turns`,
|
||||
Kua'Taak `47`, Kam'Tara `53`, Kao'Lolto `50`, Ko'Kotan `47`. The state was **built deliberately**
|
||||
from that readout rather than hoped for.
|
||||
|
||||
### 1.2 Falsifiable predictions
|
||||
|
||||
> **P1 — call count from the two player orders: 8.** Each accepted order costs **two**
|
||||
> `PathSolver` calls (the UI's dry run `FUN_005e6d50`, then the server's `OrderFleetMove`), and
|
||||
> each `PathSolver` call on a one-destination list costs **two** `ClassifyLeg` calls (the
|
||||
> pre-flight probe against `MinRange`, then the loop leg). Two orders × 2 × 2 = **8**, with
|
||||
> **4** `PathSolver` records. `count` is 1 on all four and the leading-destination drop does
|
||||
> **not** fire (neither destination is the fleet or the fleet's own system).
|
||||
> *Falsified by:* a different multiple of 2 — most likely because hovering also runs the dry run,
|
||||
> which would add calls in pairs and is worth knowing either way.
|
||||
>
|
||||
> **P2 — the Isis leg raises `0x002`, not `0x020`.** The game's own prompt says the fleet cannot
|
||||
> reach Isis **"using nodes"**, which is `errBits` seeded at `0x00703a0d` and ORed at
|
||||
> `0x00703b10`: a node line **exists** and is beyond the fuel budget. So on that call
|
||||
> `line_found >= 0`, the returned kind is **0**, and the flag word is `0x002`.
|
||||
> *Falsified by:* `line_found == -1` on that call, which would make the flag `0x020` and the
|
||||
> prompt string generic rather than diagnostic.
|
||||
>
|
||||
> **P3 — the Venkman leg returns kind 3 with a live route.** `line_found >= 0`, return value 3,
|
||||
> `nrp == line_found`, `nrf ==` Epsilon Eridani's network id, `nrt ==` Venkman's, flags `0`.
|
||||
>
|
||||
> **P4 — no Human or Zuul call returns 1, 2 or 6; no Tarkas call returns 3; every Tarkas call
|
||||
> leaves the route record at `{-1,0,0}` and performs no range check.** This is
|
||||
> `DriveTypeOfSpecies` and step E of the decision order, live. The record carries the original's
|
||||
> own `StarFleet_GetDriveType` next to the species, so the table is checkable rather than assumed.
|
||||
>
|
||||
> **P5 — `can_bore == 0` on every call, so the bore arm is reached 0 times.** No Zuul player, and
|
||||
> the `0x20000` capability is the Zuul node cruiser. This is the declared gap that makes itself
|
||||
> inert, and the trace proves it instead of the brief asserting it (rule 28 practice 1).
|
||||
>
|
||||
> **P6 — `gate_cap == 0` on every call, so no call returns kind 4 or 5 and flag `0x004` never
|
||||
> appears.** Every player in every corpus save has `NGts == 0`. Kinds 4 and 5 stay at **zero
|
||||
> observations** and this lane says so rather than quietly counting a clean sheet as coverage.
|
||||
>
|
||||
> **P7 — 0 divergences, and the reason is the narrowings already in the module.** The `MoveFleet`
|
||||
> lane's eight live divergences were all float32 narrowings; `game/nav` already carries the five
|
||||
> of `LegLength` and the unstored `r*r` of `LegInRange`. I predict the compare comes back clean,
|
||||
> and I predict that a *boundary* fuel case is the only thing in this subsystem that could
|
||||
> break it — which this workload is not built to hit.
|
||||
> *Falsification shapes, each with a distinct symptom:*
|
||||
> * kind differs on a Tarkas/NPC call → the drive table is wrong (visible directly against
|
||||
> `drive_orig`);
|
||||
> * flags differ on `0x001` / `0x800` → the pending-action-mask split is wrong;
|
||||
> * flags differ between `0x002` and `0x020` → `LegInRange` disagrees off a boundary, i.e. the
|
||||
> range or the tank-capacity input is wrong, not the arithmetic;
|
||||
> * `nrf` / `nrt` differ → the origin/destination identification in the node arm is wrong;
|
||||
> * the `fleet` guard fires → the classifier writes the moving fleet and the module's purity
|
||||
> claim is false.
|
||||
>
|
||||
> **P8 — neutrality holds.** The compare run's `(Autosave EndTurn)` and `(Autosave)` are
|
||||
> byte-identical to the control's. If they are not, every number in this lane is void (rule 19).
|
||||
>
|
||||
> **P9 — at least 8 distinct input states.** A count of calls is not coverage (rule 23); this is
|
||||
> the number the verdict has to stand on.
|
||||
|
||||
### 1.3 What is deliberately not compared
|
||||
|
||||
* **`PathSolver` is `trace` only, and its `ours` throws if anyone sets it to `compare`.** The plan
|
||||
needs the fleet's effective range, which comes from `FUN_00705d60`'s tanker-redistribution fold
|
||||
— a function `path-solver.md` §9 explicitly did not read. A hook that returned "no plan" there
|
||||
would print a green verdict over a missing input, which is rule 1's failure mode.
|
||||
* **`nrp` on an existing line is an input, not a result.** The single-hop answer is taken from the
|
||||
original's own `NodeGraph_FindNodeLine` **before the original runs** (afterwards it could return
|
||||
a line the original itself had just bored). Its agreement is therefore not evidence; the kind,
|
||||
the flags and `nrf`/`nrt` are.
|
||||
* **Three input gaps, counted per call rather than argued away:** the intercept solve for a
|
||||
destination fleet already on a node leg (`intercept_gap`), the node bore (`bore_arm`), and the
|
||||
unread friendliness test `FUN_00817890` on the point↔system arms (`friendly_recon`).
|
||||
87
ghidra/addresses.d/dn.json
Normal file
87
ghidra/addresses.d/dn.json
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
{ "entries": [
|
||||
|
||||
{ "name": "MapObject_off_Id",
|
||||
"offset": "0x00000004",
|
||||
"convention": "offset",
|
||||
"prototype": "int Id -- the network handle of a map object. This is what OrderFleetMove writes into Waypoint.Wpt(+0x4) (dests[i]->+0x4) and what ClassifyLeg writes into the route record's nrf/nrt, which is why nrt == Wpt on every type-3 waypoint in the corpus",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "MapObject_off_Kind",
|
||||
"offset": "0x00000014",
|
||||
"convention": "offset",
|
||||
"prototype": "int Kind -- the map object's shape tag: 0 system, 1 fleet, 2 deep-space point. Read by MapObject_GetOwner 0x0071e280, MapObject_AsSystem 0x0071e340, StarFleet_GetLocationIfNode 0x006fe300, PathSolver's refuel test and ClassifyLeg's endpoint classification, so the three values are established from four independent sites",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "MapObject_off_Pos",
|
||||
"offset": "0x00000018",
|
||||
"convention": "offset",
|
||||
"prototype": "float Pos[3] at +0x18/+0x1c/+0x20 -- the position every leg length and every range check reads. PathSolver's inlined Mars_Vec3_Length loads exactly these three slots (fld [edi+0x18] / +0x1c / +0x20 at 0x00706844)",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "MapObject_off_PointVisibleMask",
|
||||
"offset": "0x0000008c",
|
||||
"convention": "offset",
|
||||
"prototype": "uint per-player bitmask on a kind-2 (deep-space point) object; bit PlyrIdx set = the point is visible to that player. The first of the two masks ClassifyLeg tests before raising 0x400; either mask alone permits the move, and a species-4 fleet bypasses both",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "MapObject_off_PointKnownMask",
|
||||
"offset": "0x00000090",
|
||||
"convention": "offset",
|
||||
"prototype": "uint per-player bitmask on a kind-2 (deep-space point) object; the second of the pair at +0x8c/+0x90. Set = the point is known to that player",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "ServerSystem_off_GFlags",
|
||||
"offset": "0x000000dc",
|
||||
"convention": "offset",
|
||||
"prototype": "uint GFlags -- the PER-PLAYER GATE MASK, read as (GFlags >> p->PlyrIdx(+0x28)) & 1 by ServerSystem_HasGate 0x00744010. combat-retreat-pipeline.md lists +0xdc only as 'a second presence source (not read here)'; the whole waypoint-type-4/5 branch of ClassifyLeg is built on it",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "ServerPlayer_off_NGts",
|
||||
"offset": "0x00000144",
|
||||
"convention": "offset",
|
||||
"prototype": "int NGts -- the player's gate count. Multiplied by PrGtTrf(+0x148) to give the gate-traffic capacity (ServerPlayer_GateTrafficCapacity 0x0080dc50), which GTraf(+0x14c) plus the moving fleet's own int16 cost is tested against. NGts == 0 on every player of every corpus save, so waypoint kinds 4 and 5 have never been produced",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "NodeRoute_off_PathIndex",
|
||||
"offset": "0x00000004",
|
||||
"convention": "offset",
|
||||
"prototype": "int nrp -- the node line's path index inside a NodeRoute {vptr, nrp@+4, nrf@+8, nrt@+0xc}, sizeof 0x10. Constructed to -1 by NodeRoute_Construct 0x006e1b20; ClassifyLeg leaves -1 for a freshly bored line or a point endpoint and writes FindNodeLine's entry->+0x8 otherwise. Copied to Waypoint+0x10 by FlightPlan_Waypoint_Set 0x007006e0",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "NodeRoute_off_FromId",
|
||||
"offset": "0x00000008",
|
||||
"convention": "offset",
|
||||
"prototype": "int nrf -- the leg ORIGIN object's network id (origin->+0x4), written only when the leg's kind is 3. Copied to Waypoint+0x14. Chains across a multi-hop plan: nrf[0] == FlightPlan.pnd and nrf[i+1] == nrt[i], checked green on 46 of 46 corpus flight plans",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "NodeRoute_off_ToId",
|
||||
"offset": "0x0000000c",
|
||||
"convention": "offset",
|
||||
"prototype": "int nrt -- the leg DESTINATION object's network id (dest->+0x4), written only when the leg's kind is 3. Copied to Waypoint+0x18, and equal to that waypoint's own Wpt(+0x4) on all 58 corpus waypoints",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "NodeRoute_sizeof",
|
||||
"offset": "0x00000010",
|
||||
"convention": "offset",
|
||||
"prototype": "sizeof(NodeRoute) -- {vptr, int nrp, int nrf, int nrt}. Pinned by PathSolver's routesOut stride (add esi,0x10 at 0x007068b2) and by the Waypoint layout {vptr, Wpt@+4, Tp@+8, NodeRoute@+0xc} summing to the 0x1c stride",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/path-solver.md" },
|
||||
|
||||
{ "name": "Galaxy_vft_off_GetNodeGraph",
|
||||
"offset": "0x00000004",
|
||||
"convention": "offset",
|
||||
"prototype": "byte offset of vtable slot 1 on the object at StarFleet+0x10 (the 'galaxy'). ClassifyLeg's node-route branch obtains the node-line graph with `(fleet->galaxy(+0x10))->vft[1]()` at 0x00703a14 -- a thiscall through slot 1, taking no stack arguments and returning the NodeGraph* that NodeGraph_FindNodeLine 0x006e4eb0 is called on. Lane P2 left this indirect edge unresolved by static reading; lane DN resolves it dynamically instead, by making the same call from the hook",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/nav-classifier-live.md" }
|
||||
|
||||
] }
|
||||
Loading…
Add table
Reference in a new issue