Probe journal listener route callbacks
This commit is contained in:
@@ -21,6 +21,11 @@ Current behavior:
|
||||
- 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.
|
||||
- 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
|
||||
vtable slot addresses, and temporarily hooks the route-adjacent slots.
|
||||
|
||||
@@ -32,12 +37,24 @@ Current route-probe focus:
|
||||
- `TrackCustomPositionMappin` wrapper/core: `0x27c4aac`, `0x27c2318`
|
||||
- mappin-system slots `0x1f0`, `0x280`, and `0x2f0`
|
||||
- `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
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
Static string scans found a tempting traffic/pathfinding cluster around RVA
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <initializer_list>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
@@ -115,6 +116,15 @@ constexpr uintptr_t kMappinPositionQuerySlotOffset = 0x2E0;
|
||||
constexpr uintptr_t kMappinCreateCustomPositionSlotOffset = 0x2F0;
|
||||
constexpr uintptr_t kMappinReleaseQuerySlotOffset = 0x368;
|
||||
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;
|
||||
std::mutex gLogMutex;
|
||||
@@ -146,6 +156,15 @@ uint32_t gMappinPositionQueryLogCount = 0;
|
||||
uint32_t gMappinCreateCustomPositionLogCount = 0;
|
||||
uint32_t gMappinReleaseQueryLogCount = 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 gLogFrequency = 0;
|
||||
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 MappinReleaseQueryFunc = void (*)(void* aSystem, void* aHandle);
|
||||
using MappinActiveRouteFunc = void* (*)(void* aSystem);
|
||||
using JournalListenerCallbackFunc = void (*)(void* aListener, void* aEvent);
|
||||
ProbeFunc gOriginalGpsSystemTick = nullptr;
|
||||
JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr;
|
||||
ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr;
|
||||
@@ -196,6 +216,15 @@ MappinPositionQueryFunc gOriginalMappinPositionQuery = nullptr;
|
||||
MappinCreateCustomPositionFunc gOriginalMappinCreateCustomPosition = nullptr;
|
||||
MappinReleaseQueryFunc gOriginalMappinReleaseQuery = 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()
|
||||
{
|
||||
@@ -463,6 +492,72 @@ bool TryReadMemory(const void* aPointer, T& aValue)
|
||||
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,
|
||||
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)
|
||||
{
|
||||
if (gFrameMappinPathWrapperLogCount < 64)
|
||||
@@ -1294,6 +1460,33 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e
|
||||
AttachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva,
|
||||
reinterpret_cast<void*>(&DetourJournalTrackEntry),
|
||||
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,
|
||||
reinterpret_cast<void*>(&DetourFrameMappinPathWrapper),
|
||||
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("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("FrameMappinPathCore", kFrameMappinPathCoreRva);
|
||||
DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);
|
||||
|
||||
Reference in New Issue
Block a user