Move reverse engineering artifacts under contrib
@@ -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`.
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 671 KiB |
|
After Width: | Height: | Size: 601 KiB |
|
After Width: | Height: | Size: 941 KiB |
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||