753 lines
36 KiB
Markdown
753 lines
36 KiB
Markdown
# 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`.
|