Files
90CNServer/client-patch/90CN_dungeon_skill_gate_fix.cpp
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

844 lines
31 KiB
C++

#include "90CN_dungeon_skill_gate_fix.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
namespace
{
// v7/v8 live evidence on the current NoPack EXE:
// sub_340BCF1 calls the native skill-state precheck, then executes
// test al, al
// je loc_340BD2E
// A non-zero result falls through into sub_99F840, which opens owner 0x49
// (the "initial skill state" warning) and then early-returns from the portal
// transition before ENUM_CMDPACKET_ENTER_SELECT_DUNGEON/msg15 is emitted.
// Force only this post-test branch to the native normal continuation. The
// precheck function itself still runs, preserving any side effects/state reads.
constexpr uintptr_t kSkillGateBranchRva = 0x0300BCFF; // 0340BCFF at base 00400000
constexpr DWORD kInstallDelayMs = 1200;
const unsigned char kExpectedBranch[2] = { 0x74, 0x2D }; // je +0x2D
const unsigned char kBypassBranch[2] = { 0xEB, 0x2D }; // jmp +0x2D
HMODULE g_selfModule = nullptr;
uintptr_t g_exeBase = 0;
HANDLE g_worker = nullptr;
volatile LONG g_stop = 0;
volatile LONG g_installed = 0;
// The current 111 MB DB4 client does not contain the legacy 74 2D branch.
// Keep the first observation pass strictly passive: this function is the
// current selector/list state routine identified by the version-locked
// prologue at RVA 0300BB80. The probe records entry state and returns to the
// original function through a trampoline; it never changes a branch result.
constexpr uintptr_t kCurrentSelectorProbeRva = 0x0300BB80;
constexpr size_t kCurrentSelectorProbePatchLength = 10;
const unsigned char kCurrentSelectorProbePrologue[
kCurrentSelectorProbePatchLength] = {
0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0xF8, 0x44, 0x27, 0x04
};
constexpr LONG kCurrentSelectorProbeMaxLoggedCalls = 4096;
HMODULE g_currentProbeModule = nullptr;
uintptr_t g_currentProbeBase = 0;
void* g_currentSelectorProbeOriginal = nullptr;
HANDLE g_currentProbeWorker = nullptr;
volatile LONG g_currentProbeStop = 0;
volatile LONG g_currentProbeInstalled = 0;
volatile LONG g_currentProbeCallCount = 0;
// The DB4 client no longer uses the legacy 0300BCFF branch. The first
// observation pass also showed that 0300BB80 is only a startup list routine.
// These two current-EXE owner/notice builders are therefore traced separately
// while reproducing the native "initial skill state" dialog. They remain
// observe-only: no return value, owner, or dialog state is changed.
constexpr uintptr_t kCurrentOwnerManagerProbeRva = 0x004A1170; // VA 008A1170
constexpr size_t kCurrentOwnerManagerProbePatchLength = 14;
const unsigned char kCurrentOwnerManagerProbePrologue[
kCurrentOwnerManagerProbePatchLength] = {
0x55, 0x8B, 0xEC, 0x83, 0xE4, 0xF8, 0x83, 0xEC,
0x18, 0x56, 0x57, 0x8D, 0x45, 0x0C,
};
constexpr uintptr_t kCurrentNoticeBuilderProbeRva = 0x023B65B0; // VA 027B65B0
constexpr size_t kCurrentNoticeBuilderProbePatchLength = 12;
const unsigned char kCurrentNoticeBuilderProbePrologue[
kCurrentNoticeBuilderProbePatchLength] = {
0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x18, 0x83, 0x7D,
0x08, 0x00, 0x56, 0x8B,
};
constexpr LONG kCurrentOwnerProbeMaxLoggedCalls = 1024;
void* g_currentOwnerManagerProbeOriginal = nullptr;
void* g_currentNoticeBuilderProbeOriginal = nullptr;
volatile LONG g_currentOwnerProbeInstalled = 0;
volatile LONG g_currentNoticeProbeInstalled = 0;
volatile LONG g_currentOwnerProbeCallCount = 0;
// Current 111 MB DB4 client. In the owner-0x73 dungeon-selector function,
// the native count comparison at VA 0x02248CAC uses `jle 0x02248CE8` to
// continue normally. The fall-through path builds text id 0x11C79 and sends
// event 0x2D6 (the "skill is in initial state" notice) before hiding the
// selector owner. Force only this local branch to the already existing normal
// continuation. The exact prologue/branch bytes are checked at runtime.
constexpr uintptr_t kCurrentSkillGateBranchRva = 0x01E48CAC;
const unsigned char kCurrentSkillGateExpected[2] = { 0x7E, 0x3A };
const unsigned char kCurrentSkillGateBypass[2] = { 0xEB, 0x3A };
HMODULE g_currentSkillGateModule = nullptr;
uintptr_t g_currentSkillGateBase = 0;
HANDLE g_currentSkillGateWorker = nullptr;
volatile LONG g_currentSkillGateStop = 0;
volatile LONG g_currentSkillGateInstalled = 0;
void BuildLogPath(wchar_t* output, size_t outputCount)
{
if (!output || outputCount == 0) return;
output[0] = L'\0';
DWORD length = g_selfModule
? GetModuleFileNameW(g_selfModule, output, static_cast<DWORD>(outputCount))
: 0;
if (!length || length >= outputCount) {
wcscpy_s(output, outputCount, L"90CN_fix.log");
return;
}
wchar_t* slash = wcsrchr(output, L'\\');
wchar_t* slash2 = wcsrchr(output, L'/');
if (slash2 && (!slash || slash2 > slash)) slash = slash2;
if (slash) {
slash[1] = L'\0';
wcscat_s(output, outputCount, L"90CN_fix.log");
} else {
wcscpy_s(output, outputCount, L"90CN_fix.log");
}
}
void WriteLogLine(const char* text)
{
if (!text) return;
wchar_t path[MAX_PATH] = {};
BuildLogPath(path, _countof(path));
HANDLE file = CreateFileW(path, FILE_APPEND_DATA,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
WriteFile(file, text, static_cast<DWORD>(strlen(text)), &written, nullptr);
CloseHandle(file);
}
bool PatchBytes(unsigned char* target, const unsigned char* bytes, size_t count)
{
DWORD oldProtection = 0;
if (!VirtualProtect(target, count, PAGE_EXECUTE_READWRITE, &oldProtection)) {
return false;
}
memcpy(target, bytes, count);
DWORD ignoredProtection = 0;
VirtualProtect(target, count, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, count);
return memcmp(target, bytes, count) == 0;
}
// 探针开关:DLL 同目录出现 90CN_gate_probe.flag 时**不**打这个旁路,让客户端
// 按原逻辑走“技能处于初始状态”分支。用来确认该检查对当前角色是否真的成立,
// 不改任何数据、删掉 flag 即恢复。2026-10-19 蛇腹剑形态排查用。
static bool GateProbeBypassDisabled()
{
wchar_t path[MAX_PATH] = {};
DWORD length = g_selfModule
? GetModuleFileNameW(g_selfModule, path, MAX_PATH)
: 0;
if (!length || length >= MAX_PATH) return false;
wchar_t* slash = wcsrchr(path, L'\\');
if (!slash) return false;
slash[1] = L'\0';
if (wcscat_s(path, L"90CN_gate_probe.flag") != 0) return false;
return GetFileAttributesW(path) != INVALID_FILE_ATTRIBUTES;
}
bool InstallBypass()
{
if (!g_exeBase) return false;
if (GateProbeBypassDisabled()) {
WriteLogLine(
"dungeon-skill-gate-fix probe_mode=1 bypass_disabled=true "
"target=0340BCFF reason=90CN_gate_probe.flag\r\n");
return true;
}
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kSkillGateBranchRva);
unsigned char observed[2] = {};
__try {
memcpy(observed, target, sizeof(observed));
if (memcmp(observed, kBypassBranch, sizeof(kBypassBranch)) == 0) {
InterlockedExchange(&g_installed, 1);
WriteLogLine(
"dungeon-skill-gate-fix already_installed target=0340BCFF bytes=EB2D\r\n");
return true;
}
if (memcmp(observed, kExpectedBranch, sizeof(kExpectedBranch)) != 0) {
char line[256] = {};
sprintf_s(line,
"dungeon-skill-gate-fix install_failed target=0340BCFF "
"observed=%02X%02X expected=742D\r\n",
observed[0], observed[1]);
WriteLogLine(line);
return false;
}
if (!PatchBytes(target, kBypassBranch, sizeof(kBypassBranch))) {
WriteLogLine(
"dungeon-skill-gate-fix install_failed reason=patch-write target=0340BCFF\r\n");
return false;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLogLine(
"dungeon-skill-gate-fix install_failed reason=exception target=0340BCFF\r\n");
return false;
}
InterlockedExchange(&g_installed, 1);
WriteLogLine(
"dungeon-skill-gate-fix installed target=0340BCFF original=742D replacement=EB2D "
"scope=initial-skill-selector-precheck-only preserves=native-skill-check-call-and-other-dungeon-gates\r\n");
return true;
}
void RestoreBypass()
{
if (InterlockedCompareExchange(&g_installed, 0, 0) == 0 || !g_exeBase) return;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kSkillGateBranchRva);
__try {
if (memcmp(target, kBypassBranch, sizeof(kBypassBranch)) == 0) {
if (PatchBytes(target, kExpectedBranch, sizeof(kExpectedBranch))) {
WriteLogLine(
"dungeon-skill-gate-fix restored target=0340BCFF bytes=742D\r\n");
} else {
WriteLogLine(
"dungeon-skill-gate-fix restore_failed target=0340BCFF\r\n");
}
} else {
char line[256] = {};
sprintf_s(line,
"dungeon-skill-gate-fix restore_skipped target=0340BCFF current=%02X%02X\r\n",
target[0], target[1]);
WriteLogLine(line);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLogLine(
"dungeon-skill-gate-fix restore_failed reason=exception target=0340BCFF\r\n");
}
InterlockedExchange(&g_installed, 0);
}
bool InstallCurrentSkillGateBypass()
{
if (!g_currentSkillGateBase) return false;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentSkillGateBase + kCurrentSkillGateBranchRva);
unsigned char observed[2] = {};
__try {
memcpy(observed, target, sizeof(observed));
if (memcmp(observed, kCurrentSkillGateBypass,
sizeof(kCurrentSkillGateBypass)) == 0) {
InterlockedExchange(&g_currentSkillGateInstalled, 1);
WriteLogLine(
"new-client dungeon-skill-gate already_installed "
"target=02248CAC bytes=EB3A scope=owner-73-selector\r\n");
return true;
}
if (memcmp(observed, kCurrentSkillGateExpected,
sizeof(kCurrentSkillGateExpected)) != 0) {
char line[256] = {};
sprintf_s(line,
"new-client dungeon-skill-gate install_failed "
"target=02248CAC observed=%02X%02X expected=7E3A\r\n",
observed[0], observed[1]);
WriteLogLine(line);
return false;
}
if (!PatchBytes(target, kCurrentSkillGateBypass,
sizeof(kCurrentSkillGateBypass))) {
WriteLogLine(
"new-client dungeon-skill-gate install_failed "
"reason=patch-write target=02248CAC\r\n");
return false;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLogLine(
"new-client dungeon-skill-gate install_failed "
"reason=exception target=02248CAC\r\n");
return false;
}
InterlockedExchange(&g_currentSkillGateInstalled, 1);
WriteLogLine(
"new-client dungeon-skill-gate installed target=02248CAC "
"original=7E3A replacement=EB3A "
"scope=owner-73-dungeon-selector-initial-skill-notice\r\n");
return true;
}
void RestoreCurrentSkillGateBypass()
{
if (InterlockedCompareExchange(&g_currentSkillGateInstalled, 0, 0) == 0 ||
!g_currentSkillGateBase) return;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentSkillGateBase + kCurrentSkillGateBranchRva);
__try {
if (memcmp(target, kCurrentSkillGateBypass,
sizeof(kCurrentSkillGateBypass)) == 0) {
if (PatchBytes(target, kCurrentSkillGateExpected,
sizeof(kCurrentSkillGateExpected))) {
WriteLogLine(
"new-client dungeon-skill-gate restored "
"target=02248CAC bytes=7E3A\r\n");
} else {
WriteLogLine(
"new-client dungeon-skill-gate restore_failed "
"target=02248CAC\r\n");
}
} else {
char line[256] = {};
sprintf_s(line,
"new-client dungeon-skill-gate restore_skipped "
"target=02248CAC current=%02X%02X\r\n",
target[0], target[1]);
WriteLogLine(line);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLogLine(
"new-client dungeon-skill-gate restore_failed "
"reason=exception target=02248CAC\r\n");
}
InterlockedExchange(&g_currentSkillGateInstalled, 0);
}
DWORD WINAPI InstallCurrentSkillGateWorker(LPVOID)
{
g_currentSkillGateBase = reinterpret_cast<uintptr_t>(
GetModuleHandleW(nullptr));
if (!g_currentSkillGateBase || !g_currentSkillGateModule) return 0;
DWORD slept = 0;
while (slept < kInstallDelayMs &&
InterlockedCompareExchange(&g_currentSkillGateStop, 0, 0) == 0) {
Sleep(100);
slept += 100;
}
if (InterlockedCompareExchange(&g_currentSkillGateStop, 0, 0) == 0) {
InstallCurrentSkillGateBypass();
}
return 0;
}
DWORD WINAPI InstallWorker(LPVOID)
{
g_exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
if (!g_exeBase || !g_selfModule) return 0;
DWORD slept = 0;
while (slept < kInstallDelayMs &&
InterlockedCompareExchange(&g_stop, 0, 0) == 0) {
Sleep(100);
slept += 100;
}
if (InterlockedCompareExchange(&g_stop, 0, 0) == 0) {
InstallBypass();
}
return 0;
}
void LogCurrentSelectorProbe(uintptr_t self, uintptr_t caller)
{
LONG callNumber = InterlockedIncrement(&g_currentProbeCallCount);
if (callNumber > kCurrentSelectorProbeMaxLoggedCalls) return;
const uintptr_t base = reinterpret_cast<uintptr_t>(
GetModuleHandleW(nullptr));
const unsigned int callerRva = (base && caller >= base)
? static_cast<unsigned int>(caller - base) : 0;
unsigned int state2fc = 0xFFFFFFFFu;
uintptr_t manager = 0;
__try {
if (self) {
state2fc = static_cast<unsigned int>(
*reinterpret_cast<volatile unsigned char*>(self + 0x2FC));
manager = *reinterpret_cast<volatile uintptr_t*>(self + 0xFC8);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
state2fc = 0xFFFFFFFEu;
manager = 0;
}
// The two statically identified callers are the useful selector/list
// paths. Keep a short prefix of other callers so a changed call graph is
// visible without flooding the client trace during startup.
const bool selectorCaller =
(callerRva >= 0x03011740u && callerRva <= 0x03011790u) ||
(callerRva >= 0x03012720u && callerRva <= 0x03012770u);
if (!selectorCaller && callNumber > 64) return;
char line[384] = {};
sprintf_s(line,
"new-client dungeon-selector-probe call=%ld self=0x%08X "
"caller_rva=0x%08X state_2fc=%u manager=0x%08X\r\n",
callNumber, static_cast<unsigned int>(self), callerRva,
state2fc, static_cast<unsigned int>(manager));
WriteLogLine(line);
}
bool CurrentProbeBytesMatch(const unsigned char* target,
const unsigned char* expected, size_t length);
void LogCurrentOwnerProbe(unsigned int kind, uintptr_t self,
uintptr_t caller, uintptr_t originalStack)
{
LONG callNumber = InterlockedIncrement(&g_currentOwnerProbeCallCount);
if (callNumber > kCurrentOwnerProbeMaxLoggedCalls) return;
const uintptr_t base = reinterpret_cast<uintptr_t>(
GetModuleHandleW(nullptr));
const unsigned int callerRva = (base && caller >= base)
? static_cast<unsigned int>(caller - base) : 0;
unsigned int arg0 = 0xFFFFFFFFu;
unsigned int arg1 = 0xFFFFFFFFu;
unsigned int arg2 = 0xFFFFFFFFu;
__try {
if (originalStack) {
arg0 = *reinterpret_cast<volatile unsigned int*>(originalStack + 4);
arg1 = *reinterpret_cast<volatile unsigned int*>(originalStack + 8);
arg2 = *reinterpret_cast<volatile unsigned int*>(originalStack + 0xC);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
arg0 = arg1 = arg2 = 0xFFFFFFFEu;
}
// Keep the normal call prefix short, but retain every call carrying the
// legacy warning owner/message values so the click window is unambiguous.
const bool suspicious = arg0 == 0x49u || arg1 == 0x49u || arg2 == 0x49u ||
arg0 == 0x73u || arg1 == 0x73u || arg2 == 0x73u ||
arg0 == 0xF6u || arg1 == 0xF6u || arg2 == 0xF6u;
if (!suspicious && callNumber > 96) return;
char line[448] = {};
sprintf_s(line,
"new-client dungeon-owner-probe kind=%u call=%ld self=0x%08X "
"caller_rva=0x%08X arg0=0x%08X arg1=0x%08X arg2=0x%08X "
"suspicious=%u\r\n",
kind, callNumber, static_cast<unsigned int>(self), callerRva,
arg0, arg1, arg2, suspicious ? 1u : 0u);
WriteLogLine(line);
}
__declspec(naked) void ProxyCurrentOwnerManagerProbe()
{
__asm {
pushfd
pushad
mov ecx, [esp + 0Ch] // pushad saved ESP (after pushfd)
mov eax, [ecx + 4] // native return address at original ESP
mov edx, [esp + 18h] // saved native ECX (this)
add ecx, 4 // original ESP, points at native return address
push ecx
push eax
push edx
push 1
call LogCurrentOwnerProbe
add esp, 10h
popad
popfd
jmp dword ptr [g_currentOwnerManagerProbeOriginal]
}
}
__declspec(naked) void ProxyCurrentNoticeBuilderProbe()
{
__asm {
pushfd
pushad
mov ecx, [esp + 0Ch] // pushad saved ESP (after pushfd)
mov eax, [ecx + 4] // native return address at original ESP
mov edx, [esp + 18h] // saved native ECX (this)
add ecx, 4 // original ESP, points at native return address
push ecx
push eax
push edx
push 2
call LogCurrentOwnerProbe
add esp, 10h
popad
popfd
jmp dword ptr [g_currentNoticeBuilderProbeOriginal]
}
}
bool InstallCurrentOwnerProbe(uintptr_t rva, const unsigned char* expected,
size_t patchLength, void* detour, void** original, const char* label,
volatile LONG* installed)
{
if (!g_currentProbeBase || !expected || !detour || !original ||
!installed || patchLength < 5) return false;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentProbeBase + rva);
if (!CurrentProbeBytesMatch(target, expected, patchLength)) {
char line[256] = {};
sprintf_s(line,
"new-client dungeon-owner-probe skipped label=%s "
"reason=prologue-mismatch rva=0x%08X\r\n",
label ? label : "unknown", static_cast<unsigned int>(rva));
WriteLogLine(line);
return false;
}
unsigned char* trampoline = static_cast<unsigned char*>(VirtualAlloc(
nullptr, patchLength + 5, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE));
if (!trampoline) return false;
memcpy(trampoline, target, patchLength);
trampoline[patchLength] = 0xE9;
*reinterpret_cast<int32_t*>(trampoline + patchLength + 1) =
static_cast<int32_t>(reinterpret_cast<intptr_t>(target + patchLength) -
reinterpret_cast<intptr_t>(trampoline + patchLength + 5));
DWORD oldProtection = 0;
if (!VirtualProtect(target, patchLength, PAGE_EXECUTE_READWRITE,
&oldProtection)) {
VirtualFree(trampoline, 0, MEM_RELEASE);
return false;
}
unsigned char jump[5] = { 0xE9 };
*reinterpret_cast<int32_t*>(jump + 1) =
static_cast<int32_t>(reinterpret_cast<intptr_t>(detour) -
reinterpret_cast<intptr_t>(target + 5));
memcpy(target, jump, sizeof(jump));
for (size_t i = sizeof(jump); i < patchLength; ++i) target[i] = 0x90;
DWORD ignoredProtection = 0;
VirtualProtect(target, patchLength, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, patchLength);
FlushInstructionCache(GetCurrentProcess(), trampoline, patchLength + 5);
*original = trampoline;
InterlockedExchange(installed, 1);
char line[256] = {};
sprintf_s(line,
"new-client dungeon-owner-probe installed label=%s "
"rva=0x%08X patch_length=%u mode=observe-only\r\n",
label ? label : "unknown", static_cast<unsigned int>(rva),
static_cast<unsigned int>(patchLength));
WriteLogLine(line);
return true;
}
void RestoreCurrentOwnerProbe(uintptr_t rva, const unsigned char* expected,
size_t patchLength, void* detour, void** original, const char* label,
volatile LONG* installed)
{
if (!g_currentProbeBase || !expected || !detour || !original ||
!installed || InterlockedCompareExchange(installed, 0, 0) == 0) return;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentProbeBase + rva);
unsigned char jump[5] = { 0xE9 };
*reinterpret_cast<int32_t*>(jump + 1) =
static_cast<int32_t>(reinterpret_cast<intptr_t>(detour) -
reinterpret_cast<intptr_t>(target + 5));
bool ours = CurrentProbeBytesMatch(target, jump, sizeof(jump));
for (size_t i = sizeof(jump); ours && i < patchLength; ++i) {
ours = target[i] == 0x90;
}
if (ours) {
DWORD oldProtection = 0;
if (VirtualProtect(target, patchLength, PAGE_EXECUTE_READWRITE,
&oldProtection)) {
memcpy(target, expected, patchLength);
DWORD ignoredProtection = 0;
VirtualProtect(target, patchLength, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, patchLength);
char line[256] = {};
sprintf_s(line,
"new-client dungeon-owner-probe restored label=%s "
"rva=0x%08X\r\n", label ? label : "unknown",
static_cast<unsigned int>(rva));
WriteLogLine(line);
}
} else {
char line[256] = {};
sprintf_s(line,
"new-client dungeon-owner-probe restore-skipped label=%s "
"reason=target-changed rva=0x%08X\r\n", label ? label : "unknown",
static_cast<unsigned int>(rva));
WriteLogLine(line);
}
if (*original) {
VirtualFree(*original, 0, MEM_RELEASE);
*original = nullptr;
}
InterlockedExchange(installed, 0);
}
// Preserve all native registers, flags, stack cleanup and the original
// return address. The stolen prologue contains no relative instruction, so
// the generic local trampoline can resume at target+10 safely.
__declspec(naked) void ProxyCurrentSelectorProbe()
{
__asm {
pushfd
pushad
mov eax, [esp + 24h] // native return address
mov edx, [esp + 18h] // saved native ECX (this)
push eax
push edx
call LogCurrentSelectorProbe
add esp, 8
popad
popfd
jmp dword ptr [g_currentSelectorProbeOriginal]
}
}
bool CurrentProbeBytesMatch(const unsigned char* target,
const unsigned char* expected, size_t length)
{
if (!target || !expected || length == 0) return false;
__try {
return memcmp(target, expected, length) == 0;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
bool InstallCurrentSelectorProbe()
{
if (!g_currentProbeBase) return false;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentProbeBase + kCurrentSelectorProbeRva);
if (!CurrentProbeBytesMatch(target, kCurrentSelectorProbePrologue,
kCurrentSelectorProbePatchLength)) {
WriteLogLine(
"new-client dungeon-selector-probe skipped: prologue-mismatch "
"rva=0300BB80 expected=558BEC6AFF68F8442704\r\n");
return false;
}
unsigned char* trampoline = static_cast<unsigned char*>(VirtualAlloc(
nullptr, kCurrentSelectorProbePatchLength + 5,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
if (!trampoline) {
WriteLogLine(
"new-client dungeon-selector-probe failed: trampoline-allocation\r\n");
return false;
}
memcpy(trampoline, target, kCurrentSelectorProbePatchLength);
trampoline[kCurrentSelectorProbePatchLength] = 0xE9;
*reinterpret_cast<int32_t*>(trampoline +
kCurrentSelectorProbePatchLength + 1) = static_cast<int32_t>(
reinterpret_cast<intptr_t>(target + kCurrentSelectorProbePatchLength) -
reinterpret_cast<intptr_t>(trampoline +
kCurrentSelectorProbePatchLength + 5));
DWORD oldProtection = 0;
if (!VirtualProtect(target, kCurrentSelectorProbePatchLength,
PAGE_EXECUTE_READWRITE, &oldProtection)) {
VirtualFree(trampoline, 0, MEM_RELEASE);
WriteLogLine(
"new-client dungeon-selector-probe failed: virtualprotect\r\n");
return false;
}
unsigned char jump[5] = { 0xE9 };
*reinterpret_cast<int32_t*>(jump + 1) = static_cast<int32_t>(
reinterpret_cast<intptr_t>(&ProxyCurrentSelectorProbe) -
reinterpret_cast<intptr_t>(target + 5));
memcpy(target, jump, sizeof(jump));
for (size_t i = sizeof(jump); i < kCurrentSelectorProbePatchLength; ++i) {
target[i] = 0x90;
}
DWORD ignoredProtection = 0;
VirtualProtect(target, kCurrentSelectorProbePatchLength,
oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target,
kCurrentSelectorProbePatchLength);
FlushInstructionCache(GetCurrentProcess(), trampoline,
kCurrentSelectorProbePatchLength + 5);
g_currentSelectorProbeOriginal = trampoline;
InterlockedExchange(&g_currentProbeInstalled, 1);
WriteLogLine(
"new-client dungeon-selector-probe installed rva=0300BB80 "
"patch_length=10 mode=observe-only\r\n");
return true;
}
bool CurrentProbeDetourMatches(const unsigned char* target)
{
if (!target) return false;
unsigned char jump[5] = { 0xE9 };
*reinterpret_cast<int32_t*>(jump + 1) = static_cast<int32_t>(
reinterpret_cast<intptr_t>(&ProxyCurrentSelectorProbe) -
reinterpret_cast<intptr_t>(target + 5));
if (!CurrentProbeBytesMatch(target, jump, sizeof(jump))) return false;
for (size_t i = sizeof(jump); i < kCurrentSelectorProbePatchLength; ++i) {
if (target[i] != 0x90) return false;
}
return true;
}
void RestoreCurrentSelectorProbe()
{
if (InterlockedCompareExchange(&g_currentProbeInstalled, 0, 0) == 0 ||
!g_currentProbeBase) return;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentProbeBase + kCurrentSelectorProbeRva);
if (CurrentProbeDetourMatches(target)) {
DWORD oldProtection = 0;
if (VirtualProtect(target, kCurrentSelectorProbePatchLength,
PAGE_EXECUTE_READWRITE, &oldProtection)) {
memcpy(target, kCurrentSelectorProbePrologue,
kCurrentSelectorProbePatchLength);
DWORD ignoredProtection = 0;
VirtualProtect(target, kCurrentSelectorProbePatchLength,
oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target,
kCurrentSelectorProbePatchLength);
WriteLogLine(
"new-client dungeon-selector-probe restored rva=0300BB80\r\n");
}
} else {
WriteLogLine(
"new-client dungeon-selector-probe restore-skipped target-changed\r\n");
}
if (g_currentSelectorProbeOriginal) {
VirtualFree(g_currentSelectorProbeOriginal, 0, MEM_RELEASE);
g_currentSelectorProbeOriginal = nullptr;
}
InterlockedExchange(&g_currentProbeInstalled, 0);
}
DWORD WINAPI CurrentSelectorProbeWorker(LPVOID)
{
g_currentProbeBase = reinterpret_cast<uintptr_t>(
GetModuleHandleW(nullptr));
if (!g_currentProbeBase) return 0;
DWORD slept = 0;
while (slept < kInstallDelayMs &&
InterlockedCompareExchange(&g_currentProbeStop, 0, 0) == 0) {
Sleep(100);
slept += 100;
}
if (InterlockedCompareExchange(&g_currentProbeStop, 0, 0) == 0) {
InstallCurrentSelectorProbe();
InstallCurrentOwnerProbe(
kCurrentOwnerManagerProbeRva,
kCurrentOwnerManagerProbePrologue,
kCurrentOwnerManagerProbePatchLength,
&ProxyCurrentOwnerManagerProbe,
&g_currentOwnerManagerProbeOriginal,
"owner-manager-008A1170",
&g_currentOwnerProbeInstalled);
InstallCurrentOwnerProbe(
kCurrentNoticeBuilderProbeRva,
kCurrentNoticeBuilderProbePrologue,
kCurrentNoticeBuilderProbePatchLength,
&ProxyCurrentNoticeBuilderProbe,
&g_currentNoticeBuilderProbeOriginal,
"notice-builder-027B65B0",
&g_currentNoticeProbeInstalled);
}
return 0;
}
}
void StartDungeonSkillGateFix(HMODULE module)
{
if (g_worker) return;
g_selfModule = module;
InterlockedExchange(&g_stop, 0);
g_worker = CreateThread(nullptr, 0, InstallWorker, nullptr, 0, nullptr);
}
void StopDungeonSkillGateFix()
{
InterlockedExchange(&g_stop, 1);
if (g_worker) {
WaitForSingleObject(g_worker, 1500);
CloseHandle(g_worker);
g_worker = nullptr;
}
RestoreBypass();
}
void StartCurrentDungeonSkillGateFix(HMODULE module)
{
if (g_currentSkillGateWorker) return;
g_currentSkillGateModule = module;
InterlockedExchange(&g_currentSkillGateStop, 0);
g_currentSkillGateWorker = CreateThread(nullptr, 0,
InstallCurrentSkillGateWorker, nullptr, 0, nullptr);
if (!g_currentSkillGateWorker) {
WriteLogLine(
"new-client dungeon-skill-gate failed: worker-create\r\n");
}
}
void StopCurrentDungeonSkillGateFix()
{
InterlockedExchange(&g_currentSkillGateStop, 1);
if (g_currentSkillGateWorker) {
WaitForSingleObject(g_currentSkillGateWorker, 1500);
CloseHandle(g_currentSkillGateWorker);
g_currentSkillGateWorker = nullptr;
}
RestoreCurrentSkillGateBypass();
}
void StartCurrentDungeonSelectorProbe(HMODULE module)
{
if (g_currentProbeWorker) return;
g_currentProbeModule = module;
InterlockedExchange(&g_currentProbeStop, 0);
g_currentProbeWorker = CreateThread(nullptr, 0,
CurrentSelectorProbeWorker, nullptr, 0, nullptr);
if (!g_currentProbeWorker) {
WriteLogLine(
"new-client dungeon-selector-probe failed: worker-create\r\n");
}
}
void StopCurrentDungeonSelectorProbe()
{
InterlockedExchange(&g_currentProbeStop, 1);
if (g_currentProbeWorker) {
WaitForSingleObject(g_currentProbeWorker, 1500);
CloseHandle(g_currentProbeWorker);
g_currentProbeWorker = nullptr;
}
RestoreCurrentSelectorProbe();
RestoreCurrentOwnerProbe(
kCurrentOwnerManagerProbeRva,
kCurrentOwnerManagerProbePrologue,
kCurrentOwnerManagerProbePatchLength,
&ProxyCurrentOwnerManagerProbe,
&g_currentOwnerManagerProbeOriginal,
"owner-manager-008A1170",
&g_currentOwnerProbeInstalled);
RestoreCurrentOwnerProbe(
kCurrentNoticeBuilderProbeRva,
kCurrentNoticeBuilderProbePrologue,
kCurrentNoticeBuilderProbePatchLength,
&ProxyCurrentNoticeBuilderProbe,
&g_currentNoticeBuilderProbeOriginal,
"notice-builder-027B65B0",
&g_currentNoticeProbeInstalled);
}