# 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 has two
front-doors:

- quest/objective pins are committed by updating the native journal's tracked
  entry
- custom/player pins are committed through native mappin tracking

Both eventually update 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.

Live result: this lead appears to be the hover/selection layer, not the route
commit layer. In a controlled test window, hovering a world-map icon and then
moving back to empty map space fired `SetSelectedMappinWrapper` and
`SetSelectedMappinCore`. Pressing the route/track action on that icon did not
fire `FrameMappinPath`, `TrackCustomPositionMappin`, `TrackMappin`, the tracked
mappin slots, or any of the custom-position slots.

That pushed the search one layer higher, into the world-map controller script
methods around tracking an objective or setting a waypoint. Static REDscript
decompilation now gives a clear high-level route action path:

```text
WorldMapMenuGameController.OnPressInput
  -> HandlePressInput
    -> TryTrackQuestOrSetWaypoint
      -> TrackQuestMappin
        -> JournalManager.TrackEntry
```

For non-quest/player pins the same `TryTrackQuestOrSetWaypoint` function calls
`TrackMappin`. For custom pins it calls `TrackCustomPositionMappin`, which
creates or updates a custom-position mappin and then tracks it.

Live result: custom waypoint routing fired the native custom-position path:

```text
TrackCustomPositionMappin wrapper/core
MappinSystem create-custom-position slot 0x2f0
TrackMappin core
MappinSystem set-tracked slot 0x220
```

Quest/objective route plotting did not fire those mappin hooks. The script
decomp explains why: quest pins go through `JournalManager.TrackEntry`, not
`TrackMappin`.

## REDscript Route Surface

The decompiled `WorldMapMenuGameController` is useful as an input-routing map,
not as the planner implementation.

Important script observations:

- `HandlePressInput` calls `TryTrackQuestOrSetWaypoint` for
  `world_map_menu_track_waypoint`.
- `TrackQuestMappin` extracts the selected mappin's journal entry and calls
  `JournalManager.TrackEntry`.
- `UpdateTrackedQuest` reads `JournalManager.GetTrackedEntry`, asks the mappin
  system for quest mappin positions with `GetQuestMappinPositionsByObjective`,
  and updates world-map UI text/position state. It does not compute the GPS
  route.
- `GPSSystem` is present as a native class, but its REDscript surface is empty.
- `GPSSettings` is presentation/refresh data: line effects, fixed path offsets,
  refresh intervals, and display length.
- `NavigationFunctionalTests.GetPathOnNavmesh`, `RunGPSQuery`, and
  `UpdateGPSQuery` exist, but runtime tests showed they are not called by normal
  map route plotting.

The remaining route planner target is therefore native code reacting to either
tracked-journal-entry changes or mappin tracking changes.

Native direct-call scanning supports that split:

```text
RunGPSQuery helper        RVA 0x29bcf14  direct callers: 1 wrapper caller
UpdateGPSQuery helper     RVA 0x29bd254  direct callers: 1 wrapper caller
JournalManager.TrackEntry RVA 0x5944fc   direct callers: 13
```

The GPS query helpers appear to be exposed helper/test surfaces, not the route
path used by the world map. `TrackEntry` is now the highest-confidence native
handoff for quest/objective route plotting.

## Journal Listener Fanout

`JournalManager.TrackEntry` at RVA `0x5944fc` is now confirmed at runtime as the
quest/objective route-plot entry point. A controlled test produced route-plot
calls at `06:32:44`, `06:32:50`, and `06:32:56`, all from return RVA
`0x26ac34e`, which is the script/native wrapper path for `TrackEntry`.

The native `TrackEntry` implementation updates the tracked-entry state and then
fans out through a listener array:

```text
JournalManager + 0x210: listener pointer array
JournalManager + 0x21c: listener count
listener vtable + 0x28: one tracked-entry callback path
listener vtable + 0x30: another tracked-entry callback path
listener vtable + 0x50: untrack/default path
```

Before the save is fully loaded the listener count is small. After the world map
is opened it rises to 254. Most entries use the no-op callback `0x14a700`, but
the live route test repeatedly exposed this small non-default set:

```text
0xea89a8 / 0xea8958
0xe63f80 / 0xe63e6c
0xe63f00 / 0x8d136c
0x431a34 / 0x55a4e4
0x14de238
```

