Document GPS route handoff analysis

This commit is contained in:
2026-06-20 00:11:42 -05:00
parent f3afb55173
commit 71c044f643
3 changed files with 182 additions and 16 deletions
+1
View File
@@ -5,3 +5,4 @@ __pycache__/
/work-*.txt
/build/
/vendor/
/scratch/
+102 -16
View File
@@ -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
+79
View File
@@ -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("<i", data, offset + 1)[0]
instr_rva = section.virtual_address + (offset - section.raw_offset)
target_va = image_base + instr_rva + 5 + disp
if target_va in target_vas:
hits[target_va].append((instr_rva, mnemonic))
# Common tail-call form: ff 25 <riprel32> 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())