# Compaction Handoff

Current date/time context: 2026-06-27, local timezone America/Chicago.

## 2026-06-27 Routing Literature And Engine Research

- Added `docs/gps-routing-research.md` as the durable summary of the GPS
  routing research pass.
- Main conclusion: stop treating "prefer highways" as the objective. It is a
  useful proxy, especially for AutoDrive, but manual player GPS should optimize
  momentum/corridor quality: low turn count, low short-segment churn, good
  continuity, fewer awkward ramps/intersections, and later slope/airtime risk.
- Production engines split edge cost from transition/turn cost. Mirror that at
  the native solver relaxation surface instead of using another broad highway
  multiplier. Prefer nonnegative additive penalties first so the vanilla
  A*/Dijkstra-like assumptions stay sane.
- The user explicitly allows external dependencies if they are justified.
  Recommendation after research: do not link a full production engine yet.
  Borrow OSRM/Valhalla/GraphHopper cost-model ideas now, and consider
  RoutingKit or a tiny custom A* runner offline for graph/cost experiments.
- Full in-game engine replacement remains feasible but expensive. It requires
  extracting/owning the full drivable graph, mapping live start/target points,
  computing the path, and converting the result back into Cyberpunk route
  handles/records. Revisit only if native relaxation cannot support the desired
  model.

## 2026-06-27 Momentum/Corridor Direction

- The user reframed the target objective: player GPS should optimize for
  keeping speed and momentum, not for legal driving or traffic-sim behavior.
  The player often ignores speed limits, stop signs, and normal traffic flow;
  in a fast car, any long clean corridor can be a high-speed corridor.
- Highway preference is still a useful proxy, and likely valuable for
  AutoDrive, but it is not the true goal for manual player routing. Short
  highway hops can lose time on ramps, deceleration, awkward merges, and
  airtime. Some side streets, especially long straight corridors around
  Heywood, can be better than nearby highways.
- Real bad-route signatures to target:
  stairstepping, repeated 90-degree turns, stop-start intersection chains,
  short-segment churn, unnecessary ramp/on-off transitions, high-conflict
  traffic areas, and airborne or steep offramp/hill segments where the car loses
  acceleration.
- This means the next model should be a momentum/corridor cost model, not a
  pure road-class model:
  distance plus turn penalties, intersection/conflict penalties, short-edge
  churn penalties, ramp transition penalties, grade/airtime risk penalties,
  with bonuses for long straight corridor continuity and modest
  highway/arterial continuity.
- The proven inline patch at `0x40bb98` only sees the current node flags. It can
  prove and tune `Highway` preference, but it cannot distinguish staying
  straight through a corridor from diving through a ramp, hairpin, or
  intersection chain. That limitation explains why highway-only tuning has a
  narrow useful range and can become pathological.
- Next RE target should be the edge expansion/relaxation cluster, especially
  `0x40b304`, `0x40b540`, and `0x40ba58`, where the solver has current and
  neighbor handles and assembles the tentative `g` cost. Identify live registers
  and struct fields for current state, neighbor state, positions, predecessor or
  inbound direction, node flags, and degree/intersection hints.
- Feasibility tiers:
  - Proven/easy: road-class multiplier at `0x40bb98`.
  - Very feasible next: additive penalties at edge relaxation for short edges,
    intersections, highway/non-highway churn, and slope if endpoint positions
    are visible.
  - Biggest likely win: turn-angle or corridor-continuity penalties if
    predecessor/current/neighbor positions or equivalent direction vectors are
    accessible.
  - Possible but harder: airtime/grade risk based on vertical deltas and road
    curvature.
  - Probably not worth first: true velocity/acceleration state in A*/Dijkstra,
    because that expands solver state and risks breaking assumptions.
- Keep the traffic-sim/spatial road-class work as supplemental data. It can
  still label highway, arterial, road, pavement, and unknown areas, but it
  should feed a broader cost function rather than act as the whole objective.