Static disassembly of these callbacks suggests the following split:

- `0x14a700` is a plain empty return.
- `0xea89a8` and `0xea8958` are objective/story tracking callbacks; their vtable
  lives beside a `quests/street_stories/.../generic_sts_objective` string.
- `0x431a34`, `0x55a4e4`, and `0x14de238` are heavy UI/tracker state callbacks.
- `0xe63f80` and `0xe63e6c` remain the most promising route-refresh callbacks:
  they read nested listener fields at `+0x70` and `+0x78` and call high vtable
  slots such as `0x218`, `0x230`, `0x240`, `0x340`, and `0x348`.

The installed probe now hooks those non-default callbacks directly and logs the
nested owner/service vtables. A later controlled run narrowed the quest route
handoff further:

```text
JournalManager.TrackEntry
  -> JournalListener::Route callbacks at 0xe63f80 / 0xe63e6c
    -> journal/mappin route bridge candidate at 0x598250
      -> MappinSystem route-event enqueue at 0x13763d8
        -> route-event handler at 0xaa62d0
          -> activate route ref at 0xaa6330
          -> or deactivate route ref at 0x27abd7c
```

The route listener object has two important nested service pointers in the live
logs:

```text
listener + 0x70: mappin system, vtable 0x14310e300
listener + 0x78: journal manager, vtable 0x1430f0890
```

The mappin system slot `0x240` points to RVA `0x13763d8`. Static disassembly
shows it is not the solver. It builds a small queued event with vtable
`0x14310e1c0`:

```text
event + 0x08: event type byte, value 5
event + 0x18: route/journal id
event + 0x20: mappin system pointer
event + 0x28: active/unactive bool
```

The event handler at `0xaa62d0` reads that bool and calls either `0xaa6330` or
`0x27abd7c` with the mappin system and a route reference taken from the event
payload. Both target functions look like active-route state toggles: they look
up a route ref in a map at `mappinSystem + 0x1a0`, set a byte at route object
offset `0x84`, then notify observers from the list at `mappinSystem + 0x280`
through virtual callbacks at offsets `0x48`/`0x50`.

Static xrefs also expose a shared route-change helper at `0x27aa2d8`, which
deactivates an old route ref and activates a new route ref. That is likely the
common mappin state transition path for both journal/objective routes and
custom/player pins.

The newest controlled run confirms the queued route event timing:

```text
quest route click
  -> JournalManager.TrackEntry from wrapper return RVA 0x26ac34e
    -> MappinSystem route-event enqueue old route active=0
    -> MappinSystem route-event enqueue new route active=1
      -> 11-18 ms later: MappinRouteEvent::Handle
        -> MappinSystem::RouteDeactivate(old route key)
        -> MappinSystem::RouteActivate(new route key)
          -> route observers at system + 0x280
```

The click-to-event delay matched the in-game behavior: the old GPS path
disappears for a few frames, then the new path appears. That makes the route
observer fanout the next most useful runtime layer to inspect.

The activation/deactivation argument is not an object pointer. It is a 64-bit
route key. Static disassembly of `0xaa6330` and `0x27abd7c` shows that key being
looked up in the active-route map at `mappinSystem + 0x1a0`; the resulting map
entry's `+0x08` field points at the actual route object/smart pointer passed to
route observers.

The first observer callback set seen at runtime is:

```text
observer vtable 0x14310e760: slot 0x48 -> 0xaa6610,  slot 0x50 -> 0x27b10c0
observer vtable 0x143136050: slot 0x48 -> 0xaa6628,  slot 0x50 -> 0x295d4a0
observer vtable 0x143121458: slot 0x48 -> 0xaa63e0,  slot 0x50 -> 0x286a85c
```

The installed probe now hooks those callbacks directly and logs the route entry,
route object, route object vtable slots, and the route object's active byte at
offset `0x84`.

The first observer-object run separated three route-object vtable families:

```text
vtable 0x142b31138: quest/objective route objects, key at +0x40
vtable 0x142af1510: quest/objective route objects, key at +0x40
vtable 0x142b30e20: custom/player pin route objects, key at +0x40
```

