# RED4ext logging shim

`EdgeWeightGPS` is a RED4ext plugin used to probe native GPS/mappin code paths
without changing gameplay data.

It writes `EdgeWeightGPS.log` beside the installed DLL:

```text
Cyberpunk 2077/red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.log
```

Current behavior:

- Logs plugin load/unload.
- Logs the RED4ext plugin handle and SDK pointer value passed to `Main`.
- Registers Running game-state callbacks so route tests can be timestamped.
- Hooks selected native wrappers and cores around `GPSSystem`, mappin tracking,
  world-map mappin selection, and map path framing.
- Hooks `JournalManager.TrackEntry` at RVA `0x5944fc`, which is the native
  handoff used by quest/objective world-map route plotting.
- Dumps the `JournalManager` listener array at offsets `0x210`/`0x21c` and the
  listener vtable slots `0x28`, `0x30`, and `0x50`, matching the indirect calls
  made by the native `TrackEntry` implementation.
- Hooks the non-default journal listener callbacks seen during route plotting:
  `0xea89a8`, `0xea8958`, `0xe63f80`, `0xe63e6c`, `0xe63f00`,
  `0x8d136c`, `0x431a34`, `0x55a4e4`, and `0x14de238`.
- For those listener callbacks, logs the listener object, event pointer, common
  object fields, and vtable slots on candidate nested owner/service objects.
- Hooks the route handoff found from the journal listener bridge:
  `0x598250`, `0x13763d8`, `0xaa62d0`, `0xaa6330`, `0x27abd7c`, and
  `0x5625a4`.
- Logs mappin route-event fields, active/deactive route keys, the mappin
  active-route map at `system + 0x1a0`, and the route observer list at
  `system + 0x280`.
- Hooks the route observer callbacks reached by route activation/deactivation:
  `0xaa6610`, `0xaa6628`, `0xaa63e0`, `0x27b10c0`, `0x295d4a0`, and
  `0x286a85c`.
- Logs the route entry pointer and route object pointer passed to those
  observers, including route object vtable slots and common fields such as the
  active byte at offset `0x84`, the service lookup key at offset `0x8c`, and
  the runtime observer1 service-owner path used before its vtable `0x220`
  lookup.
- Hooks observer1's generic service lookup at `0x287c44`, filters it to the
  runtime type pointer at `.data` RVA `0x342f6a8`, logs the returned service
  object and slot `0x220`, then dynamically hooks that service `0x220` route
  lookup to log its route-id output handle.
- The observer1 service branch was later identified as a false positive:
  `0x287c44` returns the `JournalManager` object (`vtable 0x1430f0890`) and
  the dynamic slot `0x220` resolves route IDs to readable journal/UI metadata
  strings such as `internet`, `home`, `clubs`, and `arasaka`. It is now left in
  the source but no longer attached in the active probe.
- Hooks the GPS query lifecycle found from the `RunGPSQuery`/`UpdateGPSQuery`
  native registration cluster:
  `0x29bd128` (`RunGPSQuery` body), `0x29bd254` (`UpdateGPSQuery` body),
  `0x70a42c` (shared query submitter), `0x70a570` (low-level query dispatch),
  `0x7094b8` (query result/path fetch), and `0xaa5704` (query status check).
- Logs query endpoints, returned query IDs, shared submitter return RVAs, query
  state fields, status values, result-fetch success, and summarized path
  buffers/point counts.
- The active probe now records query IDs returned from `0x70a42c` and always
  logs result/status polling for those tracked IDs. Startup polling of query
  IDs `0`/`1` is sampled only briefly so it cannot exhaust the route-click log
  budget.
- Resolves the native mappin system when one of those paths fires, logs relevant
  vtable slot addresses, and temporarily hooks the route-adjacent slots.

Active build note:

- The currently installed build is active as
  `red4ext/plugins/EdgeWeightGPS/EdgeWeightGPS.dll`.
- Source defaults enable the stable inline solver-cost patch at `0x40bb98`
  (`kEnableGpsNodeMultiplierInlinePatch = true`) and keep the old full-function
  `0x40bb40` C++ detour disabled (`kEnableGpsMultiplierHooks = false`).
- The inline patch preserves vanilla road cost except for highway lanes in the
  road-tail branch. Highway lanes are multiplied by the live value in
  `solver_weights.bin`, defaulting to `0.80`.
- The patch is intentionally not hard-gated on `solver+0xc4 == 2`; the vanilla
  branch already limits it to road-style modes `0/2/4`, which keeps manual
  drive and autodrive-like callers from diverging while leaving pedestrian mode
  `1` alone.
