Document GPS route handoff analysis
This commit is contained in:
@@ -5,3 +5,4 @@ __pycache__/
|
||||
/work-*.txt
|
||||
/build/
|
||||
/vendor/
|
||||
/scratch/
|
||||
|
||||
+102
-16
@@ -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
|
||||
|
||||
|
||||
Executable
+79
@@ -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())
|
||||
Reference in New Issue
Block a user