Move reverse engineering artifacts under contrib

This commit is contained in:
2026-06-27 04:43:40 -05:00
parent 2fdc92b8ef
commit 8ee791ad0c
102 changed files with 34224 additions and 67 deletions
+752
View File
@@ -0,0 +1,752 @@
# Compaction Handoff
Current date/time context: 2026-06-27, local timezone America/Chicago.
## 2026-06-27 Momentum Default Chosen
- After live A/B testing and two drive validations, `80.0` is the working
default for `momentum_weights.bin`.
- Source fallback was changed to
`kGpsMomentumFixedEdgePenaltyDefault = 80.0f`.
- `../tools/write_momentum_weights.py default` now writes `80.0`.
The old `8.0` value is preserved as `legacy-default`.
- Current preset meanings:
`vanilla=0.0`, `mild=4.0`, `legacy-default=8.0`, `strong=16.0`,
`default=80.0`, `silly=80.0`, `overstrong=160.0`,
`pathological=250.0`.
- `160.0` was tested and is too high: The Damned can fall back to a straight
line, and local access can develop bad hairpins/backtracking.
- Drive validation at `80.0`:
The Damned/Dogtown route drove cleanly at high speed, live minimap
recalculation worked after a detour, and the Claire route drove cleanly aside
from a driver-unfamiliarity ramp/hill surprise.
- Updated detailed notes in `./gps-momentum-field-tests.md`.
- The rebuilt source-default-80 DLL was installed while the game was cold at
about `04:17` local. Installed defaults were rewritten:
`momentum_weights.bin = 80.0`, `solver_weights.bin = 1.0`.
- Added release packaging:
`toolbox run -c 2077 ./tools/package_red4ext_mod.sh`.
Verified package:
`dist/EdgeWeightGPS-red4ext-20260627-041942.zip`, containing
`red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.dll`,
`momentum_weights.bin = 80.0`, and `solver_weights.bin = 1.0`.
## 2026-06-27 Live Momentum Knob Installed
- Installed a new telemetry/control DLL at about `03:21` local while the game was
cold. Installed size: `16899519` bytes.
- Active installed config:
`solver_weights.bin = 1.0` and `momentum_weights.bin = 8.0`.
- The plugin log beside the DLL was truncated to zero after install:
`red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.log`.
- `momentum_weights.bin` is one little-endian `float32`, hot-polled every
500 ms. `0.0` disables the fixed ordinary-edge penalty; `8.0` is the current
prototype; larger values are stress tests. Presets are in
`../presets/momentum/` and can be written live with
`python3 ../tools/write_momentum_weights.py <preset>`.
- New presets:
`vanilla=0.0`, `mild=4.0`, `default=8.0`, `strong=16.0`,
`silly=80.0`, `pathological=250.0`.
- The `gps-momentum-summary` line now waits for a nonzero result route-record
count before marking a query as summarized. It should report
`gpsResultRouteCount` directly instead of showing `0` from the staging fetch.
- Build verification passed:
`toolbox run -c 2077 ./tools/build_red4ext_shim.sh`.
- Next test should start with current `momentum=8.0`, then while the game is
open switch to `vanilla` and `silly` on one or two fixed routes to confirm
live A/B behavior. Good candidates from the last run: The Damned/Dogtown
pathological route and the SW Badlands fuel station route.
## 2026-06-27 Momentum Telemetry Test Installed
- Installed telemetry DLL at about `03:04` local while the game was cold.
Installed size: `16792931` bytes.
- Source flags for the installed test:
`kEnableGpsNodeMultiplierInlinePatch = true`,
`kEnableGpsMomentumPenaltyInlinePatch = true`,
`kEnableGpsQueryResultTraceHooks = true`, and
`kEnableGpsQueryResultRecordDump = false`.
- `solver_weights.bin` is vanilla `1.0`, so the highway multiplier hook should
be neutral. The behavioral change remains the fixed ordinary-edge additive
penalty only: `kGpsMomentumFixedEdgePenalty = 8.0f`.
- New telemetry is a compact `gps-momentum-summary` line emitted once per
completed tracked map-route query. It logs `callsA`, `callsB`, `callsTotal`,
`penaltyTotal`, global call counters, plus light route/result counts. The
counters are incremented directly in the two patched ordinary expansion caves.
- The raw route-record dump is disabled for this run to keep logs readable.
- The installed RED4ext log was truncated to zero before handing the game back.
- Next test routine: launch, load the fixed car save, open the map, plot the
usual sequence with a few seconds between each: side job, Sinnerman, Claire,
custom pin. More destinations are useful only after verifying the summary
lines are present.
## 2026-06-27 Momentum Penalty Test Installed
- The game was cold, so an enabled momentum-penalty test DLL was installed to
the Cyberpunk RED4ext plugin directory.
- Source flag for the installed test:
`kEnableGpsMomentumPenaltyInlinePatch = true`.
- The old highway multiplier hook is still compiled in, but
`solver_weights.bin` was written to `vanilla` (`1.0`) so highway preference is
neutral for this A/B. The active behavioral change should be only the fixed
ordinary-edge additive penalty.
- Active fixed penalty:
`kGpsMomentumFixedEdgePenalty = 8.0f`.
- Expected log lines on launch:
`momentum fixed-edge penalty active value=8 sites=ordinary-expansion` plus
`gps-momentum-penalty-inline-patch applied site=expand-list-a` and
`site=expand-list-b`.
- First test routine should be the fixed car save and usual route sequence:
side job, Sinnerman, Claire, custom pin. If stable, also test Violence,
The Gig/Cassius Rider, Aldecaldo/Badlands, and unfinished-highway
cyberpsycho route.
## 2026-06-27 Disabled Momentum Penalty Prototype
- Static disassembly of the full async solver relaxation cluster is updated.
`0x40ba58` has exactly four direct callers:
`0x40b175`, `0x40b4ac`, `0x40b6d5`, and `0x40bca7`.
- The ordinary adjacency walkers are still the clean first patch surface:
`0x40b304` reaches relax at `0x40b4ac`, and `0x40b540` reaches relax at
`0x40b6d5`. Both compute vanilla tentative `g` in `xmm6`, copy it to
`xmm3`, write `f = g + h` at `[rsp+0x20]`, and then call `0x40ba58`.
- Patch starts are `0x40b49a` for list A and `0x40b6c3` for list B. Each
replaces 23 bytes ending at the vanilla `0x40ba58` call and jumps to a
private executable cave. The cave reproduces the overwritten vanilla math,
adds a fixed nonnegative cost to `xmm6`, recomputes `f` from the penalized
`g`, calls vanilla `0x40ba58`, and jumps back after the overwritten block.
- Added a disabled-by-default RED4ext prototype in
`red4ext/EdgeWeightGPS/src/Main.cpp`:
`kEnableGpsMomentumPenaltyInlinePatch = false`,
`kGpsMomentumFixedEdgePenalty = 8.0f`.
- This first prototype is only a short-segment/churn lever. It adds a fixed
per-edge penalty on ordinary neighbor expansion, so routes that stair-step
through many small pieces become more expensive without making highways
cheaper. It does not yet compute turn angle, ramp transition cost, or
predecessor/current/neighbor geometry.
- The first prototype intentionally leaves the two non-ordinary relax paths
vanilla:
`0x40b175` appears to be a target/terminal candidate path, and `0x40bca7`
appears to be a special continuation/end-candidate path from `0x40bbbc`.
Patch them only after the ordinary expansion test proves stable.
- Build verification passed in toolbox:
`toolbox run -c 2077 ./tools/build_red4ext_shim.sh`.
- The prototype was not installed into the live game directory. To test later,
flip `kEnableGpsMomentumPenaltyInlinePatch` to `true`, rebuild, install only
after the game is cold, and compare against the fixed El Coyote route set.
## 2026-06-27 Routing Literature And Engine Research
- Added `./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`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 KiB

