Files
90CNServer/client-patch/90CN_weapon_skill_relogin_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

294 lines
9.8 KiB
C++

#include "90CN_weapon_skill_relogin_fix.h"
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <wchar.h>
namespace
{
constexpr uintptr_t kRequirementBranchRva = 0x01C07739;
constexpr uintptr_t kRequirementInvalidResumeRva = 0x01C07748;
constexpr uintptr_t kRequirementValidResumeRva = 0x01C07780;
constexpr uintptr_t kNativeSkillValidityRva = 0x0225F610;
constexpr uintptr_t kReloginEquipmentCallerReturnRva = 0x01C0859E;
constexpr DWORD kInstallDelayMs = 1500;
// Native bytes immediately after actor->lookup(requirementId, 1):
// je invalid
// push 1
// mov ecx,eax
const unsigned char kExpectedBytes[6] = { 0x74, 0x0D, 0x6A, 0x01, 0x8B, 0xC8 };
HMODULE g_selfModule = nullptr;
uintptr_t g_exeBase = 0;
HANDLE g_worker = nullptr;
volatile LONG g_stop = 0;
volatile LONG g_installed = 0;
volatile LONG g_bypassLogCount = 0;
SRWLOCK g_logLock = SRWLOCK_INIT;
void* g_nativeSkillValidity = nullptr;
void* g_invalidResume = nullptr;
void* g_validResume = nullptr;
uintptr_t g_expectedCallerReturn = 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 WriteLog(const char* format, ...)
{
if (!format) return;
char message[2048] = {};
va_list args;
va_start(args, format);
_vsnprintf_s(message, sizeof(message), _TRUNCATE, format, args);
va_end(args);
SYSTEMTIME now = {};
GetLocalTime(&now);
char line[2304] = {};
_snprintf_s(line, sizeof(line), _TRUNCATE,
"%04u-%02u-%02u %02u:%02u:%02u.%03u tid=%lu %s\r\n",
now.wYear, now.wMonth, now.wDay,
now.wHour, now.wMinute, now.wSecond, now.wMilliseconds,
static_cast<unsigned long>(GetCurrentThreadId()), message);
wchar_t path[MAX_PATH] = {};
BuildLogPath(path, _countof(path));
AcquireSRWLockExclusive(&g_logLock);
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) {
DWORD written = 0;
WriteFile(file, line, static_cast<DWORD>(strlen(line)), &written, nullptr);
CloseHandle(file);
}
ReleaseSRWLockExclusive(&g_logLock);
}
__declspec(noinline) void __cdecl LogReloginBypass(
unsigned int requirementId, uintptr_t callerReturn)
{
LONG count = InterlockedIncrement(&g_bypassLogCount);
if (count > 8) return;
WriteLog(
"weapon-relogin-fix bypass hit=%ld requirement_id=%u caller_rva=0x%08X "
"lookup_record=present native_validity=0 action=treat-valid "
"scope=op24-relogin-main-weapon-only",
count, requirementId,
g_exeBase && callerReturn >= g_exeBase
? static_cast<unsigned int>(callerReturn - g_exeBase)
: 0u);
}
// Original sequence at RVA 0x01C07737:
// test eax,eax
// je invalid
// push 1
// mov ecx,eax
// call sub_265F610
// test eax,eax
// jne valid
//
// The caller has already decoded the equipped main weapon's requirement id,
// skipped requirementId == -1, and resolved actor->lookup(requirementId, 1).
// Live evidence proved that op24 can reach this point with a real requirement
// record while sub_265F610 transiently returns 0 before skill initialization is
// fully settled. Preserve every native decision except that timing window.
//
// The standard EBP frame lets us require [EBP+4] == base + 0x01C0859E, the
// observed op24 relogin main-weapon revalidation caller. Therefore normal
// manual equipping, other callers, missing requirement records, and native
// validity successes all keep their original behavior. No skill/profession
// allow list is needed, so the same mechanism covers other exclusive weapons.
__declspec(naked) void ProxyWeaponSkillReloginFix()
{
__asm {
// EAX is actor->lookup(requirementId, 1). A missing record remains
// invalid exactly as in native code.
test eax, eax
jz requirement_invalid
// Preserve the native validity call and accept native success first.
push 1
mov ecx, eax
call dword ptr [g_nativeSkillValidity]
test eax, eax
jnz requirement_valid
// Only repair the known relogin main-weapon revalidation call site.
// EBX still holds the decoded requirement id for logging.
mov edx, dword ptr [ebp + 4]
cmp edx, dword ptr [g_expectedCallerReturn]
jne requirement_invalid
pushfd
pushad
push edx
push ebx
call LogReloginBypass
add esp, 8
popad
popfd
mov eax, 1
jmp dword ptr [g_validResume]
requirement_invalid:
jmp dword ptr [g_invalidResume]
requirement_valid:
jmp dword ptr [g_validResume]
}
}
bool PatchFix(unsigned char* target)
{
if (!target) return false;
unsigned char patch[6] = { 0xE9, 0, 0, 0, 0, 0x90 };
LONG relative = static_cast<LONG>(
reinterpret_cast<uintptr_t>(&ProxyWeaponSkillReloginFix) -
reinterpret_cast<uintptr_t>(target + 5));
*reinterpret_cast<LONG*>(patch + 1) = relative;
DWORD oldProtection = 0;
if (!VirtualProtect(target, sizeof(patch), PAGE_EXECUTE_READWRITE,
&oldProtection)) {
return false;
}
memcpy(target, patch, sizeof(patch));
DWORD ignored = 0;
VirtualProtect(target, sizeof(patch), oldProtection, &ignored);
FlushInstructionCache(GetCurrentProcess(), target, sizeof(patch));
return memcmp(target, patch, sizeof(patch)) == 0;
}
bool RestoreFix(unsigned char* target)
{
if (!target) return false;
DWORD oldProtection = 0;
if (!VirtualProtect(target, sizeof(kExpectedBytes), PAGE_EXECUTE_READWRITE,
&oldProtection)) {
return false;
}
memcpy(target, kExpectedBytes, sizeof(kExpectedBytes));
DWORD ignored = 0;
VirtualProtect(target, sizeof(kExpectedBytes), oldProtection, &ignored);
FlushInstructionCache(GetCurrentProcess(), target, sizeof(kExpectedBytes));
return memcmp(target, kExpectedBytes, sizeof(kExpectedBytes)) == 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) return 0;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kRequirementBranchRva);
g_nativeSkillValidity = reinterpret_cast<void*>(
g_exeBase + kNativeSkillValidityRva);
g_invalidResume = reinterpret_cast<void*>(
g_exeBase + kRequirementInvalidResumeRva);
g_validResume = reinterpret_cast<void*>(
g_exeBase + kRequirementValidResumeRva);
g_expectedCallerReturn = g_exeBase + kReloginEquipmentCallerReturnRva;
__try {
if (memcmp(target, kExpectedBytes, sizeof(kExpectedBytes)) != 0) {
WriteLog(
"weapon-relogin-fix install-failed target_rva=0x%08X "
"observed=%02X%02X%02X%02X%02X%02X expected=740D6A018BC8",
static_cast<unsigned int>(kRequirementBranchRva),
target[0], target[1], target[2], target[3], target[4], target[5]);
return 0;
}
if (!PatchFix(target)) {
WriteLog(
"weapon-relogin-fix install-failed reason=patch-write target_rva=0x%08X",
static_cast<unsigned int>(kRequirementBranchRva));
return 0;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLog("weapon-relogin-fix install exception code=0x%08X",
GetExceptionCode());
return 0;
}
InterlockedExchange(&g_installed, 1);
WriteLog(
"weapon-relogin-fix installed target_rva=0x%08X native_validity_rva=0x%08X "
"caller_rva=0x%08X requirement_scope=any-present-record "
"scope=op24-relogin-main-weapon-only",
static_cast<unsigned int>(kRequirementBranchRva),
static_cast<unsigned int>(kNativeSkillValidityRva),
static_cast<unsigned int>(kReloginEquipmentCallerReturnRva));
return 0;
}
}
void StartWeaponSkillReloginFix(HMODULE module)
{
if (g_worker || !module) return;
g_selfModule = module;
InterlockedExchange(&g_stop, 0);
g_worker = CreateThread(nullptr, 0, InstallWorker, nullptr, 0, nullptr);
if (!g_worker) {
WriteLog("weapon-relogin-fix start failed reason=CreateThread error=%lu",
GetLastError());
}
}
void StopWeaponSkillReloginFix()
{
InterlockedExchange(&g_stop, 1);
if (g_worker) {
WaitForSingleObject(g_worker, 1500);
CloseHandle(g_worker);
g_worker = nullptr;
}
if (InterlockedCompareExchange(&g_installed, 0, 0) != 0 && g_exeBase) {
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kRequirementBranchRva);
bool restored = false;
__try {
restored = RestoreFix(target);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
restored = false;
}
WriteLog("weapon-relogin-fix restore result=%d", restored ? 1 : 0);
InterlockedExchange(&g_installed, 0);
}
}