Files
2077-gps-mod/docs/red4ext-logging-shim.md
T

402 lines
22 KiB
Markdown

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