Document GPS route handoff analysis
This commit is contained in:
@@ -5,3 +5,4 @@ __pycache__/
|
|||||||
/work-*.txt
|
/work-*.txt
|
||||||
/build/
|
/build/
|
||||||
/vendor/
|
/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
|
- probing the obvious `RunGPSQuery` and `UpdateGPSQuery` helpers also did not
|
||||||
fire during deliberate world-map route plotting
|
fire during deliberate world-map route plotting
|
||||||
|
|
||||||
The stronger current theory is that player GPS route selection goes through a
|
The stronger current theory is that player GPS route selection has two
|
||||||
native world-map mappin tracking path, which then updates native GPS state
|
front-doors:
|
||||||
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
|
- quest/objective pins are committed by updating the native journal's tracked
|
||||||
simulation inputs, not the live player-GPS edge-cost knobs.
|
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
|
## Static GPS Query Candidates
|
||||||
|
|
||||||
@@ -145,23 +151,103 @@ moving back to empty map space fired `SetSelectedMappinWrapper` and
|
|||||||
fire `FrameMappinPath`, `TrackCustomPositionMappin`, `TrackMappin`, the tracked
|
fire `FrameMappinPath`, `TrackCustomPositionMappin`, `TrackMappin`, the tracked
|
||||||
mappin slots, or any of the custom-position slots.
|
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
|
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
|
```text
|
||||||
WorldMapMenuGameController.TryTrackQuestOrSetWaypoint
|
|
||||||
WorldMapMenuGameController.UpdateTrackedQuest
|
|
||||||
WorldMapMenuGameController.UpdateTravelDestination
|
|
||||||
WorldMapMenuGameController.TrackQuestMappin
|
|
||||||
WorldMapMenuGameController.OnPressInput
|
WorldMapMenuGameController.OnPressInput
|
||||||
WorldMapMenuGameController.OnHoldInput
|
-> HandlePressInput
|
||||||
|
-> TryTrackQuestOrSetWaypoint
|
||||||
|
-> TrackQuestMappin
|
||||||
|
-> JournalManager.TrackEntry
|
||||||
```
|
```
|
||||||
|
|
||||||
The current read-only REDscript probe wraps the no-argument route/tracking
|
For non-quest/player pins the same `TryTrackQuestOrSetWaypoint` function calls
|
||||||
methods plus `TrackQuestMappin`. If those fire on route plotting, the next step
|
`TrackMappin`. For custom pins it calls `TrackCustomPositionMappin`, which
|
||||||
is to trace their native calls or wrap the specific player-quest/custom-waypoint
|
creates or updates a custom-position mappin and then tracks it.
|
||||||
operation they delegate to.
|
|
||||||
|
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
|
## 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