From 3b0a66a87a62dab3935ea7d319e3523285214a73 Mon Sep 17 00:00:00 2001 From: Jacob Babor Date: Sat, 20 Jun 2026 00:40:34 -0500 Subject: [PATCH] Probe journal listener route callbacks --- docs/red4ext-logging-shim.md | 17 +++ docs/traffic-system-debrief.md | 45 +++++++ red4ext/EdgeWeightGPS/src/Main.cpp | 202 +++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) diff --git a/docs/red4ext-logging-shim.md b/docs/red4ext-logging-shim.md index ab71bdc..978dddc 100644 --- a/docs/red4ext-logging-shim.md +++ b/docs/red4ext-logging-shim.md @@ -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 diff --git a/docs/traffic-system-debrief.md b/docs/traffic-system-debrief.md index a0f0960..62698b4 100644 --- a/docs/traffic-system-debrief.md +++ b/docs/traffic-system-debrief.md @@ -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 diff --git a/red4ext/EdgeWeightGPS/src/Main.cpp b/red4ext/EdgeWeightGPS/src/Main.cpp index 804c6cc..0b34e53 100644 --- a/red4ext/EdgeWeightGPS/src/Main.cpp +++ b/red4ext/EdgeWeightGPS/src/Main.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -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(reinterpret_cast(aObject) + aOffset), value); + return value; +} + +void AppendObjectVtableSlots(std::ostringstream& aLine, void* aObject, std::initializer_list 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(&DetourJournalTrackEntry), reinterpret_cast(&gOriginalJournalTrackEntry)); + AttachProbeHook("JournalListener::Objective vt28", kJournalListenerObjectiveVt28Rva, + reinterpret_cast(&DetourJournalListenerObjectiveVt28), + reinterpret_cast(&gOriginalJournalListenerObjectiveVt28)); + AttachProbeHook("JournalListener::Objective vt30", kJournalListenerObjectiveVt30Rva, + reinterpret_cast(&DetourJournalListenerObjectiveVt30), + reinterpret_cast(&gOriginalJournalListenerObjectiveVt30)); + AttachProbeHook("JournalListener::Route vt28", kJournalListenerRouteVt28Rva, + reinterpret_cast(&DetourJournalListenerRouteVt28), + reinterpret_cast(&gOriginalJournalListenerRouteVt28)); + AttachProbeHook("JournalListener::Route vt30", kJournalListenerRouteVt30Rva, + reinterpret_cast(&DetourJournalListenerRouteVt30), + reinterpret_cast(&gOriginalJournalListenerRouteVt30)); + AttachProbeHook("JournalListener::RouteAlt vt28", kJournalListenerRouteAltVt28Rva, + reinterpret_cast(&DetourJournalListenerRouteAltVt28), + reinterpret_cast(&gOriginalJournalListenerRouteAltVt28)); + AttachProbeHook("JournalListener::RouteAlt vt30", kJournalListenerRouteAltVt30Rva, + reinterpret_cast(&DetourJournalListenerRouteAltVt30), + reinterpret_cast(&gOriginalJournalListenerRouteAltVt30)); + AttachProbeHook("JournalListener::UI vt28", kJournalListenerUiVt28Rva, + reinterpret_cast(&DetourJournalListenerUiVt28), + reinterpret_cast(&gOriginalJournalListenerUiVt28)); + AttachProbeHook("JournalListener::UI vt30", kJournalListenerUiVt30Rva, + reinterpret_cast(&DetourJournalListenerUiVt30), + reinterpret_cast(&gOriginalJournalListenerUiVt30)); + AttachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva, + reinterpret_cast(&DetourJournalListenerTrackerVt28), + reinterpret_cast(&gOriginalJournalListenerTrackerVt28)); AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva, reinterpret_cast(&DetourFrameMappinPathWrapper), reinterpret_cast(&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);