392 lines
21 KiB
Markdown
392 lines
21 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 disabled on disk:
|
|
`EdgeWeightGPS.dll.disabled`. There is no active `EdgeWeightGPS.dll` in the
|
|
game plugin directory.
|
|
- Source defaults are still read-only for route behavior. They disable provider
|
|
class multiplier overrides and the spatial edge-cost patch, while keeping GPS
|
|
query lifecycle tracing available for probe builds.
|
|
- 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.
|