#include #include #include #include #include #include #include #include "90CN.h" #include "90CN_op3_parent_fix.h" #include "90CN_selected_page_fix.h" #include "90CN_dungeon_skill_gate_fix.h" #include "90CN_weapon_skill_relogin_fix.h" #include "90CN_3149_writer_resync_fix.h" #define EXPAPI(Name) EXTERN_C __declspec(dllexport) void Name() {} EXPAPI(A) namespace { // Current NoPack's direct-scene route predicate is replaced asynchronously by // 90CN.cpp. The imported source baseline accidentally omitted class-1 opcode // 47 even though the 2026-08-19 live fix proved that DROP_ITEM acknowledgements // must enter the native class-1 handler to release the client's discard // pending token. Keep this compatibility shim deliberately narrower than the // main allow-list: opcode 47 is admitted here and every other opcode tail-calls // the already-installed scene-route proxy unchanged. constexpr uintptr_t kDprotoRoutePredicateRva = 0x030734E0; // Current-EXE PVP ready gate. Runtime proof on 2026-09-09 showed that the // resident PK element is a real type-40 channel object and that the virtual // predicate at 0x018928E0 returns false solely because sub_36B0C90's legacy // type whitelist does not contain 40. The same predicate may be used by other // channel/UI paths, so do not modify the global whitelist. Admit type 40 only // when the call comes from the proven PVP-ready vcall return at 0x0189537A. constexpr uintptr_t kPvpReadyElementPredicateRva = 0x014928E0; constexpr uintptr_t kPvpReadyTypeWhitelistRva = 0x032B0C90; constexpr uintptr_t kPvpReadyElementCallReturnRva = 0x0149537A; constexpr unsigned int kPvpNativeChannelType = 40; HMODULE g_selfModule = nullptr; PVOID g_previousSceneRoute = nullptr; typedef int (__cdecl* PvpReadyTypePredicateFn)(unsigned int); PvpReadyTypePredicateFn g_pvpReadyTypePredicate = nullptr; void PvpTraceLine(const char* format, ...) { wchar_t exePath[MAX_PATH] = {}; if (!GetModuleFileNameW(nullptr, exePath, MAX_PATH)) return; wchar_t* slash = wcsrchr(exePath, L'\\'); if (slash) slash[1] = L'\0'; wchar_t logPath[MAX_PATH] = {}; _snwprintf_s(logPath, _countof(logPath), _TRUNCATE, L"%ls90CN_trace.log", exePath); char body[768] = {}; va_list args; va_start(args, format); _vsnprintf_s(body, sizeof(body), _TRUNCATE, format, args); va_end(args); SYSTEMTIME now = {}; GetLocalTime(&now); char line[1024] = {}; int length = _snprintf_s(line, sizeof(line), _TRUNCATE, "[%04u-%02u-%02u %02u:%02u:%02u.%03u] [TRACE] [pid=%lu tid=%lu] %s\r\n", now.wYear, now.wMonth, now.wDay, now.wHour, now.wMinute, now.wSecond, now.wMilliseconds, GetCurrentProcessId(), GetCurrentThreadId(), body); if (length <= 0) return; HANDLE file = CreateFileW(logPath, FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return; DWORD ignored = 0; WriteFile(file, line, static_cast(length), &ignored, nullptr); CloseHandle(file); } int __fastcall ProxyPvpReadyElement(void* element, void*) { uintptr_t executable = reinterpret_cast(GetModuleHandleW(nullptr)); uintptr_t caller = reinterpret_cast(_ReturnAddress()); uintptr_t callerRva = caller >= executable ? caller - executable : 0; unsigned int type = 0; __try { type = *reinterpret_cast( reinterpret_cast(element) + 8); } __except (EXCEPTION_EXECUTE_HANDLER) { PvpTraceLine( "pvp ready element compatibility read failed caller_rva=0x%08X element=%p", static_cast(callerRva), element); return 0; } if (callerRva == kPvpReadyElementCallReturnRva && type == kPvpNativeChannelType) { PvpTraceLine( "pvp ready element type40 admitted caller_rva=0x%08X element=%p type=%u reason=proved_current_pvp_ready_gate_20260909", static_cast(callerRva), element, type); return 1; } return g_pvpReadyTypePredicate ? g_pvpReadyTypePredicate(type) : 0; } DWORD WINAPI InstallPvpReadyElementCompatibility(LPVOID) { HMODULE executable = GetModuleHandleW(nullptr); if (!executable || !g_selfModule) return 0; unsigned char* base = reinterpret_cast(executable); unsigned char* target = base + kPvpReadyElementPredicateRva; unsigned char* whitelist = base + kPvpReadyTypeWhitelistRva; const uintptr_t replacement = reinterpret_cast( &ProxyPvpReadyElement); static const unsigned char kExpectedTarget[] = { 0x8B, 0x41, 0x08, 0x50, 0xE8, 0xA7, 0xE3, 0xE1, 0x01 }; static const unsigned char kExpectedWhitelist[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08, 0x83, 0xF8, 0x04 }; unsigned int waitedMs = 0; for (; waitedMs < 120000; waitedMs += 50) { bool ready = false; __try { ready = memcmp(target, kExpectedTarget, sizeof(kExpectedTarget)) == 0 && memcmp(whitelist, kExpectedWhitelist, sizeof(kExpectedWhitelist)) == 0; } __except (EXCEPTION_EXECUTE_HANDLER) { return 0; } if (ready) break; Sleep(50); } if (waitedMs >= 120000) { PvpTraceLine( "pvp ready element compatibility skipped target=0x%08X whitelist=0x%08X reason=native-bytes-timeout", reinterpret_cast(target), reinterpret_cast(whitelist)); return 0; } g_pvpReadyTypePredicate = reinterpret_cast( whitelist); DWORD oldProtection = 0; if (!VirtualProtect(target, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) { PvpTraceLine( "pvp ready element compatibility failed reason=VirtualProtect gle=%lu", GetLastError()); return 0; } target[0] = 0xE9; *reinterpret_cast(target + 1) = static_cast( replacement - reinterpret_cast(target + 5)); DWORD ignoredProtection = 0; VirtualProtect(target, 5, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), target, 5); PvpTraceLine( "pvp ready element compatibility installed target=0x%08X whitelist=0x%08X pvp_return_rva=0x%08X admitted_type=%u waited_ms=%u", reinterpret_cast(target), reinterpret_cast(whitelist), static_cast(kPvpReadyElementCallReturnRva), kPvpNativeChannelType, waitedMs); return 0; } __declspec(naked) void ProxyDropItemAckSceneRoute() { __asm { cmp word ptr [esp + 4], 47 jne drop_item_route_fallback mov eax, 1 ret 4 drop_item_route_fallback: jmp dword ptr [g_previousSceneRoute] } } DWORD WINAPI InstallDropItemAckSceneRoute(LPVOID) { HMODULE executable = GetModuleHandleW(nullptr); if (!executable || !g_selfModule) return 0; unsigned char* target = reinterpret_cast(executable) + kDprotoRoutePredicateRva; const uintptr_t replacement = reinterpret_cast( &ProxyDropItemAckSceneRoute); // Start90CNPatch installs the ordinary route proxy on its own worker. // Wait until its verified E9+NOP patch is present, then chain this one // opcode in front of it. If DNF_ROUTE_COMPAT=0, no matching patch appears // and this worker exits without changing the native predicate. for (unsigned int waitedMs = 0; waitedMs < 120000; waitedMs += 100) { __try { if (target[0] != 0xE9 || target[5] != 0x90) { Sleep(100); continue; } LONG previousRelative = *reinterpret_cast(target + 1); unsigned char* previous = target + 5 + previousRelative; if (reinterpret_cast(previous) == replacement) { return 0; } MEMORY_BASIC_INFORMATION memory = {}; if (VirtualQuery(previous, &memory, sizeof(memory)) != sizeof(memory) || memory.AllocationBase != g_selfModule) { Sleep(100); continue; } g_previousSceneRoute = previous; LONG replacementRelative = static_cast( replacement - reinterpret_cast(target + 5)); DWORD oldProtection = 0; if (!VirtualProtect(target, 6, PAGE_EXECUTE_READWRITE, &oldProtection)) { return 0; } target[0] = 0xE9; *reinterpret_cast(target + 1) = replacementRelative; target[5] = 0x90; DWORD ignoredProtection = 0; VirtualProtect(target, 6, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), target, 6); return 0; } __except (EXCEPTION_EXECUTE_HANDLER) { return 0; } } return 0; } } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: g_selfModule = hModule; DisableThreadLibraryCalls(hModule); // The new standalone client has a different PE layout from the // production NoPack executable. Keep every legacy RVA hook out of // that process; only the proven native channel selection bridge is // safe there. The current-client dungeon selector/owner/state // observer rewrites the DB4 native entry points and was only added // for a short diagnostic capture. Keep it opt-in so a normal game // launch cannot be destabilised by an observation probe. if (IsNewClientCompatibilityMode()) { wchar_t probeEnabled[8] = {}; DWORD probeLength = GetEnvironmentVariableW( L"DNF_DUNGEON_SELECTOR_PROBE", probeEnabled, static_cast(_countof(probeEnabled))); if (probeLength > 0 && probeEnabled[0] == L'1') { StartCurrentDungeonSelectorProbe(hModule); PvpTraceLine( "new-client dungeon selector probe enabled by environment"); } else { PvpTraceLine( "new-client dungeon selector/owner/state probes disabled " "for normal launch"); } StartCurrentDungeonSkillGateFix(hModule); StartNewClientChannelCompatibility(); break; } StartOp3ParentReadinessFix(hModule); Start90CNPatch(); Start3149WriterResyncFix(hModule); StartSelectedPageReadinessFix(hModule); StartDungeonSkillGateFix(hModule); StartWeaponSkillReloginFix(hModule); { HANDLE worker = CreateThread( nullptr, 0, InstallPvpReadyElementCompatibility, nullptr, 0, nullptr); if (worker) CloseHandle(worker); } { HANDLE worker = CreateThread( nullptr, 0, InstallDropItemAckSceneRoute, nullptr, 0, nullptr); if (worker) CloseHandle(worker); } break; case DLL_PROCESS_DETACH: StopCurrentDungeonSelectorProbe(); StopCurrentDungeonSkillGateFix(); StopWeaponSkillReloginFix(); StopDungeonSkillGateFix(); break; } return TRUE; }