Files
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

313 lines
11 KiB
C++

#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;
}