- `solver_weights.bin` lives beside the DLL and is exactly one little-endian
  float32. The plugin polls it every 500 ms from the Running game-state update.
  `tools/write_solver_weights.py` writes the installed file or generates preset
  binaries under `presets/solver_weights/`.
- The async route-job hooks are split behind `kEnableGpsRouteJobLifecycleHooks`
  and are disabled in the active probe; this keeps the log focused on
  `0x70a42c` submissions, `0x7094b8` result records, and local
  search/provider-cost stats.
- `kEnableGpsSpatialEdgeWeightPatch` is a disabled-by-default implementation
  path. When enabled, it hooks `GPSRouteProducer` (`0x44cc7c`) and
  `GPSEdgeCost` (`0x44f838`), samples a generated traffic-lane spatial grid
  using the search-state coordinates, and applies highway/road/pavement
  multipliers to returned edge costs.
- When the spatial patch is enabled, the plugin hot-reloads
  `spatial_weights.bin` from the same directory as the DLL. The file is exactly
  five little-endian float32 values in this order:
  `highway`, `road`, `GPSOnly`, `pavement`, `unknown`.
- `tools/write_spatial_weights.py` writes that file directly, or can generate
  named preset files under `work/spatial-weights/` for live copy-over testing.
  The prepared presets are:
  `vanilla`, `current`, `highway-free`, `highway-expensive`, and
  `surface-extreme`.
- The old verbose local-search probe is still available behind
  `kEnableGpsLocalSearchHooks`. It hooks `GPSSearch` (`0x44f054`),
  `GPSResolveHandle` (`0x44e1a8`), `GPSEdgeCost` (`0x44f838`), and the two
  cost-provider filters `0x44ff68`/`0x450b08`.
- The filter probe records pass/fail counts by point class and by the
  `point + 0x10` mask bits tested against provider masks at `+0x108`/`+0x10a`.
  This is meant to identify whether class/mask filtering, rather than the edge
  multiplier table, is dominating route choice.
- The resolver probe logs selected packed-handle resolutions from the
  search/materialization corridor. The selected post-call return sites are
  `0x44cb9e` (0x38 intermediate-record builder) and `0x44dd97`
  (geometry/materialization path). For full-map GPS queries from caller
  `0x8d20d4` with `query_fcc=5`, it opens a five-second resolver trace window,
  decodes point index/segment index/generation bits, then dumps the resolved
  segment/point records plus small raw slices behind segment pointer fields.
  The purpose is to see whether route materialization can reach final lane
  hashes/resource-side road classes before the final 0x28 records are emitted.

Current route-probe focus:

- `FrameMappinPath` wrapper/core: `0x27c4314`, `0x27bc1ec`
- `SetSelectedMappin` wrappers/core: `0x27c4a38`, `0x27c4944`,
  `0x27c49c4`, `0x27c1684`
- `TrackCustomPositionMappin` wrapper/core: `0x27c4aac`, `0x27c2318`
- mappin-system slots `0x1f0`, `0x280`, and `0x2f0`
- `JournalManager.TrackEntry` implementation: `0x5944fc`
- non-default `TrackEntry` listener callbacks listed above
- journal/mappin route bridge candidate: `0x598250`
- mappin route-event enqueue: `0x13763d8`
- mappin route-event handler: `0xaa62d0`
- mappin route activate/deactivate: `0xaa6330`, `0x27abd7c`
- mappin route observer callbacks: `0xaa6610`, `0xaa6628`, `0xaa63e0`,
  `0x27b10c0`, `0x295d4a0`, `0x286a85c`
- observer1 service lookup and returned service route lookup:
  `0x287c44`, dynamic service vtable slot `0x220` (disabled after being
  classified as journal metadata)
- route-build candidate called by the bridge: `0x5625a4`
- GPS query lifecycle: `0x29bd128`, `0x29bd254`, `0x70a42c`, `0x70a570`,
  `0x7094b8`, `0xaa5704`

Quest/objective pins did not fire the mappin tracking hooks in live tests. The
REDscript decompile shows that those pins call `JournalManager.TrackEntry`
instead, so the current useful runtime question is which native listener reacts
to tracked-entry changes.

Most recent controlled test:

- `06:31:43`: user hit Continue / GPS tick logged.
- `06:32:02`: automatic `TrackEntry` during load, no route input.
- `06:32:27`: map opened.
- `06:32:44`, `06:32:50`, `06:32:56`: deliberate quest/objective route plots
  produced `JournalManager::TrackEntry` calls from return RVA `0x26ac34e`.