- Suggested prototype order after compaction:
  1. Static analyze the edge relaxation cluster around `0x40b304` and
     `0x40ba58`.
  2. Build a low-noise trace or inline probe around route-windowed calls only;
     do not reintroduce pathological hot-loop logging.
  3. First patch nonnegative additive costs only, so vanilla A*/Dijkstra
     ordering remains sane.
  4. Start with short-edge and turn/continuity penalties, then add ramp churn
     and slope/airtime penalties if the required fields are available.
  5. Validate on the fixed El Coyote city-center set plus long cross-town
     routes: Violence, No-Tell Motel, The Gig/Cassius Rider, Aldecaldo/Badlands,
     and the unfinished-highway cyberpsycho route. Also verify AutoDrive is not
     accidentally given a different cost surface from manual driving.

## 2026-06-27 Tuning Status And Safe Installed State

- The mod is disabled on disk for the next game launch. Active
  `EdgeWeightGPS.dll` was renamed to
  `EdgeWeightGPS.live-tunable-92287bd.dll.disabled` in the Cyberpunk RED4ext
  plugin directory. `solver_weights.bin` was reset to vanilla `1.0`.
- This does not unload the DLL from a currently running game process; it only
  guarantees the next cold start has no active EdgeWeightGPS plugin.
- Latest committed live-tunable solver patch before this note:
  `92287bd Add live-tunable GPS solver highway weight`.
- Live tuning results from the El Coyote / city-center route set:
  - `0.80`: plugin loaded and patched, but route handles were identical to
    vanilla for side job and Claire; Sinnerman/custom changed only packed
    metadata on the same handles.
  - `0.70`: still effectively identical at the handle level. Side and Claire
    matched vanilla/0.80; Sinnerman and custom were metadata-only changes.
  - `0.55`: visibly crossed the threshold. User reported the custom pin did the
    funky city-center hairpin again. Archived log shows post-reload route
    records changing into 37-49 record alternatives, not just metadata tweaks.
- Interpretation: the useful tuning threshold for this start/target set is
  between `0.70` and `0.55`, but `0.55` is already too aggressive for at least
  the custom pin. Next candidate should be around `0.62` or `0.65`, then test a
  wider set of long routes before choosing a default.
- New evidence logs:
  `logs/EdgeWeightGPS_solver080_3548.log`,
  `logs/EdgeWeightGPS_solver070_3900.log`,
  `logs/EdgeWeightGPS_solver055_hairpin_4156.log`.

## 2026-06-26 Confirmed Solver-Cost Patch

- We have unequivocally found the player GPS route-cost lever.
- The crashing RED4ext detour on `0x40bb40` has been replaced by an inline
  patch at the road-tail block `0x40bb98`. The patch preserves vanilla
  `node+0x93` behavior, checks `node+0x88 & 0x4000` for `Highway`, and then
  multiplies highway lane cost by a tunable float.
- This inline patch avoids the register-clobber crash from the C++ detour.
  Static disassembly showed the optimized caller reusing volatile state after
  calling the tiny vanilla multiplier helper.
- The proof build used highway multiplier `0.35` and visibly changed player
  map routes. It also caused over-aggressive behavior, including a bad custom
  pin hairpin around the city-center roundabout, proving the hook is real but
  that `0.35` is too low.
- The vanilla A/B build disabled only the inline multiplier patch and restored
  obviously different route choices. User confirmed the custom pin returned to
  the straight city-center roundabout route, while the `0.35` build took the
  wrong hairpin.
- The live-tunable build enables the inline patch with default highway
  multiplier `0.80` and polls
  `red4ext/plugins/EdgeWeightGPS/solver_weights.bin` every 500 ms. The file is
  exactly one little-endian float32 highway multiplier.
- The inline patch is deliberately broad across the vanilla road-tail branch:
  reaching `0x40bb98` already means vanilla selected road-style modes `0/2/4`,
  so we do not hard-gate on `solver+0xc4 == 2`. This avoids creating a manual
  drive/autodrive disparity. Pedestrian mode `1` uses a different vanilla
  branch and is not changed by this road-tail patch.
