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

21 KiB

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:

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.
  • 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:

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.