DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,312 @@
|
||||
#include <windows.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <wchar.h>
|
||||
#include <string.h>
|
||||
#include <intrin.h>
|
||||
#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<DWORD>(length), &ignored, nullptr);
|
||||
CloseHandle(file);
|
||||
}
|
||||
|
||||
int __fastcall ProxyPvpReadyElement(void* element, void*)
|
||||
{
|
||||
uintptr_t executable = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
|
||||
uintptr_t caller = reinterpret_cast<uintptr_t>(_ReturnAddress());
|
||||
uintptr_t callerRva = caller >= executable ? caller - executable : 0;
|
||||
unsigned int type = 0;
|
||||
|
||||
__try {
|
||||
type = *reinterpret_cast<unsigned int*>(
|
||||
reinterpret_cast<unsigned char*>(element) + 8);
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
PvpTraceLine(
|
||||
"pvp ready element compatibility read failed caller_rva=0x%08X element=%p",
|
||||
static_cast<unsigned int>(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<unsigned int>(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<unsigned char*>(executable);
|
||||
unsigned char* target = base + kPvpReadyElementPredicateRva;
|
||||
unsigned char* whitelist = base + kPvpReadyTypeWhitelistRva;
|
||||
const uintptr_t replacement = reinterpret_cast<uintptr_t>(
|
||||
&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<uintptr_t>(target),
|
||||
reinterpret_cast<uintptr_t>(whitelist));
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_pvpReadyTypePredicate = reinterpret_cast<PvpReadyTypePredicateFn>(
|
||||
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<LONG*>(target + 1) = static_cast<LONG>(
|
||||
replacement - reinterpret_cast<uintptr_t>(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<uintptr_t>(target),
|
||||
reinterpret_cast<uintptr_t>(whitelist),
|
||||
static_cast<unsigned int>(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<unsigned char*>(executable) +
|
||||
kDprotoRoutePredicateRva;
|
||||
const uintptr_t replacement = reinterpret_cast<uintptr_t>(
|
||||
&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<LONG*>(target + 1);
|
||||
unsigned char* previous = target + 5 + previousRelative;
|
||||
if (reinterpret_cast<uintptr_t>(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<LONG>(
|
||||
replacement - reinterpret_cast<uintptr_t>(target + 5));
|
||||
|
||||
DWORD oldProtection = 0;
|
||||
if (!VirtualProtect(target, 6, PAGE_EXECUTE_READWRITE,
|
||||
&oldProtection)) {
|
||||
return 0;
|
||||
}
|
||||
target[0] = 0xE9;
|
||||
*reinterpret_cast<LONG*>(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<DWORD>(_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;
|
||||
}
|
||||
Reference in new issue
Block a user