Files
2077-gps-mod/docs/compaction-handoff.md
T
2026-06-27 03:22:39 -05:00

35 KiB

Compaction Handoff

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

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

2026-06-27 Momentum/Corridor Direction

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

2026-06-27 Tuning Status And Safe Installed State

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

2026-06-26 Confirmed Solver-Cost Patch

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

2026-06-26 Static Update

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

2026-06-26 Spatial Edge-Cost Prototype

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

Important caveat:

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

Current Installed State

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

2026-06-26 Full Async Solver Findings

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

New Toolbox Tools

The Fedora toolbox 2077 now has:

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

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

Active Probe Configuration

In red4ext/EdgeWeightGPS/src/Main.cpp:

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

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

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

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

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

Why This Probe Exists

Data and behavior patches tested so far:

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

Static disassembly clarified the provider split:

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

Important Native Findings

GPS pipeline landmarks:

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

The search loop is A*/Dijkstra-like:

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

Provider details:

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

Route result records:

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

Packed-handle resolver:

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

Latest 23:08 Test Result

The user clicked:

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

Use:

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:

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

Clear plugin log:

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:

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:

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.