- After map open, the journal listener count rose to 254. Most listener entries
  dispatch to no-op `0x14a700`; the non-default callbacks above are the current
  drill-down targets.

Newest controlled test:

- User clock `01:05`: Continue on the main menu. The log saw
  `GPSSystem/Tick`.
- User clock `01:27`: Space to continue. The log saw automatic
  `JournalManager.TrackEntry`, a dense `JournalRouteBridge` burst, and thirty
  `RouteBuildCandidate 0x5625a4` calls. About 5.5 seconds later the mappin route
  activation/deactivation functions fired.
- User clock `01:52`: map open. Hovering icons produced the expected
  `SetSelectedMappin` bursts.
- Deliberate quest route clicks produced `JournalManager.TrackEntry`, route
  event enqueue calls for old-route-off/new-route-on, and route event handler
  calls roughly 11-18 ms later.
- Custom pin routing used the separate custom-position mappin path, then the
  shared route activate/deactivate helper.

Latest observer1 probe result:

- The `Observer1ServiceLookup` hook fired, but it proved observer1 is not route
  solving. It returned the same `JournalManager` vtable seen in earlier journal
  traces, and its slot `0x220` resolved numeric route IDs into journal/UI
  category strings. That branch is closed as marker metadata.

Current static lead:

- `RunGPSQuery` and `UpdateGPSQuery` are registered native thunks at
  `0x29bd5ac` and `0x29bd6c8`. Their deeper bodies are `0x29bd128` and
  `0x29bd254`.
- `RunGPSQuery` submits a query through a subsystem reached via an object field
  at `+0x130`, returning a query ID or `-1`.
- `UpdateGPSQuery` fetches a completed query result through `0x7094b8`, copies a
  path point buffer, and writes the resulting point array for script/UI
  consumption.
- The shared submitter `0x70a42c` is also used by non-GPS navigation callers,
  so return RVAs and query IDs are important for separating map GPS from traffic
  or AI path requests.

Latest GPS-query finding:

- During a controlled route test, the normal world-map GPS path did not call
  the script-native `RunGPSQuery`/`UpdateGPSQuery` bodies.
- Each deliberate quest/custom route click called the shared submitter
  `0x70a42c` from return RVA `0x8d20d4`. In the 2026-06-20 19:03 run, the
  deliberately plotted routes returned query IDs `17`, `18`, `19`, and `20`
  for the side job, Sinnerman, Claire's Garage, and a custom pin.
- Each submitted route produced three `0x70a570` dispatches: current/player
  position, a nearby snapped/lane position, then the destination position.
- The dense log stream immediately after Space-to-Continue is repeated
  `0x7094b8` result polling for startup/minimap route queries, matching the HUD
  route initialization delay.
- `GPSQueryResultFetch 0x7094b8` returning `1` is the route-solve completion
  signal. The caller at return RVA `0x52069c` starts handling a completed
  result, then the caller at return RVA `0x520783` drains result records.
  The visible route record appears in the object passed as `outPath`: offset
  `0x30` is a packed point count (`N/N` on the first drain record, then `N`),
  and offset `0x28` looks like the route point array pointer. The deliberate
  routes produced the familiar `11`, `16`, `15`, and `14` point-like counts in
  the 2026-06-20 19:03 run, and `11`, `16`, `15`, `11` in the 19:16 map
  routine where the custom pin was different.
- Static disassembly confirms `0xaa5704` takes `(manager, queryId)` and returns
  a small status code; one caller checks for value `1`. Runtime logging shows
  this is HUD/display lifecycle polling rather than solve completion: it polls
  the currently displayed query once per second and pauses when the minimap is
  not rendered.
- The current installed probe adds a read-only dump of result-object fields
  around offsets `0x20` through `0x5f` on successful active-route fetches.
  The field at `outPath + 0x28` is the data pointer for an internal route
  element vector, but live data does not decode as plain `Vector3`/`Vector4`
  coordinates.

Latest route-result findings:

- User clock `26:38`: Continue on the main menu.
- User clock `27:01`: Space to continue, with the car in a different position
  after a previous drive.
- User clock `27:21`: map opened.
- User clock `28:06`: normal route-click routine complete.
- Same three quest destinations from the shifted start produced different
  result counts: side job `12`, Sinnerman `23`, Claire's Garage `18`, and the
  custom pin `17`.
- Static disassembly of the `0x520783` result-drain caller shows `0x7094b8`
  drains one route result record at a time from an internal vector, then copies
  that record into `outPath`.
