Track GPS query status lifecycle

This commit is contained in:
2026-06-20 13:00:35 -05:00
parent 50c81b780a
commit 751e2175f7
2 changed files with 283 additions and 79 deletions
+26 -3
View File
@@ -53,9 +53,14 @@ Current behavior:
native registration cluster:
`0x29bd128` (`RunGPSQuery` body), `0x29bd254` (`UpdateGPSQuery` body),
`0x70a42c` (shared query submitter), `0x70a570` (low-level query dispatch),
and `0x7094b8` (query result/path fetch).
`0x7094b8` (query result/path fetch), and `0xaa5704` (query status check).
- Logs query endpoints, returned query IDs, shared submitter return RVAs, query
state fields, result-fetch success, and summarized path buffers/point counts.
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.
@@ -79,7 +84,7 @@ Current route-probe focus:
classified as journal metadata)
- route-build candidate called by the bridge: `0x5625a4`
- GPS query lifecycle: `0x29bd128`, `0x29bd254`, `0x70a42c`, `0x70a570`,
`0x7094b8`
`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`
@@ -134,6 +139,24 @@ Current static lead:
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`, then returned query IDs `3`, `4`, `5`,
and `6` for the three quest routes plus the 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 was repeated
`0x7094b8` result polling for startup query IDs `0`/`1`, matching the HUD /
minimap route initialization delay. The next probe tracks submitted IDs so
route-click completion can be captured without being drowned by startup
polling.
- Static disassembly confirms `0xaa5704` takes `(manager, queryId)` and returns
a small status code; one caller checks for value `1` as the completed-query
state.
Build and install from the Fedora toolbox:
```bash
+197 -16
View File
@@ -92,6 +92,9 @@ struct Sdk
namespace
{
constexpr uint32_t kGameStateRunning = 2;
constexpr bool kEnableVerboseJournalListenerHooks = false;
constexpr bool kEnableVerboseRouteObserverHooks = false;
constexpr bool kEnableVerboseMappinRouteState = false;
constexpr uintptr_t kGpsSystemTickRva = 0x0E6B62C;
constexpr uintptr_t kJournalTrackEntryImplRva = 0x05944FC;
constexpr uintptr_t kJournalListenerArrayOffset = 0x210;
@@ -146,6 +149,7 @@ constexpr uintptr_t kUpdateGpsQueryBodyRva = 0x29BD254;
constexpr uintptr_t kGpsQuerySubmitRva = 0x070A42C;
constexpr uintptr_t kGpsQueryDispatchRva = 0x070A570;
constexpr uintptr_t kGpsQueryResultFetchRva = 0x07094B8;
constexpr uintptr_t kGpsQueryStatusRva = 0x0AA5704;
HMODULE gModule = nullptr;
std::mutex gLogMutex;
@@ -205,6 +209,7 @@ uint32_t gUpdateGpsQueryBodyLogCount = 0;
uint32_t gGpsQuerySubmitLogCount = 0;
uint32_t gGpsQueryDispatchLogCount = 0;
uint32_t gGpsQueryResultFetchLogCount = 0;
uint32_t gGpsQueryStatusLogCount = 0;
uint64_t gLogStartTick = 0;
uint64_t gLogFrequency = 0;
void* gMappinSetTrackedTarget = nullptr;
@@ -219,6 +224,21 @@ void* gObserver1ServiceRouteLookupTarget = nullptr;
bool gMappinSlotHookAttachAttempted = false;
bool gObserver1ServiceRouteLookupHookAttachAttempted = false;
struct TrackedGpsQuery
{
bool active = false;
uint32_t id = 0;
void* manager = nullptr;
uintptr_t submitReturnRva = 0;
uint32_t submitCall = 0;
uint32_t resultFetchCalls = 0;
uint32_t statusCalls = 0;
};
std::mutex gTrackedGpsQueryMutex;
std::array<TrackedGpsQuery, 32> gTrackedGpsQueries{};
uint32_t gTrackedGpsQueryNext = 0;
using ProbeFunc = void (*)(void* aThis, void* a2, void* a3, void* a4);
using JournalTrackEntryFunc = void (*)(void* aThis, void* aEntryHandle);
using ScriptNativeVoidFunc = void (*)(void* aThis, void* aStack, void* aResult);
@@ -253,6 +273,7 @@ using UpdateGpsQueryBodyFunc = bool (*)(void* aThis, uint32_t aQueryId, void* aO
using GpsQuerySubmitFunc = int32_t (*)(void* aManager, void* aQuery);
using GpsQueryDispatchFunc = void (*)(void* aRuntimeQuery, void* aTargetPosition, void* aArg3, void* aArg4);
using GpsQueryResultFetchFunc = bool (*)(void* aManager, uint32_t aQueryId, void* aOutPath);
using GpsQueryStatusFunc = int32_t (*)(void* aManager, uint32_t aQueryId);
ProbeFunc gOriginalGpsSystemTick = nullptr;
JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr;
ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr;
@@ -303,6 +324,7 @@ UpdateGpsQueryBodyFunc gOriginalUpdateGpsQueryBody = nullptr;
GpsQuerySubmitFunc gOriginalGpsQuerySubmit = nullptr;
GpsQueryDispatchFunc gOriginalGpsQueryDispatch = nullptr;
GpsQueryResultFetchFunc gOriginalGpsQueryResultFetch = nullptr;
GpsQueryStatusFunc gOriginalGpsQueryStatus = nullptr;
std::filesystem::path GetLogPath()
{
@@ -390,6 +412,17 @@ bool IsInImage(const void* aPointer)
return imageBase && imageEnd && address >= imageBase && address < imageEnd;
}
bool TryGetImageRva(const void* aPointer, uintptr_t& aRva)
{
if (!IsInImage(aPointer))
{
return false;
}
aRva = reinterpret_cast<uintptr_t>(aPointer) - GetImageBase();
return true;
}
void AppendPointerWithRva(std::ostringstream& aLine, const char* aLabel, const void* aPointer)
{
aLine << " " << aLabel << "=" << aPointer;
@@ -710,6 +743,81 @@ void AppendPathBufferSummary(std::ostringstream& aLine, const char* aPrefix, voi
}
}
void TrackGpsQuery(uint32_t aQueryId, void* aManager, uintptr_t aSubmitReturnRva, uint32_t aSubmitCall)
{
if (aQueryId == UINT32_MAX)
{
return;
}
std::lock_guard lock(gTrackedGpsQueryMutex);
for (auto& query : gTrackedGpsQueries)
{
if (query.active && query.id == aQueryId && query.manager == aManager)
{
query.submitReturnRva = aSubmitReturnRva;
query.submitCall = aSubmitCall;
return;
}
}
auto& query = gTrackedGpsQueries[gTrackedGpsQueryNext++ % gTrackedGpsQueries.size()];
query.active = true;
query.id = aQueryId;
query.manager = aManager;
query.submitReturnRva = aSubmitReturnRva;
query.submitCall = aSubmitCall;
query.resultFetchCalls = 0;
query.statusCalls = 0;
}
bool NoteTrackedGpsQueryResultFetch(uint32_t aQueryId, void* aManager, TrackedGpsQuery& aSnapshot,
uint32_t& aPerQueryCall)
{
std::lock_guard lock(gTrackedGpsQueryMutex);
for (auto& query : gTrackedGpsQueries)
{
if (!query.active || query.id != aQueryId || query.manager != aManager)
{
continue;
}
aPerQueryCall = query.resultFetchCalls++;
aSnapshot = query;
return true;
}
return false;
}
bool NoteTrackedGpsQueryStatus(uint32_t aQueryId, void* aManager, TrackedGpsQuery& aSnapshot,
uint32_t& aPerQueryCall)
{
std::lock_guard lock(gTrackedGpsQueryMutex);
for (auto& query : gTrackedGpsQueries)
{
if (!query.active || query.id != aQueryId || query.manager != aManager)
{
continue;
}
aPerQueryCall = query.statusCalls++;
aSnapshot = query;
return true;
}
return false;
}
void AppendTrackedGpsQuery(std::ostringstream& aLine, const TrackedGpsQuery& aQuery)
{
aLine << " submitCall=" << aQuery.submitCall << " submitRetRva=0x" << std::hex << aQuery.submitReturnRva
<< std::dec;
}
void AppendRouteRefFields(std::ostringstream& aLine, const char* aPrefix, void* aRouteRef)
{
AppendPointerWithRva(aLine, aPrefix, aRouteRef);
@@ -730,6 +838,11 @@ void AppendRouteRefFields(std::ostringstream& aLine, const char* aPrefix, void*
void LogMappinRouteCollections(const char* aName, uint32_t aCount, void* aSystem)
{
if (!kEnableVerboseMappinRouteState)
{
return;
}
if (!aSystem)
{
return;
@@ -1133,7 +1246,6 @@ void DetourJournalTrackEntry(void* aThis, void* aEntryHandle)
AppendReturnRva(line, __builtin_return_address(0));
LogRed4ext(line.str());
LogJournalListeners(callCount, aThis);
++gJournalTrackEntryLogCount;
}
@@ -1906,6 +2018,10 @@ int32_t DetourGpsQuerySubmit(void* aManager, void* aQuery)
{
constexpr uint32_t kMaxCalls = 128;
const auto returnAddress = __builtin_return_address(0);
uintptr_t returnRva = 0;
TryGetImageRva(returnAddress, returnRva);
const auto callCount = gGpsQuerySubmitLogCount;
const auto shouldLog = callCount < kMaxCalls;
if (shouldLog)
@@ -1920,7 +2036,7 @@ int32_t DetourGpsQuerySubmit(void* aManager, void* aQuery)
AppendUint32Field(line, "query_f0c", aQuery, 0x0C);
AppendUint32Field(line, "query_fcc", aQuery, 0xCC);
AppendVector4Field(line, "query_pos58", aQuery, 0x58);
AppendReturnRva(line, __builtin_return_address(0));
AppendReturnRva(line, returnAddress);
LogRed4ext(line.str());
++gGpsQuerySubmitLogCount;
}
@@ -1931,6 +2047,11 @@ int32_t DetourGpsQuerySubmit(void* aManager, void* aQuery)
result = gOriginalGpsQuerySubmit(aManager, aQuery);
}
if (result >= 0)
{
TrackGpsQuery(static_cast<uint32_t>(result), aManager, returnRva, callCount);
}
if (shouldLog)
{
std::ostringstream line;
@@ -1969,20 +2090,11 @@ void DetourGpsQueryDispatch(void* aRuntimeQuery, void* aTargetPosition, void* aA
bool DetourGpsQueryResultFetch(void* aManager, uint32_t aQueryId, void* aOutPath)
{
constexpr uint32_t kMaxCalls = 128;
constexpr uint32_t kMaxUntrackedSamples = 32;
constexpr uint32_t kMaxTrackedSamplesPerQuery = 24;
const auto callCount = gGpsQueryResultFetchLogCount;
const auto shouldLog = callCount < kMaxCalls;
if (shouldLog)
{
std::ostringstream line;
line << "hook GPSQueryResultFetch 0x7094b8 call=" << callCount << " queryId=" << aQueryId;
AppendPointerWithRva(line, "manager", aManager);
AppendPointerWithRva(line, "outPath", aOutPath);
AppendReturnRva(line, __builtin_return_address(0));
LogRed4ext(line.str());
++gGpsQueryResultFetchLogCount;
}
const auto callCount = gGpsQueryResultFetchLogCount++;
const auto returnAddress = __builtin_return_address(0);
bool result = false;
if (gOriginalGpsQueryResultFetch)
@@ -1990,10 +2102,25 @@ bool DetourGpsQueryResultFetch(void* aManager, uint32_t aQueryId, void* aOutPath
result = gOriginalGpsQueryResultFetch(aManager, aQueryId, aOutPath);
}
TrackedGpsQuery trackedQuery{};
uint32_t perQueryCall = 0;
const auto isTracked = NoteTrackedGpsQueryResultFetch(aQueryId, aManager, trackedQuery, perQueryCall);
const auto shouldLog = (isTracked && (perQueryCall < kMaxTrackedSamplesPerQuery || result)) ||
(!isTracked && callCount < kMaxUntrackedSamples && aQueryId < 2);
if (shouldLog)
{
std::ostringstream line;
line << "hook GPSQueryResultFetch result call=" << callCount << " value=" << static_cast<uint32_t>(result);
line << "hook GPSQueryResultFetch 0x7094b8 call=" << callCount << " queryId=" << aQueryId
<< " tracked=" << static_cast<uint32_t>(isTracked) << " perQueryCall=" << perQueryCall
<< " value=" << static_cast<uint32_t>(result);
if (isTracked)
{
AppendTrackedGpsQuery(line, trackedQuery);
}
AppendPointerWithRva(line, "manager", aManager);
AppendPointerWithRva(line, "outPath", aOutPath);
AppendReturnRva(line, returnAddress);
AppendPathBufferSummary(line, "outPath", aOutPath);
LogRed4ext(line.str());
}
@@ -2001,6 +2128,44 @@ bool DetourGpsQueryResultFetch(void* aManager, uint32_t aQueryId, void* aOutPath
return result;
}
int32_t DetourGpsQueryStatus(void* aManager, uint32_t aQueryId)
{
constexpr uint32_t kMaxUntrackedSamples = 32;
constexpr uint32_t kMaxTrackedSamplesPerQuery = 24;
const auto callCount = gGpsQueryStatusLogCount++;
const auto returnAddress = __builtin_return_address(0);
int32_t result = 0;
if (gOriginalGpsQueryStatus)
{
result = gOriginalGpsQueryStatus(aManager, aQueryId);
}
TrackedGpsQuery trackedQuery{};
uint32_t perQueryCall = 0;
const auto isTracked = NoteTrackedGpsQueryStatus(aQueryId, aManager, trackedQuery, perQueryCall);
const auto shouldLog = (isTracked && (perQueryCall < kMaxTrackedSamplesPerQuery || result != 0)) ||
(!isTracked && callCount < kMaxUntrackedSamples && aQueryId < 2);
if (shouldLog)
{
std::ostringstream line;
line << "hook GPSQueryStatus 0xaa5704 call=" << callCount << " queryId=" << aQueryId
<< " tracked=" << static_cast<uint32_t>(isTracked) << " perQueryCall=" << perQueryCall
<< " value=" << result;
if (isTracked)
{
AppendTrackedGpsQuery(line, trackedQuery);
}
AppendPointerWithRva(line, "manager", aManager);
AppendReturnRva(line, returnAddress);
LogRed4ext(line.str());
}
return result;
}
void AttachProbeHook(const char* aName, uintptr_t aRva, void* aDetour, void** aOriginal)
{
if (!gSdk || !gSdk->hooking || !gSdk->hooking->Attach)
@@ -2324,11 +2489,16 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
AttachProbeHook("GPSQueryResultFetch 0x7094b8", kGpsQueryResultFetchRva,
reinterpret_cast<void*>(&DetourGpsQueryResultFetch),
reinterpret_cast<void**>(&gOriginalGpsQueryResultFetch));
AttachProbeHook("GPSQueryStatus 0xaa5704", kGpsQueryStatusRva,
reinterpret_cast<void*>(&DetourGpsQueryStatus),
reinterpret_cast<void**>(&gOriginalGpsQueryStatus));
AttachProbeHook("GPSSystem/Tick", kGpsSystemTickRva, reinterpret_cast<void*>(&DetourGpsSystemTick),
reinterpret_cast<void**>(&gOriginalGpsSystemTick));
AttachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva,
reinterpret_cast<void*>(&DetourJournalTrackEntry),
reinterpret_cast<void**>(&gOriginalJournalTrackEntry));
if (kEnableVerboseJournalListenerHooks)
{
AttachProbeHook("JournalListener::Objective vt28", kJournalListenerObjectiveVt28Rva,
reinterpret_cast<void*>(&DetourJournalListenerObjectiveVt28),
reinterpret_cast<void**>(&gOriginalJournalListenerObjectiveVt28));
@@ -2356,6 +2526,7 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
AttachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva,
reinterpret_cast<void*>(&DetourJournalListenerTrackerVt28),
reinterpret_cast<void**>(&gOriginalJournalListenerTrackerVt28));
}
AttachProbeHook("JournalRouteBridge::Update candidate", kJournalRouteBridgeRva,
reinterpret_cast<void*>(&DetourJournalRouteBridge),
reinterpret_cast<void**>(&gOriginalJournalRouteBridge));
@@ -2374,6 +2545,8 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
AttachProbeHook("RouteBuildCandidate 0x5625a4", kRouteBuildCandidateRva,
reinterpret_cast<void*>(&DetourRouteBuildCandidate),
reinterpret_cast<void**>(&gOriginalRouteBuildCandidate));
if (kEnableVerboseRouteObserverHooks)
{
AttachProbeHook("RouteObserver0::Activate slot48", kRouteObserver0ActivateRva,
reinterpret_cast<void*>(&DetourRouteObserver0Activate),
reinterpret_cast<void**>(&gOriginalRouteObserver0Activate));
@@ -2392,6 +2565,7 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
AttachProbeHook("RouteObserver23::Deactivate slot50", kRouteObserver23DeactivateRva,
reinterpret_cast<void*>(&DetourRouteObserver23Deactivate),
reinterpret_cast<void**>(&gOriginalRouteObserver23Deactivate));
}
AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva,
reinterpret_cast<void*>(&DetourFrameMappinPathWrapper),
reinterpret_cast<void**>(&gOriginalFrameMappinPathWrapper));
@@ -2436,8 +2610,11 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
DetachProbeHook("GPSQuerySubmit 0x70a42c", kGpsQuerySubmitRva);
DetachProbeHook("GPSQueryDispatch 0x70a570", kGpsQueryDispatchRva);
DetachProbeHook("GPSQueryResultFetch 0x7094b8", kGpsQueryResultFetchRva);
DetachProbeHook("GPSQueryStatus 0xaa5704", kGpsQueryStatusRva);
DetachProbeHook("GPSSystem/Tick", kGpsSystemTickRva);
DetachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva);
if (kEnableVerboseJournalListenerHooks)
{
DetachProbeHook("JournalListener::Objective vt28", kJournalListenerObjectiveVt28Rva);
DetachProbeHook("JournalListener::Objective vt30", kJournalListenerObjectiveVt30Rva);
DetachProbeHook("JournalListener::Route vt28", kJournalListenerRouteVt28Rva);
@@ -2447,18 +2624,22 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
DetachProbeHook("JournalListener::UI vt28", kJournalListenerUiVt28Rva);
DetachProbeHook("JournalListener::UI vt30", kJournalListenerUiVt30Rva);
DetachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva);
}
DetachProbeHook("JournalRouteBridge::Update candidate", kJournalRouteBridgeRva);
DetachProbeHook("MappinSystem::RouteEventEnqueue slot240", kMappinRouteEventEnqueueRva);
DetachProbeHook("MappinRouteEvent::Handle", kMappinRouteEventHandlerRva);
DetachProbeHook("MappinSystem::RouteActivate", kMappinRouteActivateRva);
DetachProbeHook("MappinSystem::RouteDeactivate", kMappinRouteDeactivateRva);
DetachProbeHook("RouteBuildCandidate 0x5625a4", kRouteBuildCandidateRva);
if (kEnableVerboseRouteObserverHooks)
{
DetachProbeHook("RouteObserver0::Activate slot48", kRouteObserver0ActivateRva);
DetachProbeHook("RouteObserver1::Activate slot48", kRouteObserver1ActivateRva);
DetachProbeHook("RouteObserver23::Activate slot48", kRouteObserver23ActivateRva);
DetachProbeHook("RouteObserver0::Deactivate slot50", kRouteObserver0DeactivateRva);
DetachProbeHook("RouteObserver1::Deactivate slot50", kRouteObserver1DeactivateRva);
DetachProbeHook("RouteObserver23::Deactivate slot50", kRouteObserver23DeactivateRva);
}
DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva);
DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva);
DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);