服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
163 lines
5.2 KiB
C++
163 lines
5.2 KiB
C++
#include "90CN_op3_parent_fix.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <wchar.h>
|
|
|
|
namespace
|
|
{
|
|
constexpr uintptr_t kOp3ParentCallRva = 0x01988B9F; // 01D88B9F
|
|
constexpr uintptr_t kParentFunctionRva = 0x021B69E0; // 025B69E0
|
|
|
|
HMODULE g_selfModule = nullptr;
|
|
uintptr_t g_exeBase = 0;
|
|
uintptr_t g_parentFunction = 0;
|
|
uintptr_t g_continueAfterParentCall = 0;
|
|
volatile LONG g_bypassSequence = 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);
|
|
}
|
|
|
|
void __stdcall RecordNullBypass(uintptr_t actor)
|
|
{
|
|
LONG sequence = InterlockedIncrement(&g_bypassSequence);
|
|
// Prevent pathological log growth while retaining enough evidence for support.
|
|
if (sequence > 64) return;
|
|
|
|
SYSTEMTIME now = {};
|
|
GetLocalTime(&now);
|
|
char line[512] = {};
|
|
sprintf_s(line,
|
|
"op3-parent-readiness-fix seq=%ld time=%04u-%02u-%02u %02u:%02u:%02u.%03u "
|
|
"tid=%lu actor=%08X action=bypass-null-parent continue=01D88BA4\r\n",
|
|
sequence,
|
|
now.wYear, now.wMonth, now.wDay,
|
|
now.wHour, now.wMinute, now.wSecond, now.wMilliseconds,
|
|
GetCurrentThreadId(), static_cast<unsigned int>(actor));
|
|
WriteLogLine(line);
|
|
}
|
|
|
|
// Replaces only the single CALL at 01D88B9F.
|
|
// Normal path is untouched: restore incoming EFLAGS and tail-jump to 025B69E0.
|
|
// Null path skips the known-invalid call, removes CALL return address + 2 args
|
|
// (12 bytes), and resumes at the original continuation 01D88BA4.
|
|
__declspec(naked) void ProxyOp3ParentCall()
|
|
{
|
|
__asm {
|
|
pushfd
|
|
test ecx, ecx
|
|
jz parent_is_null
|
|
popfd
|
|
jmp dword ptr [g_parentFunction]
|
|
|
|
parent_is_null:
|
|
popfd
|
|
pushfd
|
|
pushad
|
|
push esi
|
|
call RecordNullBypass
|
|
popad
|
|
popfd
|
|
add esp, 0Ch
|
|
jmp dword ptr [g_continueAfterParentCall]
|
|
}
|
|
}
|
|
|
|
DWORD WINAPI InstallOp3ParentReadinessFix(LPVOID)
|
|
{
|
|
HMODULE executable = GetModuleHandleW(nullptr);
|
|
g_exeBase = reinterpret_cast<uintptr_t>(executable);
|
|
if (!g_exeBase) return 0;
|
|
|
|
unsigned char* callSite = reinterpret_cast<unsigned char*>(
|
|
g_exeBase + kOp3ParentCallRva);
|
|
g_parentFunction = g_exeBase + kParentFunctionRva;
|
|
g_continueAfterParentCall = reinterpret_cast<uintptr_t>(callSite + 5);
|
|
|
|
bool verified = false;
|
|
uintptr_t observedTarget = 0;
|
|
__try {
|
|
if (callSite[0] == 0xE8) {
|
|
LONG currentRelative = *reinterpret_cast<LONG*>(callSite + 1);
|
|
observedTarget = reinterpret_cast<uintptr_t>(callSite + 5) + currentRelative;
|
|
verified = observedTarget == g_parentFunction;
|
|
}
|
|
}
|
|
__except (EXCEPTION_EXECUTE_HANDLER) {
|
|
verified = false;
|
|
}
|
|
|
|
if (!verified) {
|
|
char line[512] = {};
|
|
sprintf_s(line,
|
|
"op3-parent-readiness-fix install_failed site=%08X opcode=%02X observed=%08X expected=%08X\r\n",
|
|
static_cast<unsigned int>(reinterpret_cast<uintptr_t>(callSite)),
|
|
static_cast<unsigned int>(callSite[0]),
|
|
static_cast<unsigned int>(observedTarget),
|
|
static_cast<unsigned int>(g_parentFunction));
|
|
WriteLogLine(line);
|
|
return 0;
|
|
}
|
|
|
|
const uintptr_t replacement = reinterpret_cast<uintptr_t>(&ProxyOp3ParentCall);
|
|
LONG replacementRelative = static_cast<LONG>(
|
|
replacement - reinterpret_cast<uintptr_t>(callSite + 5));
|
|
|
|
DWORD oldProtection = 0;
|
|
if (!VirtualProtect(callSite, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) {
|
|
WriteLogLine("op3-parent-readiness-fix install_failed reason=VirtualProtect\r\n");
|
|
return 0;
|
|
}
|
|
|
|
*reinterpret_cast<LONG*>(callSite + 1) = replacementRelative;
|
|
DWORD ignoredProtection = 0;
|
|
VirtualProtect(callSite, 5, oldProtection, &ignoredProtection);
|
|
FlushInstructionCache(GetCurrentProcess(), callSite, 5);
|
|
|
|
WriteLogLine("op3-parent-readiness-fix installed site=01D88B9F native=025B69E0 continue=01D88BA4\r\n");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void StartOp3ParentReadinessFix(HMODULE module)
|
|
{
|
|
g_selfModule = module;
|
|
HANDLE worker = CreateThread(nullptr, 0, InstallOp3ParentReadinessFix, nullptr, 0, nullptr);
|
|
if (worker) CloseHandle(worker);
|
|
}
|