- The copied route-result subobject contains a vector at `outPath + 0x28`.
  Its live count is the dword at `outPath + 0x34`; in the latest run this
  cleanly matched the visible routes: side job `12`, Sinnerman `23`, Claire's
  Garage `18`, and custom pin `17`.
- The vector elements are 0x28-byte records, not pointers. `0x41be2c` calls
  `0x41be94` with `(data, data + count * 40)`. The active probe logs each
  route element as a compact 40-byte record tuple:
  `h00,u08hex,u0c,u10,u14,h18,h18lowFloat,h18highFloat,h20`.
- A nearby helper at `0x41c508` does iterate an array of 8-byte pointers at
  offset `+0x28`, but that is a different route-adjacent object type, not the
  `0x7094b8` output record captured in `outPath`.

Latest route-element decode:

- User clock `45:37`: Continue on the main menu.
- User clock `46:05`: Space to continue.
- User clock `46:15`: map opened and the usual route-click routine was run.
- The startup/minimap route and the later explicit Claire's Garage map route
  produced identical 18-record `ptr28` sequences, so the same solved route
  object feeds both HUD startup and deliberate map plotting.
- The four deliberate map routes completed as route result IDs `2` through `5`:
  side job `12` records / final span end `68`, Sinnerman `23` records / final
  span end `188`, Claire's Garage `18` records / final span end `86`, custom
  pin `14` records / final span end `78`.
- Within every route, `u10` and `u14` are monotonically increasing start/end
  indexes into a flattened point/span buffer. The final `u14` equals
  `gpsResult_u20` for every route.
- `h00` is a stable per-segment handle: common route prefixes share identical
  `h00` sequences, and the custom pin and Claire's Garage routes share their
  first ten solved segments before diverging.
- `u08` is packed metadata. Values appear byte-structured, for example
  `0x00020202`, `0x01020203`, `0x02020103`, and `0x03020003`, rather than a
  simple scalar weight.
- `h18` splits into two 32-bit floats. The high half is consistently a sane
  road-scale float, often in the `20` to `230` range, and is likely segment
  length/cost-like output. The low half is usually zero or a tiny value, with
  non-zero values appearing around the start segment.
- `h20` has been zero in all captured full-map GPS result records.

Route-record/resource correlation:

- The `h00` field in the 0x28-byte route-result records matches
  `worldTrafficLanePersistent.nodeRefHash`. In the current route logs, every
  unique route handle checked matched a lane hash in the raw
  `all.traffic_persistent` resource.
- This lets us join emitted route segments back to resource-side lane metadata:
  `flags`, `maxSpeed`, `length`, and `playerGPSInfo`.
- The packed `u08` metadata is not a clean copy of resource-side lane flags.
  Its low byte commonly appears as `2`, `3`, `4`, `5`, `8`, `9`, etc., while
  the resource categories are bitflag bundles such as `Road`, `Highway`,
  `GPSOnly`, `Intersection`, and `Pavement`.
- A joined sample from `EdgeWeightGPS_cost_table_mild_2121.log` showed mixed
  resource categories per low-byte class: class `2` was mostly pavement but
  included highway lanes, class `8` was split between pavement and highway, and
  class `1` was mostly road. This means the final route record's low byte is
  useful metadata, but it is not by itself the class multiplier index from
  `0x44f838`.
- The next better lever is still the search-side cost provider table keyed by
  `(*(routePoint + 0x13) & 0x3f)`. To tune it intelligently, we need either the
  provider initialization site or a way to join search-side route points back to
  `nodeRefHash`/lane flags before the edge cost is returned.

Latest full-route join:

- `logs/EdgeWeightGPS_query_route_probe_2308.log` contains nine solved route
  results from the 23:08 user test. All 362 emitted route records matched
  `worldTrafficLanePersistent.nodeRefHash` in
  `work/raw-segment-json/all.traffic_persistent.json`.
- The log's wall timestamp currently wraps milliseconds without incrementing
  the displayed second. Use the `+Nms` elapsed field when comparing submit and
  result times.
- Representative solved route composition:
  - side job: 12 records, 32 ms, 578.6 pavement length.
  - Sinnerman: 23 records, 45 ms, 897.4 pavement, 160.4 highway, 89.8 road.
  - Claire: 18 records, 34 ms, 1216.5 pavement.
  - custom highway-ish pin: 17 records, 44 ms, 1006.3 pavement,
    241.0 highway, 38.0 road.
  - Aldecaldo camp: 80 records, 325 ms, 5853.5 road, 1492.9 highway,
    1111.5 pavement, 24.6 gpsonly.
  - far east gig: 81 records, 144 ms, 5086.2 road, 1523.0 highway,
    1111.5 pavement, 24.6 gpsonly.
  - Violence: 55 records, 97 ms, 1991.5 pavement, 358.5 highway,
    86.1 road.
  - cyberpsycho unfinished highway: 58 records, 147 ms, 2657.4 pavement,
    519.0 highway, 113.2 road, 56.7 gpsonly.
