421 lines
19 KiB
Markdown
421 lines
19 KiB
Markdown
# GPS Routing Research Notes
|
|
|
|
Current date: 2026-06-27.
|
|
|
|
This note summarizes the research pass prompted by the realization that "prefer
|
|
highways" is a proxy, not the goal. The actual player-facing goal is a route
|
|
that feels fast and drivable in Cyberpunk 2077, especially when the player is
|
|
driving aggressively and ignoring normal traffic rules.
|
|
|
|
The conclusion is that the next patch should not be another broad highway
|
|
multiplier. Production route engines and routing papers treat route quality as
|
|
multi-criteria: edge traversal cost, transition/turn cost, road hierarchy,
|
|
intersection delay, and user preferences are separate signals. For manual
|
|
Cyberpunk driving, the best analogue is a momentum/corridor model: prefer
|
|
routes that preserve speed and minimize decision points, sharp turns,
|
|
short-segment churn, and awkward ramp transitions.
|
|
|
|
## Sources Reviewed
|
|
|
|
- Bast et al., "Route Planning in Transportation Networks":
|
|
https://arxiv.org/abs/1504.05140
|
|
- Jiang and Liu, "Computing the Fewest-turn Map Directions based on the
|
|
Connectivity of Natural Roads": https://arxiv.org/abs/1003.3536
|
|
- Sacharidis and Bouros, "Routing Directions: Keeping it Fast and Simple":
|
|
https://arxiv.org/abs/1309.4396
|
|
- Hlineny and Moris, "Generalized Maneuvers in Route Planning":
|
|
https://arxiv.org/abs/1107.0798
|
|
- Dibbelt, Strasser, and Wagner, "Fast Exact Shortest Path and Distance Queries
|
|
on Road Networks with Parametrized Costs": https://arxiv.org/abs/1509.03165
|
|
- OSRM profile docs:
|
|
https://github.com/Project-OSRM/osrm-backend/blob/master/docs/profiles.md
|
|
- OSRM car profile:
|
|
https://github.com/Project-OSRM/osrm-backend/blob/master/profiles/car.lua
|
|
- GraphHopper profile docs:
|
|
https://github.com/graphhopper/graphhopper/blob/master/docs/core/profiles.md
|
|
- Valhalla turn-by-turn API costing docs:
|
|
https://valhalla.github.io/valhalla/api/turn-by-turn/api-reference/
|
|
- Valhalla auto costing implementation:
|
|
https://github.com/valhalla/valhalla/blob/master/src/sif/autocost.cc
|
|
- RoutingKit README:
|
|
https://github.com/RoutingKit/RoutingKit
|
|
- OSRM license:
|
|
https://github.com/Project-OSRM/osrm-backend/blob/master/LICENSE.TXT
|
|
- Valhalla license:
|
|
https://github.com/valhalla/valhalla/blob/master/COPYING
|
|
- GraphHopper license:
|
|
https://github.com/graphhopper/graphhopper/blob/master/LICENSE.txt
|
|
|
|
## High-Level Findings
|
|
|
|
### There Is No Single "Good GPS" Objective
|
|
|
|
The route-planning survey by Bast et al. frames road routing as shortest-path
|
|
search over nonnegative edge weights, then shows that most practical complexity
|
|
comes from speed, preprocessing, traffic, and multiple criteria. It explicitly
|
|
notes there is no single best route-planning method because systems are judged
|
|
by different tradeoffs: query time, preprocessing effort, space, robustness to
|
|
changing inputs, and model quality.
|
|
|
|
For our mod, that means "shortest distance" and "prefer highways" are both too
|
|
thin. A better route is the minimum of a cost function we choose. The game
|
|
already appears to run an A*/Dijkstra-style solver with a geometric heuristic
|
|
and nonnegative edge costs. Our task is not to replace that algorithm; it is to
|
|
feed it a better cost surface.
|
|
|
|
### Production Engines Separate Edge Cost From Transition Cost
|
|
|
|
OSRM profiles define way speed/rate/weight separately from turn processing.
|
|
The docs say speed should estimate actual travel time, while `rate` or `weight`
|
|
should encode preference; changing speed to express preference skews duration
|
|
estimates. OSRM also has a `process_turn` stage that can assign turn penalties
|
|
by angle, traffic signals, obstacles, U-turns, road classes, and intersection
|
|
context.
|
|
|
|
Valhalla does the same separation. Its auto costing computes edge cost from
|
|
time, distance preference, density, surface, tolls, alley/service/track factors,
|
|
and highway preference. Transition cost then adds turn/intersection behavior:
|
|
OSRM-style turn duration, stop impact, turn type, ramp transition cost,
|
|
roundabout cost, and U-turn penalties. This is exactly the split we should
|
|
mirror: road class is only one part of the edge cost; turn and transition costs
|
|
are a separate part of the route's feel.
|
|
|
|
GraphHopper's profile model reinforces this. It exposes road prioritization,
|
|
turn costs, speed, priority, and `distance_influence` as separate tuning
|
|
surfaces. Its docs also warn that reusing preprocessed heuristic data can
|
|
require new weights to be greater than or equal to base weights to preserve
|
|
correctness. That maps to our A* concern: negative bonuses and broad discounts
|
|
can make a geometric heuristic less trustworthy. Nonnegative additive penalties
|
|
are safer than "make good edges cheaper" as a first serious patch.
|
|
|
|
### Human-Friendly Routes Minimize Turns And Decision Points
|
|
|
|
Jiang and Liu's fewest-turn paper is directly relevant even though Cyberpunk is
|
|
not normal driving. The paper argues that people often choose routes by road
|
|
continuity rather than segment-by-segment geometric distance. It highlights
|
|
fewer turns as lower cognitive burden and explicitly connects fewer turns to
|
|
fewer slow-down/speed-up events.
|
|
|
|
The important concept is "natural roads": sequences of segments joined by good
|
|
continuity. A curve along a ring road is not necessarily a "turn" in the human
|
|
navigation sense; changing from one road/corridor to another is the turn. The
|
|
paper used a 45-degree deflection threshold when generating natural roads, but
|
|
the exact threshold is less important than the idea: penalize route changes and
|
|
poor continuity, not curvature by itself.
|
|
|
|
Sacharidis and Bouros extend this into a useful tradeoff model. They define
|
|
route length and route complexity separately, with complexity as turn/road-change
|
|
cost. They then discuss near-fastest routes that are as simple as possible and
|
|
near-simplest routes that are as fast as possible. This is a good conceptual
|
|
target for us: do not minimize turns at all costs, and do not minimize distance
|
|
at all costs. Prefer a near-fast route that is much simpler and smoother.
|
|
|
|
### Maneuvers Are State-Dependent
|
|
|
|
Hlineny and Moris model maneuvers such as turn prohibitions, traffic light
|
|
delays, roundabouts, and multi-edge restrictions. Their important point for us
|
|
is that a maneuver depends on how a vertex was reached, not only the current
|
|
vertex. A plain Dijkstra state of "best cost to node" is insufficient when
|
|
cost depends on a sequence of prior edges. Their M-Dijkstra expands state to
|
|
vertex-plus-context.
|
|
|
|
We probably should not implement a full context-expanded solver inside a game
|
|
binary patch. But the paper explains why our next hook needs predecessor or
|
|
incoming-direction information. A turn penalty computed only from the current
|
|
node flags is inadequate. The local RE already found the game keeps predecessor
|
|
state at solver-state `+0x1e`, and the relax call receives current state,
|
|
neighbor state, and candidate state. That is enough for a practical one-step
|
|
turn/corridor approximation without redesigning the whole solver.
|
|
|
|
### Speedups Are Mostly Not The Problem Here
|
|
|
|
Modern routing engines use A*, bidirectional search, contraction hierarchies,
|
|
landmarks, arc flags, and related preprocessing to answer large road-network
|
|
queries quickly. This matters academically, but not as the primary mod target.
|
|
Night City's graph is small enough that the game already computes paths during
|
|
play. The player's complaint is not "the route takes too long to compute"; it is
|
|
"the route is dumb."
|
|
|
|
That said, we should respect solver assumptions. The game uses a straight-line
|
|
heuristic in the full async solver. If we add only nonnegative penalties to `g`
|
|
and to the candidate `f = g + h`, the heuristic remains conservative relative
|
|
to the original distance-like base. If we add negative bonuses or deep highway
|
|
discounts, we may make the heuristic too optimistic or otherwise distort
|
|
ordering. The proof highway multiplier worked, but the 0.55/0.35 tests already
|
|
showed how quickly discounts become pathological.
|
|
|
|
## Open-Source Routing Engines
|
|
|
|
It is viable to use open-source routing software, but there are three different
|
|
levels of "use":
|
|
|
|
- Use an engine's ideas and cost model.
|
|
- Use an engine offline to preprocess or classify Night City.
|
|
- Link or run an engine as the actual in-game route planner.
|
|
|
|
The first two are practical. The last one is possible, but much more invasive
|
|
than patching the native solver.
|
|
|
|
### Candidate Engines And Libraries
|
|
|
|
RoutingKit is the most interesting native-library candidate. It is a C++ route
|
|
planning library under a BSD-2-Clause license. Its README emphasizes
|
|
customizable contraction hierarchy support, flexible arc weights, and
|
|
millisecond-or-better queries on continental-scale data. It exposes direct
|
|
graph, index, and query APIs. If we wanted a true replacement solver inside a
|
|
RED4ext DLL, RoutingKit is the most plausible starting point.
|
|
|
|
OSRM is a high-performance C++ routing engine and service under a permissive
|
|
BSD-style license. It has excellent production lessons: profile-based cost
|
|
modeling, turn processing, road classes, and CH/MLD preprocessing. However, it
|
|
is built around its own extraction/preprocessing pipeline and OpenStreetMap-like
|
|
data. Embedding OSRM directly would likely mean generating OSRM-compatible data
|
|
from Cyberpunk resources, running preprocessing, shipping those artifacts, then
|
|
calling libosrm or a local service.
|
|
|
|
Valhalla is a C++ routing engine under the MIT license. Its costing model is
|
|
especially useful because auto routing separates edge cost from transition cost,
|
|
includes highway preference as a mild factor, and explicitly handles ramps,
|
|
U-turns, stop impact, density, alleys, service roads, surfaces, and closures.
|
|
Direct embedding has the same issue as OSRM: Valhalla expects its own graph
|
|
tiles and attribution model.
|
|
|
|
GraphHopper is Apache-2.0 and has a strong custom model/profile system, but it
|
|
is Java. It is useful as a design reference and potentially as an offline
|
|
analysis tool, but bundling a JVM or running a Java service beside Cyberpunk is
|
|
not a good first mod architecture.
|
|
|
|
Generic graph libraries such as Boost.Graph or LEMON are options if we only
|
|
need Dijkstra/A* and want to own the data model ourselves. They are lighter than
|
|
OSRM/Valhalla but do not bring road-routing domain logic; we would still write
|
|
the road/corridor cost model.
|
|
|
|
### Integration Problem
|
|
|
|
External engines need a graph they own. Cyberpunk's GPS solver operates on
|
|
internal route/node records, packed handles, and final route records. Linking an
|
|
engine does not solve either boundary.
|
|
|
|
To replace the solver, we need:
|
|
|
|
- Extract or observe the full drivable graph: directed edges, coordinates,
|
|
flags/classes, turn/intersection topology.
|
|
- Convert that graph into the engine or custom graph format.
|
|
- Map live start and target positions to graph nodes.
|
|
- Compute a route.
|
|
- Convert the chosen path back into handles/records the game expects, or hook
|
|
late enough to draw and consume a custom route.
|
|
- Keep manual GPS, AutoDrive, pedestrian, quest-pin, and custom-pin behavior
|
|
sane.
|
|
|
|
This is a bigger RE surface than modifying native edge relaxation. It may be
|
|
worth it if the native solver proves too rigid, but it is not the cheapest next
|
|
step.
|
|
|
|
### Practical Ways To Use Engines
|
|
|
|
Best near-term:
|
|
|
|
- Use Valhalla, OSRM, and GraphHopper as cost-model references.
|
|
- Implement their proven split inside the native solver hook: edge cost plus
|
|
transition cost plus mode/user preference.
|
|
- Use additive nonnegative penalties first.
|
|
|
|
Best offline/precompute:
|
|
|
|
- Extract VAND/navigation and traffic companion data.
|
|
- Build a standalone graph in the repo.
|
|
- Use RoutingKit or a small custom A* to experiment rapidly outside the game.
|
|
- Generate baked labels: corridor id, natural-road id, intersection complexity,
|
|
ramp/connector classification, slope/airtime risk, and road-class
|
|
confidence.
|
|
- Feed those labels back into the RED4ext patch as compact lookup tables.
|
|
|
|
Possible long-term replacement:
|
|
|
|
- Use RoutingKit or a custom solver in-game.
|
|
- Hook full solver output building around `0x818ba8` or later result
|
|
materialization.
|
|
- Emit game-native route records from our computed node/handle path.
|
|
|
|
This replacement path is feasible only after we can round-trip a native route:
|
|
decode a vanilla route into graph handles, reproduce it externally, then inject
|
|
the same route back without changing behavior.
|
|
|
|
### Recommendation
|
|
|
|
Do not link a full production engine yet. Use RoutingKit or a small custom graph
|
|
runner offline to prototype and validate the cost function, then patch the
|
|
native solver relaxation/cost surface. The native patch has hard game
|
|
integration for free: target selection, start projection, route output,
|
|
minimap/world-map drawing, and AutoDrive consumers.
|
|
|
|
Revisit full engine integration if:
|
|
|
|
- Predecessor/current/neighbor geometry is inaccessible.
|
|
- Native route output cannot express the desired route.
|
|
- The cost model requires expanded solver state that the game's one-label node
|
|
state cannot support.
|
|
|
|
## Implications For Cyberpunk 2077
|
|
|
|
### Manual Player GPS Should Optimize Momentum
|
|
|
|
A real GPS assumes legal speed, stop signs, lights, congestion, safety, and
|
|
human compliance. A Cyberpunk player in a Caliburn does not. The manual-driving
|
|
cost model should assume:
|
|
|
|
- Posted speed is weak evidence. The player can exceed it.
|
|
- Traffic simulation metadata is useful but secondary. The player often cuts
|
|
through or around traffic.
|
|
- Highway classification is useful but not decisive. Short freeway hops can be
|
|
worse than a straight arterial or side-street corridor.
|
|
- Route smoothness matters because every hard turn, ramp dive, or intersection
|
|
chain costs player speed and control.
|
|
- Roads with good continuity should be preferred even if they are not highways.
|
|
|
|
### AutoDrive Should Remain More Traffic-Like
|
|
|
|
AutoDrive behaves more like an NPC/legal driver than the player does. It should
|
|
benefit more from road hierarchy, speed limits, traffic-sim classes, and
|
|
avoiding odd local shortcuts. Manual drive and AutoDrive probably should share
|
|
the same hook site if possible, but they may need different parameter presets.
|
|
|
|
The previous warning still stands: do not accidentally apply a manual-only
|
|
surface to AutoDrive while leaving manual GPS on another surface, or vice versa,
|
|
unless the mode split is deliberate and documented.
|
|
|
|
## Recommended Cost Model
|
|
|
|
The next prototype should use additive penalties at the solver edge-relaxation
|
|
site rather than more highway discounts.
|
|
|
|
Base:
|
|
|
|
```text
|
|
candidate_g =
|
|
vanilla_g
|
|
+ short_segment_penalty
|
|
+ turn_or_continuity_penalty
|
|
+ intersection_complexity_penalty
|
|
+ ramp_transition_penalty
|
|
+ grade_or_airtime_risk_penalty
|
|
+ optional_road_class_penalty
|
|
```
|
|
|
|
Where:
|
|
|
|
- `vanilla_g` is the game's current `current.g + delta * nodeMultiplier *
|
|
directionMultiplier`.
|
|
- `short_segment_penalty` discourages stair-stepping through many tiny edges.
|
|
- `turn_or_continuity_penalty` is based on predecessor/current/neighbor angle,
|
|
not on road class alone.
|
|
- `intersection_complexity_penalty` approximates stop-start risk and decision
|
|
points, probably from node degree, flags, or short connector density.
|
|
- `ramp_transition_penalty` applies when moving highway <-> non-highway through
|
|
a short/angled connector.
|
|
- `grade_or_airtime_risk_penalty` applies when vertical delta or local shape
|
|
indicates a steep launch/drop. This is lower priority until we prove the
|
|
height fields are stable.
|
|
- `optional_road_class_penalty` can still mildly prefer highways/arterials, but
|
|
should be subordinate to continuity.
|
|
|
|
Avoid negative "bonuses" in the first real version. If a road is desirable, make
|
|
its alternatives pay an added cost. This preserves a safer A*/Dijkstra-style
|
|
cost surface and avoids the pathological over-attraction seen with highway
|
|
discounts.
|
|
|
|
## Candidate Patch Sites
|
|
|
|
The proven but blunt hook:
|
|
|
|
- `0x40bb98`: road-tail multiplier block in `0x40bb40`.
|
|
- It sees `solver` and `node` and can inspect flags such as `Highway`.
|
|
- It cannot see predecessor/current/neighbor geometry, so it cannot distinguish
|
|
a good corridor from a bad ramp hairpin.
|
|
|
|
The better next hook:
|
|
|
|
- `0x40b304` and `0x40b540`: neighbor expansion helpers.
|
|
- They compute vanilla tentative `g`, straight-line heuristic `h`, and then
|
|
call `0x40ba58`.
|
|
- At the `0x40ba58` callsites, the live context includes solver pointer,
|
|
current state, neighbor state, current node, neighbor node, candidate state,
|
|
route mode, progress/delta, and enough vector data for one-step geometry.
|
|
|
|
The likely patch shape:
|
|
|
|
- Inline patch at the relax path, not a C detour on a tiny helper.
|
|
- Adjust candidate `g` and candidate `f` by the same nonnegative penalty before
|
|
vanilla `0x40ba58` compares against `neighbor_state+0x14` and writes
|
|
`neighbor_state+0x10`.
|
|
- Keep the first build low-noise. Logging every edge expansion is pathological;
|
|
route-window sampling or aggregate counters are enough.
|
|
|
|
## First Prototype Parameters
|
|
|
|
Start conservative. The goal is to prove the model changes routes in the right
|
|
direction without causing highway hairpins or search blowups.
|
|
|
|
Suggested first-pass terms:
|
|
|
|
- Short segment churn:
|
|
- If edge distance/progress delta is under roughly 25-40 meters, add a small
|
|
fixed penalty or a penalty proportional to the missing length.
|
|
- Turn continuity:
|
|
- If predecessor position is available, compute normalized vectors
|
|
`prev -> current` and `current -> neighbor`.
|
|
- No penalty for very straight movement.
|
|
- Mild penalty for shallow deflection.
|
|
- Stronger penalty for 60-120 degree turns.
|
|
- Very strong penalty for U-turn-like reversals unless near origin/destination.
|
|
- Corridor continuation:
|
|
- Treat same-road or good-continuity transitions as neutral, not discounted.
|
|
- A curved ring/highway should stay cheap if it is continuous.
|
|
- Ramp churn:
|
|
- Penalize highway/non-highway transitions when the connector is short and the
|
|
turn angle is high.
|
|
- Do not penalize a long smooth ramp the same way.
|
|
- Intersection complexity:
|
|
- Penalize nodes with many outgoing options or multiple short connectors if
|
|
the relevant fields can be identified.
|
|
- Slope/airtime:
|
|
- Start as logging-only unless height deltas clearly correlate with known bad
|
|
places. Add as a later penalty.
|
|
|
|
## Testing Plan
|
|
|
|
Keep the route set from previous testing:
|
|
|
|
- El Coyote / city-center custom pin, because it exposed the bad hairpin.
|
|
- Side job, Sinnerman, and Claire from the fixed car save.
|
|
- Violence / No-Tell Motel cross-city routes.
|
|
- The Gig / Cassius Rider long Watson route.
|
|
- Aldecaldo / Badlands routes.
|
|
- Cyberpsycho sighting on the unfinished highway.
|
|
|
|
Add tests targeted at the new model:
|
|
|
|
- A straight long side-street corridor in Heywood vs a short freeway hop.
|
|
- A route with several 90-degree stair-step alternatives.
|
|
- A known suspended-highway offramp or hilly road that causes airtime at speed.
|
|
- AutoDrive on the same destination, to check whether its behavior remains sane.
|
|
|
|
## Practical Next Step
|
|
|
|
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+0xc4` for 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.
|