- Preset writer:
  `tools/write_solver_weights.py`. Preset binaries live in
  `presets/solver_weights/`: `vanilla`, `mild`, `default`, `strong`,
  `proof-highway-cheap`, and `highway-expensive`.
- Evidence logs:
  `logs/EdgeWeightGPS_inline_highway035_changed_routes_1449.log` and
  `logs/EdgeWeightGPS_vanilla_ab_2120.log`.

## 2026-06-26 Static Update

- Installed RED4ext state is safe: the game plugin directory contains
  `EdgeWeightGPS.dll.disabled`, not an active `EdgeWeightGPS.dll`.
- Latest committed source before this handoff update:
  `7c2ef33 Hook full GPS solver node weights`.
- Current source has `kEnableGpsTrafficEdgeWeightPatch = false` and
  `kEnableGpsLocalSearchHooks = false`.
- The real route search is confirmed at `0x44f054`, called from
  `0x44cc7c`. It is an A*/Dijkstra-style loop: validate packed handles, push
  start state, pop best open-list state, traverse adjacent handles, call
  provider filter slot `+0x08`, call provider edge-cost slot `+0x10`, update
  predecessor/cost, then materialize handles through `0x44eb68`.
- Both base and filtered cost-provider vtables point slot `+0x10` at
  `0x44f838`. The provider vtable RVAs are still `0x2ae6120` (base) and
  `0x2ae60f8` (filtered).
- `0x44f838` computes geometric edge distance and multiplies by
  `provider[0x08 + ((neighborPoint[0x13] & 0x3f) * 4)]`.
- Base provider constructor `0x451158` fills all 64 class multipliers with
  `1.0`. Filtered provider constructor `0x44b9b4` changes the vtable and
  `0x44bda4` copies route masks/special zones into the provider, but ordinary
  GPS route edge cost is still effectively geometric distance.
- `0x44f7bc` is the heuristic helper, not edge cost. It returns distance to the
  target scaled by the constant at `0x1431ef3a0` unless the special route-shape
  helper supplies an adjusted distance.
- `0x44eb68` only walks predecessor links and writes packed handles. The richer
  final route records are produced afterward by `0x44cc7c`/query result code.
- The packed handles resolve into `VAND` navigation blobs from
  `base\worlds\03_night_city\_compiled\default\navigation_*.streamingsector`,
  not directly into `all.traffic_persistent`. Final route records can still be
  joined to `worldTrafficLanePersistent.nodeRefHash`, but that is downstream of
  the search.
- Added `tools/analyze_vand_navigation.py` to decode VAND blobs from extracted
  streamingsector JSON. On the three extracted samples:
  `files=3 blobs=300 points=5059 coords=5046`, all masks were `0x0003`, and
  dominant classes were `1`, `4`, `5`, `3`.
- Practical implication: traffic `maxSpeed` and `all.lane_connections`
  probabilities are not the edge-cost surface. The next viable patch should
  either hook `0x44f838` and classify edges spatially against precomputed
  highway/arterial corridors, or patch the navigation VAND graph/classes after
  extracting the relevant streamingsector resources.

## 2026-06-26 Spatial Edge-Cost Prototype

- Added `tools/generate_spatial_edge_grid.py`, which combines
  `work/raw-segment-json/all.traffic_persistent.json` lane flags with
  `work/traffic-companions/json/all.lane_polygons.json` polygons.
- Generated
  `red4ext/EdgeWeightGPS/src/GeneratedSpatialRoadGrid.hpp`. The grid is
  332 x 361 cells at 32 m/cell, origin `(-4640, -7392)`, with final cell
  counts `H=7574`, `R=19617`, `G=20`, `P=996`, `unknown=91645`.