`RouteActivate` and `RouteDeactivate` really do flip the route object's active
byte at `+0x84`. The observer callbacks receive the same route entry after that
flip.

Static disassembly of observer0 (`0xaa6610 -> 0xaa6678`) shows it consuming
route-object virtuals at `+0x1a8`, `+0x188`, `+0x140`, and `+0x198`.
`+0x198` returns a route enum from a nested descriptor; `+0x188` returns a
descriptor payload pointer; and `+0x140` is a route-family-specific boolean.
That observer stores derived route state into buckets under the observer object,
choosing one bucket only when the enum is `0x8e`.

Static disassembly of observer1 (`0xaa6628`) is now the highest-value
downstream branch. It resolves a route object, then calls a generic system
getter with a runtime type/global at `.data` RVA `0x342f6a8`, calls the returned
service's vtable slot `0x220` with `routeObject + 0x8c`, and stores generated
handles/strings under its inner object at offsets around `+0x180`, `+0x1a0`,
`+0x1c0`, and `+0x1e0`. The current probe logs that service-owner path and
`routeObject + 0x8c` so the next runtime test can tell whether observer1 is
crossing into GPS/path computation or just updating presentation state.

The `07:22:14` controlled run confirmed observer1 is in the deliberate route
click window, not just load-time setup. The three quest route clicks produced
the expected sequence:

```text
JournalManager.TrackEntry
MappinSystem.RouteEventEnqueue old active=0 / new active=1
MappinSystem.RouteDeactivate old route key
MappinSystem.RouteActivate new route key
RouteObserver1 deactivate/activate with the matching routeObject + 0x8c route id
```

The observer1 service-owner object was stable across those calls
(`serviceOwner` vtable `0x142b74ea8`, slot `0x08` target `0x140287c44`).
Static disassembly of `0x140287c44` shows a generic type-to-service lookup, so
the next probe hooks that function, filters on the runtime type pointer at
`0x342f6a8`, logs the returned service object, and dynamically hooks the
returned service's vtable slot `0x220`. That slot is the immediate next native
call after the lookup and receives the route id from `routeObject + 0x8c`.

The most route-building-looking static candidate currently in this chain is
`0x5625a4`, called once from the bridge at `0x5984ec`. The bridge resolves
journal route ids through virtual journal-manager calls, collects several route
descriptor/data pointers, and passes them to `0x5625a4`. Runtime evidence now
shows this function firing in a dense load-time/minimap setup burst, not during
later deliberate map-click route plotting. It may build cached route descriptors
or GPS display metadata, but it is probably not the direct click-time solver
entry point.

## Native False Positives

Static string scans found a tempting traffic/pathfinding cluster around RVA
`0x512000` with messages such as:

```text
Pathfinding Algorithm Failed
Find Straight Path Failed
No Path Found in Traffic
There's no point found to reach traffic
```

Disassembly around the string xrefs shows those strings being loaded into a
large constructor/settings/result-description table rather than a solver loop.
Related functions around `0x50f680`, `0x50fc24`, `0x513430`, and
`0x513824` clearly touch traffic/path data, but their direct caller context
references vehicle behavior and stuck-detection settings:

```text
vehicles.common.stuck_detection_check_distance
vehicles.common.stuck_detection_interval
DriveState*
```

That cluster is likely autonomous vehicle traffic/path behavior. It may share
the same road graph as GPS, but it is not yet evidence of the player map-route
planner.

The `GPSSystem/Tick` string is also mostly a profiling/event landmark. Nearby
code builds profiling scopes and RTTI/type registration scaffolding; it has not
yet exposed a clean planner function.

## 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.

## Native Async Traffic Route Pipeline

The later RED4ext probes moved the active lead away from journal/mappin fanout
and into the native async traffic route pipeline.

The useful runtime path is:

```text
GPS query dispatch          0x70a570
traffic request enqueue     0x8d17d8
traffic result take         0x709d5c
route-record conversion     0x44a398
route postprocess           0x44830c
```

`0x8d17d8` enqueues work into the traffic route service. `0x709d5c` later takes
the finished result from the service result table, and `0x44830c` postprocesses
the traffic result into the path object consumed by the game. Route records
captured at this stage matched `worldTrafficLanePersistent.nodeRefHash` values
from `all.traffic_persistent`, which confirms that the displayed/player GPS path
is backed by the traffic lane graph even though `maxSpeed` and connection
probability patches did not affect route choice.

