Probe journal listener route callbacks

This commit is contained in:
2026-06-20 00:40:34 -05:00
parent 451e84beb1
commit 3b0a66a87a
3 changed files with 264 additions and 0 deletions
+17
View File
@@ -21,6 +21,11 @@ Current behavior:
- Dumps the `JournalManager` listener array at offsets `0x210`/`0x21c` and the - Dumps the `JournalManager` listener array at offsets `0x210`/`0x21c` and the
listener vtable slots `0x28`, `0x30`, and `0x50`, matching the indirect calls listener vtable slots `0x28`, `0x30`, and `0x50`, matching the indirect calls
made by the native `TrackEntry` implementation. made by the native `TrackEntry` implementation.
- Hooks the non-default journal listener callbacks seen during route plotting:
`0xea89a8`, `0xea8958`, `0xe63f80`, `0xe63e6c`, `0xe63f00`,
`0x8d136c`, `0x431a34`, `0x55a4e4`, and `0x14de238`.
- For those listener callbacks, logs the listener object, event pointer, common
object fields, and vtable slots on candidate nested owner/service objects.
- 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.
@@ -32,12 +37,24 @@ Current route-probe focus:
- `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` - `JournalManager.TrackEntry` implementation: `0x5944fc`
- non-default `TrackEntry` listener callbacks listed above
Quest/objective pins did not fire the mappin tracking hooks in live tests. The Quest/objective pins did not fire the mappin tracking hooks in live tests. The
REDscript decompile shows that those pins call `JournalManager.TrackEntry` REDscript decompile shows that those pins call `JournalManager.TrackEntry`
instead, so the current useful runtime question is which native listener reacts instead, so the current useful runtime question is which native listener reacts
to tracked-entry changes. to tracked-entry changes.
Most recent controlled test:
- `06:31:43`: user hit Continue / GPS tick logged.
- `06:32:02`: automatic `TrackEntry` during load, no route input.
- `06:32:27`: map opened.
- `06:32:44`, `06:32:50`, `06:32:56`: deliberate quest/objective route plots
produced `JournalManager::TrackEntry` calls from return RVA `0x26ac34e`.
- After map open, the journal listener count rose to 254. Most listener entries
dispatch to no-op `0x14a700`; the non-default callbacks above are the current
drill-down targets.
Build and install from the Fedora toolbox: Build and install from the Fedora toolbox:
```bash ```bash
+45
View File
@@ -217,6 +217,51 @@ The GPS query helpers appear to be exposed helper/test surfaces, not the route
path used by the world map. `TrackEntry` is now the highest-confidence native path used by the world map. `TrackEntry` is now the highest-confidence native
handoff for quest/objective route plotting. handoff for quest/objective route plotting.
## Journal Listener Fanout
`JournalManager.TrackEntry` at RVA `0x5944fc` is now confirmed at runtime as the
quest/objective route-plot entry point. A controlled test produced route-plot
calls at `06:32:44`, `06:32:50`, and `06:32:56`, all from return RVA
`0x26ac34e`, which is the script/native wrapper path for `TrackEntry`.
The native `TrackEntry` implementation updates the tracked-entry state and then
fans out through a listener array:
```text
JournalManager + 0x210: listener pointer array
JournalManager + 0x21c: listener count
listener vtable + 0x28: one tracked-entry callback path
listener vtable + 0x30: another tracked-entry callback path
listener vtable + 0x50: untrack/default path
```
Before the save is fully loaded the listener count is small. After the world map
is opened it rises to 254. Most entries use the no-op callback `0x14a700`, but
the live route test repeatedly exposed this small non-default set:
```text
0xea89a8 / 0xea8958
0xe63f80 / 0xe63e6c
0xe63f00 / 0x8d136c
0x431a34 / 0x55a4e4
0x14de238
```
Static disassembly of these callbacks suggests the following split:
- `0x14a700` is a plain empty return.
- `0xea89a8` and `0xea8958` are objective/story tracking callbacks; their vtable
lives beside a `quests/street_stories/.../generic_sts_objective` string.
- `0x431a34`, `0x55a4e4`, and `0x14de238` are heavy UI/tracker state callbacks.
- `0xe63f80` and `0xe63e6c` remain the most promising route-refresh callbacks:
they read nested listener fields at `+0x70` and `+0x78` and call high vtable
slots such as `0x218`, `0x230`, `0x240`, `0x340`, and `0x348`.
The installed probe now hooks those non-default callbacks directly and logs the
nested owner/service vtables. The next target is to identify which nested vtable
slot performs the route query or route cache update, then disassemble or patch
that function.
## Native False Positives ## Native False Positives
Static string scans found a tempting traffic/pathfinding cluster around RVA Static string scans found a tempting traffic/pathfinding cluster around RVA
+202
View File
@@ -16,6 +16,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <initializer_list>
#include <mutex> #include <mutex>
#include <sstream> #include <sstream>
#include <string_view> #include <string_view>
@@ -115,6 +116,15 @@ constexpr uintptr_t kMappinPositionQuerySlotOffset = 0x2E0;
constexpr uintptr_t kMappinCreateCustomPositionSlotOffset = 0x2F0; constexpr uintptr_t kMappinCreateCustomPositionSlotOffset = 0x2F0;
constexpr uintptr_t kMappinReleaseQuerySlotOffset = 0x368; constexpr uintptr_t kMappinReleaseQuerySlotOffset = 0x368;
constexpr uintptr_t kMappinActiveRouteSlotOffset = 0x3A8; constexpr uintptr_t kMappinActiveRouteSlotOffset = 0x3A8;
constexpr uintptr_t kJournalListenerObjectiveVt28Rva = 0x0EA89A8;
constexpr uintptr_t kJournalListenerObjectiveVt30Rva = 0x0EA8958;
constexpr uintptr_t kJournalListenerRouteVt28Rva = 0x0E63F80;
constexpr uintptr_t kJournalListenerRouteVt30Rva = 0x0E63E6C;
constexpr uintptr_t kJournalListenerRouteAltVt28Rva = 0x0E63F00;
constexpr uintptr_t kJournalListenerRouteAltVt30Rva = 0x08D136C;
constexpr uintptr_t kJournalListenerUiVt28Rva = 0x0431A34;
constexpr uintptr_t kJournalListenerUiVt30Rva = 0x055A4E4;
constexpr uintptr_t kJournalListenerTrackerVt28Rva = 0x14DE238;
HMODULE gModule = nullptr; HMODULE gModule = nullptr;
std::mutex gLogMutex; std::mutex gLogMutex;
@@ -146,6 +156,15 @@ uint32_t gMappinPositionQueryLogCount = 0;
uint32_t gMappinCreateCustomPositionLogCount = 0; uint32_t gMappinCreateCustomPositionLogCount = 0;
uint32_t gMappinReleaseQueryLogCount = 0; uint32_t gMappinReleaseQueryLogCount = 0;
uint32_t gMappinActiveRouteLogCount = 0; uint32_t gMappinActiveRouteLogCount = 0;
uint32_t gJournalListenerObjectiveVt28LogCount = 0;
uint32_t gJournalListenerObjectiveVt30LogCount = 0;
uint32_t gJournalListenerRouteVt28LogCount = 0;
uint32_t gJournalListenerRouteVt30LogCount = 0;
uint32_t gJournalListenerRouteAltVt28LogCount = 0;
uint32_t gJournalListenerRouteAltVt30LogCount = 0;
uint32_t gJournalListenerUiVt28LogCount = 0;
uint32_t gJournalListenerUiVt30LogCount = 0;
uint32_t gJournalListenerTrackerVt28LogCount = 0;
uint64_t gLogStartTick = 0; uint64_t gLogStartTick = 0;
uint64_t gLogFrequency = 0; uint64_t gLogFrequency = 0;
void* gMappinSetTrackedTarget = nullptr; void* gMappinSetTrackedTarget = nullptr;
@@ -174,6 +193,7 @@ using MappinPositionQueryFunc = void* (*)(void* aSystem, void* a2, void* a3, voi
using MappinCreateCustomPositionFunc = void* (*)(void* aSystem, void* aName, void* aParams, void* aPositionData); using MappinCreateCustomPositionFunc = void* (*)(void* aSystem, void* aName, void* aParams, void* aPositionData);
using MappinReleaseQueryFunc = void (*)(void* aSystem, void* aHandle); using MappinReleaseQueryFunc = void (*)(void* aSystem, void* aHandle);
using MappinActiveRouteFunc = void* (*)(void* aSystem); using MappinActiveRouteFunc = void* (*)(void* aSystem);
using JournalListenerCallbackFunc = void (*)(void* aListener, void* aEvent);
ProbeFunc gOriginalGpsSystemTick = nullptr; ProbeFunc gOriginalGpsSystemTick = nullptr;
JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr; JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr;
ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr; ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr;
@@ -196,6 +216,15 @@ MappinPositionQueryFunc gOriginalMappinPositionQuery = nullptr;
MappinCreateCustomPositionFunc gOriginalMappinCreateCustomPosition = nullptr; MappinCreateCustomPositionFunc gOriginalMappinCreateCustomPosition = nullptr;
MappinReleaseQueryFunc gOriginalMappinReleaseQuery = nullptr; MappinReleaseQueryFunc gOriginalMappinReleaseQuery = nullptr;
MappinActiveRouteFunc gOriginalMappinActiveRoute = nullptr; MappinActiveRouteFunc gOriginalMappinActiveRoute = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerObjectiveVt28 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerObjectiveVt30 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerRouteVt28 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerRouteVt30 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerRouteAltVt28 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerRouteAltVt30 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerUiVt28 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerUiVt30 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerTrackerVt28 = nullptr;
std::filesystem::path GetLogPath() std::filesystem::path GetLogPath()
{ {
@@ -463,6 +492,72 @@ bool TryReadMemory(const void* aPointer, T& aValue)
return true; return true;
} }
void* ReadPointerField(void* aObject, uintptr_t aOffset)
{
void* value = nullptr;
if (!aObject)
{
return nullptr;
}
TryReadMemory(reinterpret_cast<const void*>(reinterpret_cast<uintptr_t>(aObject) + aOffset), value);
return value;
}
void AppendObjectVtableSlots(std::ostringstream& aLine, void* aObject, std::initializer_list<uintptr_t> aSlots)
{
auto* vtable = ReadPointerField(aObject, 0);
AppendPointerWithRva(aLine, "vtable", vtable);
for (const auto slot : aSlots)
{
auto* target = vtable ? ReadPointerField(vtable, slot) : nullptr;
std::ostringstream label;
label << "slot" << std::hex << slot;
AppendPointerWithRva(aLine, label.str().c_str(), target);
}
}
void LogJournalListenerContext(const char* aName, uint32_t aCount, void* aListener, void* aEvent)
{
std::ostringstream line;
line << "hook " << aName << " call=" << aCount;
AppendPointerWithRva(line, "listener", aListener);
AppendPointerWithRva(line, "event", aEvent);
AppendReturnRva(line, __builtin_return_address(0));
auto* field08 = ReadPointerField(aListener, 0x08);
auto* field18 = ReadPointerField(aListener, 0x18);
auto* field20 = ReadPointerField(aListener, 0x20);
auto* field70 = ReadPointerField(aListener, 0x70);
auto* field78 = ReadPointerField(aListener, 0x78);
AppendPointerWithRva(line, "field08", field08);
AppendPointerWithRva(line, "field18", field18);
AppendPointerWithRva(line, "field20", field20);
AppendPointerWithRva(line, "field70", field70);
AppendPointerWithRva(line, "field78", field78);
LogRed4ext(line.str());
if (field70)
{
std::ostringstream owner;
owner << "journal-listener-context " << aName << " call=" << aCount;
AppendPointerWithRva(owner, "field70", field70);
AppendObjectVtableSlots(owner, field70, {0x1C8, 0x218, 0x230, 0x238, 0x240, 0x248, 0x340, 0x348, 0x358});
LogRed4ext(owner.str());
}
if (field78 && field78 != field70)
{
std::ostringstream service;
service << "journal-listener-context " << aName << " call=" << aCount;
AppendPointerWithRva(service, "field78", field78);
AppendObjectVtableSlots(service, field78,
{0x1C8, 0x218, 0x220, 0x230, 0x238, 0x240, 0x248, 0x340, 0x348, 0x358});
LogRed4ext(service.str());
}
}
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)
{ {
@@ -697,6 +792,77 @@ void DetourJournalTrackEntry(void* aThis, void* aEntryHandle)
} }
} }
void DispatchJournalListenerCallback(const char* aName, uint32_t& aCounter, JournalListenerCallbackFunc aOriginal,
void* aListener, void* aEvent)
{
constexpr uint32_t kMaxCalls = 64;
if (aCounter < kMaxCalls)
{
LogJournalListenerContext(aName, aCounter, aListener, aEvent);
++aCounter;
}
if (aOriginal)
{
aOriginal(aListener, aEvent);
}
}
void DetourJournalListenerObjectiveVt28(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::Objective vt28", gJournalListenerObjectiveVt28LogCount,
gOriginalJournalListenerObjectiveVt28, aListener, aEvent);
}
void DetourJournalListenerObjectiveVt30(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::Objective vt30", gJournalListenerObjectiveVt30LogCount,
gOriginalJournalListenerObjectiveVt30, aListener, aEvent);
}
void DetourJournalListenerRouteVt28(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::Route vt28", gJournalListenerRouteVt28LogCount,
gOriginalJournalListenerRouteVt28, aListener, aEvent);
}
void DetourJournalListenerRouteVt30(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::Route vt30", gJournalListenerRouteVt30LogCount,
gOriginalJournalListenerRouteVt30, aListener, aEvent);
}
void DetourJournalListenerRouteAltVt28(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::RouteAlt vt28", gJournalListenerRouteAltVt28LogCount,
gOriginalJournalListenerRouteAltVt28, aListener, aEvent);
}
void DetourJournalListenerRouteAltVt30(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::RouteAlt vt30", gJournalListenerRouteAltVt30LogCount,
gOriginalJournalListenerRouteAltVt30, aListener, aEvent);
}
void DetourJournalListenerUiVt28(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::UI vt28", gJournalListenerUiVt28LogCount,
gOriginalJournalListenerUiVt28, aListener, aEvent);
}
void DetourJournalListenerUiVt30(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::UI vt30", gJournalListenerUiVt30LogCount,
gOriginalJournalListenerUiVt30, aListener, aEvent);
}
void DetourJournalListenerTrackerVt28(void* aListener, void* aEvent)
{
DispatchJournalListenerCallback("JournalListener::Tracker vt28", gJournalListenerTrackerVt28LogCount,
gOriginalJournalListenerTrackerVt28, aListener, aEvent);
}
void DetourFrameMappinPathWrapper(void* aThis, void* aStack, void* aResult) void DetourFrameMappinPathWrapper(void* aThis, void* aStack, void* aResult)
{ {
if (gFrameMappinPathWrapperLogCount < 64) if (gFrameMappinPathWrapperLogCount < 64)
@@ -1294,6 +1460,33 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
AttachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva, AttachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva,
reinterpret_cast<void*>(&DetourJournalTrackEntry), reinterpret_cast<void*>(&DetourJournalTrackEntry),
reinterpret_cast<void**>(&gOriginalJournalTrackEntry)); reinterpret_cast<void**>(&gOriginalJournalTrackEntry));
AttachProbeHook("JournalListener::Objective vt28", kJournalListenerObjectiveVt28Rva,
reinterpret_cast<void*>(&DetourJournalListenerObjectiveVt28),
reinterpret_cast<void**>(&gOriginalJournalListenerObjectiveVt28));
AttachProbeHook("JournalListener::Objective vt30", kJournalListenerObjectiveVt30Rva,
reinterpret_cast<void*>(&DetourJournalListenerObjectiveVt30),
reinterpret_cast<void**>(&gOriginalJournalListenerObjectiveVt30));
AttachProbeHook("JournalListener::Route vt28", kJournalListenerRouteVt28Rva,
reinterpret_cast<void*>(&DetourJournalListenerRouteVt28),
reinterpret_cast<void**>(&gOriginalJournalListenerRouteVt28));
AttachProbeHook("JournalListener::Route vt30", kJournalListenerRouteVt30Rva,
reinterpret_cast<void*>(&DetourJournalListenerRouteVt30),
reinterpret_cast<void**>(&gOriginalJournalListenerRouteVt30));
AttachProbeHook("JournalListener::RouteAlt vt28", kJournalListenerRouteAltVt28Rva,
reinterpret_cast<void*>(&DetourJournalListenerRouteAltVt28),
reinterpret_cast<void**>(&gOriginalJournalListenerRouteAltVt28));
AttachProbeHook("JournalListener::RouteAlt vt30", kJournalListenerRouteAltVt30Rva,
reinterpret_cast<void*>(&DetourJournalListenerRouteAltVt30),
reinterpret_cast<void**>(&gOriginalJournalListenerRouteAltVt30));
AttachProbeHook("JournalListener::UI vt28", kJournalListenerUiVt28Rva,
reinterpret_cast<void*>(&DetourJournalListenerUiVt28),
reinterpret_cast<void**>(&gOriginalJournalListenerUiVt28));
AttachProbeHook("JournalListener::UI vt30", kJournalListenerUiVt30Rva,
reinterpret_cast<void*>(&DetourJournalListenerUiVt30),
reinterpret_cast<void**>(&gOriginalJournalListenerUiVt30));
AttachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva,
reinterpret_cast<void*>(&DetourJournalListenerTrackerVt28),
reinterpret_cast<void**>(&gOriginalJournalListenerTrackerVt28));
AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva, AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva,
reinterpret_cast<void*>(&DetourFrameMappinPathWrapper), reinterpret_cast<void*>(&DetourFrameMappinPathWrapper),
reinterpret_cast<void**>(&gOriginalFrameMappinPathWrapper)); reinterpret_cast<void**>(&gOriginalFrameMappinPathWrapper));
@@ -1335,6 +1528,15 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
{ {
DetachProbeHook("GPSSystem/Tick", kGpsSystemTickRva); DetachProbeHook("GPSSystem/Tick", kGpsSystemTickRva);
DetachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva); DetachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva);
DetachProbeHook("JournalListener::Objective vt28", kJournalListenerObjectiveVt28Rva);
DetachProbeHook("JournalListener::Objective vt30", kJournalListenerObjectiveVt30Rva);
DetachProbeHook("JournalListener::Route vt28", kJournalListenerRouteVt28Rva);
DetachProbeHook("JournalListener::Route vt30", kJournalListenerRouteVt30Rva);
DetachProbeHook("JournalListener::RouteAlt vt28", kJournalListenerRouteAltVt28Rva);
DetachProbeHook("JournalListener::RouteAlt vt30", kJournalListenerRouteAltVt30Rva);
DetachProbeHook("JournalListener::UI vt28", kJournalListenerUiVt28Rva);
DetachProbeHook("JournalListener::UI vt30", kJournalListenerUiVt30Rva);
DetachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva);
DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva); DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva);
DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva); DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva);
DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva); DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);