- Added disabled-by-default runtime plumbing behind
  `kEnableGpsSpatialEdgeWeightPatch = false`. When enabled, it attaches only
  `GPSRouteProducer 0x44cc7c` and `GPSEdgeCost 0x44f838` unless the old verbose
  local-search probe is also enabled.
- The patch reads search-state vectors as `(world X, height, world Y)`, samples
  the current endpoint, neighbor endpoint, and midpoint, chooses the strongest
  spatial road class, then applies a multiplier after native `0x44f838`
  returns. Current test multipliers are highway `0.62`, road `1.0`,
  GPS-only `1.20`, pavement `1.35`, unknown `1.05`.
- The patch intentionally does not rewrite VAND point class bits. Static
  materialization code branches on those bits later, so changing them in data is
  riskier than adjusting cost at the edge-cost vtable slot.
- Fixed `DetourGpsEdgeCost` logging: native `0x44f838` indexes the provider
  cost table by neighbor point class, not source point class.
- Build verification passed in the Fedora toolbox:
  `toolbox run -c 2077 tools/build_red4ext_shim.sh`.
- The resulting DLL remains uninstalled. Game install still has only
  `EdgeWeightGPS.dll.disabled`, not an active plugin DLL.
- Follow-up live-tuning support adds
  `red4ext/plugins/EdgeWeightGPS/spatial_weights.bin`: five little-endian
  float32 values in highway, road, GPSOnly, pavement, unknown order. The plugin
  polls this file on route-producer entry, so changing it and replotting a route
  applies new weights without rebuild/relaunch.
- `tools/write_spatial_weights.py` can write the active file or generate
  presets. Generated presets live in `work/spatial-weights/`:
  `vanilla.bin`, `current.bin`, `highway-free.bin`,
  `highway-expensive.bin`, and `surface-extreme.bin`.

Important caveat:

- Runtime logs show long visible GPS routes are decomposed into many
  `0x44cc7c` local searches. The `ret_rva=0x70aa22` caller with
  `query_f78=10` and `query_ptr98_rva=0x8d76c4` mostly produces tiny connector
  searches, not the whole route. Do not gate the spatial patch only on that
  caller unless a later test proves it covers the desired route segment.
- Live inversion test log:
  `logs/EdgeWeightGPS_spatial_inversion_no_route_change_1845.log`.
  The plugin reloaded highway-free weights at `+74681ms`
  (`H=0.05`, all others `19.0`), then highway-expensive weights at `+235135ms`
  (`H=19.0`, all others `0.05`). The four repeated map routes after the flip
  produced final `GPSQueryResultFetch 0x7094b8` route records that were exactly
  identical to the pre-flip route records:
  route 3 vs 7, 4 vs 8, 5 vs 9, and 6 vs 10 all had identical lane handles and
  packed metadata.
- Practical conclusion: the spatial `0x44f838` detour is active and changes
  many local route-producer edge costs, but it does not change the final
  selected world-map route records for the tested player GPS routes. Treat this
  hook as a local-search/connector lever, not the decisive full-route weighting
  surface.

## Current Installed State

- Active RED4ext test plugin is installed:
  `Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.dll`.
- Older DLLs are preserved disabled as:
  `Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.dll.disabled`
  and `EdgeWeightGPS.dd1423b.dll.disabled`.
- Source defaults enable the inline road-tail patch:
  `kEnableGpsNodeMultiplierInlinePatch = true`.
- Source defaults do not enable the old full-function detour:
  `kEnableGpsMultiplierHooks = false`.
- Current behavior patch is the inline `0x40bb98` code-cave patch. It preserves
  vanilla road cost except for highway lanes, whose cost is multiplied by the
  current `solver_weights.bin` float. Installed default is `0.80`.
- `solver_weights.bin` lives beside the DLL and is hot-polled every 500 ms.
- The plugin log was truncated immediately before installing this build.
- Latest archived test log:
  `logs/EdgeWeightGPS_vanilla_ab_2120.log`.
