diff --git a/docs/red4ext-logging-shim.md b/docs/red4ext-logging-shim.md index 25e33ae..ec87303 100644 --- a/docs/red4ext-logging-shim.md +++ b/docs/red4ext-logging-shim.md @@ -40,6 +40,10 @@ Current behavior: active byte at offset `0x84`, the service lookup key at offset `0x8c`, and the runtime observer1 service-owner path used before its vtable `0x220` lookup. +- Hooks observer1's generic service lookup at `0x287c44`, filters it to the + runtime type pointer at `.data` RVA `0x342f6a8`, logs the returned service + object and slot `0x220`, then dynamically hooks that service `0x220` route + lookup to log its route-id output handle. - Resolves the native mappin system when one of those paths fires, logs relevant vtable slot addresses, and temporarily hooks the route-adjacent slots. @@ -58,6 +62,8 @@ Current route-probe focus: - mappin route activate/deactivate: `0xaa6330`, `0x27abd7c` - mappin route observer callbacks: `0xaa6610`, `0xaa6628`, `0xaa63e0`, `0x27b10c0`, `0x295d4a0`, `0x286a85c` +- observer1 service lookup and returned service route lookup: + `0x287c44`, dynamic service vtable slot `0x220` - route-build candidate called by the bridge: `0x5625a4` Quest/objective pins did not fire the mappin tracking hooks in live tests. The diff --git a/docs/traffic-system-debrief.md b/docs/traffic-system-debrief.md index 2d87565..638d8cc 100644 --- a/docs/traffic-system-debrief.md +++ b/docs/traffic-system-debrief.md @@ -365,6 +365,26 @@ handles/strings under its inner object at offsets around `+0x180`, `+0x1a0`, `routeObject + 0x8c` so the next runtime test can tell whether observer1 is crossing into GPS/path computation or just updating presentation state. +The `07:22:14` controlled run confirmed observer1 is in the deliberate route +click window, not just load-time setup. The three quest route clicks produced +the expected sequence: + +```text +JournalManager.TrackEntry +MappinSystem.RouteEventEnqueue old active=0 / new active=1 +MappinSystem.RouteDeactivate old route key +MappinSystem.RouteActivate new route key +RouteObserver1 deactivate/activate with the matching routeObject + 0x8c route id +``` + +The observer1 service-owner object was stable across those calls +(`serviceOwner` vtable `0x142b74ea8`, slot `0x08` target `0x140287c44`). +Static disassembly of `0x140287c44` shows a generic type-to-service lookup, so +the next probe hooks that function, filters on the runtime type pointer at +`0x342f6a8`, logs the returned service object, and dynamically hooks the +returned service's vtable slot `0x220`. That slot is the immediate next native +call after the lookup and receives the route id from `routeObject + 0x8c`. + The most route-building-looking static candidate currently in this chain is `0x5625a4`, called once from the bridge at `0x5984ec`. The bridge resolves journal route ids through virtual journal-manager calls, collects several route diff --git a/red4ext/EdgeWeightGPS/src/Main.cpp b/red4ext/EdgeWeightGPS/src/Main.cpp index 02e5d52..67cfea4 100644 --- a/red4ext/EdgeWeightGPS/src/Main.cpp +++ b/red4ext/EdgeWeightGPS/src/Main.cpp @@ -138,6 +138,8 @@ constexpr uintptr_t kRouteObserver0DeactivateRva = 0x27B10C0; constexpr uintptr_t kRouteObserver1DeactivateRva = 0x295D4A0; constexpr uintptr_t kRouteObserver23DeactivateRva = 0x286A85C; constexpr uintptr_t kObserver1SystemTypeGlobalRva = 0x342F6A8; +constexpr uintptr_t kObserver1ServiceLookupRva = 0x0287C44; +constexpr uintptr_t kObserver1ServiceRouteLookupSlotOffset = 0x220; HMODULE gModule = nullptr; std::mutex gLogMutex; @@ -190,6 +192,8 @@ uint32_t gRouteObserver23ActivateLogCount = 0; uint32_t gRouteObserver0DeactivateLogCount = 0; uint32_t gRouteObserver1DeactivateLogCount = 0; uint32_t gRouteObserver23DeactivateLogCount = 0; +uint32_t gObserver1ServiceLookupLogCount = 0; +uint32_t gObserver1ServiceRouteLookupLogCount = 0; uint64_t gLogStartTick = 0; uint64_t gLogFrequency = 0; void* gMappinSetTrackedTarget = nullptr; @@ -200,7 +204,9 @@ void* gMappinPositionQueryTarget = nullptr; void* gMappinCreateCustomPositionTarget = nullptr; void* gMappinReleaseQueryTarget = nullptr; void* gMappinActiveRouteTarget = nullptr; +void* gObserver1ServiceRouteLookupTarget = nullptr; bool gMappinSlotHookAttachAttempted = false; +bool gObserver1ServiceRouteLookupHookAttachAttempted = false; using ProbeFunc = void (*)(void* aThis, void* a2, void* a3, void* a4); using JournalTrackEntryFunc = void (*)(void* aThis, void* aEntryHandle); @@ -229,6 +235,8 @@ using RouteBuildCandidateFunc = void (*)(void* aThis, uint32_t aRouteId, uint32_ void* aRouteData108, void* aRouteData114, uint8_t aFlag, uint32_t aMode); using RouteObserverCallbackFunc = void (*)(void* aObserver, void* aRouteEntry); +using Observer1ServiceLookupFunc = void* (*)(void* aServiceOwner, void* aSystemType); +using Observer1ServiceRouteLookupFunc = void (*)(void* aService, void* aOut, uint32_t aRouteId); ProbeFunc gOriginalGpsSystemTick = nullptr; JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr; ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr; @@ -272,6 +280,8 @@ RouteObserverCallbackFunc gOriginalRouteObserver23Activate = nullptr; RouteObserverCallbackFunc gOriginalRouteObserver0Deactivate = nullptr; RouteObserverCallbackFunc gOriginalRouteObserver1Deactivate = nullptr; RouteObserverCallbackFunc gOriginalRouteObserver23Deactivate = nullptr; +Observer1ServiceLookupFunc gOriginalObserver1ServiceLookup = nullptr; +Observer1ServiceRouteLookupFunc gOriginalObserver1ServiceRouteLookup = nullptr; std::filesystem::path GetLogPath() { @@ -556,6 +566,11 @@ void* ReadPointerField(void* aObject, uintptr_t aOffset) return value; } +void* GetObserver1SystemType() +{ + return ReadPointerField(reinterpret_cast(GetImageBase() + kObserver1SystemTypeGlobalRva), 0x00); +} + void AppendObjectVtableSlots(std::ostringstream& aLine, void* aObject, std::initializer_list aSlots) { auto* vtable = ReadPointerField(aObject, 0); @@ -679,8 +694,7 @@ void LogRouteObserverContext(const char* aName, uint32_t aCount, void* aObserver { auto* observerInner = ReadPointerField(aObserver, 0x08); auto* observerInnerServiceOwner = ReadPointerField(observerInner, 0x18); - auto* observer1SystemType = - ReadPointerField(reinterpret_cast(GetImageBase() + kObserver1SystemTypeGlobalRva), 0x00); + auto* observer1SystemType = GetObserver1SystemType(); auto* routeObject = ReadPointerField(aRouteEntry, 0x00); std::ostringstream line; @@ -1801,6 +1815,108 @@ void DetachAbsoluteHook(const char* aName, void* aTarget) LogRed4ext(line.str()); } +void DetourObserver1ServiceRouteLookup(void* aService, void* aOut, uint32_t aRouteId) +{ + const auto shouldLog = gObserver1ServiceRouteLookupLogCount < 96; + const auto callCount = gObserver1ServiceRouteLookupLogCount; + + if (shouldLog) + { + std::ostringstream line; + line << "hook Observer1Service::RouteLookup slot220 call=" << callCount << " routeId=" << aRouteId + << "/0x" << std::hex << aRouteId << std::dec; + AppendPointerWithRva(line, "service", aService); + AppendPointerWithRva(line, "out", aOut); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + ++gObserver1ServiceRouteLookupLogCount; + } + + if (gOriginalObserver1ServiceRouteLookup) + { + gOriginalObserver1ServiceRouteLookup(aService, aOut, aRouteId); + } + + if (shouldLog) + { + std::ostringstream line; + line << "hook Observer1Service::RouteLookup slot220 result call=" << callCount; + AppendPointerField(line, "out_f00", aOut, 0x00); + AppendPointerField(line, "out_f08", aOut, 0x08); + AppendPointerField(line, "out_f10", aOut, 0x10); + + auto* routeData = ReadPointerField(aOut, 0x00); + if (routeData) + { + AppendPointerWithRva(line, "routeData", routeData); + AppendPointerField(line, "routeData_f40", routeData, 0x40); + AppendPointerField(line, "routeData_f48", routeData, 0x48); + AppendPointerField(line, "routeData_f50", routeData, 0x50); + AppendPointerField(line, "routeData_f58", routeData, 0x58); + } + + LogRed4ext(line.str()); + } +} + +void MaybeAttachObserver1ServiceRouteLookup(void* aService) +{ + if (gObserver1ServiceRouteLookupHookAttachAttempted || !aService) + { + return; + } + + auto* vtable = ReadPointerField(aService, 0x00); + if (!IsInImage(vtable)) + { + return; + } + + gObserver1ServiceRouteLookupTarget = + ReadPointerField(vtable, kObserver1ServiceRouteLookupSlotOffset); + if (!gObserver1ServiceRouteLookupTarget) + { + return; + } + + gObserver1ServiceRouteLookupHookAttachAttempted = true; + AttachAbsoluteHook("Observer1Service::RouteLookup slot220", gObserver1ServiceRouteLookupTarget, + reinterpret_cast(&DetourObserver1ServiceRouteLookup), + reinterpret_cast(&gOriginalObserver1ServiceRouteLookup)); +} + +void* DetourObserver1ServiceLookup(void* aServiceOwner, void* aSystemType) +{ + void* result = nullptr; + if (gOriginalObserver1ServiceLookup) + { + result = gOriginalObserver1ServiceLookup(aServiceOwner, aSystemType); + } + + auto* observer1SystemType = GetObserver1SystemType(); + if (!observer1SystemType || aSystemType != observer1SystemType) + { + return result; + } + + if (gObserver1ServiceLookupLogCount < 96) + { + std::ostringstream line; + line << "hook Observer1ServiceLookup 0x287c44 call=" << gObserver1ServiceLookupLogCount; + AppendPointerWithRva(line, "serviceOwner", aServiceOwner); + AppendPointerWithRva(line, "systemType", aSystemType); + AppendPointerWithRva(line, "result", result); + AppendObjectVtableSlots(line, result, {0x218, 0x220, 0x228, 0x230, 0x238, 0x240}); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + ++gObserver1ServiceLookupLogCount; + } + + MaybeAttachObserver1ServiceRouteLookup(result); + + return result; +} + void InspectAndHookMappinSystem(void* aThis) { auto* resolver = reinterpret_cast(GetImageBase() + kMappinSystemResolverRva); @@ -2001,6 +2117,9 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e AttachProbeHook("RouteObserver23::Deactivate slot50", kRouteObserver23DeactivateRva, reinterpret_cast(&DetourRouteObserver23Deactivate), reinterpret_cast(&gOriginalRouteObserver23Deactivate)); + AttachProbeHook("Observer1ServiceLookup 0x287c44", kObserver1ServiceLookupRva, + reinterpret_cast(&DetourObserver1ServiceLookup), + reinterpret_cast(&gOriginalObserver1ServiceLookup)); AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva, reinterpret_cast(&DetourFrameMappinPathWrapper), reinterpret_cast(&gOriginalFrameMappinPathWrapper)); @@ -2063,6 +2182,7 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e DetachProbeHook("RouteObserver0::Deactivate slot50", kRouteObserver0DeactivateRva); DetachProbeHook("RouteObserver1::Deactivate slot50", kRouteObserver1DeactivateRva); DetachProbeHook("RouteObserver23::Deactivate slot50", kRouteObserver23DeactivateRva); + DetachProbeHook("Observer1ServiceLookup 0x287c44", kObserver1ServiceLookupRva); DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva); DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva); DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva); @@ -2083,6 +2203,7 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e DetachAbsoluteHook("MappinSystem::CreateCustomPosition slot2f0", gMappinCreateCustomPositionTarget); DetachAbsoluteHook("MappinSystem::ReleaseQuery slot368", gMappinReleaseQueryTarget); DetachAbsoluteHook("MappinSystem::ActiveRoute slot3a8", gMappinActiveRouteTarget); + DetachAbsoluteHook("Observer1Service::RouteLookup slot220", gObserver1ServiceRouteLookupTarget); } return true;