服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
294 lines
9.8 KiB
C++
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);
|
|
}
|
|
}
|