19 KiB
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
0x818ba8or 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:
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_gis the game's currentcurrent.g + delta * nodeMultiplier * directionMultiplier.short_segment_penaltydiscourages stair-stepping through many tiny edges.turn_or_continuity_penaltyis based on predecessor/current/neighbor angle, not on road class alone.intersection_complexity_penaltyapproximates stop-start risk and decision points, probably from node degree, flags, or short connector density.ramp_transition_penaltyapplies when moving highway <-> non-highway through a short/angled connector.grade_or_airtime_risk_penaltyapplies 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_penaltycan 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 in0x40bb40.- It sees
solverandnodeand can inspect flags such asHighway. - It cannot see predecessor/current/neighbor geometry, so it cannot distinguish a good corridor from a bad ramp hairpin.
The better next hook:
0x40b304and0x40b540: neighbor expansion helpers.- They compute vanilla tentative
g, straight-line heuristich, and then call0x40ba58. - At the
0x40ba58callsites, 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
gand candidatefby the same nonnegative penalty before vanilla0x40ba58compares againstneighbor_state+0x14and writesneighbor_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 -> currentandcurrent -> 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.
- If predecessor position is available, compute normalized vectors
- 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
Before coding the full patch, do one static pass to finish the field map around
0x40b304, 0x40b540, and 0x40ba58:
- Confirm candidate vector layout at
0x40b8f0/state allocation. - Confirm predecessor state pointer/index path from
state+0x1e. - Confirm route-mode values at
solver+0xc4for manual driving vs AutoDrive. - Identify a cheap way to detect road-class transition from current/neighbor node flags.
- Decide whether the first prototype patches only the two main relax callsites
(
0x40b4ac,0x40b6d5) or also the special/helper relax paths (0x40b175,0x40bca7).
After that, implement the smallest live-tunable prototype with additive penalties only. Keep highway multiplier support available as a diagnostic knob, but do not use it as the primary default behavior.