+108
View File
@@ -0,0 +1,108 @@
# GPS Momentum Field Tests
Current patch surface: fixed additive penalty on ordinary GPS solver edge
expansion, patched at the two ordinary relaxation callsites feeding
`0x40ba58`. Live tuning is through `momentum_weights.bin`.
## 2026-06-27 North Watson To Dogtown/Badlands
Starting context: player in North Watson near the motel/Cassius area. The two
main comparison routes fit in the same map viewport:
- The Damned / Dogtown, south into Pacifica.
- Fuel Station fast travel point in the southwest Badlands/Biotechnica flats.
Screenshots:
- `../logs/screenshots/the-damned-and-fuel-station-0.webp`
- `../logs/screenshots/the-damned-and-fuel-station-8.webp`
- `../logs/screenshots/the-damned-and-fuel-station-80.webp`
Telemetry log:
- `../logs/EdgeWeightGPS_live_momentum_ab_8_0_80.log`
Results:
| Route | Penalty | Expansion calls | Route records | Observed route |
| --- | ---: | ---: | ---: | --- |
| The Damned | 8 | 1,057,410 | 78 | Similar to vanilla; threads city blocks and a side highway. |
| The Damned | 0 | 1,110,243 | 87 | Similar visible corridor, slightly more route-record churn. |
| The Damned | 80 | 1,010,035 | 70 | Visibly smoother highway corridor, avoids City Center stair-stepping. |
| Fuel Station | 8 | 296,976 | 89 | Uses City Center roundabout/city threading. |
| Fuel Station | 0 | about 308k-314k | 90-92 | Similar visible corridor to 8. |
| Fuel Station | 80 | about 345k-347k | 77-78 | Converges onto the smoother highway corridor before splitting south. |
Interpretation:
- `0` and `8` are close visually in this location, despite small telemetry
changes.
- `80` is a strong proof value: it changes the coarse corridor and reduces
route-record count for the tested long routes.
- This supports the main hypothesis that a fixed per-segment additive penalty
can optimize for smoother player-driving corridors, not merely highway class.
## 2026-06-27 Broad 80 Smoke
Telemetry log:
- `../logs/EdgeWeightGPS_momentum80_broad_smoke.log`
- `../logs/EdgeWeightGPS_momentum80_drive_validation.log`
User reported no bad routes after probing:
- Claire routes differently than nearby Sinnerman, choosing a smooth Japantown
highway leg.
- Sinnerman still cuts through City Center, plausibly because that corridor is
appropriate for that destination.
- Short local routing to Cassius remains fine.
- Heywood routes remain fine and may avoid some stair-stepping.
- Routes near highways but not traffic-feasible still detour appropriately.
- One NCPD bounty used a road the user had not previously seen GPS select, and
it looked like a good fit.
Interpretation:
- No obvious local-route breakage was found at `80`.
- `80` was not immediately pathological in the sampled routes and later became
the working default after drive validation.
- Higher values still need caution; `160` later proved too high.
Drive validation at `80`:
- The Damned / Dogtown route drove cleanly in the Outlaw at sustained high
speed.
- The route surfaced a back/elevated highway through Japantown/Westbrook that
the user had not seen vanilla GPS select before.
- Live minimap recalculation after a deliberate detour still worked.
- Claire route also drove cleanly. One ramp/hill surprised the driver, but the
issue was road unfamiliarity at full speed, not bad routing.
Conclusion after the drive test: `80` is the working default.
## 2026-06-27 40/160 Bounds
Telemetry log:
- `../logs/EdgeWeightGPS_momentum40_160_tuning.log`
At `40`, the highlighted routes improved only partially:
- Claire still used the Sinnerman/City Center path instead of the smoother outer
road.
- Sinnerman remained sensible.
- The near-highway case remained acceptable.
- The NCPD bounty took the smoother outer City Center road, but retained a
Watson stair-step that `80` avoided.
At `160`, overcorrection appeared:
- Claire showed a questionable hairpin.
- The Damned produced a fallback/straight-line style route instead of a usable
road route.
- The Gig/Cassius local access got worse, asking for a backtrack and hairpin
instead of a simple nearby approach.
- NCPD bounty regressed to stair-stepping.
Conclusion: `160` is too high. The useful bracket is roughly `40 < default <
160`, with `80` currently best validated by map inspection and driving.
+435
View File
@@ -0,0 +1,435 @@
# GPS Routing Research Notes
Current date: 2026-06-27.
This note summarizes the research pass prompted by the realization that "prefer
highways" is a proxy, not the goal. The actual player-facing goal is a route
that feels fast and drivable in Cyberpunk 2077, especially when the player is
driving aggressively and ignoring normal traffic rules.
The conclusion is that the next patch should not be another broad highway
multiplier. Production route engines and routing papers treat route quality as
multi-criteria: edge traversal cost, transition/turn cost, road hierarchy,
intersection delay, and user preferences are separate signals. For manual
Cyberpunk driving, the best analogue is a momentum/corridor model: prefer
routes that preserve speed and minimize decision points, sharp turns,
short-segment churn, and awkward ramp transitions.
## Sources Reviewed
- Bast et al., "Route Planning in Transportation Networks":
https://arxiv.org/abs/1504.05140
- Jiang and Liu, "Computing the Fewest-turn Map Directions based on the
Connectivity of Natural Roads": https://arxiv.org/abs/1003.3536
- Sacharidis and Bouros, "Routing Directions: Keeping it Fast and Simple":
https://arxiv.org/abs/1309.4396
- Hlineny and Moris, "Generalized Maneuvers in Route Planning":
https://arxiv.org/abs/1107.0798
- Dibbelt, Strasser, and Wagner, "Fast Exact Shortest Path and Distance Queries
on Road Networks with Parametrized Costs": https://arxiv.org/abs/1509.03165
- OSRM profile docs:
https://github.com/Project-OSRM/osrm-backend/blob/master/docs/profiles.md
- OSRM car profile:
https://github.com/Project-OSRM/osrm-backend/blob/master/profiles/car.lua
- GraphHopper profile docs:
https://github.com/graphhopper/graphhopper/blob/master/docs/core/profiles.md
- Valhalla turn-by-turn API costing docs:
https://valhalla.github.io/valhalla/api/turn-by-turn/api-reference/
- Valhalla auto costing implementation:
https://github.com/valhalla/valhalla/blob/master/src/sif/autocost.cc
- RoutingKit README:
https://github.com/RoutingKit/RoutingKit
- OSRM license:
https://github.com/Project-OSRM/osrm-backend/blob/master/LICENSE.TXT
- Valhalla license:
https://github.com/valhalla/valhalla/blob/master/COPYING
- GraphHopper license:
https://github.com/graphhopper/graphhopper/blob/master/LICENSE.txt
## High-Level Findings
### There Is No Single "Good GPS" Objective
The route-planning survey by Bast et al. frames road routing as shortest-path
search over nonnegative edge weights, then shows that most practical complexity
comes from speed, preprocessing, traffic, and multiple criteria. It explicitly
notes there is no single best route-planning method because systems are judged
by different tradeoffs: query time, preprocessing effort, space, robustness to
changing inputs, and model quality.
For our mod, that means "shortest distance" and "prefer highways" are both too
thin. A better route is the minimum of a cost function we choose. The game
already appears to run an A*/Dijkstra-style solver with a geometric heuristic
and nonnegative edge costs. Our task is not to replace that algorithm; it is to
feed it a better cost surface.
### Production Engines Separate Edge Cost From Transition Cost
OSRM profiles define way speed/rate/weight separately from turn processing.
The docs say speed should estimate actual travel time, while `rate` or `weight`
should encode preference; changing speed to express preference skews duration
estimates. OSRM also has a `process_turn` stage that can assign turn penalties
by angle, traffic signals, obstacles, U-turns, road classes, and intersection
context.
Valhalla does the same separation. Its auto costing computes edge cost from
time, distance preference, density, surface, tolls, alley/service/track factors,
and highway preference. Transition cost then adds turn/intersection behavior:
OSRM-style turn duration, stop impact, turn type, ramp transition cost,
roundabout cost, and U-turn penalties. This is exactly the split we should
mirror: road class is only one part of the edge cost; turn and transition costs
are a separate part of the route's feel.
GraphHopper's profile model reinforces this. It exposes road prioritization,
turn costs, speed, priority, and `distance_influence` as separate tuning
surfaces. Its docs also warn that reusing preprocessed heuristic data can
require new weights to be greater than or equal to base weights to preserve
correctness. That maps to our A* concern: negative bonuses and broad discounts
can make a geometric heuristic less trustworthy. Nonnegative additive penalties
are safer than "make good edges cheaper" as a first serious patch.
### Human-Friendly Routes Minimize Turns And Decision Points
Jiang and Liu's fewest-turn paper is directly relevant even though Cyberpunk is
not normal driving. The paper argues that people often choose routes by road
continuity rather than segment-by-segment geometric distance. It highlights
fewer turns as lower cognitive burden and explicitly connects fewer turns to
fewer slow-down/speed-up events.
The important concept is "natural roads": sequences of segments joined by good
continuity. A curve along a ring road is not necessarily a "turn" in the human
navigation sense; changing from one road/corridor to another is the turn. The
paper used a 45-degree deflection threshold when generating natural roads, but
the exact threshold is less important than the idea: penalize route changes and
poor continuity, not curvature by itself.
Sacharidis and Bouros extend this into a useful tradeoff model. They define
route length and route complexity separately, with complexity as turn/road-change
cost. They then discuss near-fastest routes that are as simple as possible and
near-simplest routes that are as fast as possible. This is a good conceptual
target for us: do not minimize turns at all costs, and do not minimize distance
at all costs. Prefer a near-fast route that is much simpler and smoother.
### Maneuvers Are State-Dependent
Hlineny and Moris model maneuvers such as turn prohibitions, traffic light
delays, roundabouts, and multi-edge restrictions. Their important point for us
is that a maneuver depends on how a vertex was reached, not only the current
vertex. A plain Dijkstra state of "best cost to node" is insufficient when
cost depends on a sequence of prior edges. Their M-Dijkstra expands state to
vertex-plus-context.
We probably should not implement a full context-expanded solver inside a game
binary patch. But the paper explains why our next hook needs predecessor or
incoming-direction information. A turn penalty computed only from the current
node flags is inadequate. The local RE already found the game keeps predecessor
state at solver-state `+0x1e`, and the relax call receives current state,
neighbor state, and candidate state. That is enough for a practical one-step
turn/corridor approximation without redesigning the whole solver.
### Speedups Are Mostly Not The Problem Here
Modern routing engines use A*, bidirectional search, contraction hierarchies,
landmarks, arc flags, and related preprocessing to answer large road-network
queries quickly. This matters academically, but not as the primary mod target.
Night City's graph is small enough that the game already computes paths during
play. The player's complaint is not "the route takes too long to compute"; it is
"the route is dumb."
That said, we should respect solver assumptions. The game uses a straight-line
heuristic in the full async solver. If we add only nonnegative penalties to `g`
and to the candidate `f = g + h`, the heuristic remains conservative relative
to the original distance-like base. If we add negative bonuses or deep highway
discounts, we may make the heuristic too optimistic or otherwise distort
ordering. The proof highway multiplier worked, but the 0.55/0.35 tests already
showed how quickly discounts become pathological.
## Open-Source Routing Engines
It is viable to use open-source routing software, but there are three different
levels of "use":
- Use an engine's ideas and cost model.
- Use an engine offline to preprocess or classify Night City.
- Link or run an engine as the actual in-game route planner.
The first two are practical. The last one is possible, but much more invasive
than patching the native solver.
### Candidate Engines And Libraries
RoutingKit is the most interesting native-library candidate. It is a C++ route
planning library under a BSD-2-Clause license. Its README emphasizes
customizable contraction hierarchy support, flexible arc weights, and
millisecond-or-better queries on continental-scale data. It exposes direct
graph, index, and query APIs. If we wanted a true replacement solver inside a
RED4ext DLL, RoutingKit is the most plausible starting point.
OSRM is a high-performance C++ routing engine and service under a permissive
BSD-style license. It has excellent production lessons: profile-based cost
modeling, turn processing, road classes, and CH/MLD preprocessing. However, it
is built around its own extraction/preprocessing pipeline and OpenStreetMap-like
data. Embedding OSRM directly would likely mean generating OSRM-compatible data
from Cyberpunk resources, running preprocessing, shipping those artifacts, then
calling libosrm or a local service.
Valhalla is a C++ routing engine under the MIT license. Its costing model is
especially useful because auto routing separates edge cost from transition cost,
includes highway preference as a mild factor, and explicitly handles ramps,
U-turns, stop impact, density, alleys, service roads, surfaces, and closures.
Direct embedding has the same issue as OSRM: Valhalla expects its own graph
tiles and attribution model.
GraphHopper is Apache-2.0 and has a strong custom model/profile system, but it
is Java. It is useful as a design reference and potentially as an offline
analysis tool, but bundling a JVM or running a Java service beside Cyberpunk is
not a good first mod architecture.
Generic graph libraries such as Boost.Graph or LEMON are options if we only
need Dijkstra/A* and want to own the data model ourselves. They are lighter than
OSRM/Valhalla but do not bring road-routing domain logic; we would still write
the road/corridor cost model.
### Integration Problem
External engines need a graph they own. Cyberpunk's GPS solver operates on
internal route/node records, packed handles, and final route records. Linking an
engine does not solve either boundary.
To replace the solver, we need:
- Extract or observe the full drivable graph: directed edges, coordinates,
flags/classes, turn/intersection topology.
- Convert that graph into the engine or custom graph format.
- Map live start and target positions to graph nodes.
- Compute a route.
- Convert the chosen path back into handles/records the game expects, or hook
late enough to draw and consume a custom route.
- Keep manual GPS, AutoDrive, pedestrian, quest-pin, and custom-pin behavior
sane.
This is a bigger RE surface than modifying native edge relaxation. It may be
worth it if the native solver proves too rigid, but it is not the cheapest next
step.
### Practical Ways To Use Engines
Best near-term:
- Use Valhalla, OSRM, and GraphHopper as cost-model references.
- Implement their proven split inside the native solver hook: edge cost plus
transition cost plus mode/user preference.
- Use additive nonnegative penalties first.
Best offline/precompute:
- Extract VAND/navigation and traffic companion data.
- Build a standalone graph in the repo.
- Use RoutingKit or a small custom A* to experiment rapidly outside the game.
- Generate baked labels: corridor id, natural-road id, intersection complexity,
ramp/connector classification, slope/airtime risk, and road-class
confidence.
- Feed those labels back into the RED4ext patch as compact lookup tables.
Possible long-term replacement:
- Use RoutingKit or a custom solver in-game.
- Hook full solver output building around `0x818ba8` or later result
materialization.
- Emit game-native route records from our computed node/handle path.
This replacement path is feasible only after we can round-trip a native route:
decode a vanilla route into graph handles, reproduce it externally, then inject
the same route back without changing behavior.
### Recommendation
Do not link a full production engine yet. Use RoutingKit or a small custom graph
runner offline to prototype and validate the cost function, then patch the
native solver relaxation/cost surface. The native patch has hard game
integration for free: target selection, start projection, route output,
minimap/world-map drawing, and AutoDrive consumers.
Revisit full engine integration if:
- Predecessor/current/neighbor geometry is inaccessible.
- Native route output cannot express the desired route.
- The cost model requires expanded solver state that the game's one-label node
state cannot support.
## Implications For Cyberpunk 2077
### Manual Player GPS Should Optimize Momentum
A real GPS assumes legal speed, stop signs, lights, congestion, safety, and
human compliance. A Cyberpunk player in a Caliburn does not. The manual-driving
cost model should assume:
- Posted speed is weak evidence. The player can exceed it.
- Traffic simulation metadata is useful but secondary. The player often cuts
through or around traffic.
- Highway classification is useful but not decisive. Short freeway hops can be
worse than a straight arterial or side-street corridor.
- Route smoothness matters because every hard turn, ramp dive, or intersection
chain costs player speed and control.
- Roads with good continuity should be preferred even if they are not highways.
### AutoDrive Should Remain More Traffic-Like
AutoDrive behaves more like an NPC/legal driver than the player does. It should
benefit more from road hierarchy, speed limits, traffic-sim classes, and
avoiding odd local shortcuts. Manual drive and AutoDrive probably should share
the same hook site if possible, but they may need different parameter presets.
The previous warning still stands: do not accidentally apply a manual-only
surface to AutoDrive while leaving manual GPS on another surface, or vice versa,
unless the mode split is deliberate and documented.
## Recommended Cost Model
The next prototype should use additive penalties at the solver edge-relaxation
site rather than more highway discounts.
Base:
```text
candidate_g =
vanilla_g
+ short_segment_penalty
+ turn_or_continuity_penalty
+ intersection_complexity_penalty
+ ramp_transition_penalty
+ grade_or_airtime_risk_penalty
+ optional_road_class_penalty
```
Where:
- `vanilla_g` is the game's current `current.g + delta * nodeMultiplier *
directionMultiplier`.
- `short_segment_penalty` discourages stair-stepping through many tiny edges.
- `turn_or_continuity_penalty` is based on predecessor/current/neighbor angle,
not on road class alone.
- `intersection_complexity_penalty` approximates stop-start risk and decision
points, probably from node degree, flags, or short connector density.
- `ramp_transition_penalty` applies when moving highway <-> non-highway through
a short/angled connector.
- `grade_or_airtime_risk_penalty` applies when vertical delta or local shape
indicates a steep launch/drop. This is lower priority until we prove the
height fields are stable.
- `optional_road_class_penalty` can still mildly prefer highways/arterials, but
should be subordinate to continuity.
Avoid negative "bonuses" in the first real version. If a road is desirable, make
its alternatives pay an added cost. This preserves a safer A*/Dijkstra-style
cost surface and avoids the pathological over-attraction seen with highway
discounts.
## Candidate Patch Sites
The proven but blunt hook:
- `0x40bb98`: road-tail multiplier block in `0x40bb40`.
- It sees `solver` and `node` and can inspect flags such as `Highway`.
- It cannot see predecessor/current/neighbor geometry, so it cannot distinguish
a good corridor from a bad ramp hairpin.
The better next hook:
- `0x40b304` and `0x40b540`: neighbor expansion helpers.
- They compute vanilla tentative `g`, straight-line heuristic `h`, and then
call `0x40ba58`.
- At the `0x40ba58` callsites, the live context includes solver pointer,
current state, neighbor state, current node, neighbor node, candidate state,
route mode, progress/delta, and enough vector data for one-step geometry.
The likely patch shape:
- Inline patch at the relax path, not a C detour on a tiny helper.
- Adjust candidate `g` and candidate `f` by the same nonnegative penalty before
vanilla `0x40ba58` compares against `neighbor_state+0x14` and writes
`neighbor_state+0x10`.
- Keep the first build low-noise. Logging every edge expansion is pathological;
route-window sampling or aggregate counters are enough.
## First Prototype Parameters
Start conservative. The goal is to prove the model changes routes in the right
direction without causing highway hairpins or search blowups.
Suggested first-pass terms:
- Short segment churn:
- If edge distance/progress delta is under roughly 25-40 meters, add a small
fixed penalty or a penalty proportional to the missing length.
- Turn continuity:
- If predecessor position is available, compute normalized vectors
`prev -> current` and `current -> neighbor`.
- No penalty for very straight movement.
- Mild penalty for shallow deflection.
- Stronger penalty for 60-120 degree turns.
- Very strong penalty for U-turn-like reversals unless near origin/destination.
- Corridor continuation:
- Treat same-road or good-continuity transitions as neutral, not discounted.
- A curved ring/highway should stay cheap if it is continuous.
- Ramp churn:
- Penalize highway/non-highway transitions when the connector is short and the
turn angle is high.
- Do not penalize a long smooth ramp the same way.
- Intersection complexity:
- Penalize nodes with many outgoing options or multiple short connectors if
the relevant fields can be identified.
- Slope/airtime:
- Start as logging-only unless height deltas clearly correlate with known bad
places. Add as a later penalty.
## Testing Plan
Keep the route set from previous testing:
- El Coyote / city-center custom pin, because it exposed the bad hairpin.
- Side job, Sinnerman, and Claire from the fixed car save.
- Violence / No-Tell Motel cross-city routes.
- The Gig / Cassius Rider long Watson route.
- Aldecaldo / Badlands routes.
- Cyberpsycho sighting on the unfinished highway.
Add tests targeted at the new model:
- A straight long side-street corridor in Heywood vs a short freeway hop.
- A route with several 90-degree stair-step alternatives.
- A known suspended-highway offramp or hilly road that causes airtime at speed.
- AutoDrive on the same destination, to check whether its behavior remains sane.
## Practical Next Step
The static pass around `0x40b304`, `0x40b540`, and `0x40ba58` is complete
enough for the first prototype:
- `0x40ba58` has four direct callers: `0x40b175`, `0x40b4ac`, `0x40b6d5`,
and `0x40bca7`.
- The ordinary adjacency walkers are the two clean first patch sites:
`0x40b4ac` from `0x40b304`, and `0x40b6d5` from `0x40b540`.
- At both ordinary sites, the live context contains solver, current state,
neighbor state, current node, neighbor node, current progress, neighbor
progress, vanilla `g`, and vanilla `f`.
- A disabled-by-default RED4ext prototype now patches the two ordinary sites by
adding a fixed nonnegative per-edge penalty before calling vanilla
`0x40ba58`.
Remaining static work before the full momentum model:
- Confirm candidate vector layout at `0x40b8f0`/state allocation.
- Confirm route-mode values at `solver+0xc4` for manual driving vs AutoDrive.
- Identify a cheap way to detect road-class transition from current/neighbor
node flags.
- Decode predecessor geometry from `state+0x1e` well enough to compute
predecessor/current/neighbor turn angle.
- Decide whether the later turn/ramp model should also patch the special/helper
relax paths (`0x40b175`, `0x40bca7`) or keep them vanilla.
Next implementation step is to enable and test the smallest additive prototype
with route-result comparisons. If the fixed per-edge penalty moves paths in the
right direction and remains stable, extend it into a live-tunable cost file and
then add turn/continuity penalties. Keep highway multiplier support available as
a diagnostic knob, but do not use it as the primary default behavior.
+401
View File
@@ -0,0 +1,401 @@
# RED4ext logging shim
`EdgeWeightGPS` is a RED4ext plugin used to probe native GPS/mappin code paths
without changing gameplay data.
It writes `EdgeWeightGPS.log` beside the installed DLL:
```text
Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.log
```
Current behavior:
- Logs plugin load/unload.
- Logs the RED4ext plugin handle and SDK pointer value passed to `Main`.
- Registers Running game-state callbacks so route tests can be timestamped.
- Hooks selected native wrappers and cores around `GPSSystem`, mappin tracking,
world-map mappin selection, and map path framing.
- Hooks `JournalManager.TrackEntry` at RVA `0x5944fc`, which is the native
handoff used by quest/objective world-map route plotting.
- Dumps the `JournalManager` listener array at offsets `0x210`/`0x21c` and the
listener vtable slots `0x28`, `0x30`, and `0x50`, matching the indirect calls
made by the native `TrackEntry` implementation.
- Hooks the non-default journal listener callbacks seen during route plotting:
`0xea89a8`, `0xea8958`, `0xe63f80`, `0xe63e6c`, `0xe63f00`,
`0x8d136c`, `0x431a34`, `0x55a4e4`, and `0x14de238`.
- For those listener callbacks, logs the listener object, event pointer, common
object fields, and vtable slots on candidate nested owner/service objects.
- Hooks the route handoff found from the journal listener bridge:
`0x598250`, `0x13763d8`, `0xaa62d0`, `0xaa6330`, `0x27abd7c`, and
`0x5625a4`.
- Logs mappin route-event fields, active/deactive route keys, the mappin
active-route map at `system + 0x1a0`, and the route observer list at
`system + 0x280`.
- Hooks the route observer callbacks reached by route activation/deactivation:
`0xaa6610`, `0xaa6628`, `0xaa63e0`, `0x27b10c0`, `0x295d4a0`, and
`0x286a85c`.
- Logs the route entry pointer and route object pointer passed to those
observers, including route object vtable slots and common fields such as the
active byte at offset `0x84`, the service lookup key at offset `0x8c`, and
the runtime observer1 service-owner path used before its vtable `0x220`
lookup.
- Hooks observer1's generic service lookup at `0x287c44`, filters it to the
runtime type pointer at `.data` RVA `0x342f6a8`, logs the returned service
object and slot `0x220`, then dynamically hooks that service `0x220` route
lookup to log its route-id output handle.
- The observer1 service branch was later identified as a false positive:
`0x287c44` returns the `JournalManager` object (`vtable 0x1430f0890`) and
the dynamic slot `0x220` resolves route IDs to readable journal/UI metadata
strings such as `internet`, `home`, `clubs`, and `arasaka`. It is now left in
the source but no longer attached in the active probe.
- Hooks the GPS query lifecycle found from the `RunGPSQuery`/`UpdateGPSQuery`
native registration cluster:
`0x29bd128` (`RunGPSQuery` body), `0x29bd254` (`UpdateGPSQuery` body),
`0x70a42c` (shared query submitter), `0x70a570` (low-level query dispatch),
`0x7094b8` (query result/path fetch), and `0xaa5704` (query status check).
- Logs query endpoints, returned query IDs, shared submitter return RVAs, query
state fields, status values, result-fetch success, and summarized path
buffers/point counts.
- The active probe now records query IDs returned from `0x70a42c` and always
logs result/status polling for those tracked IDs. Startup polling of query
IDs `0`/`1` is sampled only briefly so it cannot exhaust the route-click log
budget.
- Resolves the native mappin system when one of those paths fires, logs relevant
vtable slot addresses, and temporarily hooks the route-adjacent slots.
Active build note:
- The currently installed build is active as
`red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.dll`.
- Source defaults enable the stable inline solver-cost patch at `0x40bb98`
(`kEnableGpsNodeMultiplierInlinePatch = true`) and keep the old full-function
`0x40bb40` C++ detour disabled (`kEnableGpsMultiplierHooks = false`).
- The inline patch preserves vanilla road cost except for highway lanes in the
road-tail branch. Highway lanes are multiplied by the live value in
`solver_weights.bin`, defaulting to `0.80`.
- The patch is intentionally not hard-gated on `solver+0xc4 == 2`; the vanilla
branch already limits it to road-style modes `0/2/4`, which keeps manual
drive and autodrive-like callers from diverging while leaving pedestrian mode
`1` alone.
- `solver_weights.bin` lives beside the DLL and is exactly one little-endian
float32. The plugin polls it every 500 ms from the Running game-state update.
`../tools/write_solver_weights.py` writes the installed file or generates preset
binaries under `../presets/solver_weights/`.
- The async route-job hooks are split behind `kEnableGpsRouteJobLifecycleHooks`
and are disabled in the active probe; this keeps the log focused on
`0x70a42c` submissions, `0x7094b8` result records, and local
search/provider-cost stats.
- `kEnableGpsSpatialEdgeWeightPatch` is a disabled-by-default implementation
path. When enabled, it hooks `GPSRouteProducer` (`0x44cc7c`) and
`GPSEdgeCost` (`0x44f838`), samples a generated traffic-lane spatial grid
using the search-state coordinates, and applies highway/road/pavement
multipliers to returned edge costs.
- When the spatial patch is enabled, the plugin hot-reloads
`spatial_weights.bin` from the same directory as the DLL. The file is exactly
five little-endian float32 values in this order:
`highway`, `road`, `GPSOnly`, `pavement`, `unknown`.
- `../tools/write_spatial_weights.py` writes that file directly, or can generate
named preset files under `../work/spatial-weights/` for live copy-over testing.
The prepared presets are:
`vanilla`, `current`, `highway-free`, `highway-expensive`, and
`surface-extreme`.
- The old verbose local-search probe is still available behind
`kEnableGpsLocalSearchHooks`. It hooks `GPSSearch` (`0x44f054`),
`GPSResolveHandle` (`0x44e1a8`), `GPSEdgeCost` (`0x44f838`), and the two
cost-provider filters `0x44ff68`/`0x450b08`.
- The filter probe records pass/fail counts by point class and by the
`point + 0x10` mask bits tested against provider masks at `+0x108`/`+0x10a`.
This is meant to identify whether class/mask filtering, rather than the edge
multiplier table, is dominating route choice.
- The resolver probe logs selected packed-handle resolutions from the
search/materialization corridor. The selected post-call return sites are
`0x44cb9e` (0x38 intermediate-record builder) and `0x44dd97`
(geometry/materialization path). For full-map GPS queries from caller
`0x8d20d4` with `query_fcc=5`, it opens a five-second resolver trace window,
decodes point index/segment index/generation bits, then dumps the resolved
segment/point records plus small raw slices behind segment pointer fields.
The purpose is to see whether route materialization can reach final lane
hashes/resource-side road classes before the final 0x28 records are emitted.
Current route-probe focus:
- `FrameMappinPath` wrapper/core: `0x27c4314`, `0x27bc1ec`
- `SetSelectedMappin` wrappers/core: `0x27c4a38`, `0x27c4944`,
`0x27c49c4`, `0x27c1684`
- `TrackCustomPositionMappin` wrapper/core: `0x27c4aac`, `0x27c2318`
- mappin-system slots `0x1f0`, `0x280`, and `0x2f0`
- `JournalManager.TrackEntry` implementation: `0x5944fc`
- non-default `TrackEntry` listener callbacks listed above
- journal/mappin route bridge candidate: `0x598250`
- mappin route-event enqueue: `0x13763d8`
- mappin route-event handler: `0xaa62d0`
- mappin route activate/deactivate: `0xaa6330`, `0x27abd7c`
- mappin route observer callbacks: `0xaa6610`, `0xaa6628`, `0xaa63e0`,
`0x27b10c0`, `0x295d4a0`, `0x286a85c`
- observer1 service lookup and returned service route lookup:
`0x287c44`, dynamic service vtable slot `0x220` (disabled after being
classified as journal metadata)
- route-build candidate called by the bridge: `0x5625a4`
- GPS query lifecycle: `0x29bd128`, `0x29bd254`, `0x70a42c`, `0x70a570`,
`0x7094b8`, `0xaa5704`
Quest/objective pins did not fire the mappin tracking hooks in live tests. The
REDscript decompile shows that those pins call `JournalManager.TrackEntry`
instead, so the current useful runtime question is which native listener reacts
to tracked-entry changes.
Most recent controlled test:
- `06:31:43`: user hit Continue / GPS tick logged.
- `06:32:02`: automatic `TrackEntry` during load, no route input.
- `06:32:27`: map opened.
- `06:32:44`, `06:32:50`, `06:32:56`: deliberate quest/objective route plots
produced `JournalManager::TrackEntry` calls from return RVA `0x26ac34e`.
- After map open, the journal listener count rose to 254. Most listener entries
dispatch to no-op `0x14a700`; the non-default callbacks above are the current
drill-down targets.
Newest controlled test:
- User clock `01:05`: Continue on the main menu. The log saw
`GPSSystem/Tick`.
- User clock `01:27`: Space to continue. The log saw automatic
`JournalManager.TrackEntry`, a dense `JournalRouteBridge` burst, and thirty
`RouteBuildCandidate 0x5625a4` calls. About 5.5 seconds later the mappin route
activation/deactivation functions fired.
- User clock `01:52`: map open. Hovering icons produced the expected
`SetSelectedMappin` bursts.
- Deliberate quest route clicks produced `JournalManager.TrackEntry`, route
event enqueue calls for old-route-off/new-route-on, and route event handler
calls roughly 11-18 ms later.
- Custom pin routing used the separate custom-position mappin path, then the
shared route activate/deactivate helper.
Latest observer1 probe result:
- The `Observer1ServiceLookup` hook fired, but it proved observer1 is not route
solving. It returned the same `JournalManager` vtable seen in earlier journal
traces, and its slot `0x220` resolved numeric route IDs into journal/UI
category strings. That branch is closed as marker metadata.
Current static lead:
- `RunGPSQuery` and `UpdateGPSQuery` are registered native thunks at
`0x29bd5ac` and `0x29bd6c8`. Their deeper bodies are `0x29bd128` and
`0x29bd254`.
- `RunGPSQuery` submits a query through a subsystem reached via an object field
at `+0x130`, returning a query ID or `-1`.
- `UpdateGPSQuery` fetches a completed query result through `0x7094b8`, copies a
path point buffer, and writes the resulting point array for script/UI
consumption.
- The shared submitter `0x70a42c` is also used by non-GPS navigation callers,
so return RVAs and query IDs are important for separating map GPS from traffic
or AI path requests.
Latest GPS-query finding:
- During a controlled route test, the normal world-map GPS path did not call
the script-native `RunGPSQuery`/`UpdateGPSQuery` bodies.
- Each deliberate quest/custom route click called the shared submitter
`0x70a42c` from return RVA `0x8d20d4`. In the 2026-06-20 19:03 run, the
deliberately plotted routes returned query IDs `17`, `18`, `19`, and `20`
for the side job, Sinnerman, Claire's Garage, and a custom pin.
- Each submitted route produced three `0x70a570` dispatches: current/player
position, a nearby snapped/lane position, then the destination position.
- The dense log stream immediately after Space-to-Continue is repeated
`0x7094b8` result polling for startup/minimap route queries, matching the HUD
route initialization delay.
- `GPSQueryResultFetch 0x7094b8` returning `1` is the route-solve completion
signal. The caller at return RVA `0x52069c` starts handling a completed
result, then the caller at return RVA `0x520783` drains result records.
The visible route record appears in the object passed as `outPath`: offset
`0x30` is a packed point count (`N/N` on the first drain record, then `N`),
and offset `0x28` looks like the route point array pointer. The deliberate
routes produced the familiar `11`, `16`, `15`, and `14` point-like counts in
the 2026-06-20 19:03 run, and `11`, `16`, `15`, `11` in the 19:16 map
routine where the custom pin was different.
- Static disassembly confirms `0xaa5704` takes `(manager, queryId)` and returns
a small status code; one caller checks for value `1`. Runtime logging shows
this is HUD/display lifecycle polling rather than solve completion: it polls
the currently displayed query once per second and pauses when the minimap is
not rendered.
- The current installed probe adds a read-only dump of result-object fields
around offsets `0x20` through `0x5f` on successful active-route fetches.
The field at `outPath + 0x28` is the data pointer for an internal route
element vector, but live data does not decode as plain `Vector3`/`Vector4`
coordinates.
Latest route-result findings:
- User clock `26:38`: Continue on the main menu.
- User clock `27:01`: Space to continue, with the car in a different position
after a previous drive.
- User clock `27:21`: map opened.
- User clock `28:06`: normal route-click routine complete.
- Same three quest destinations from the shifted start produced different
result counts: side job `12`, Sinnerman `23`, Claire's Garage `18`, and the
custom pin `17`.
- Static disassembly of the `0x520783` result-drain caller shows `0x7094b8`
drains one route result record at a time from an internal vector, then copies
that record into `outPath`.
- The copied route-result subobject contains a vector at `outPath + 0x28`.
Its live count is the dword at `outPath + 0x34`; in the latest run this
cleanly matched the visible routes: side job `12`, Sinnerman `23`, Claire's
Garage `18`, and custom pin `17`.
- The vector elements are 0x28-byte records, not pointers. `0x41be2c` calls
`0x41be94` with `(data, data + count * 40)`. The active probe logs each
route element as a compact 40-byte record tuple:
`h00,u08hex,u0c,u10,u14,h18,h18lowFloat,h18highFloat,h20`.
- A nearby helper at `0x41c508` does iterate an array of 8-byte pointers at
offset `+0x28`, but that is a different route-adjacent object type, not the
`0x7094b8` output record captured in `outPath`.
Latest route-element decode:
- User clock `45:37`: Continue on the main menu.
- User clock `46:05`: Space to continue.
- User clock `46:15`: map opened and the usual route-click routine was run.
- The startup/minimap route and the later explicit Claire's Garage map route
produced identical 18-record `ptr28` sequences, so the same solved route
object feeds both HUD startup and deliberate map plotting.
- The four deliberate map routes completed as route result IDs `2` through `5`:
side job `12` records / final span end `68`, Sinnerman `23` records / final
span end `188`, Claire's Garage `18` records / final span end `86`, custom
pin `14` records / final span end `78`.
- Within every route, `u10` and `u14` are monotonically increasing start/end
indexes into a flattened point/span buffer. The final `u14` equals
`gpsResult_u20` for every route.
- `h00` is a stable per-segment handle: common route prefixes share identical
`h00` sequences, and the custom pin and Claire's Garage routes share their
first ten solved segments before diverging.
- `u08` is packed metadata. Values appear byte-structured, for example
`0x00020202`, `0x01020203`, `0x02020103`, and `0x03020003`, rather than a
simple scalar weight.
- `h18` splits into two 32-bit floats. The high half is consistently a sane
road-scale float, often in the `20` to `230` range, and is likely segment
length/cost-like output. The low half is usually zero or a tiny value, with
non-zero values appearing around the start segment.
- `h20` has been zero in all captured full-map GPS result records.
Route-record/resource correlation:
- The `h00` field in the 0x28-byte route-result records matches
`worldTrafficLanePersistent.nodeRefHash`. In the current route logs, every
unique route handle checked matched a lane hash in the raw
`all.traffic_persistent` resource.
- This lets us join emitted route segments back to resource-side lane metadata:
`flags`, `maxSpeed`, `length`, and `playerGPSInfo`.
- The packed `u08` metadata is not a clean copy of resource-side lane flags.
Its low byte commonly appears as `2`, `3`, `4`, `5`, `8`, `9`, etc., while
the resource categories are bitflag bundles such as `Road`, `Highway`,
`GPSOnly`, `Intersection`, and `Pavement`.
- A joined sample from `EdgeWeightGPS_cost_table_mild_2121.log` showed mixed
resource categories per low-byte class: class `2` was mostly pavement but
included highway lanes, class `8` was split between pavement and highway, and
class `1` was mostly road. This means the final route record's low byte is
useful metadata, but it is not by itself the class multiplier index from
`0x44f838`.
- The next better lever is still the search-side cost provider table keyed by
`(*(routePoint + 0x13) & 0x3f)`. To tune it intelligently, we need either the
provider initialization site or a way to join search-side route points back to
`nodeRefHash`/lane flags before the edge cost is returned.
Latest full-route join:
- `../logs/EdgeWeightGPS_query_route_probe_2308.log` contains nine solved route
results from the 23:08 user test. All 362 emitted route records matched
`worldTrafficLanePersistent.nodeRefHash` in
`../work/raw-segment-json/all.traffic_persistent.json`.
- The log's wall timestamp currently wraps milliseconds without incrementing
the displayed second. Use the `+Nms` elapsed field when comparing submit and
result times.
- Representative solved route composition:
- side job: 12 records, 32 ms, 578.6 pavement length.
- Sinnerman: 23 records, 45 ms, 897.4 pavement, 160.4 highway, 89.8 road.
- Claire: 18 records, 34 ms, 1216.5 pavement.
- custom highway-ish pin: 17 records, 44 ms, 1006.3 pavement,
241.0 highway, 38.0 road.
- Aldecaldo camp: 80 records, 325 ms, 5853.5 road, 1492.9 highway,
1111.5 pavement, 24.6 gpsonly.
- far east gig: 81 records, 144 ms, 5086.2 road, 1523.0 highway,
1111.5 pavement, 24.6 gpsonly.
- Violence: 55 records, 97 ms, 1991.5 pavement, 358.5 highway,
86.1 road.
- cyberpsycho unfinished highway: 58 records, 147 ms, 2657.4 pavement,
519.0 highway, 113.2 road, 56.7 gpsonly.
- Search/provider aggregate in the same log: 159 searches, 4036 provider
filter calls, only 3 filter failures. Filter slot `+0x08` is therefore
almost never pruning this test graph.
- Search-side point classes observed by `0x44f838` are `{1,3,4,5,15}`. Final
route-record low-byte classes are `{0,1,2,3,4,5,7,8,9}`, and route-record
classes 7/8/9 are highway-heavy. These are different domains, which explains
why provider point-class tuning did not map cleanly to highway preference.
- Later static analysis tied the search-side segment/point records to `VAND`
blobs in compiled navigation streamingsector resources, not directly to
`all.traffic_persistent`. The traffic lane hash join happens downstream in
the result/materialization path.
- `../tools/analyze_vand_navigation.py` decodes those `VAND` blobs from extracted
streamingsector JSON. In the three extracted samples, all masks were `0x0003`
and the dominant point classes were `1`, `4`, `5`, and `3`, matching the live
edge-cost trace.
Current static producer lead:
- `0x70a908` packages the route endpoints and query settings, then calls
`0x44cc7c`.
- `0x44cc7c` is now the main producer corridor, not merely a generic unknown
helper. It allocates large scratch state, projects the query endpoints, and
calls deeper pathfinding routines that should decide route cost before the
`0x7094b8` fetch/drain path copies final result records.
- Direct xrefs confirm `0x41be94` has only the result-copy caller
`0x41be69`; patching there would alter copied/displayed records after route
choice. The cost patch needs to happen upstream of the `0x44cc7c` result
construction path.
- `0x44f054` is the strongest route-planning lead so far. It validates the
packed start/target lane handles, initializes a search-state table and open
list, pushes the start state, repeatedly pops the best state, checks for the
destination handle, traverses adjacency lists, and writes predecessor,
accumulated-cost, and priority fields for improved neighbor states. This is
the native GPS graph search loop.
- The search-state record is 0x20 bytes. Offsets `+0x00/+0x04/+0x08` are a
world position, `+0x0c` is accumulated path cost, `+0x10` is priority
(`cost + heuristic`), `+0x14` stores open/closed flags and the predecessor
index, and `+0x18` stores the packed lane handle.
- `0x44f7bc` is the heuristic helper. It returns world-space distance to the
target scaled by the constant at `0x1431ef3a0` (`0.9990000129`), unless a
route-shape helper supplies a shorter adjusted distance.
- The search loop calls a strategy object's vtable slot `+0x08` as an edge
admissibility predicate and slot `+0x10` as the edge-cost callback. For the
base/class-filter strategies, slot `+0x10` resolves to `0x44f838`.
- `0x44f838` computes the geometric edge distance and multiplies it by
`provider[0x08 + classId * 4]`, where `classId` is
`(*(routePoint + 0x13) & 0x3f)`. The GPS planner is therefore weighted, but
by a compact baked road-class table, not by traffic `maxSpeed` or lane
connection probabilities.
- The read-only local-search probe can still hook `0x44f054` and `0x44f838` to
log the live provider vtable, the 64 class multipliers, start/target handles,
and a capped sample of edge-cost calls with point class IDs, masks, handles,
and returned costs.
- The new spatial edge-cost path is implemented but disabled by default. Its
generated grid is built from traffic lane polygons/flags, not from VAND point
classes, so it can bias the search toward highway cells without rewriting
VAND metadata that later materialization code branches on.
Autodrive finding:
- User clock `28:28` and `28:48`: enabling autodrive did not use the full
world-map GPS caller. It submitted repeated `0x70a42c` queries from return
RVA `0x8ed760`, with `query_fcc=4`, near-current vehicle positions, and
result fetches from return RVA `0x8ed7df`.
- These autodrive queries appear to be short local vehicle navigation requests
using the same query service, separate from the full player GPS map route
caller at return RVA `0x8d20d4` with `query_fcc=5`.
Build and install from the Fedora toolbox:
```bash
toolbox run -c 2077 ./tools/install_red4ext_shim.sh
```
This shim is read-only. It does not enable any of the archived traffic data patches.
+51
View File
@@ -0,0 +1,51 @@
# Tooling
The Fedora toolbox container is expected to be named `2077`.
Installed inside that toolbox:
- .NET SDK 8, 9, and 10
- GCC/G++, CMake, make
- `jq`, `ripgrep`
- WolvenKit CLI NuGet tool `wolvenkit.cli` 8.18.1
- `rizin` 0.7.4
- `python3-capstone` 5.0.5
- `python3-pefile` 2024.8.26
The WolvenKit command is:
```bash
toolbox run --container 2077 bash -lc '$HOME/.dotnet/tools/cp77tools --help'
```
## Discover Traffic Resources
```bash
../tools/discover_traffic_resources.sh \
"/path/to/Cyberpunk 2077/archive/pc/content" \
traffic-resources.txt
```
This lists archive paths matching traffic/persistent/lane naming. The regex is
intentionally broad because CDPR naming can vary between base game and DLC
archives.
## Build Patch Archive
```bash
../tools/build_lane_weight_archive.sh \
"/path/to/Cyberpunk 2077" \
"/path/to/Cyberpunk 2077/archive/pc/content" \
../work/lane-weight-build
```
The script performs:
1. `cp77tools extract` for traffic lane resources
2. `cp77tools convert serialize` to JSON
3. `../tools/patch_traffic_lanes.py`
4. `cp77tools convert deserialize` back to CR2W
5. `cp77tools pack` into an archive
Copy the resulting archive from `../work/lane-weight-build/05_packed` into
`Cyberpunk 2077/archive/pc/mod` for manual testing.
File diff suppressed because it is too large Load Diff