- Search/provider aggregate in the same log: 159 searches, 4036 provider
  filter calls, only 3 filter failures. Filter slot `+0x08` is therefore
  almost never pruning this test graph.
- Search-side point classes observed by `0x44f838` are `{1,3,4,5,15}`. Final
  route-record low-byte classes are `{0,1,2,3,4,5,7,8,9}`, and route-record
  classes 7/8/9 are highway-heavy. These are different domains, which explains
  why provider point-class tuning did not map cleanly to highway preference.
- Later static analysis tied the search-side segment/point records to `VAND`
  blobs in compiled navigation streamingsector resources, not directly to
  `all.traffic_persistent`. The traffic lane hash join happens downstream in
  the result/materialization path.
- `tools/analyze_vand_navigation.py` decodes those `VAND` blobs from extracted
  streamingsector JSON. In the three extracted samples, all masks were `0x0003`
  and the dominant point classes were `1`, `4`, `5`, and `3`, matching the live
  edge-cost trace.

Current static producer lead:

- `0x70a908` packages the route endpoints and query settings, then calls
  `0x44cc7c`.
- `0x44cc7c` is now the main producer corridor, not merely a generic unknown
  helper. It allocates large scratch state, projects the query endpoints, and
  calls deeper pathfinding routines that should decide route cost before the
  `0x7094b8` fetch/drain path copies final result records.
- Direct xrefs confirm `0x41be94` has only the result-copy caller
  `0x41be69`; patching there would alter copied/displayed records after route
  choice. The cost patch needs to happen upstream of the `0x44cc7c` result
  construction path.
- `0x44f054` is the strongest route-planning lead so far. It validates the
  packed start/target lane handles, initializes a search-state table and open
  list, pushes the start state, repeatedly pops the best state, checks for the
  destination handle, traverses adjacency lists, and writes predecessor,
  accumulated-cost, and priority fields for improved neighbor states. This is
  the native GPS graph search loop.
- The search-state record is 0x20 bytes. Offsets `+0x00/+0x04/+0x08` are a
  world position, `+0x0c` is accumulated path cost, `+0x10` is priority
  (`cost + heuristic`), `+0x14` stores open/closed flags and the predecessor
  index, and `+0x18` stores the packed lane handle.
- `0x44f7bc` is the heuristic helper. It returns world-space distance to the
  target scaled by the constant at `0x1431ef3a0` (`0.9990000129`), unless a
  route-shape helper supplies a shorter adjusted distance.
- The search loop calls a strategy object's vtable slot `+0x08` as an edge
  admissibility predicate and slot `+0x10` as the edge-cost callback. For the
  base/class-filter strategies, slot `+0x10` resolves to `0x44f838`.
- `0x44f838` computes the geometric edge distance and multiplies it by
  `provider[0x08 + classId * 4]`, where `classId` is
  `(*(routePoint + 0x13) & 0x3f)`. The GPS planner is therefore weighted, but
  by a compact baked road-class table, not by traffic `maxSpeed` or lane
  connection probabilities.
- The read-only local-search probe can still hook `0x44f054` and `0x44f838` to
  log the live provider vtable, the 64 class multipliers, start/target handles,
  and a capped sample of edge-cost calls with point class IDs, masks, handles,
  and returned costs.
- The new spatial edge-cost path is implemented but disabled by default. Its
  generated grid is built from traffic lane polygons/flags, not from VAND point
  classes, so it can bias the search toward highway cells without rewriting
  VAND metadata that later materialization code branches on.

Autodrive finding:

- User clock `28:28` and `28:48`: enabling autodrive did not use the full
  world-map GPS caller. It submitted repeated `0x70a42c` queries from return
  RVA `0x8ed760`, with `query_fcc=4`, near-current vehicle positions, and
  result fetches from return RVA `0x8ed7df`.
- These autodrive queries appear to be short local vehicle navigation requests
  using the same query service, separate from the full player GPS map route
  caller at return RVA `0x8d20d4` with `query_fcc=5`.

Build and install from the Fedora toolbox:

```bash
toolbox run -c 2077 ./tools/install_red4ext_shim.sh
```

This shim is read-only. It does not enable any of the archived traffic data patches.
