Probe journal route listeners
This commit is contained in:
@@ -16,6 +16,11 @@ Current behavior:
|
|||||||
- Registers Running game-state callbacks so route tests can be timestamped.
|
- Registers Running game-state callbacks so route tests can be timestamped.
|
||||||
- Hooks selected native wrappers and cores around `GPSSystem`, mappin tracking,
|
- Hooks selected native wrappers and cores around `GPSSystem`, mappin tracking,
|
||||||
world-map mappin selection, and map path framing.
|
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
|
- Resolves the native mappin system when one of those paths fires, logs relevant
|
||||||
vtable slot addresses, and temporarily hooks the route-adjacent slots.
|
vtable slot addresses, and temporarily hooks the route-adjacent slots.
|
||||||
|
|
||||||
@@ -26,6 +31,12 @@ Current route-probe focus:
|
|||||||
`0x27c49c4`, `0x27c1684`
|
`0x27c49c4`, `0x27c1684`
|
||||||
- `TrackCustomPositionMappin` wrapper/core: `0x27c4aac`, `0x27c2318`
|
- `TrackCustomPositionMappin` wrapper/core: `0x27c4aac`, `0x27c2318`
|
||||||
- mappin-system slots `0x1f0`, `0x280`, and `0x2f0`
|
- 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:
|
Build and install from the Fedora toolbox:
|
||||||
|
|
||||||
|
|||||||
@@ -546,6 +546,8 @@ Better versions of this mod could:
|
|||||||
graph component
|
graph component
|
||||||
- ship multiple archives only if a data patch is proven to affect routing
|
- 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
|
The next practical runtime step is no longer broad map/mappin logging. The
|
||||||
RED4ext mappin-system slot probe installed. If it logs concrete slot RVAs, the
|
installed RED4ext shim now hooks `JournalManager.TrackEntry` directly and dumps
|
||||||
next static-disassembly target is no longer a guess.
|
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.
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ namespace
|
|||||||
{
|
{
|
||||||
constexpr uint32_t kGameStateRunning = 2;
|
constexpr uint32_t kGameStateRunning = 2;
|
||||||
constexpr uintptr_t kGpsSystemTickRva = 0x0E6B62C;
|
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 kMappinSystemResolverRva = 0x02BB840;
|
||||||
constexpr uintptr_t kFrameMappinPathWrapperRva = 0x27C4314;
|
constexpr uintptr_t kFrameMappinPathWrapperRva = 0x27C4314;
|
||||||
constexpr uintptr_t kFrameMappinPathCoreRva = 0x27BC1EC;
|
constexpr uintptr_t kFrameMappinPathCoreRva = 0x27BC1EC;
|
||||||
@@ -120,6 +123,8 @@ const RED4ext::v1::Sdk* gSdk = nullptr;
|
|||||||
uint32_t gUpdateLogCount = 0;
|
uint32_t gUpdateLogCount = 0;
|
||||||
bool gScannedExecutable = false;
|
bool gScannedExecutable = false;
|
||||||
uint32_t gGpsTickLogCount = 0;
|
uint32_t gGpsTickLogCount = 0;
|
||||||
|
uint32_t gJournalTrackEntryLogCount = 0;
|
||||||
|
uint32_t gJournalTrackEntryListenerDumpCount = 0;
|
||||||
uint32_t gFrameMappinPathWrapperLogCount = 0;
|
uint32_t gFrameMappinPathWrapperLogCount = 0;
|
||||||
uint32_t gFrameMappinPathCoreLogCount = 0;
|
uint32_t gFrameMappinPathCoreLogCount = 0;
|
||||||
uint32_t gSetSelectedMappinWrapperLogCount = 0;
|
uint32_t gSetSelectedMappinWrapperLogCount = 0;
|
||||||
@@ -154,6 +159,7 @@ void* gMappinActiveRouteTarget = nullptr;
|
|||||||
bool gMappinSlotHookAttachAttempted = false;
|
bool gMappinSlotHookAttachAttempted = false;
|
||||||
|
|
||||||
using ProbeFunc = void (*)(void* aThis, void* a2, void* a3, void* a4);
|
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 ScriptNativeVoidFunc = void (*)(void* aThis, void* aStack, void* aResult);
|
||||||
using MappinSystemResolverFunc = void* (*)(void* aThis);
|
using MappinSystemResolverFunc = void* (*)(void* aThis);
|
||||||
using FrameMappinPathCoreFunc = void (*)(void* aThis, uint32_t aPathKind, void* a3, void* aPathRect);
|
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 MappinReleaseQueryFunc = void (*)(void* aSystem, void* aHandle);
|
||||||
using MappinActiveRouteFunc = void* (*)(void* aSystem);
|
using MappinActiveRouteFunc = void* (*)(void* aSystem);
|
||||||
ProbeFunc gOriginalGpsSystemTick = nullptr;
|
ProbeFunc gOriginalGpsSystemTick = nullptr;
|
||||||
|
JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr;
|
||||||
ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr;
|
ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr;
|
||||||
FrameMappinPathCoreFunc gOriginalFrameMappinPathCore = nullptr;
|
FrameMappinPathCoreFunc gOriginalFrameMappinPathCore = nullptr;
|
||||||
ScriptNativeVoidFunc gOriginalSetSelectedMappinWrapper = nullptr;
|
ScriptNativeVoidFunc gOriginalSetSelectedMappinWrapper = nullptr;
|
||||||
@@ -404,6 +411,58 @@ bool IsReadablePage(DWORD aProtect)
|
|||||||
baseProtect == PAGE_EXECUTE_WRITECOPY;
|
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,
|
void ScanRegionForString(const uint8_t* aRegionBegin, size_t aRegionSize, uintptr_t aImageBase, const char* aNeedle,
|
||||||
uint32_t& aMatches)
|
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)
|
void DetourFrameMappinPathWrapper(void* aThis, void* aStack, void* aResult)
|
||||||
{
|
{
|
||||||
if (gFrameMappinPathWrapperLogCount < 64)
|
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),
|
AttachProbeHook("GPSSystem/Tick", kGpsSystemTickRva, reinterpret_cast<void*>(&DetourGpsSystemTick),
|
||||||
reinterpret_cast<void**>(&gOriginalGpsSystemTick));
|
reinterpret_cast<void**>(&gOriginalGpsSystemTick));
|
||||||
|
AttachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva,
|
||||||
|
reinterpret_cast<void*>(&DetourJournalTrackEntry),
|
||||||
|
reinterpret_cast<void**>(&gOriginalJournalTrackEntry));
|
||||||
AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva,
|
AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva,
|
||||||
reinterpret_cast<void*>(&DetourFrameMappinPathWrapper),
|
reinterpret_cast<void*>(&DetourFrameMappinPathWrapper),
|
||||||
reinterpret_cast<void**>(&gOriginalFrameMappinPathWrapper));
|
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)
|
if (aReason == RED4ext::v1::EMainReason::Unload)
|
||||||
{
|
{
|
||||||
DetachProbeHook("GPSSystem/Tick", kGpsSystemTickRva);
|
DetachProbeHook("GPSSystem/Tick", kGpsSystemTickRva);
|
||||||
|
DetachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva);
|
||||||
DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva);
|
DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva);
|
||||||
DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva);
|
DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva);
|
||||||
DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);
|
DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);
|
||||||
|
|||||||
Reference in New Issue
Block a user