Later static work found the actual graph-search loop upstream of those result
records:

```text
route producer             0x44cc7c
packed-handle resolver     0x44e1a8
path search loop           0x44f054
path materializer          0x44eb68
heuristic distance helper  0x44f7bc
edge-cost callback         0x44f838
base provider filter       0x450b08
filtered provider filter   0x44ff68
```

`0x44f054` is an A*/Dijkstra-style open-list search. It validates packed start
and target handles, pushes the start node, repeatedly pops the lowest-priority
state, checks the target handle, traverses adjacent handles from the current
navigation segment, calls a provider filter through vtable slot `+0x08`, calls
edge cost through vtable slot `+0x10`, and records the predecessor/cost when a
neighbor improves.

Both the base and filtered cost providers use `0x44f838` for slot `+0x10`.
That function computes geometric distance between the current and neighbor
points and multiplies it by:

```text
provider[0x08 + ((neighborPoint[0x13] & 0x3f) * 4)]
```

The base provider constructor at `0x451158` initializes all 64 class
multipliers to `1.0`. The filtered provider constructor at `0x44b9b4` reuses the
same edge-cost function and mainly adds route masks/special zones. For normal
map routes, this means the live GPS edge cost is effectively geometric distance
over a compact navigation graph, not `lane.length / lane.maxSpeed`.

The packed handles resolved by `0x44e1a8` point into `VAND` navigation blobs
loaded from `base\worlds\03_night_city\_compiled\default\navigation_*.streamingsector`.
The 0x14-byte point record has:

- `+0x00`: adjacency/index field used by the search loop
- `+0x10`: 16-bit provider mask
- `+0x12`: small point metadata byte
- `+0x13`: packed route point class/flags; low six bits are the class used by
  `0x44f838`

The helper `tools/analyze_vand_navigation.py` decodes these blobs from
streamingsector JSON. In the three extracted navigation samples, all blobs were
version 8, all point masks were `0x0003`, and the dominant classes were
`1`, `4`, `5`, and `3`. That matches the runtime edge-cost logs. This explains
why the earlier `all.traffic_persistent` patches changed vehicle simulation but
did not steer player GPS: the route chooser is walking the baked navigation
`VAND` graph, then later materializing/copying results that can be joined to
traffic lane hashes.

Static disassembly found a small traffic candidate scorer at `0x8d46cc`:

```text
score ~= sqrt(candidate + 0x08)
      + 2 * abs(candidate_z-ish - reference_z-ish)
```

It reads lane flags from `[candidate_lane + 0x88]` and branches on
`query + 0x41`:

- `query41 != 0`: pavement lanes (`0x0008`) get a vanilla `+20.0` penalty.
- `query41 == 0`: road-flagged lanes (`0x0010`) get a vanilla `*1.75`
  penalty and caller `0x8d4630` applies an additional geometry normalization.

Only two direct callers reach this scorer:

```text
0x8d4568 -> 0x8d46cc
0x8d466f -> 0x8d46cc
```

Both iterate 0x30-byte candidate records and keep the lowest score. This makes
`0x8d46cc` a bounded candidate/projection selector, not the full city-wide graph
search loop.

An experimental hook multiplied the returned score by lane flag:

- highway: `0.35`
- road: `0.70`
- GPSOnly: `1.35`
- pavement: `2.75`
- other: `1.50`

Live logging proved the hook was attached and the corrected mode check was
active:

```text
pavement query41=1 base=37.854 multiplier=2.750 result=104.099
road     query41=1 base=23.986 multiplier=0.700 result=16.790
highway  query41=1 base=39.640 multiplier=0.350 result=13.874
```

However, that detour crashed during the load-time GPS warmup. The crash report
was an access violation reading `0x71`, and the crash reproduced until the
RED4ext plugin was disabled. The hook is therefore disabled by default in source
(`kEnableGpsTrafficEdgeWeightPatch = false`) and the local installed DLL is
renamed to `EdgeWeightGPS.dll.disabled`.