- A cold restart is required after installing a new DLL; changing
  `solver_weights.bin` after that is hot-reloaded.

## 2026-06-26 Full Async Solver Findings

- Static route pipeline:
  `0x8d20d4` map route submit -> `0x70a42c` query manager ->
  `0x70a570` dispatch -> `0x8d17d8`/`0x8d1880` async enqueue.
- Worker loop around `0x126c06` consumes queued route requests from
  `routeSystem+0x47c0`.
- Worker calls either `0x818928` directly or calls `0x883cd8` first, then
  `0x818928`. Successful results pass through `0x11f5f60`.
- `0x883cd8` resolves/projects start and end candidates with helpers including
  `0x198fb8`, `0x886958`, and `0x197b04`, then calls `0x1caa7a0`.
- `0x1caa7a0` initializes the full async traffic solver: it sets
  `solver+0x10 = 1`, stores route mode at `solver+0xc4`, sets route masks at
  `solver+0xc0/+0xc2`, copies candidate arrays into `solver+0x70/+0x80`, and
  calls `0x88367c`.
- `0x88367c` marks start candidate lane/node records with `node+0x9a |= 1`,
  marks end candidates with `node+0x9a |= 2`, computes initial costs at
  node-state `+0x10/+0x14`, and queues nodes into open list `solver+0xd0`.
- `0x818928` calls `0x8189f0`; when complete, `0x818ba8` builds the output
  route records.
- `0x8189f0` advances with `0x40ade4`, then materializes output with
  `0x818ba8`.
- `0x40ade4` is the true full-route solver advance loop. It pops items from
  `solver+0xd0` while `solver+0xdc != 0`, calls expansion helpers including
  `0x40b304` and `0x40b540`, then marks state byte `+0x22 = 2`.
- `0x40b304` and `0x40b540` expand neighbor lists from lane/node records at
  `solver+0xf8 + index*0xa0`.
- Neighbor admissibility is `0x40b774`, which checks flags at `node+0x88`:
  pass iff `(solver+0xc0 & flags88) != 0` and
  `(solver+0xc2 & flags88) == 0`.
- Main cost formula in `0x40b304`:
  `g = current.g + delta * 0x40bb40(solver,node) * directionMultiplier`;
  heuristic is straight-line distance to the solver target position; `f = g+h`.
- `0x40ba58` is the relax/update helper. It writes candidate `f` to node-state
  `+0x14`, `g` to `+0x10`, progress to `+0x18`, predecessor state to `+0x1e`,
  marks state byte `+0x22 = 1`, and pushes to open list `solver+0xd0`.
- Vanilla `0x40bb40` is the key full-solver per-node multiplier. For driving
  modes it treats `Road` (`0x0010`) as `1.0` or `1.2`, with an extra `1.1`
  multiplier if byte `node+0x93 == 1`. It does not special-case `Highway`
  (`0x4000`).
- Vanilla non-road fallback uses the float table at `0x3154d28`:
  `10, 6, 2, 0, 4, 2.5, 1.5, 0`.
- Vanilla `0x40bb00` auxiliary multiplier returns `1.1` for most road cases,
  or uses the table at `0x3154d38` (`4, 2.5, 1.5, 0`) for one mode-2/retry
  branch.
- Practical inference: the full solver already has a real edge-cost surface,
  but vanilla highways are merely roads. The installed build patches that exact
  multiplier instead of the earlier local VAND search callback.

## New Toolbox Tools

The Fedora toolbox `2077` now has:

- `rizin` 0.7.4
- `python3-capstone` 5.0.5
- `python3-pefile` 2024.8.26

If adding Python-only tooling, create/use a venv and maintain `requirements.txt`
instead of one-off pip installs.

## Active Probe Configuration

In `red4ext/EdgeWeightGPS/src/Main.cpp`:

- `kEnableGpsMultiplierHooks = true`
- `kEnableGpsProviderClassPatch = false`
- `kEnableGpsProviderClassPatchTrace = false`
- `kEnableGpsTraceHooks = true`
- `kEnableGpsQueryLifecycleHooks = false`
- `kEnableGpsRouteJobLifecycleHooks = false`
- `kEnableGpsRouteJobStartTrace = false`
- `kEnableGpsQueryResultTraceHooks = true`
- `kEnableGpsSpatialEdgeWeightPatch = false`
- `kEnableGpsLocalSearchHooks = false`
- `kEnableRouteUiTraceHooks = false`

If rebuilt with the current defaults, active hooks are focused map-route submit,
final route-result fetch tracing, and the full solver multiplier hook at
`0x40bb00`/`0x40bb40`. The spatial edge-cost patch requires
`kEnableGpsSpatialEdgeWeightPatch = true`. The full local search/cost hooks below require
`kEnableGpsLocalSearchHooks = true`:

- query lifecycle:
  - `GPSQuerySubmit 0x70a42c`
  - `GPSQueryDispatch 0x70a570`
  - `GPSQueryResultFetch 0x7094b8`
  - `GPSQueryStatus 0xaa5704`
- `GPSSearch 0x44f054`
- `GPSRouteProducer 0x44cc7c`
- `GPSResolveHandle 0x44e1a8`
- `GPSEdgeCost 0x44f838`
- filtered provider filter `0x44ff68`
- base provider filter `0x450b08`
- full solver auxiliary multiplier `0x40bb00`
- full solver node multiplier `0x40bb40`

Async route-job hooks (`0x8d17d8`, `0x126b28`, `0x883cd8`,
`0x11f5f60`, `0x818ba8`) are split behind
`kEnableGpsRouteJobLifecycleHooks` and currently disabled to keep the next
run focused on solved route result records plus local search/cost stats.

The filter probe records pass/fail totals by GPS point class and by bits in
`point + 0x10`, tested against provider masks at `+0x108` and `+0x10a`.

## Why This Probe Exists

Data and behavior patches tested so far:

- Traffic `maxSpeed` patches affected NPC freeway speed but not GPS route
  choice.
- Lane connection probability patches did not change GPS route choice.
- Global cost-table patch produced weird routing but mostly confirmed we were
  near the solver, not a clean highway preference knob.
- Provider class override patches fired correctly but produced no perceptible
  route change after preserving vanilla class `14`.

Static disassembly clarified the provider split:

- Provider vtable slot `+0x08` is an admissibility filter, not cost.
- Provider vtable slot `+0x10` is geometric edge cost, multiplying distance by
  `provider[0x08 + class * 4]`.
- We patched only slot `+0x10` inputs. If slot `+0x08` filtering dominates the
  candidate graph, those cost changes can fire and still not visibly steer GPS.

## Important Native Findings

GPS pipeline landmarks:

- shared query submit: `0x70a42c`
- query dispatch: `0x70a570`
- route producer: `0x44cc7c`
- local search loop: `0x44f054`
- packed-handle resolver: `0x44e1a8`
- edge cost: `0x44f838`
- query result fetch: `0x7094b8`

The search loop is A*/Dijkstra-like:

- initialize/open list with start node
- pop best node
- check destination handle
- traverse adjacent lane handles
- call provider filter
- call provider cost
- update candidate cost/best predecessor

Provider details:

- Base provider vtable RVA: `0x2ae6120`
- Filtered provider vtable RVA: `0x2ae60f8`
- Base provider constructor `0x451158` fills 64 class multipliers with `1.0`
  and sets mask `+0x108` to `0xffff`.
- Filtered provider constructor `0x44b9b4` changes vtable to `0x2ae60f8`.
- Filtered provider commonly has class `14 = 0.05`; preserve this unless
  deliberately testing it.

Route result records:

- `0x7094b8` output records are 0x28-byte route segment records.
- `h00` in those records matches `worldTrafficLanePersistent.nodeRefHash`.
- Route records can be joined to `work/raw-segment-json/all.traffic_persistent.json`.
- The low byte of route-record `u08` is not the same as the search point class.
- The log's human wall timestamp has a millisecond rollover bug around second
  boundaries. Use the `+Nms` elapsed field for ordering and durations.

