#pragma once #include "90CN_companion_p0.h" #include "90CN_companion_projection.h" #include #include #include #include // DB4 client only. The installer owns exact-image/signature checks. These // adapters preserve the shared companion parser, queue and lifecycle owner. namespace dnf90_companion_new_native { using NewNativeClass0DispatchFn = int (__stdcall*)(int, int); using NewNativeCaptureLogFn = void (*)(const char*, ...); static std::uintptr_t g_captureBase = 0; static std::uintptr_t g_captureResume = 0; static std::uintptr_t g_captureConsumed = 0; static NewNativeClass0DispatchFn g_originalClass0Dispatch = nullptr; static NewNativeCaptureLogFn g_captureLog = nullptr; static DWORD g_dispatchThread = 0; static HWND g_safePointWindow = nullptr; constexpr UINT_PTR kSafePointTimer = 0x90C07F41u; constexpr std::size_t kCapturePacketCapacity = dnf90_companion_projection::kPacketHeaderSize + dnf90_companion_projection::kBodyHeaderSize + dnf90_companion_projection::kMaxDynamicAicBytes + 2 * dnf90_companion_projection::kMaxProjectionBody; constexpr std::uintptr_t kReaderStateRva = 0x053F9AD0; struct NewNativeReaderState { const unsigned char* cursor; int remaining; const unsigned char* packet; int packetLength; }; static_assert(sizeof(NewNativeReaderState) == 16, "DB4 x86 native reader ABI"); // SetTimer's callback is dispatched by the window's own message thread. Only // the native packet thread is allowed to own this timer or call shared APC code. static void CALLBACK NewNativeSafePointTimer(HWND window, UINT, UINT_PTR id, DWORD) { if (id != kSafePointTimer || window != g_safePointWindow || GetCurrentThreadId() != g_dispatchThread) return; dnf90_companion_p0::NewNativeSafePoint(-2); } static BOOL CALLBACK FindNewNativeThreadWindow(HWND window, LPARAM parameter) { DWORD process = 0; const DWORD thread = GetWindowThreadProcessId(window, &process); if (process != GetCurrentProcessId() || thread != g_dispatchThread || !IsWindowVisible(window) || GetWindow(window, GW_OWNER)) return TRUE; *reinterpret_cast(parameter) = window; return FALSE; } static void EnsureNewNativeSafePointTimer() { if (!g_dispatchThread) g_dispatchThread = GetCurrentThreadId(); if (GetCurrentThreadId() != g_dispatchThread) return; if (g_safePointWindow && IsWindow(g_safePointWindow)) return; HWND window = nullptr; EnumThreadWindows(g_dispatchThread, FindNewNativeThreadWindow, reinterpret_cast(&window)); if (!window) return; if (SetTimer(window, kSafePointTimer, 100, NewNativeSafePointTimer)) { g_safePointWindow = window; if (g_captureLog) g_captureLog( "new-native companion UI safe-point timer installed window=%p thread=%lu interval_ms=100", window, g_dispatchThread); } } struct NewNativeTownSnapshot { const unsigned char* start = nullptr; std::size_t bodyLength = 0; std::size_t consumedMinimum = 0; std::uint16_t localKey = 0; }; struct NewNativeDungeonInfoSnapshot { const unsigned char* start = nullptr; std::size_t bodyLength = 0; std::uint32_t dungeon = 0; }; // Current class0 op28 is registered to 01FF5050 at 02001F73. This is a // per-dungeon notification, while op29 is also used for ordinary room changes. // Only the former opens a new run; no duplicate companion lifecycle is stored. static NewNativeDungeonInfoSnapshot SnapshotNewNativeDungeonInfo( int opcode, const unsigned char* header) { NewNativeDungeonInfoSnapshot result{}; if (opcode != 28 || !g_captureBase || !header) return result; __try { const std::uint32_t total = dnf90_companion_projection::Read32(header + 3); if (total < 52 || total > kCapturePacketCapacity) return result; const NewNativeReaderState reader = *reinterpret_cast( g_captureBase + kReaderStateRva); const std::size_t bodyLength = total - 16; if (!reader.cursor || reader.remaining < 0 || static_cast(reader.remaining) < bodyLength) return result; const std::uint32_t dungeon = dnf90_companion_projection::Read32(reader.cursor); if (!dungeon) return result; result.start = reader.cursor; result.bodyLength = bodyLength; result.dungeon = dungeon; } __except (EXCEPTION_EXECUTE_HANDLER) { result = {}; } return result; } static bool NewNativeBodyWasConsumed( const unsigned char* startPointer, std::size_t bodyLength, std::size_t minimum) { if (!startPointer || !g_captureBase) return false; __try { const NewNativeReaderState reader = *reinterpret_cast( g_captureBase + kReaderStateRva); const std::uintptr_t start = reinterpret_cast(startPointer); const std::uintptr_t cursor = reinterpret_cast(reader.cursor); return reader.remaining >= 0 && cursor >= start + minimum && cursor <= start + bodyLength; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } // DB4 class0 initializer 02524320 calls 020460E0, which registers op24 to // 02042900 at 020461A3. Its row reads at 020432C2..02043312 are 3 WORDs and // 3 BYTEs (nine bytes, versus eight in the old client). Early-return gates at // 02042948/02042956 run before any reader consumption. static NewNativeTownSnapshot SnapshotNewNativeTown(int opcode, const unsigned char* header) { NewNativeTownSnapshot result{}; if (opcode != 24 || !g_captureBase || !header) return result; __try { const std::uint32_t total = dnf90_companion_projection::Read32(header + 3); if (total < 20 || total > kCapturePacketCapacity) return result; const NewNativeReaderState reader = *reinterpret_cast( g_captureBase + kReaderStateRva); const std::size_t bodyLength = total - 16; if (!reader.cursor || reader.remaining < 0 || static_cast(reader.remaining) < bodyLength) return result; const std::size_t rows = dnf90_companion_projection::Read16(reader.cursor + 2); if (!rows || rows > (bodyLength - 4) / 9) return result; const std::uint16_t key = dnf90_companion_projection::Read16( reader.cursor + 4 + (rows - 1) * 9); if (!key) return result; result.start = reader.cursor; result.bodyLength = bodyLength; result.consumedMinimum = 4 + rows * 9; result.localKey = key; } __except (EXCEPTION_EXECUTE_HANDLER) { result = {}; } return result; } static bool NewNativeTownWasAccepted(const NewNativeTownSnapshot& snapshot) { if (!snapshot.start || !snapshot.localKey || !g_captureBase) return false; __try { if (!NewNativeBodyWasConsumed(snapshot.start, snapshot.bodyLength, snapshot.consumedMinimum)) return false; using CurrentActor = void* (__cdecl*)(); using ActorByKey = void* (__cdecl*)(unsigned short); void* current = reinterpret_cast(g_captureBase + 0x025610E0)(); void* selected = reinterpret_cast(g_captureBase + 0x0256D730)(snapshot.localKey); return current && current == selected; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } static void ConfigureNewNativeCaptureResume( std::uintptr_t base, NewNativeCaptureLogFn logger = nullptr) { g_captureBase = base; g_captureResume = base ? base + 0x02123C94 : 0; g_captureConsumed = base ? base + 0x02123F50 : 0; g_captureLog = logger; } static void SetOriginalNewNativeClass0Dispatch(void* original) { g_originalClass0Dispatch = reinterpret_cast(original); } // Called only at VA 02523C8A, after native decoding/decompression and reader // binding, before admission is enforced. EDI may still be the protected outer // packet, so its header is copied but plaintext always comes from reader.cursor. // The native decompressor at 02523820 updates header+3 to decoded body size+16; // the enclosing router then binds that body size as reader.remaining. Using the // original socket receive length here would truncate compressed private frames. // Return true for every recognized private frame, including malformed frames: // the native router must never index its opcode tables with private opcode 7F41. static bool __cdecl CaptureNewNativeDecodedProjection( int sourceContext, const unsigned char* header) { bool privatePacket = false; bool captured = false; unsigned char* ownedFrame = nullptr; std::uint32_t packetLength = 0; int remaining = -1; unsigned long exceptionCode = 0; __try { do { if (!g_captureBase || !header || header[0] != 0 || dnf90_companion_projection::Read16(header + 1) != dnf90_companion_projection::kPrivateOpcode) { break; } privatePacket = true; packetLength = dnf90_companion_projection::Read32(header + 3); if (packetLength < dnf90_companion_projection::kPacketHeaderSize + dnf90_companion_projection::kBodyHeaderSize || packetLength > kCapturePacketCapacity) { break; } const NewNativeReaderState reader = *reinterpret_cast( g_captureBase + kReaderStateRva); remaining = reader.remaining; const std::size_t bodyLength = packetLength - dnf90_companion_projection::kPacketHeaderSize; if (!reader.cursor || reader.remaining < 0 || static_cast(reader.remaining) < bodyLength) { break; } ownedFrame = static_cast( HeapAlloc(GetProcessHeap(), 0, packetLength)); if (!ownedFrame) break; std::memcpy(ownedFrame, header, dnf90_companion_projection::kPacketHeaderSize); std::memcpy(ownedFrame + dnf90_companion_projection::kPacketHeaderSize, reader.cursor, bodyLength); // Lifecycle work can reenter native readers. The complete private // frame is owned before calling it, so no borrowed body survives. dnf90_companion_p0::NewNativeSafePoint(-1); captured = dnf90_companion_p0::CaptureProjectionClass0Packet( sourceContext, reinterpret_cast(ownedFrame)); if (captured) { dnf90_companion_p0::NewNativeSafePoint( dnf90_companion_projection::kPrivateOpcode); } else { dnf90_companion_p0::LogProjectionDecodeRejected( ownedFrame, packetLength, "new-native-decoded-boundary"); } } while (false); } __except (EXCEPTION_EXECUTE_HANDLER) { exceptionCode = GetExceptionCode(); } if (ownedFrame) HeapFree(GetProcessHeap(), 0, ownedFrame); if (privatePacket && g_captureLog) { g_captureLog("new-native companion decoded capture context=%d length=%u remaining=%d captured=%u exception=0x%08lX", sourceContext, static_cast(packetLength), remaining, captured ? 1u : 0u, exceptionCode); } return privatePacket; } // The displaced 10 bytes contain no relative instructions: // 8A 45 A3 mov al,[ebp-5D] // C7 45 AC 01 00 00 00 mov dword ptr [ebp-54],1 // Resuming at 02523C94 preserves admission for all non-private packets. The // private branch enters the router's normal consumed epilogue at 02523F50. // That epilogue writes EAX=1 itself and never reads [EBP-54], so the private // branch deliberately does not initialize the displaced dispatch-result local. __declspec(naked) static void ProxyNewNativeProjectionCapture() { __asm { pushfd pushad push edi push dword ptr [ebp - 70h] call CaptureNewNativeDecodedProjection add esp, 8 test al, al jnz private_consumed popad popfd mov al, byte ptr [ebp - 5Dh] mov dword ptr [ebp - 54h], 1 jmp dword ptr [g_captureResume] private_consumed: popad popfd jmp dword ptr [g_captureConsumed] } } // Current VA 0251E340 has the same two-argument stdcall ABI as the shared // legacy owner. Its callbacks consume global reader state before this point. static int __stdcall ProxyNewNativeClass0Dispatch(int sourceContext, int packet) { NewNativeClass0DispatchFn original = g_originalClass0Dispatch; if (!original) return 1; int opcode = -1; __try { const unsigned char* bytes = reinterpret_cast(packet); if (bytes && bytes[0] == 0) { opcode = dnf90_companion_projection::Read16(bytes + 1); } } __except (EXCEPTION_EXECUTE_HANDLER) { opcode = -1; } const NewNativeTownSnapshot town = SnapshotNewNativeTown(opcode, reinterpret_cast(packet)); const NewNativeDungeonInfoSnapshot dungeon = SnapshotNewNativeDungeonInfo(opcode, reinterpret_cast(packet)); const int result = original(sourceContext, packet); EnsureNewNativeSafePointTimer(); if (NewNativeTownWasAccepted(town)) { dnf90_companion_p0::CancelFixedApc5405(); if (g_captureLog) g_captureLog( "new-native companion lifecycle cancelled reason=accepted-town opcode=24 key=%u consumed_min=%zu", static_cast(town.localKey), town.consumedMinimum); return result; } if (dungeon.dungeon && NewNativeBodyWasConsumed(dungeon.start, dungeon.bodyLength, 36)) { dnf90_companion_p0::CancelFixedApc5405(); if (g_captureLog) g_captureLog( "new-native companion lifecycle reset reason=accepted-dungeon-info opcode=28 dungeon=%u", static_cast(dungeon.dungeon)); } if (opcode >= 0) { dnf90_companion_p0::NewNativeSafePoint(opcode); } return result; } } // namespace dnf90_companion_new_native