Probe GPS query lifecycle

This commit is contained in:
2026-06-20 12:47:30 -05:00
parent 8c8cc784e0
commit 50c81b780a
2 changed files with 317 additions and 5 deletions
+37 -1
View File
@@ -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
+280 -4
View File
@@ -19,6 +19,7 @@
#include <initializer_list>
#include <mutex>
#include <sstream>
#include <string>
#include <string_view>
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 << "<unreadable>";
}
}
void AppendVector4Field(std::ostringstream& aLine, const char* aLabel, void* aObject, uintptr_t aOffset)
{
if (!aObject)
{
aLine << " " << aLabel << "=<null>";
return;
}
AppendVector4Pointer(aLine, aLabel, reinterpret_cast<const void*>(reinterpret_cast<uintptr_t>(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<const void*>(reinterpret_cast<uintptr_t>(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<const void*>(reinterpret_cast<uintptr_t>(data) +
static_cast<uintptr_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<void*>(&DetourRunGpsQueryBody),
reinterpret_cast<void**>(&gOriginalRunGpsQueryBody));
AttachProbeHook("UpdateGPSQuery body 0x29bd254", kUpdateGpsQueryBodyRva,
reinterpret_cast<void*>(&DetourUpdateGpsQueryBody),
reinterpret_cast<void**>(&gOriginalUpdateGpsQueryBody));
AttachProbeHook("GPSQuerySubmit 0x70a42c", kGpsQuerySubmitRva,
reinterpret_cast<void*>(&DetourGpsQuerySubmit),
reinterpret_cast<void**>(&gOriginalGpsQuerySubmit));
AttachProbeHook("GPSQueryDispatch 0x70a570", kGpsQueryDispatchRva,
reinterpret_cast<void*>(&DetourGpsQueryDispatch),
reinterpret_cast<void**>(&gOriginalGpsQueryDispatch));
AttachProbeHook("GPSQueryResultFetch 0x7094b8", kGpsQueryResultFetchRva,
reinterpret_cast<void*>(&DetourGpsQueryResultFetch),
reinterpret_cast<void**>(&gOriginalGpsQueryResultFetch));
AttachProbeHook("GPSSystem/Tick", kGpsSystemTickRva, reinterpret_cast<void*>(&DetourGpsSystemTick),
reinterpret_cast<void**>(&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<void*>(&DetourRouteObserver23Deactivate),
reinterpret_cast<void**>(&gOriginalRouteObserver23Deactivate));
AttachProbeHook("Observer1ServiceLookup 0x287c44", kObserver1ServiceLookupRva,
reinterpret_cast<void*>(&DetourObserver1ServiceLookup),
reinterpret_cast<void**>(&gOriginalObserver1ServiceLookup));
AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva,
reinterpret_cast<void*>(&DetourFrameMappinPathWrapper),
reinterpret_cast<void**>(&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);