Probe journal route listeners

This commit is contained in:
2026-06-20 00:25:47 -05:00
parent 71c044f643
commit 451e84beb1
3 changed files with 167 additions and 3 deletions
+11
View File
@@ -16,6 +16,11 @@ Current behavior:
- Registers Running game-state callbacks so route tests can be timestamped.
- Hooks selected native wrappers and cores around `GPSSystem`, mappin tracking,
world-map mappin selection, and map path framing.
- Hooks `JournalManager.TrackEntry` at RVA `0x5944fc`, which is the native
handoff used by quest/objective world-map route plotting.
- Dumps the `JournalManager` listener array at offsets `0x210`/`0x21c` and the
listener vtable slots `0x28`, `0x30`, and `0x50`, matching the indirect calls
made by the native `TrackEntry` implementation.
- Resolves the native mappin system when one of those paths fires, logs relevant
vtable slot addresses, and temporarily hooks the route-adjacent slots.
@@ -26,6 +31,12 @@ Current route-probe focus:
`0x27c49c4`, `0x27c1684`
- `TrackCustomPositionMappin` wrapper/core: `0x27c4aac`, `0x27c2318`
- mappin-system slots `0x1f0`, `0x280`, and `0x2f0`
- `JournalManager.TrackEntry` implementation: `0x5944fc`
Quest/objective pins did not fire the mappin tracking hooks in live tests. The
REDscript decompile shows that those pins call `JournalManager.TrackEntry`
instead, so the current useful runtime question is which native listener reacts
to tracked-entry changes.
Build and install from the Fedora toolbox:
+5 -3
View File
@@ -546,6 +546,8 @@ Better versions of this mod could:
graph component
- ship multiple archives only if a data patch is proven to affect routing
The next practical step is one more in-game route-plotting run with the current
RED4ext mappin-system slot probe installed. If it logs concrete slot RVAs, the
next static-disassembly target is no longer a guess.
The next practical runtime step is no longer broad map/mappin logging. The
installed RED4ext shim now hooks `JournalManager.TrackEntry` directly and dumps
the listener array that native `TrackEntry` calls through. A short route-plotting
run should identify which native listener consumes tracked quest/objective
changes; that listener is the next focused static-disassembly target.
+151
View File
@@ -91,6 +91,9 @@ namespace
{
constexpr uint32_t kGameStateRunning = 2;
constexpr uintptr_t kGpsSystemTickRva = 0x0E6B62C;
constexpr uintptr_t kJournalTrackEntryImplRva = 0x05944FC;
constexpr uintptr_t kJournalListenerArrayOffset = 0x210;
constexpr uintptr_t kJournalListenerCountOffset = 0x21C;
constexpr uintptr_t kMappinSystemResolverRva = 0x02BB840;
constexpr uintptr_t kFrameMappinPathWrapperRva = 0x27C4314;
constexpr uintptr_t kFrameMappinPathCoreRva = 0x27BC1EC;
@@ -120,6 +123,8 @@ const RED4ext::v1::Sdk* gSdk = nullptr;
uint32_t gUpdateLogCount = 0;
bool gScannedExecutable = false;
uint32_t gGpsTickLogCount = 0;
uint32_t gJournalTrackEntryLogCount = 0;
uint32_t gJournalTrackEntryListenerDumpCount = 0;
uint32_t gFrameMappinPathWrapperLogCount = 0;
uint32_t gFrameMappinPathCoreLogCount = 0;
uint32_t gSetSelectedMappinWrapperLogCount = 0;
@@ -154,6 +159,7 @@ void* gMappinActiveRouteTarget = nullptr;
bool gMappinSlotHookAttachAttempted = false;
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);
using MappinSystemResolverFunc = void* (*)(void* aThis);
using FrameMappinPathCoreFunc = void (*)(void* aThis, uint32_t aPathKind, void* a3, void* aPathRect);
@@ -169,6 +175,7 @@ using MappinCreateCustomPositionFunc = void* (*)(void* aSystem, void* aName, voi
using MappinReleaseQueryFunc = void (*)(void* aSystem, void* aHandle);
using MappinActiveRouteFunc = void* (*)(void* aSystem);
ProbeFunc gOriginalGpsSystemTick = nullptr;
JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr;
ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr;
FrameMappinPathCoreFunc gOriginalFrameMappinPathCore = nullptr;
ScriptNativeVoidFunc gOriginalSetSelectedMappinWrapper = nullptr;
@@ -404,6 +411,58 @@ bool IsReadablePage(DWORD aProtect)
baseProtect == PAGE_EXECUTE_WRITECOPY;
}
bool IsReadableMemory(const void* aPointer, size_t aSize)
{
if (!aPointer || aSize == 0)
{
return false;
}
const auto begin = reinterpret_cast<uintptr_t>(aPointer);
const auto end = begin + aSize;
if (end < begin)
{
return false;
}
auto cursor = begin;
while (cursor < end)
{
MEMORY_BASIC_INFORMATION mbi{};
if (!VirtualQuery(reinterpret_cast<const void*>(cursor), &mbi, sizeof(mbi)))
{
return false;
}
if (mbi.State != MEM_COMMIT || !IsReadablePage(mbi.Protect))
{
return false;
}
const auto regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
if (regionEnd <= cursor)
{
return false;
}
cursor = regionEnd;
}
return true;
}
template<typename T>
bool TryReadMemory(const void* aPointer, T& aValue)
{
if (!IsReadableMemory(aPointer, sizeof(T)))
{
return false;
}
aValue = *reinterpret_cast<const T*>(aPointer);
return true;
}
void ScanRegionForString(const uint8_t* aRegionBegin, size_t aRegionSize, uintptr_t aImageBase, const char* aNeedle,
uint32_t& aMatches)
{
@@ -550,6 +609,94 @@ void DetourGpsSystemTick(void* aThis, void* a2, void* a3, void* a4)
}
}
void LogJournalListeners(uint32_t aCallCount, void* aThis)
{
constexpr uint32_t kMaxListenerDumps = 16;
constexpr uint32_t kMaxListenersPerDump = 24;
constexpr uint32_t kSuspiciousListenerCount = 512;
if (gJournalTrackEntryListenerDumpCount >= kMaxListenerDumps || !aThis)
{
return;
}
uintptr_t listenerBase = 0;
uint32_t listenerCount = 0;
const auto objectAddress = reinterpret_cast<uintptr_t>(aThis);
const auto hasBase = TryReadMemory(reinterpret_cast<const void*>(objectAddress + kJournalListenerArrayOffset),
listenerBase);
const auto hasCount = TryReadMemory(reinterpret_cast<const void*>(objectAddress + kJournalListenerCountOffset),
listenerCount);
std::ostringstream header;
header << "journal-listeners dump=" << gJournalTrackEntryListenerDumpCount << " call=" << aCallCount
<< " hasBase=" << hasBase << " hasCount=" << hasCount << " count=" << listenerCount;
AppendPointerWithRva(header, "base", reinterpret_cast<const void*>(listenerBase));
LogRed4ext(header.str());
++gJournalTrackEntryListenerDumpCount;
if (!hasBase || !hasCount || !listenerBase || listenerCount > kSuspiciousListenerCount)
{
return;
}
const auto cappedCount = std::min(listenerCount, kMaxListenersPerDump);
for (uint32_t index = 0; index < cappedCount; ++index)
{
void* listener = nullptr;
void* vtable = nullptr;
void* callback28 = nullptr;
void* callback30 = nullptr;
void* callback50 = nullptr;
const auto listenerAddress = reinterpret_cast<const void*>(listenerBase + index * sizeof(void*));
const auto hasListener = TryReadMemory(listenerAddress, listener);
const auto hasVtable = hasListener && TryReadMemory(listener, vtable);
if (hasVtable && vtable)
{
const auto vtableAddress = reinterpret_cast<uintptr_t>(vtable);
TryReadMemory(reinterpret_cast<const void*>(vtableAddress + 0x28), callback28);
TryReadMemory(reinterpret_cast<const void*>(vtableAddress + 0x30), callback30);
TryReadMemory(reinterpret_cast<const void*>(vtableAddress + 0x50), callback50);
}
std::ostringstream line;
line << "journal-listener call=" << aCallCount << " index=" << index << " hasListener=" << hasListener
<< " hasVtable=" << hasVtable;
AppendPointerWithRva(line, "listener", listener);
AppendPointerWithRva(line, "vtable", vtable);
AppendPointerWithRva(line, "vt28", callback28);
AppendPointerWithRva(line, "vt30", callback30);
AppendPointerWithRva(line, "vt50", callback50);
LogRed4ext(line.str());
}
}
void DetourJournalTrackEntry(void* aThis, void* aEntryHandle)
{
constexpr uint32_t kMaxCalls = 96;
const auto callCount = gJournalTrackEntryLogCount;
if (callCount < kMaxCalls)
{
std::ostringstream line;
line << "hook JournalManager::TrackEntry impl call=" << callCount;
AppendPointerWithRva(line, "this", aThis);
AppendPointerWithRva(line, "entryHandle", aEntryHandle);
AppendReturnRva(line, __builtin_return_address(0));
LogRed4ext(line.str());
LogJournalListeners(callCount, aThis);
++gJournalTrackEntryLogCount;
}
if (gOriginalJournalTrackEntry)
{
gOriginalJournalTrackEntry(aThis, aEntryHandle);
}
}
void DetourFrameMappinPathWrapper(void* aThis, void* aStack, void* aResult)
{
if (gFrameMappinPathWrapperLogCount < 64)
@@ -1144,6 +1291,9 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
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));
AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva,
reinterpret_cast<void*>(&DetourFrameMappinPathWrapper),
reinterpret_cast<void**>(&gOriginalFrameMappinPathWrapper));
@@ -1184,6 +1334,7 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
if (aReason == RED4ext::v1::EMainReason::Unload)
{
DetachProbeHook("GPSSystem/Tick", kGpsSystemTickRva);
DetachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva);
DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva);
DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva);
DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);