Packed-handle resolver:

- `0x44e1a8` decodes the packed route handle used by the search/materialization
  side:
  - low 20 bits: point index
  - bits 20..47: segment index
  - high bits: generation/check value
- It resolves the handle into a 0x68-byte segment record and a 0x14-byte point
  record. The point record's `+0x13 & 0x3f` is the search-side class consumed by
  `0x44f838`.
- Live logging corrected the selected post-call return RVAs to:
  - `0x44cb9e`: 0x38 intermediate-record builder. It writes packed handle,
    `point+0x10` mask, `point+0x13` class, and coordinates from
    `segment+0x18`.
  - `0x44dd97`: geometry/materialization path. It walks the `segment+0x28`,
    `segment+0x30`, and `segment+0x38` arrays for the resolved point.
- The current probe opens a five-second resolver trace window before the
  original `0x70a42c` submitter runs when the caller is `0x8d20d4` and
  `query_fcc=5`. Inside that window it logs selected resolver callsites with
  per-return-site caps instead of spending the whole budget on startup noise.
- Each resolver line includes the decoded handle, segment/point pointers,
  segment pointer fields `+0x08` through `+0x38`, raw segment bytes, raw point
  bytes, raw `segment+0x08` metadata, the `segment+0x28` point-index record,
  and small raw dumps from `segment+0x20`, `+0x30`, and `+0x38`. The goal is to
  determine whether the selected-path segment still carries, or points to, the
  traffic lane hash/resource flags before the final 0x28 route records are
  emitted.

## Latest 23:08 Test Result

The user clicked:

- 23:08 continue on main menu
- 23:30 space to continue
- 23:46 map open
- 23:56 side job
- 24:06 Sinnerman
- 24:14 Claire
- 24:21 custom
- 24:30 Aldecaldo camp
- 24:47 far gig east
- 25:03 Violence
- 25:10 cyberpsycho unfinished highway

Use:

```bash
python3 tools/summarize_gps_route_records.py \
  --traffic-json work/raw-segment-json/all.traffic_persistent.json \
  logs/EdgeWeightGPS_query_route_probe_2308.log
```

Important result table:

- qid 1: startup/minimap route, 18 records, 50 ms, 1216.5 pavement length.
- qid 2: side job, 12 records, 32 ms, 578.6 pavement length.
- qid 3: Sinnerman, 23 records, 45 ms, 897.4 pavement, 160.4 highway,
  89.8 road.
- qid 4: Claire, 18 records, 34 ms, 1216.5 pavement.
- qid 5: custom, 17 records, 44 ms, 1006.3 pavement, 241.0 highway,
  38.0 road.
- qid 6: Aldecaldo camp, 80 records, 325 ms, 5853.5 road, 1492.9 highway,
  1111.5 pavement, 24.6 gpsonly.
- qid 7: far east gig, 81 records, 144 ms, 5086.2 road, 1523.0 highway,
  1111.5 pavement, 24.6 gpsonly.
- qid 8: Violence, 55 records, 97 ms, 1991.5 pavement, 358.5 highway,
  86.1 road.
- qid 9: cyberpsycho unfinished highway, 58 records, 147 ms, 2657.4 pavement,
  519.0 highway, 113.2 road, 56.7 gpsonly.

All 362 route-record handles matched traffic lane hashes in the extracted
resource.

Local search/provider aggregate from this run:

- 159 searches, 4036 provider filter calls, only 3 filter failures.
- Filter pass classes: `3:3114`, `5:451`, `15:310`, `1:116`, `4:42`.
- Edge source classes: `3:3244`, `5:487`, `15:318`, `1:117`, `4:58`.
- Edge return RVAs: `0x44f457:4033`, `0x44f629:151`,
  `0x8d74d3:30`, `0x8d76b0:10`.