The main takeaway is still valuable, but narrower than first suspected:
traffic lane flags are available in native route-side endpoint/candidate code,
and `0x8d46cc` is an excellent landmark. It is not the city-wide graph-cost
function.

The current prototype therefore targets `0x44f838` instead. It generates a
compact spatial grid from traffic lane polygons/flags and samples that grid
from the search-state coordinates passed to the edge-cost callback. The first
test multipliers are:

```text
highway  0.62
road     1.00
GPSOnly  1.20
pavement 1.35
unknown  1.05
```

This deliberately leaves VAND point class bits untouched. The materialization
path branches on those bits after search, so rewriting them in data could alter
route construction or instructions as well as cost.

Runtime logs also show that long visible routes are decomposed into many local
`0x44cc7c` route-producer searches. The tempting world-map caller
`ret_rva=0x70aa22` mostly produced tiny connector searches in the resolver
logs, so the first spatial test should patch edge cost across route-producer
calls instead of gating only on that caller.

The spatial edge-cost prototype is now also disproved as the decisive
world-map GPS weighting surface. In
`logs/EdgeWeightGPS_spatial_inversion_no_route_change_1845.log`, the plugin
first ran with highway-free weights (`H=0.05`, all other spatial classes
`19.0`), then hot-reloaded the exact inverse (`H=19.0`, all others `0.05`).
The four repeated world-map routes after the flip produced final
`GPSQueryResultFetch 0x7094b8` 0x28-byte route records that were exact matches
for the pre-flip routes: identical lane handles, identical packed metadata, and
identical record counts. Therefore `0x44f838` remains a real local graph edge
cost callback, but changing it does not change the final selected route records
for the tested full player GPS routes.

Static disassembly after that failure found a separate, broader async traffic
solver upstream of final route-record materialization. The useful path is:

```text
map route submit                    0x8d20d4
shared query submit                 0x70a42c
query dispatch                      0x70a570
async request enqueue               0x8d17d8 / 0x8d1880
worker queue loop                   0x126c06
candidate/projection setup          0x883cd8
solver start wrapper                0x1caa7a0
solver candidate initialization     0x88367c
route job start/advance             0x818928
solver advance loop                 0x40ade4
neighbor expansion                  0x40b304 / 0x40b540
relax/update helper                 0x40ba58
output route build                  0x818ba8
```

This path is distinct from the local `0x44f054` VAND search. The worker around
`0x126c06` pulls queued route requests from `routeSystem+0x47c0`. Depending on
state it either calls `0x818928` directly or first calls `0x883cd8`, which
resolves/projects the start and end positions into candidate arrays. The
wrapper at `0x1caa7a0` starts the solver by storing the route mode at
`solver+0xc4`, writing route masks at `solver+0xc0/+0xc2`, copying candidate
arrays into `solver+0x70/+0x80`, and calling `0x88367c`.

`0x88367c` marks start candidate lane/node records with `node+0x9a |= 1`, marks
end candidates with `node+0x9a |= 2`, computes initial node-state costs at
offsets `+0x10/+0x14`, and queues initial states into the open list at
`solver+0xd0`.

`0x40ade4` is the full-route solver advance loop. It processes the open list
while `solver+0xdc != 0`, calls helpers including `0x40b304` and `0x40b540` to
expand neighbors, and marks completed node states with byte `+0x22 = 2`.
Current lane/node records are addressed as:

```text
laneNode = solver + 0xf8 + currentNodeIndex * 0xa0
```

`0x40b304` walks one neighbor list from the current record; `0x40b540` walks a
second list with a slightly different edge-record shape. Both filter neighbors
through `0x40b774`, which reads the 16-bit lane/node flags at `node+0x88`:

```text
pass iff (solver+0xc0 & flags88) != 0
     and (solver+0xc2 & flags88) == 0
```

The main cost equation visible in `0x40b304` is:

```text
g = current.g + progress_delta
              * 0x40bb40(solver, currentLaneNode)
              * directionMultiplier
h = distance(neighborPosition, solverTargetPosition)
f = g + h
```

