From 2bbc6cbb46b563765fffc1a00d6bf5d1489fa6c1 Mon Sep 17 00:00:00 2001 From: Jacob Babor Date: Sat, 20 Jun 2026 00:59:35 -0500 Subject: [PATCH] Probe mappin route event dispatch --- docs/red4ext-logging-shim.md | 11 + docs/traffic-system-debrief.md | 54 ++++- red4ext/EdgeWeightGPS/src/Main.cpp | 350 ++++++++++++++++++++++++++++- 3 files changed, 411 insertions(+), 4 deletions(-) diff --git a/docs/red4ext-logging-shim.md b/docs/red4ext-logging-shim.md index 978dddc..c224188 100644 --- a/docs/red4ext-logging-shim.md +++ b/docs/red4ext-logging-shim.md @@ -26,6 +26,12 @@ Current behavior: `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. +- Hooks the route handoff found from the journal listener bridge: + `0x598250`, `0x13763d8`, `0xaa62d0`, `0xaa6330`, `0x27abd7c`, and + `0x5625a4`. +- Logs mappin route-event fields, active/deactive route refs, the mappin + active-route map at `system + 0x1a0`, and the route observer list at + `system + 0x280`. - Resolves the native mappin system when one of those paths fires, logs relevant vtable slot addresses, and temporarily hooks the route-adjacent slots. @@ -38,6 +44,11 @@ Current route-probe focus: - mappin-system slots `0x1f0`, `0x280`, and `0x2f0` - `JournalManager.TrackEntry` implementation: `0x5944fc` - non-default `TrackEntry` listener callbacks listed above +- journal/mappin route bridge candidate: `0x598250` +- mappin route-event enqueue: `0x13763d8` +- mappin route-event handler: `0xaa62d0` +- mappin route activate/deactivate: `0xaa6330`, `0x27abd7c` +- route-build candidate called by the bridge: `0x5625a4` Quest/objective pins did not fire the mappin tracking hooks in live tests. The REDscript decompile shows that those pins call `JournalManager.TrackEntry` diff --git a/docs/traffic-system-debrief.md b/docs/traffic-system-debrief.md index 62698b4..b1991a8 100644 --- a/docs/traffic-system-debrief.md +++ b/docs/traffic-system-debrief.md @@ -258,9 +258,57 @@ Static disassembly of these callbacks suggests the following split: 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. +nested owner/service vtables. A later controlled run narrowed the quest route +handoff further: + +```text +JournalManager.TrackEntry + -> JournalListener::Route callbacks at 0xe63f80 / 0xe63e6c + -> journal/mappin route bridge candidate at 0x598250 + -> MappinSystem route-event enqueue at 0x13763d8 + -> route-event handler at 0xaa62d0 + -> activate route ref at 0xaa6330 + -> or deactivate route ref at 0x27abd7c +``` + +The route listener object has two important nested service pointers in the live +logs: + +```text +listener + 0x70: mappin system, vtable 0x14310e300 +listener + 0x78: journal manager, vtable 0x1430f0890 +``` + +The mappin system slot `0x240` points to RVA `0x13763d8`. Static disassembly +shows it is not the solver. It builds a small queued event with vtable +`0x14310e1c0`: + +```text +event + 0x08: event type byte, value 5 +event + 0x18: route/journal id +event + 0x20: mappin system pointer +event + 0x28: active/unactive bool +``` + +The event handler at `0xaa62d0` reads that bool and calls either `0xaa6330` or +`0x27abd7c` with the mappin system and a route reference taken from the event +payload. Both target functions look like active-route state toggles: they look +up a route ref in a map at `mappinSystem + 0x1a0`, set a byte at route object +offset `0x84`, then notify observers from the list at `mappinSystem + 0x280` +through virtual callbacks at offsets `0x48`/`0x50`. + +Static xrefs also expose a shared route-change helper at `0x27aa2d8`, which +deactivates an old route ref and activates a new route ref. That is likely the +common mappin state transition path for both journal/objective routes and +custom/player pins. + +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 +descriptor/data pointers, and passes them to `0x5625a4`. The current RED4ext +probe hooks this function with a 12-argument inferred signature to determine +whether it builds GPS/path display data or merely updates UI-facing route +metadata. ## Native False Positives diff --git a/red4ext/EdgeWeightGPS/src/Main.cpp b/red4ext/EdgeWeightGPS/src/Main.cpp index 0b34e53..13ae5c6 100644 --- a/red4ext/EdgeWeightGPS/src/Main.cpp +++ b/red4ext/EdgeWeightGPS/src/Main.cpp @@ -125,6 +125,12 @@ constexpr uintptr_t kJournalListenerRouteAltVt30Rva = 0x08D136C; constexpr uintptr_t kJournalListenerUiVt28Rva = 0x0431A34; constexpr uintptr_t kJournalListenerUiVt30Rva = 0x055A4E4; constexpr uintptr_t kJournalListenerTrackerVt28Rva = 0x14DE238; +constexpr uintptr_t kJournalRouteBridgeRva = 0x0598250; +constexpr uintptr_t kMappinRouteEventEnqueueRva = 0x13763D8; +constexpr uintptr_t kMappinRouteEventHandlerRva = 0x0AA62D0; +constexpr uintptr_t kMappinRouteActivateRva = 0x0AA6330; +constexpr uintptr_t kMappinRouteDeactivateRva = 0x27ABD7C; +constexpr uintptr_t kRouteBuildCandidateRva = 0x05625A4; HMODULE gModule = nullptr; std::mutex gLogMutex; @@ -165,6 +171,12 @@ uint32_t gJournalListenerRouteAltVt30LogCount = 0; uint32_t gJournalListenerUiVt28LogCount = 0; uint32_t gJournalListenerUiVt30LogCount = 0; uint32_t gJournalListenerTrackerVt28LogCount = 0; +uint32_t gJournalRouteBridgeLogCount = 0; +uint32_t gMappinRouteEventEnqueueLogCount = 0; +uint32_t gMappinRouteEventHandlerLogCount = 0; +uint32_t gMappinRouteActivateLogCount = 0; +uint32_t gMappinRouteDeactivateLogCount = 0; +uint32_t gRouteBuildCandidateLogCount = 0; uint64_t gLogStartTick = 0; uint64_t gLogFrequency = 0; void* gMappinSetTrackedTarget = nullptr; @@ -194,6 +206,15 @@ using MappinCreateCustomPositionFunc = void* (*)(void* aSystem, void* aName, voi using MappinReleaseQueryFunc = void (*)(void* aSystem, void* aHandle); using MappinActiveRouteFunc = void* (*)(void* aSystem); using JournalListenerCallbackFunc = void (*)(void* aListener, void* aEvent); +using JournalRouteBridgeFunc = bool (*)(void* aThis, void* aEntryHandle); +using MappinRouteEventEnqueueFunc = void (*)(void* aSystem, uint32_t aRouteId, uint8_t aActive); +using MappinRouteEventHandlerFunc = void (*)(void* aEvent, void* aRouteHandle); +using MappinRouteToggleFunc = void (*)(void* aSystem, void* aRouteRef); +using RouteBuildCandidateFunc = void (*)(void* aThis, uint32_t aRouteId, uint32_t aAltRouteId, + uint32_t aPreviousRouteId, void* aRoutePositionData, + void* aPrimaryDescriptor, void* aSecondaryDescriptor, void* aRouteData88, + void* aRouteData108, void* aRouteData114, uint8_t aFlag, + uint32_t aMode); ProbeFunc gOriginalGpsSystemTick = nullptr; JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr; ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr; @@ -225,6 +246,12 @@ JournalListenerCallbackFunc gOriginalJournalListenerRouteAltVt30 = nullptr; JournalListenerCallbackFunc gOriginalJournalListenerUiVt28 = nullptr; JournalListenerCallbackFunc gOriginalJournalListenerUiVt30 = nullptr; JournalListenerCallbackFunc gOriginalJournalListenerTrackerVt28 = nullptr; +JournalRouteBridgeFunc gOriginalJournalRouteBridge = nullptr; +MappinRouteEventEnqueueFunc gOriginalMappinRouteEventEnqueue = nullptr; +MappinRouteEventHandlerFunc gOriginalMappinRouteEventHandler = nullptr; +MappinRouteToggleFunc gOriginalMappinRouteActivate = nullptr; +MappinRouteToggleFunc gOriginalMappinRouteDeactivate = nullptr; +RouteBuildCandidateFunc gOriginalRouteBuildCandidate = nullptr; std::filesystem::path GetLogPath() { @@ -508,16 +535,142 @@ void AppendObjectVtableSlots(std::ostringstream& aLine, void* aObject, std::init { auto* vtable = ReadPointerField(aObject, 0); AppendPointerWithRva(aLine, "vtable", vtable); + if (!IsInImage(vtable)) + { + return; + } 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); + const auto labelText = label.str(); + AppendPointerWithRva(aLine, labelText.c_str(), target); } } +void AppendPointerField(std::ostringstream& aLine, const char* aLabel, void* aObject, uintptr_t aOffset) +{ + AppendPointerWithRva(aLine, aLabel, ReadPointerField(aObject, aOffset)); +} + +void AppendUint32Field(std::ostringstream& aLine, const char* aLabel, void* aObject, uintptr_t aOffset) +{ + uint32_t value = 0; + const auto hasValue = aObject && TryReadMemory(reinterpret_cast(reinterpret_cast(aObject) + aOffset), + value); + aLine << " " << aLabel << "="; + if (hasValue) + { + aLine << value << "/0x" << std::hex << value << std::dec; + } + else + { + aLine << ""; + } +} + +void AppendByteField(std::ostringstream& aLine, const char* aLabel, void* aObject, uintptr_t aOffset) +{ + uint8_t value = 0; + const auto hasValue = aObject && TryReadMemory(reinterpret_cast(reinterpret_cast(aObject) + aOffset), + value); + aLine << " " << aLabel << "="; + if (hasValue) + { + aLine << static_cast(value); + } + else + { + aLine << ""; + } +} + +void AppendRouteRefFields(std::ostringstream& aLine, const char* aPrefix, void* aRouteRef) +{ + AppendPointerWithRva(aLine, aPrefix, aRouteRef); + if (!aRouteRef) + { + return; + } + + std::array offsets{0x00, 0x08, 0x10, 0x18, 0x30, 0x40, 0x48}; + for (const auto offset : offsets) + { + std::ostringstream label; + label << aPrefix << "_f" << std::hex << offset; + const auto labelText = label.str(); + AppendPointerField(aLine, labelText.c_str(), aRouteRef, offset); + } +} + +void LogMappinRouteCollections(const char* aName, uint32_t aCount, void* aSystem) +{ + if (!aSystem) + { + return; + } + + void* activeRouteMap = nullptr; + uint32_t activeRouteCount = 0; + uint32_t activeRouteFlags = 0; + void* observerBase = nullptr; + uint32_t observerCount = 0; + TryReadMemory(reinterpret_cast(reinterpret_cast(aSystem) + 0x1A0), activeRouteMap); + TryReadMemory(reinterpret_cast(reinterpret_cast(aSystem) + 0x1AC), activeRouteCount); + TryReadMemory(reinterpret_cast(reinterpret_cast(aSystem) + 0x1B0), activeRouteFlags); + TryReadMemory(reinterpret_cast(reinterpret_cast(aSystem) + 0x288), observerBase); + TryReadMemory(reinterpret_cast(reinterpret_cast(aSystem) + 0x294), observerCount); + + std::ostringstream line; + line << "mappin-route-state " << aName << " call=" << aCount; + AppendPointerWithRva(line, "system", aSystem); + AppendPointerWithRva(line, "activeRouteMap", activeRouteMap); + line << " activeRouteCount=" << activeRouteCount << " activeRouteFlags=0x" << std::hex << activeRouteFlags + << std::dec; + AppendPointerWithRva(line, "observerBase", observerBase); + line << " observerCount=" << observerCount; + LogRed4ext(line.str()); + + if (!observerBase || observerCount == 0 || observerCount > 128) + { + return; + } + + const auto cappedCount = std::min(observerCount, 4u); + for (uint32_t index = 0; index < cappedCount; ++index) + { + auto* observer = ReadPointerField(observerBase, index * 0x10); + std::ostringstream observerLine; + observerLine << "mappin-route-observer " << aName << " call=" << aCount << " index=" << index; + AppendPointerWithRva(observerLine, "observer", observer); + AppendObjectVtableSlots(observerLine, observer, {0x48, 0x50, 0x210, 0x260}); + LogRed4ext(observerLine.str()); + } +} + +void LogRouteEventFields(const char* aName, uint32_t aCount, void* aEvent) +{ + uint32_t routeId = 0; + uint8_t active = 0; + void* system = nullptr; + if (aEvent) + { + TryReadMemory(reinterpret_cast(reinterpret_cast(aEvent) + 0x18), routeId); + TryReadMemory(reinterpret_cast(reinterpret_cast(aEvent) + 0x20), system); + TryReadMemory(reinterpret_cast(reinterpret_cast(aEvent) + 0x28), active); + } + + std::ostringstream line; + line << "route-event-fields " << aName << " call=" << aCount << " routeId=" << routeId << " active=" + << static_cast(active); + AppendPointerWithRva(line, "event", aEvent); + AppendPointerWithRva(line, "system", system); + AppendObjectVtableSlots(line, aEvent, {0x20, 0x28, 0x30}); + LogRed4ext(line.str()); +} + void LogJournalListenerContext(const char* aName, uint32_t aCount, void* aListener, void* aEvent) { std::ostringstream line; @@ -863,6 +1016,177 @@ void DetourJournalListenerTrackerVt28(void* aListener, void* aEvent) gOriginalJournalListenerTrackerVt28, aListener, aEvent); } +bool DetourJournalRouteBridge(void* aThis, void* aEntryHandle) +{ + constexpr uint32_t kMaxCalls = 64; + + const auto callCount = gJournalRouteBridgeLogCount; + const auto shouldLog = callCount < kMaxCalls; + if (shouldLog) + { + std::ostringstream line; + line << "hook JournalRouteBridge::Update candidate call=" << callCount; + AppendPointerWithRva(line, "this", aThis); + AppendPointerWithRva(line, "entryHandle", aEntryHandle); + AppendReturnRva(line, __builtin_return_address(0)); + AppendPointerField(line, "field10", aThis, 0x10); + AppendPointerField(line, "field18", aThis, 0x18); + AppendPointerField(line, "field20", aThis, 0x20); + AppendByteField(line, "field30", aThis, 0x30); + AppendUint32Field(line, "field34", aThis, 0x34); + AppendUint32Field(line, "field38", aThis, 0x38); + LogRed4ext(line.str()); + ++gJournalRouteBridgeLogCount; + } + + bool result = false; + if (gOriginalJournalRouteBridge) + { + result = gOriginalJournalRouteBridge(aThis, aEntryHandle); + } + + if (shouldLog) + { + std::ostringstream line; + line << "hook JournalRouteBridge::Update candidate result call=" << callCount << " value=" + << static_cast(result); + LogRed4ext(line.str()); + } + + return result; +} + +void DetourMappinRouteEventEnqueue(void* aSystem, uint32_t aRouteId, uint8_t aActive) +{ + constexpr uint32_t kMaxCalls = 64; + + if (gMappinRouteEventEnqueueLogCount < kMaxCalls) + { + const auto callCount = gMappinRouteEventEnqueueLogCount; + std::ostringstream line; + line << "hook MappinSystem::RouteEventEnqueue slot240 call=" << callCount << " routeId=" << aRouteId + << " active=" << static_cast(aActive); + AppendPointerWithRva(line, "system", aSystem); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + LogMappinRouteCollections("enqueue-before", callCount, aSystem); + ++gMappinRouteEventEnqueueLogCount; + } + + if (gOriginalMappinRouteEventEnqueue) + { + gOriginalMappinRouteEventEnqueue(aSystem, aRouteId, aActive); + } +} + +void DetourMappinRouteEventHandler(void* aEvent, void* aRouteHandle) +{ + constexpr uint32_t kMaxCalls = 64; + + if (gMappinRouteEventHandlerLogCount < kMaxCalls) + { + const auto callCount = gMappinRouteEventHandlerLogCount; + void* routeOwner = ReadPointerField(aRouteHandle, 0); + void* routeRef = ReadPointerField(routeOwner, 0x40); + + std::ostringstream line; + line << "hook MappinRouteEvent::Handle call=" << callCount; + AppendPointerWithRva(line, "event", aEvent); + AppendPointerWithRva(line, "routeHandle", aRouteHandle); + AppendPointerWithRva(line, "routeOwner", routeOwner); + AppendRouteRefFields(line, "routeRef", routeRef); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + LogRouteEventFields("handler-before", callCount, aEvent); + ++gMappinRouteEventHandlerLogCount; + } + + if (gOriginalMappinRouteEventHandler) + { + gOriginalMappinRouteEventHandler(aEvent, aRouteHandle); + } +} + +void DetourMappinRouteActivate(void* aSystem, void* aRouteRef) +{ + constexpr uint32_t kMaxCalls = 64; + + if (gMappinRouteActivateLogCount < kMaxCalls) + { + const auto callCount = gMappinRouteActivateLogCount; + std::ostringstream line; + line << "hook MappinSystem::RouteActivate call=" << callCount; + AppendPointerWithRva(line, "system", aSystem); + AppendRouteRefFields(line, "routeRef", aRouteRef); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + LogMappinRouteCollections("activate-before", callCount, aSystem); + ++gMappinRouteActivateLogCount; + } + + if (gOriginalMappinRouteActivate) + { + gOriginalMappinRouteActivate(aSystem, aRouteRef); + } +} + +void DetourMappinRouteDeactivate(void* aSystem, void* aRouteRef) +{ + constexpr uint32_t kMaxCalls = 64; + + if (gMappinRouteDeactivateLogCount < kMaxCalls) + { + const auto callCount = gMappinRouteDeactivateLogCount; + std::ostringstream line; + line << "hook MappinSystem::RouteDeactivate call=" << callCount; + AppendPointerWithRva(line, "system", aSystem); + AppendRouteRefFields(line, "routeRef", aRouteRef); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + LogMappinRouteCollections("deactivate-before", callCount, aSystem); + ++gMappinRouteDeactivateLogCount; + } + + if (gOriginalMappinRouteDeactivate) + { + gOriginalMappinRouteDeactivate(aSystem, aRouteRef); + } +} + +void DetourRouteBuildCandidate(void* aThis, uint32_t aRouteId, uint32_t aAltRouteId, uint32_t aPreviousRouteId, + void* aRoutePositionData, void* aPrimaryDescriptor, void* aSecondaryDescriptor, + void* aRouteData88, void* aRouteData108, void* aRouteData114, uint8_t aFlag, + uint32_t aMode) +{ + constexpr uint32_t kMaxCalls = 64; + + if (gRouteBuildCandidateLogCount < kMaxCalls) + { + std::ostringstream line; + line << "hook RouteBuildCandidate 0x5625a4 call=" << gRouteBuildCandidateLogCount + << " routeId=" << aRouteId << " altRouteId=" << aAltRouteId + << " previousRouteId=" << aPreviousRouteId << " flag=" << static_cast(aFlag) + << " mode=" << aMode; + AppendPointerWithRva(line, "this", aThis); + AppendPointerWithRva(line, "routePositionData", aRoutePositionData); + AppendPointerWithRva(line, "primaryDescriptor", aPrimaryDescriptor); + AppendPointerWithRva(line, "secondaryDescriptor", aSecondaryDescriptor); + AppendPointerWithRva(line, "routeData88", aRouteData88); + AppendPointerWithRva(line, "routeData108", aRouteData108); + AppendPointerWithRva(line, "routeData114", aRouteData114); + AppendReturnRva(line, __builtin_return_address(0)); + LogRed4ext(line.str()); + ++gRouteBuildCandidateLogCount; + } + + if (gOriginalRouteBuildCandidate) + { + gOriginalRouteBuildCandidate(aThis, aRouteId, aAltRouteId, aPreviousRouteId, aRoutePositionData, + aPrimaryDescriptor, aSecondaryDescriptor, aRouteData88, aRouteData108, + aRouteData114, aFlag, aMode); + } +} + void DetourFrameMappinPathWrapper(void* aThis, void* aStack, void* aResult) { if (gFrameMappinPathWrapperLogCount < 64) @@ -1487,6 +1811,24 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e AttachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva, reinterpret_cast(&DetourJournalListenerTrackerVt28), reinterpret_cast(&gOriginalJournalListenerTrackerVt28)); + AttachProbeHook("JournalRouteBridge::Update candidate", kJournalRouteBridgeRva, + reinterpret_cast(&DetourJournalRouteBridge), + reinterpret_cast(&gOriginalJournalRouteBridge)); + AttachProbeHook("MappinSystem::RouteEventEnqueue slot240", kMappinRouteEventEnqueueRva, + reinterpret_cast(&DetourMappinRouteEventEnqueue), + reinterpret_cast(&gOriginalMappinRouteEventEnqueue)); + AttachProbeHook("MappinRouteEvent::Handle", kMappinRouteEventHandlerRva, + reinterpret_cast(&DetourMappinRouteEventHandler), + reinterpret_cast(&gOriginalMappinRouteEventHandler)); + AttachProbeHook("MappinSystem::RouteActivate", kMappinRouteActivateRva, + reinterpret_cast(&DetourMappinRouteActivate), + reinterpret_cast(&gOriginalMappinRouteActivate)); + AttachProbeHook("MappinSystem::RouteDeactivate", kMappinRouteDeactivateRva, + reinterpret_cast(&DetourMappinRouteDeactivate), + reinterpret_cast(&gOriginalMappinRouteDeactivate)); + AttachProbeHook("RouteBuildCandidate 0x5625a4", kRouteBuildCandidateRva, + reinterpret_cast(&DetourRouteBuildCandidate), + reinterpret_cast(&gOriginalRouteBuildCandidate)); AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva, reinterpret_cast(&DetourFrameMappinPathWrapper), reinterpret_cast(&gOriginalFrameMappinPathWrapper)); @@ -1537,6 +1879,12 @@ RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4e DetachProbeHook("JournalListener::UI vt28", kJournalListenerUiVt28Rva); DetachProbeHook("JournalListener::UI vt30", kJournalListenerUiVt30Rva); DetachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva); + DetachProbeHook("JournalRouteBridge::Update candidate", kJournalRouteBridgeRva); + DetachProbeHook("MappinSystem::RouteEventEnqueue slot240", kMappinRouteEventEnqueueRva); + DetachProbeHook("MappinRouteEvent::Handle", kMappinRouteEventHandlerRva); + DetachProbeHook("MappinSystem::RouteActivate", kMappinRouteActivateRva); + DetachProbeHook("MappinSystem::RouteDeactivate", kMappinRouteDeactivateRva); + DetachProbeHook("RouteBuildCandidate 0x5625a4", kRouteBuildCandidateRva); DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva); DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva); DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);