Inference:

- Provider vtable slot `+0x08` filtering is almost not pruning these route
  searches, so it is not the dominant reason class-cost patches failed to
  visibly steer GPS.
- Search-side edge classes are only `{1,3,4,5,15}` in this run. Final emitted
  route-record classes include `{0,1,2,3,4,5,7,8,9}` and class 7/8/9 are
  highway-heavy. Therefore final route-record `u08` low byte and search point
  `point+0x13` class are different domains.

## Latest 46:50 Resolver Test Result

Archived log:
`logs/EdgeWeightGPS_resolve_handle_probe_4650.log`.

User timing:

- 46:50 Continue on main menu
- 47:23 Space to continue
- 47:35 map open
- 47:43 side job
- 47:51 Sinnerman
- 47:57 Claire
- 48:09 custom
- 48:2x Aldecaldo camp
- 48:57 game cold / `OnExit`

Route summary:

- routes=6, records=170, matched route records to lane hashes=170/170.
- qid 1: startup/minimap, 18 records, 43 ms, 1216.5 pavement.
- qid 2: side job, 12 records, 34 ms, 578.6 pavement.
- qid 3: Sinnerman, 23 records, 45 ms, 897.4 pavement, 160.4 highway,
  89.8 road.
- qid 4: Claire, 18 records, 35 ms, 1216.5 pavement.
- qid 5: custom, 19 records, 45 ms, 1006.3 pavement, 343.4 highway,
  38.0 road.
- qid 6: Aldecaldo, 80 records, 340 ms, 5853.5 road, 1492.9 highway,
  1111.5 pavement, 24.6 gpsonly.

Resolver sample findings:

- The first resolver probe captured only the startup guard window because the
  guessed return RVAs were post-call-off by a few bytes.
- It still proved `GPSResolveHandle 0x44e1a8` attached successfully.
- Actual resolver returns in the first 64 logged samples:
  `0x44dd97:57`, `0x44cb9e:7`.
- Resolver point classes in those samples: `4:60`, `5:4`.
- The 0x68 segment header contains generation/counts/pointers; final
  `nodeRefHash` values are not inline in those 0x68 bytes.
- Static disassembly shows `segment+0x10` is the 20-byte point array,
  `segment+0x18` is the coordinate array, and the materializer indexes
  `segment+0x28` by `pointIndex*12`.

## Next Work

- Pivot away from the spatial `0x44f838` cost detour for world-map routing.
  It is proven active but does not alter final selected route records under
  extreme inverse weights.
- Statically disassemble the caller chain around the full map-route submit and
  result path: `0x8d20d4`, `0x70a42c`, `0x70a570`, `0x7094b8`, `0x520783`,
  and the async route-service cluster around `0x8d17d8` through `0x8d49xx`.
- Identify where the final 0x28 route records are selected or copied before
  `0x7094b8` returns them. The current evidence says that is closer to the real
  full-route weighting/selection surface than the local `0x44cc7c` searches.
- Avoid post-`0x7094b8` rewriting unless every upstream option fails; that would
  likely only change displayed path records and risks desynchronizing route
  instructions from the actual planner.

## Useful Commands

Build/install:

```bash
toolbox run -c 2077 ./tools/build_red4ext_shim.sh
toolbox run -c 2077 ./tools/install_red4ext_shim.sh
```

Clear plugin log:

```bash
truncate -s 0 '/var/home/salt/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.log'
```

Inspect current plugin log:

```bash
tail -n 240 '/var/home/salt/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.log'
```

Disassemble with rizin in toolbox:

```bash
toolbox run -c 2077 rizin -q -A '/var/home/salt/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/Cyberpunk 2077/bin/x64/Cyberpunk2077.exe'
```

## Caution

- Do not revert user changes or old untracked logs unless explicitly asked.
- Commit early/often.
- Do not install Python packages one by one with pip; use `requirements.txt`.
