From 50c81b780a9fcc737b17782db9bc3da888ca51b9 Mon Sep 17 00:00:00 2001 From: Jacob Babor Date: Sat, 20 Jun 2026 12:47:30 -0500 Subject: [PATCH] Probe GPS query lifecycle --- docs/red4ext-logging-shim.md | 38 +++- red4ext/EdgeWeightGPS/src/Main.cpp | 284 ++++++++++++++++++++++++++++- 2 files changed, 317 insertions(+), 5 deletions(-) diff --git a/docs/red4ext-logging-shim.md b/docs/red4ext-logging-shim.md index ec87303..e15eddc 100644 --- a/docs/red4ext-logging-shim.md +++ b/docs/red4ext-logging-shim.md @@ -44,6 +44,18 @@ Current behavior: runtime type pointer at `.data` RVA `0x342f6a8`, logs the returned service object and slot `0x220`, then dynamically hooks that service `0x220` route lookup to log its route-id output handle. +- The observer1 service branch was later identified as a false positive: + `0x287c44` returns the `JournalManager` object (`vtable 0x1430f0890`) and + the dynamic slot `0x220` resolves route IDs to readable journal/UI metadata + strings such as `internet`, `home`, `clubs`, and `arasaka`. It is now left in + the source but no longer attached in the active probe. +- Hooks the GPS query lifecycle found from the `RunGPSQuery`/`UpdateGPSQuery` + native registration cluster: + `0x29bd128` (`RunGPSQuery` body), `0x29bd254` (`UpdateGPSQuery` body), + `0x70a42c` (shared query submitter), `0x70a570` (low-level query dispatch), + and `0x7094b8` (query result/path fetch). +- Logs query endpoints, returned query IDs, shared submitter return RVAs, query + state fields, result-fetch success, and summarized path buffers/point counts. - Resolves the native mappin system when one of those paths fires, logs relevant vtable slot addresses, and temporarily hooks the route-adjacent slots. @@ -63,8 +75,11 @@ Current route-probe focus: - mappin route observer callbacks: `0xaa6610`, `0xaa6628`, `0xaa63e0`, `0x27b10c0`, `0x295d4a0`, `0x286a85c` - observer1 service lookup and returned service route lookup: - `0x287c44`, dynamic service vtable slot `0x220` + `0x287c44`, dynamic service vtable slot `0x220` (disabled after being + classified as journal metadata) - route-build candidate called by the bridge: `0x5625a4` +- GPS query lifecycle: `0x29bd128`, `0x29bd254`, `0x70a42c`, `0x70a570`, + `0x7094b8` Quest/objective pins did not fire the mappin tracking hooks in live tests. The REDscript decompile shows that those pins call `JournalManager.TrackEntry` @@ -98,6 +113,27 @@ Newest controlled test: - Custom pin routing used the separate custom-position mappin path, then the shared route activate/deactivate helper. +Latest observer1 probe result: + +- The `Observer1ServiceLookup` hook fired, but it proved observer1 is not route + solving. It returned the same `JournalManager` vtable seen in earlier journal + traces, and its slot `0x220` resolved numeric route IDs into journal/UI + category strings. That branch is closed as marker metadata. + +Current static lead: + +- `RunGPSQuery` and `UpdateGPSQuery` are registered native thunks at + `0x29bd5ac` and `0x29bd6c8`. Their deeper bodies are `0x29bd128` and + `0x29bd254`. +- `RunGPSQuery` submits a query through a subsystem reached via an object field + at `+0x130`, returning a query ID or `-1`. +- `UpdateGPSQuery` fetches a completed query result through `0x7094b8`, copies a + path point buffer, and writes the resulting point array for script/UI + consumption. +- The shared submitter `0x70a42c` is also used by non-GPS navigation callers, + so return RVAs and query IDs are important for separating map GPS from traffic + or AI path requests. + Build and install from the Fedora toolbox: ```bash diff --git a/red4ext/EdgeWeightGPS/src/Main.cpp b/red4ext/EdgeWeightGPS/src/Main.cpp index 67cfea4..fff5a86 100644 --- a/red4ext/EdgeWeightGPS/src/Main.cpp +++ b/red4ext/EdgeWeightGPS/src/Main.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include namespace RED4ext::v1 @@ -140,6 +141,11 @@ constexpr uintptr_t kRouteObserver23DeactivateRva = 0x286A85C; constexpr uintptr_t kObserver1SystemTypeGlobalRva = 0x342F6A8; constexpr uintptr_t kObserver1ServiceLookupRva = 0x0287C44; constexpr uintptr_t kObserver1ServiceRouteLookupSlotOffset = 0x220; +constexpr uintptr_t kRunGpsQueryBodyRva = 0x29BD128; +constexpr uintptr_t kUpdateGpsQueryBodyRva = 0x29BD254; +constexpr uintptr_t kGpsQuerySubmitRva = 0x070A42C; +constexpr uintptr_t kGpsQueryDispatchRva = 0x070A570; +constexpr uintptr_t kGpsQueryResultFetchRva = 0x07094B8; HMODULE gModule = nullptr; std::mutex gLogMutex; @@ -194,6 +200,11 @@ uint32_t gRouteObserver1DeactivateLogCount = 0; uint32_t gRouteObserver23DeactivateLogCount = 0; uint32_t gObserver1ServiceLookupLogCount = 0; uint32_t gObserver1ServiceRouteLookupLogCount = 0; +uint32_t gRunGpsQueryBodyLogCount = 0; +uint32_t gUpdateGpsQueryBodyLogCount = 0; +uint32_t gGpsQuerySubmitLogCount = 0; +uint32_t gGpsQueryDispatchLogCount = 0; +uint32_t gGpsQueryResultFetchLogCount = 0; uint64_t gLogStartTick = 0; uint64_t gLogFrequency = 0; void* gMappinSetTrackedTarget = nullptr; @@ -237,6 +248,11 @@ using RouteBuildCandidateFunc = void (*)(void* aThis, uint32_t aRouteId, uint32_ using RouteObserverCallbackFunc = void (*)(void* aObserver, void* aRouteEntry); using Observer1ServiceLookupFunc = void* (*)(void* aServiceOwner, void* aSystemType); using Observer1ServiceRouteLookupFunc = void (*)(void* aService, void* aOut, uint32_t aRouteId); +using RunGpsQueryBodyFunc = bool (*)(void* aThis, void* aStart, void* aEnd, int32_t* aOutQueryId); +using UpdateGpsQueryBodyFunc = bool (*)(void* aThis, uint32_t aQueryId, void* aOutPoints, void* aOutError); +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); ProbeFunc gOriginalGpsSystemTick = nullptr; JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr; ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr; @@ -282,6 +298,11 @@ RouteObserverCallbackFunc gOriginalRouteObserver1Deactivate = nullptr; RouteObserverCallbackFunc gOriginalRouteObserver23Deactivate = nullptr; Observer1ServiceLookupFunc gOriginalObserver1ServiceLookup = nullptr; Observer1ServiceRouteLookupFunc gOriginalObserver1ServiceRouteLookup = nullptr; +RunGpsQueryBodyFunc gOriginalRunGpsQueryBody = nullptr; +UpdateGpsQueryBodyFunc gOriginalUpdateGpsQueryBody = nullptr; +GpsQuerySubmitFunc gOriginalGpsQuerySubmit = nullptr; +GpsQueryDispatchFunc gOriginalGpsQueryDispatch = nullptr; +GpsQueryResultFetchFunc gOriginalGpsQueryResultFetch = nullptr; std::filesystem::path GetLogPath() { @@ -627,6 +648,68 @@ void AppendByteField(std::ostringstream& aLine, const char* aLabel, void* aObjec } } +struct Vector4Value +{ + float x; + float y; + float z; + float w; +}; + +void AppendVector4Pointer(std::ostringstream& aLine, const char* aLabel, const void* aVector) +{ + Vector4Value value{}; + aLine << " " << aLabel << "="; + if (aVector && TryReadMemory(aVector, value)) + { + aLine << "(" << value.x << "," << value.y << "," << value.z << "," << value.w << ")"; + } + else + { + aLine << ""; + } +} + +void AppendVector4Field(std::ostringstream& aLine, const char* aLabel, void* aObject, uintptr_t aOffset) +{ + if (!aObject) + { + aLine << " " << aLabel << "="; + return; + } + + AppendVector4Pointer(aLine, aLabel, reinterpret_cast(reinterpret_cast(aObject) + aOffset)); +} + +void AppendPathBufferSummary(std::ostringstream& aLine, const char* aPrefix, void* aPathBuffer) +{ + AppendPointerWithRva(aLine, aPrefix, aPathBuffer); + if (!aPathBuffer) + { + return; + } + + uint32_t count = 0; + auto* data = ReadPointerField(aPathBuffer, 0x30); + TryReadMemory(reinterpret_cast(reinterpret_cast(aPathBuffer) + 0x3C), count); + + aLine << " " << aPrefix << "_count=" << count; + AppendPointerWithRva(aLine, (std::string(aPrefix) + "_data").c_str(), data); + + if (!data || count == 0 || count > 100000) + { + return; + } + + AppendVector4Pointer(aLine, (std::string(aPrefix) + "_first").c_str(), data); + if (count > 1) + { + AppendVector4Pointer(aLine, (std::string(aPrefix) + "_last").c_str(), + reinterpret_cast(reinterpret_cast(data) + + static_cast(count - 1) * 12)); + } +} + void AppendRouteRefFields(std::ostringstream& aLine, const char* aPrefix, void* aRouteRef) { AppendPointerWithRva(aLine, aPrefix, aRouteRef); @@ -1741,6 +1824,183 @@ void* DetourMappinActiveRoute(void* aSystem) return result; } +bool DetourRunGpsQueryBody(void* aThis, void* aStart, void* aEnd, int32_t* aOutQueryId) +{ + constexpr uint32_t kMaxCalls = 96; + + const auto callCount = gRunGpsQueryBodyLogCount; + const auto shouldLog = callCount < kMaxCalls; + if (shouldLog) + { + std::ostringstream line; + line << "hook RunGPSQuery body 0x29bd128 call=" << callCount; + AppendPointerWithRva(line, "this", aThis); + AppendPointerWithRva(line, "outQueryId", aOutQueryId); + AppendVector4Pointer(line, "start", aStart); + AppendVector4Pointer(line, "end", aEnd); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + ++gRunGpsQueryBodyLogCount; + } + + bool result = false; + if (gOriginalRunGpsQueryBody) + { + result = gOriginalRunGpsQueryBody(aThis, aStart, aEnd, aOutQueryId); + } + + if (shouldLog) + { + int32_t queryId = 0; + const auto hasQueryId = aOutQueryId && TryReadMemory(aOutQueryId, queryId); + + std::ostringstream line; + line << "hook RunGPSQuery body result call=" << callCount << " value=" << static_cast(result) + << " hasQueryId=" << hasQueryId; + if (hasQueryId) + { + line << " queryId=" << queryId; + } + LogRed4ext(line.str()); + } + + return result; +} + +bool DetourUpdateGpsQueryBody(void* aThis, uint32_t aQueryId, void* aOutPoints, void* aOutError) +{ + constexpr uint32_t kMaxCalls = 128; + + const auto callCount = gUpdateGpsQueryBodyLogCount; + const auto shouldLog = callCount < kMaxCalls; + if (shouldLog) + { + std::ostringstream line; + line << "hook UpdateGPSQuery body 0x29bd254 call=" << callCount << " queryId=" << aQueryId; + AppendPointerWithRva(line, "this", aThis); + AppendPointerWithRva(line, "outPoints", aOutPoints); + AppendPointerWithRva(line, "outError", aOutError); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + ++gUpdateGpsQueryBodyLogCount; + } + + bool result = false; + if (gOriginalUpdateGpsQueryBody) + { + result = gOriginalUpdateGpsQueryBody(aThis, aQueryId, aOutPoints, aOutError); + } + + if (shouldLog) + { + std::ostringstream line; + line << "hook UpdateGPSQuery body result call=" << callCount << " value=" << static_cast(result); + AppendPathBufferSummary(line, "cachedPath", ReadPointerField(aThis, 0x58)); + LogRed4ext(line.str()); + } + + return result; +} + +int32_t DetourGpsQuerySubmit(void* aManager, void* aQuery) +{ + constexpr uint32_t kMaxCalls = 128; + + const auto callCount = gGpsQuerySubmitLogCount; + const auto shouldLog = callCount < kMaxCalls; + if (shouldLog) + { + std::ostringstream line; + line << "hook GPSQuerySubmit 0x70a42c call=" << callCount; + AppendPointerWithRva(line, "manager", aManager); + AppendPointerWithRva(line, "query", aQuery); + AppendUint32Field(line, "query_f00", aQuery, 0x00); + AppendUint32Field(line, "query_f04", aQuery, 0x04); + AppendUint32Field(line, "query_f08", aQuery, 0x08); + AppendUint32Field(line, "query_f0c", aQuery, 0x0C); + AppendUint32Field(line, "query_fcc", aQuery, 0xCC); + AppendVector4Field(line, "query_pos58", aQuery, 0x58); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + ++gGpsQuerySubmitLogCount; + } + + int32_t result = -1; + if (gOriginalGpsQuerySubmit) + { + result = gOriginalGpsQuerySubmit(aManager, aQuery); + } + + if (shouldLog) + { + std::ostringstream line; + line << "hook GPSQuerySubmit result call=" << callCount << " queryId=" << result; + AppendUint32Field(line, "manager_nextIdD4", aManager, 0xD4); + LogRed4ext(line.str()); + } + + return result; +} + +void DetourGpsQueryDispatch(void* aRuntimeQuery, void* aTargetPosition, void* aArg3, void* aArg4) +{ + constexpr uint32_t kMaxCalls = 128; + + if (gGpsQueryDispatchLogCount < kMaxCalls) + { + std::ostringstream line; + line << "hook GPSQueryDispatch 0x70a570 call=" << gGpsQueryDispatchLogCount; + AppendPointerWithRva(line, "runtimeQuery", aRuntimeQuery); + AppendPointerWithRva(line, "arg3", aArg3); + AppendPointerWithRva(line, "arg4", aArg4); + AppendUint32Field(line, "runtimeQuery_state138", aRuntimeQuery, 0x138); + AppendUint32Field(line, "runtimeQuery_bank13c", aRuntimeQuery, 0x13C); + AppendVector4Pointer(line, "target", aTargetPosition); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + ++gGpsQueryDispatchLogCount; + } + + if (gOriginalGpsQueryDispatch) + { + gOriginalGpsQueryDispatch(aRuntimeQuery, aTargetPosition, aArg3, aArg4); + } +} + +bool DetourGpsQueryResultFetch(void* aManager, uint32_t aQueryId, void* aOutPath) +{ + constexpr uint32_t kMaxCalls = 128; + + 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; + } + + bool result = false; + if (gOriginalGpsQueryResultFetch) + { + result = gOriginalGpsQueryResultFetch(aManager, aQueryId, aOutPath); + } + + if (shouldLog) + { + std::ostringstream line; + line << "hook GPSQueryResultFetch result call=" << callCount << " value=" << static_cast(result); + AppendPathBufferSummary(line, "outPath", aOutPath); + LogRed4ext(line.str()); + } + + return result; +} + void AttachProbeHook(const char* aName, uintptr_t aRva, void* aDetour, void** aOriginal) { if (!gSdk || !gSdk->hooking || !gSdk->hooking->Attach) @@ -2049,6 +2309,21 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e const auto added = aSdk->gameStates->Add(aHandle, kGameStateRunning, &gRunningState); LogRed4ext(added ? "registered Running game-state callbacks" : "failed to register Running game-state callbacks"); + AttachProbeHook("RunGPSQuery body 0x29bd128", kRunGpsQueryBodyRva, + reinterpret_cast(&DetourRunGpsQueryBody), + reinterpret_cast(&gOriginalRunGpsQueryBody)); + AttachProbeHook("UpdateGPSQuery body 0x29bd254", kUpdateGpsQueryBodyRva, + reinterpret_cast(&DetourUpdateGpsQueryBody), + reinterpret_cast(&gOriginalUpdateGpsQueryBody)); + AttachProbeHook("GPSQuerySubmit 0x70a42c", kGpsQuerySubmitRva, + reinterpret_cast(&DetourGpsQuerySubmit), + reinterpret_cast(&gOriginalGpsQuerySubmit)); + AttachProbeHook("GPSQueryDispatch 0x70a570", kGpsQueryDispatchRva, + reinterpret_cast(&DetourGpsQueryDispatch), + reinterpret_cast(&gOriginalGpsQueryDispatch)); + AttachProbeHook("GPSQueryResultFetch 0x7094b8", kGpsQueryResultFetchRva, + reinterpret_cast(&DetourGpsQueryResultFetch), + reinterpret_cast(&gOriginalGpsQueryResultFetch)); AttachProbeHook("GPSSystem/Tick", kGpsSystemTickRva, reinterpret_cast(&DetourGpsSystemTick), reinterpret_cast(&gOriginalGpsSystemTick)); AttachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva, @@ -2117,9 +2392,6 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e AttachProbeHook("RouteObserver23::Deactivate slot50", kRouteObserver23DeactivateRva, reinterpret_cast(&DetourRouteObserver23Deactivate), reinterpret_cast(&gOriginalRouteObserver23Deactivate)); - AttachProbeHook("Observer1ServiceLookup 0x287c44", kObserver1ServiceLookupRva, - reinterpret_cast(&DetourObserver1ServiceLookup), - reinterpret_cast(&gOriginalObserver1ServiceLookup)); AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva, reinterpret_cast(&DetourFrameMappinPathWrapper), reinterpret_cast(&gOriginalFrameMappinPathWrapper)); @@ -2159,6 +2431,11 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e if (aReason == RED4ext::v1::EMainReason::Unload) { + DetachProbeHook("RunGPSQuery body 0x29bd128", kRunGpsQueryBodyRva); + DetachProbeHook("UpdateGPSQuery body 0x29bd254", kUpdateGpsQueryBodyRva); + DetachProbeHook("GPSQuerySubmit 0x70a42c", kGpsQuerySubmitRva); + DetachProbeHook("GPSQueryDispatch 0x70a570", kGpsQueryDispatchRva); + DetachProbeHook("GPSQueryResultFetch 0x7094b8", kGpsQueryResultFetchRva); DetachProbeHook("GPSSystem/Tick", kGpsSystemTickRva); DetachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva); DetachProbeHook("JournalListener::Objective vt28", kJournalListenerObjectiveVt28Rva); @@ -2182,7 +2459,6 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e DetachProbeHook("RouteObserver0::Deactivate slot50", kRouteObserver0DeactivateRva); DetachProbeHook("RouteObserver1::Deactivate slot50", kRouteObserver1DeactivateRva); DetachProbeHook("RouteObserver23::Deactivate slot50", kRouteObserver23DeactivateRva); - DetachProbeHook("Observer1ServiceLookup 0x287c44", kObserver1ServiceLookupRva); DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva); DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva); DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);