Files
2077-gps-mod/docs/traffic-system-debrief.md
T
2026-06-19 23:33:12 -05:00

441 lines
15 KiB
Markdown

# Cyberpunk 2077 Traffic/GPS System Debrief
This is based on the public NativeDB RTTI dump, WolvenKit exports from the local
Steam install, and the generated `all.traffic_persistent` resource from the
Phantom Liberty archive.
## Current Working Theory
The first hypothesis was that the in-game car GPS routes over the generated
traffic lane graph and consumes lane metadata such as `maxSpeed`, highway flags,
and lane connection probabilities. Live tests have not supported that:
- boosting highway `maxSpeed` changed freeway traffic speed but did not change
player GPS route choice
- changing `all.lane_connections` exit probabilities did not change player GPS
route choice
- probing the obvious `TrafficSystem_Pathfinding` and
`StartPathfinding`/`StopPathfinding` string-xref wrappers did not fire during
deliberate world-map route plotting
- probing the obvious `RunGPSQuery` and `UpdateGPSQuery` helpers also did not
fire during deliberate world-map route plotting
The stronger current theory is that player GPS route selection goes through a
native world-map mappin tracking path, which then updates native GPS state
downstream. Traffic lane data is still likely used somewhere in the final route
line, but the tested `maxSpeed` and lane-exit probability fields are traffic
simulation inputs, not the live player-GPS edge-cost knobs.
## Static GPS Query Candidates
Static disassembly of `Cyberpunk2077.exe` found script/native registration
thunks for `RunGPSQuery` and `UpdateGPSQuery`. The string xrefs themselves are
registration code, but their registered function pointers lead to more
interesting native wrappers:
```text
RunGPSQuery string rva: 0x2cd9978
RunGPSQuery registration xref: 0x147d068
RunGPSQuery wrapper rva: 0x29bd5ac
RunGPSQuery helper rva: 0x29bcf14
UpdateGPSQuery string rva: 0x2cd9968
UpdateGPSQuery registration xref: 0x147cfb4
UpdateGPSQuery wrapper rva: 0x29bd6c8
UpdateGPSQuery helper rva: 0x29bd254
```
`RunGPSQueryHelper` appears to read source/target vector-ish script arguments,
resolve a target object/mappin, build temporary bitsets/sets, and call helpers
near `0x204abdc` and `0x204ac1c`. Its return value is used as a success flag by
the wrapper.
`UpdateGPSQueryHelper` appears related but takes a request type and target. Its
return value is also used as a success flag.
These helpers were hooked with return-value-preserving signatures, not the
registration thunks:
```cpp
bool RunGPSQueryHelper(void* this_, void* from, void* to, void* debugText,
void* resultText, float maxDistance);
bool UpdateGPSQueryHelper(void* this_, uint32_t requestType, void* target,
void* debugText);
```
These signatures are inferred from the wrapper call sites and Windows x64
calling convention.
Live result: neither helper fired when plotting multiple world-map routes. They
remain useful static landmarks for GPS-like script APIs, but they are not the
world-map destination tracking path being exercised by normal player route
selection.
## Mappin Tracking Handoff
The first native lead was the script/native mappin tracking path:
```text
TrackMappin string rva: 0x2e82c30
TrackMappin registration xref: 0x1843eb8
TrackMappin wrapper rva: 0x27c4ab4
TrackMappin core rva: 0x27c11d0
Mappin system resolver rva: 0x02bb840
SetMappinTrackingAlternative string rva: 0x2c672e0
SetMappinTrackingAlternative registration xref: 0x121c194
SetMappinTrackingAlternative wrapper rva: 0x1246794
```
`TrackMappinCore` resolves a native mappin system through `0x02bb840`. The
resolver loads an object pointer from `this + 0xa8`, reads the mappin-system
type/singleton global at `0x342f138`, and calls vtable slot `0x08` on the
object it loaded.
Once the native mappin system is resolved, `TrackMappinCore` branches on whether
the supplied mappin handle is null:
- non-null tracked mappin: call resolved system vtable slot `0x220`
- null tracked mappin: call resolved system vtable slot `0x228`
That looks exactly like "set tracked mappin" versus "clear tracked mappin".
However, a clean live test disproved it as the normal world-map route plotting
path: `TrackMappin`, `TrackMappinCore`, and
`SetMappinTrackingAlternative` did not fire while plotting three map routes.
`SetMappinTrackingAlternative` parses two script arguments and calls vtable slot
`0x418` on its receiver. It is probably related to alternate tracking behavior,
but static disassembly and live logging have not shown it to be the main route
planner.
## World-Map Mappin Selection Lead
The next native lead is the world-map mappin controller block around
`0x1427c4314`. These functions are still mappin-facing, but they are closer to
selection, framing, and custom-position route display than the script helper
above:
```text
FrameMappinPath wrapper rva: 0x27c4314
FrameMappinPath core rva: 0x27bc1ec
SetSelectedMappin wrapper rva: 0x27c4a38
SetSelectedMappin by-id wrapper rva: 0x27c4944
SetSelectedMappin by-position wrapper rva: 0x27c49c4
SetSelectedMappin core rva: 0x27c1684
TrackCustomPositionMappin wrapper rva: 0x27c4aac
TrackCustomPositionMappin core rva: 0x27c2318
UntrackCustomPositionMappin wrapper rva: 0x27c4ba0
Mappin system resolver rva: 0x02bb840
```
`FrameMappinPath` calls the resolved mappin-system vtable slot `0x280`.
`TrackCustomPositionMappin` calls slot `0x2f0` to create a custom-position
mappin/route handle and slot `0x1f0` to update an existing one. Those calls are
currently the most route-like native behavior found in static disassembly.
The installed RED4ext probe now logs the wrappers and cores listed above. When
one of them can resolve the mappin system, it also logs concrete vtable RVAs for
slots `0x1f0`, `0x220`, `0x228`, `0x250`, `0x280`, `0x2e0`, `0x2f0`, `0x368`,
and `0x3a8`, then attaches temporary hooks to the route-adjacent slots.
## The High-Level Shape
Cyberpunk appears to split "navigation" into at least two major domains:
- pedestrian/NPC navigation over navmesh resources
- vehicle/GPS routing over generated traffic lane resources
The public script/native type dump exposes `worldNavigationScriptInterface` and
`AINavigationSystem` methods for navmesh path queries, but those are character
navigation tools. They are not the car GPS. The car GPS system type exists as
`gamegpsGPSSystem`, but its public RTTI surface does not expose planner methods
that a REDscript mod can override.
That means the practical mod surface is not yet settled. The data-only surface
has been tested and appears insufficient. The next credible surface is a
RED4ext hook in the native mappin/GPS handoff, after the concrete target
function has been identified.
## Relevant Game Types
The useful RTTI types are:
- `gamegpsGPSSystem`
- `gamegpsSettings`
- `worldTrafficPersistentResource`
- `worldTrafficPersistentData`
- `worldTrafficLanePersistent`
- `worldTrafficLanePlayerGPSInfo`
- `worldTrafficLanePersistentFlags`
`gamegpsSettings` contains display/refresh settings:
- `lineEffectOnFoot`
- `lineEffectVehicle`
- `fixedPathOffset`
- `fixedPortalMappinOffset`
- `pathRefreshTimeInterval`
- `lastPlayerNavmeshPositionRefreshTimeIntervalSecs`
- `maxPathDisplayLength`
Those affect GPS presentation and refresh behavior, not edge selection.
`worldTrafficPersistentResource` is the key resource type. Its root chunk has
`data: worldTrafficPersistentData`, which has:
- `lanes: array<worldTrafficLanePersistent>`
- `neighborGroups: array<array<Uint16>>`
Each lane has the actual graph metadata:
- `outLanes`
- `inLanes`
- `outline`
- `accumulatedLengths`
- `crowdCreationInfo`
- `maxSpeed`
- `deadEndStart`
- `length`
- `width`
- `area`
- `flags`
- `subGraphId`
- `playerGPSInfo`
- `neighborGroupIndex`
- `nodeRefHash`
- `laneNumber`
- `seqNumber`
- `isReversed`
- `roadMaterials`
- `polygon`
`playerGPSInfo` contains:
- `subGraphId`
- `stronglyConnectedComponentId`
That strongly suggests the GPS planner has a precomputed connectivity graph and
uses connected-component/subgraph metadata to reject impossible paths quickly.
## Resource Location
In this install, the important resource is:
```text
base\worlds\03_night_city\sectors\_generated\traffic\all.traffic_persistent
```
It exists in both the basegame Night City archive and the Phantom Liberty Night
City archive. For a Phantom Liberty install, the EP1 archive version should win:
```text
archive/pc/ep1/ep1_1_nightcity.archive
```
WolvenKit can extract, serialize, deserialize, and pack this resource on Linux
through `wolvenkit.cli` 8.18.1.
## Lane Flags
`worldTrafficLanePersistentFlags`:
```text
FromRoadSpline = 1
Bidirectional = 2
PatrolRoute = 4
Pavement = 8
Road = 16
Intersection = 32
NeverDeadEnd = 64
TrafficDisabled = 128
CrossWalk = 256
GPSOnly = 512
ShowDebug = 1024
Blockade = 2048
Yield = 4096
NoAIDriving = 8192
Highway = 16384
NoAutodrive = 32768
```
The important flags for GPS weighting are `Road`, `Intersection`, `GPSOnly`, and
`Highway`. The important flags to avoid touching are `Pavement`, `CrossWalk`,
`TrafficDisabled`, and `Blockade`.
## Observed Lane Distribution
The local EP1 `all.traffic_persistent` resource contains `33,952` lanes when
extracted from the archive's raw compressed segment.
Observed `maxSpeed` distribution:
```text
all lanes:
1: 12852
15: 12107
10: 5859
17: 3027
14: 96
8: 8
9: 2
4: 1
highway lanes:
17: 2700
15: 164
1: 2
road lanes:
15: 11943
10: 671
17: 327
14: 96
8: 4
pavement lanes:
1: 12848
10: 5188
8: 4
9: 2
4: 1
```
The values are not real-world mph/kph. They are compact game-scale lane speeds.
Because highways are already mostly `17` and normal roads are mostly `15`, a
large value like `90` was expected to be reckless. The first conservative
archive used:
- highway speed target: `25`
- road speed floor: `15`
That changes `2,519` lanes:
- `2,429` highway boosts
- `90` road floor boosts
It leaves pavement/crosswalk/blocked/disabled lanes alone.
## What The Data Archive Experiment Did
The data archive experiment did not replace the pathfinding algorithm.
It edited the traffic graph's lane metadata so the native GPS planner might have
better cost inputs. The original hypothesis was that if the planner factored
`maxSpeed` into traversal cost in the usual way, then a segment with the same
physical length but higher `maxSpeed` would become cheaper. That should have
nudged the native planner toward highways and high-capacity roads without
hooking the planner itself.
Conceptually:
```text
old cost ~= lane.length / lane.maxSpeed
new cost ~= lane.length / patched_lane.maxSpeed
```
Live testing disproved this as a player-GPS lever. The patched value clearly
reached the game because traffic speed changed, but player GPS route choice did
not.
## Why Not Just Write A New A*?
A standalone A* is easy. Integrating it into Cyberpunk's GPS is the hard part.
The game already has:
- traffic lane connectivity
- lane polygons/outlines
- player lane matching
- destination lane matching
- dynamic refresh
- minimap/world map rendering
- quest mappin target integration
- portal/entrance handling
- route line effects
The script-facing `gamegpsGPSSystem` type does not expose hooks for replacing
that pipeline. A custom planner would still need a way to feed its result back
into the GPS renderer and route-following systems. Without native RED4ext hooks,
that is much riskier than patching the resource data the existing pipeline
already consumes.
## Packaging Flow
The working flow is:
1. Read the archive index and extract the exact compressed segment for resource
hash `3419764573789342681`.
2. Decompress that KARK segment with WolvenKit CLI's Oodle command.
3. Serialize the CR2W resource to JSON.
4. Patch lane `maxSpeed` values.
5. Deserialize the patched JSON back to CR2W.
6. Put the CR2W back under the original resource path.
7. Pack an archive.
8. Place the archive in `archive/pc/mod`.
Do not use plain `cp77tools extract` for this resource. In this install,
WolvenKit CLI extracted a `9,984,106` byte CR2W, while the stock archive segment
declares and decompresses to `10,673,648` bytes. A vanilla control archive built
from the smaller extraction crashed during load. A vanilla control archive built
from the raw segment reached the game and the GPS worked normally.
The generated archive currently contains exactly:
```text
base\worlds\03_night_city\sectors\_generated\traffic\all.traffic_persistent
```
## Risks And Unknowns
The first major finding from in-game testing is that `maxSpeed` is not a useful
GPS weighting lever. Raising highway lanes as high as `120` did not change GPS
routes in the test case, but it did make freeway traffic noticeably faster. That
means `maxSpeed` should be treated as traffic simulation data, not GPS route
cost data.
The actual lane adjacency is not populated in `all.traffic_persistent`; every
lane's `outLanes` array serialized empty. The adjacency is in:
```text
base\worlds\03_night_city\sectors\_generated\traffic\all.lane_connections
```
That resource is a `worldTrafficPersistentLaneConnectionsResource` with one row
per lane. Each row has `inLanes` and `outlanes`. Each outgoing connection
contains `laneIndex`, `exitProbabilityCompressed`, `isSharpAngle`,
`nextLaneEntryPosition`, and `thisLaneExitPosition`.
There is still no explicit edge cost field. A follow-up data-only test patched
`exitProbabilityCompressed`, strongly favoring road/GPSOnly/highway connections
that enter or remain on highways and lowering competing non-highway exits. That
archive changed 145 outgoing connection probabilities across 136 lanes. It also
loaded and ran, but it did not change the tested player GPS routes.
The Oodle library was not available inside the toolbox, so WolvenKit packed with
its Kraken fallback. A vanilla full-resource control archive loaded in game and
GPS worked normally, so the earlier crash was caused by bad extracted data rather
than the Kraken-packed archive container.
The patch targets the EP1 traffic resource because Phantom Liberty is installed.
On a non-PL install, the basegame archive resource would need to be patched
instead.
## Future Improvements
Better versions of this mod could:
- hook the native destination-tracking handoff after the concrete mappin-system
method is identified
- trace the downstream GPS update that consumes the tracked mappin
- locate any native or baked edge-cost data not exposed in RTTI
- inspect `GPSOnly` connector lanes if a later native trace proves they are
consumed by the player GPS
- build district-specific presets if a real data-cost surface is found
- generate a diff report of every changed lane with flags, length, speed, and
graph component
- ship multiple archives only if a data patch is proven to affect routing
The next practical step is one more in-game route-plotting run with the current
RED4ext mappin-system slot probe installed. If it logs concrete slot RVAs, the
next static-disassembly target is no longer a guess.