From 71c044f643eb9fe732083b83290557a2e91180f1 Mon Sep 17 00:00:00 2001 From: Jacob Babor Date: Sat, 20 Jun 2026 00:11:42 -0500 Subject: [PATCH] Document GPS route handoff analysis --- .gitignore | 1 + docs/traffic-system-debrief.md | 118 ++++++++++++++++++++++++++++----- tools/find_pe_code_xrefs.py | 79 ++++++++++++++++++++++ 3 files changed, 182 insertions(+), 16 deletions(-) create mode 100755 tools/find_pe_code_xrefs.py diff --git a/.gitignore b/.gitignore index 293b902..868c171 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ /work-*.txt /build/ /vendor/ +/scratch/ diff --git a/docs/traffic-system-debrief.md b/docs/traffic-system-debrief.md index 7dda3a9..d098a14 100644 --- a/docs/traffic-system-debrief.md +++ b/docs/traffic-system-debrief.md @@ -20,11 +20,17 @@ and lane connection probabilities. Live tests have not supported that: - 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 goes through a -native world-map mappin tracking path, which then updates 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. +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 @@ -145,23 +151,103 @@ moving back to empty map space fired `SetSelectedMappinWrapper` and fire `FrameMappinPath`, `TrackCustomPositionMappin`, `TrackMappin`, the tracked mappin slots, or any of the custom-position slots. -That pushes the search one layer higher, into the world-map controller script +That pushed the search one layer higher, into the world-map controller script methods around tracking an objective or setting a waypoint. Static REDscript -strings name these relevant methods: +decompilation now gives a clear high-level route action path: ```text -WorldMapMenuGameController.TryTrackQuestOrSetWaypoint -WorldMapMenuGameController.UpdateTrackedQuest -WorldMapMenuGameController.UpdateTravelDestination -WorldMapMenuGameController.TrackQuestMappin WorldMapMenuGameController.OnPressInput -WorldMapMenuGameController.OnHoldInput + -> HandlePressInput + -> TryTrackQuestOrSetWaypoint + -> TrackQuestMappin + -> JournalManager.TrackEntry ``` -The current read-only REDscript probe wraps the no-argument route/tracking -methods plus `TrackQuestMappin`. If those fire on route plotting, the next step -is to trace their native calls or wrap the specific player-quest/custom-waypoint -operation they delegate to. +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. + +## 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 diff --git a/tools/find_pe_code_xrefs.py b/tools/find_pe_code_xrefs.py new file mode 100755 index 0000000..f1f78ad --- /dev/null +++ b/tools/find_pe_code_xrefs.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Find simple direct x64 call/jump xrefs to code RVAs in a PE file.""" + +from __future__ import annotations + +import argparse +import struct +from pathlib import Path + +from find_pe_string_xrefs import Section, parse_pe + + +def iter_rel32_xrefs(data: bytes, section: Section, image_base: int, target_vas: set[int]) -> dict[int, list[tuple[int, str]]]: + hits: dict[int, list[tuple[int, str]]] = {target_va: [] for target_va in target_vas} + start = section.raw_offset + end = min(len(data), section.raw_offset + section.raw_size) + + opcodes = { + 0xE8: "call", + 0xE9: "jmp", + } + for offset in range(start, max(start, end - 5)): + opcode = data[offset] + mnemonic = opcodes.get(opcode) + if mnemonic is None: + continue + + disp = struct.unpack_from(" imports/thunks are not resolved + # here. This helper intentionally stays small and deterministic. + return hits + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("pe", type=Path) + parser.add_argument("targets", nargs="+", help="name=rva_hex pairs") + args = parser.parse_args() + + data = args.pe.read_bytes() + image_base, sections = parse_pe(data) + code_sections = [section for section in sections if section.is_code] + + targets: list[tuple[str, int, int]] = [] + target_vas: set[int] = set() + for item in args.targets: + name, _, rva_text = item.partition("=") + if not name or not rva_text: + raise ValueError(f"target must be name=rva_hex: {item}") + target_rva = int(rva_text, 16) + target_va = image_base + target_rva + targets.append((name, target_rva, target_va)) + target_vas.add(target_va) + + xrefs: dict[int, list[tuple[int, str]]] = {target_va: [] for target_va in target_vas} + for section in code_sections: + section_hits = iter_rel32_xrefs(data, section, image_base, target_vas) + for target_va, hits in section_hits.items(): + xrefs[target_va].extend(hits) + + print(f"image_base=0x{image_base:x}") + for name, target_rva, target_va in targets: + all_hits = sorted(xrefs[target_va]) + print(f"{name}: target_rva=0x{target_rva:x} direct_xrefs={len(all_hits)}") + for instr_rva, mnemonic in all_hits[:64]: + print(f" {mnemonic}_rva=0x{instr_rva:x}") + if len(all_hits) > 64: + print(f" ... {len(all_hits) - 64} more") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())