`directionMultiplier` is normally `1.0`; for reverse/special progress branches
it can come from `0x40bb00(solver, currentLaneNode)` or become `FLT_MAX`.
`0x40ba58` is the relax/update function. When a candidate improves, it writes
`f` to node-state `+0x14`, `g` to `+0x10`, progress to `+0x18`, predecessor to
`+0x1e`, marks state byte `+0x22 = 1`, and pushes the state back into
`solver+0xd0`.

The decisive vanilla multiplier is `0x40bb40`. For driving-style modes it reads
the same `node+0x88` flags and handles them roughly as:

```text
if mode == 1:
  pavement flag 0x0008 -> 1.0
  otherwise            -> 1.5

if mode in {0, 2, 4}:
  if Road flag 0x0010:
    base = 1.0 or 1.2 depending on a low node flag
    if node+0x93 == 1: base *= 1.1
    return base
  else:
    return table[solver+0x54]

fallback:
  return 1.0
```

The non-road table at `0x3154d28` starts:

```text
10.0, 6.0, 2.0, 0.0, 4.0, 2.5, 1.5, 0.0
```

The auxiliary multiplier at `0x40bb00` usually returns `1.1` for road cases,
or, for one mode-2 branch, reads a smaller table at `0x3154d38`:

```text
4.0, 2.5, 1.5, 0.0
```

The important design finding is that vanilla has a real edge-cost surface in
the final async solver, but it does not special-case the `Highway` flag
(`0x4000`). A highway lane that is also a road is simply a road. That matches
the original symptom: the solver is weighted enough to avoid obviously invalid
surfaces, but it has no strong reason to stay on highway corridors for long
vehicle trips.

The first RED4ext prototype tried to hook `0x40bb40` as a normal C++ detour.
That found the correct function but crashed during load-time GPS warmup because
the optimized caller expects the tiny vanilla helper not to disturb volatile
register state used immediately afterward.

The stable prototype now patches inline at the road-tail block `0x40bb98`
instead. Reaching that block already means the vanilla function selected one of
the road-style modes `0/2/4`; pedestrian mode `1` uses a different branch. The
patch preserves vanilla `node+0x93` behavior, checks `node+0x88 & 0x4000`, and
applies an extra multiplier only for highway lanes.

The proof multiplier was:

```text
Highway  0.35
```

Live A/B testing confirmed this is the decisive world-map route-cost lever.
With `0.35`, routes visibly changed and a custom pin around the city-center
roundabout took an obviously bad highway/ramp hairpin. With the inline patch
disabled, the same route returned to the straight vanilla path through the
roundabout.

The current installed build uses a default highway multiplier of `0.80` and
hot-reloads `solver_weights.bin` from the plugin directory. The file is exactly
one little-endian float32. `tools/write_solver_weights.py` writes the installed
file and can generate named presets:

```text
vanilla              1.00
mild                 0.90
default              0.80
strong               0.70
proof-highway-cheap  0.35
highway-expensive    3.00
```

The patch intentionally does not hard-gate on `solver+0xc4 == 2`, because
autodrive-like local vehicle navigation may use another road-style mode. Since
the patch lives in the vanilla road-tail branch, it applies to road-style modes
`0/2/4` and still leaves pedestrian mode `1` alone.

`0x40bb00` is not patched yet. If tuned highway weights still produce
reverse/turnaround artifacts, inspect or patch that auxiliary multiplier so
reverse/special progress branches receive a compatible policy.

## Future Improvements

Better versions of this mod could:

- tune the `0x40bb98` inline highway multiplier from the current `0.80` default
  using live `solver_weights.bin` presets
- inspect and, if needed, patch `0x40bb00` so reverse/special progress branches
  receive the same road-class policy as forward expansion
- extend `solver_weights.bin` beyond one highway float only if GPSOnly/pavement
  tuning becomes necessary
- replace the crashing `0x8d46cc` detour with a narrower inline patch or a
  safer caller-side hook
- decode enough of the `VAND` graph to join navigation points to traffic lane
  polygons and highway flags
- extract the full Night City navigation streamingsector set into workspace
  storage and summarize it with `tools/analyze_vand_navigation.py`
- keep the generated spatial grid as a local-search diagnostic, but do not
  treat its multipliers as a proven world-map route weighting knob
- add route-mode gating if the spatial edge-cost hook affects pedestrians,
  autodrive, or other local navigation too broadly
- 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
