DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+335
View File
@@ -0,0 +1,335 @@
#pragma once
#include "90CN_companion_p0.h"
#include "90CN_companion_projection.h"
#include <windows.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
// DB4 client only. The installer owns exact-image/signature checks. These
// adapters preserve the shared companion parser, queue and lifecycle owner.
namespace dnf90_companion_new_native {
using NewNativeClass0DispatchFn = int (__stdcall*)(int, int);
using NewNativeCaptureLogFn = void (*)(const char*, ...);
static std::uintptr_t g_captureBase = 0;
static std::uintptr_t g_captureResume = 0;
static std::uintptr_t g_captureConsumed = 0;
static NewNativeClass0DispatchFn g_originalClass0Dispatch = nullptr;
static NewNativeCaptureLogFn g_captureLog = nullptr;
static DWORD g_dispatchThread = 0;
static HWND g_safePointWindow = nullptr;
constexpr UINT_PTR kSafePointTimer = 0x90C07F41u;
constexpr std::size_t kCapturePacketCapacity =
dnf90_companion_projection::kPacketHeaderSize +
dnf90_companion_projection::kBodyHeaderSize +
dnf90_companion_projection::kMaxDynamicAicBytes +
2 * dnf90_companion_projection::kMaxProjectionBody;
constexpr std::uintptr_t kReaderStateRva = 0x053F9AD0;
struct NewNativeReaderState {
const unsigned char* cursor;
int remaining;
const unsigned char* packet;
int packetLength;
};
static_assert(sizeof(NewNativeReaderState) == 16, "DB4 x86 native reader ABI");
// SetTimer's callback is dispatched by the window's own message thread. Only
// the native packet thread is allowed to own this timer or call shared APC code.
static void CALLBACK NewNativeSafePointTimer(HWND window, UINT, UINT_PTR id, DWORD) {
if (id != kSafePointTimer || window != g_safePointWindow ||
GetCurrentThreadId() != g_dispatchThread) return;
dnf90_companion_p0::NewNativeSafePoint(-2);
}
static BOOL CALLBACK FindNewNativeThreadWindow(HWND window, LPARAM parameter) {
DWORD process = 0;
const DWORD thread = GetWindowThreadProcessId(window, &process);
if (process != GetCurrentProcessId() || thread != g_dispatchThread ||
!IsWindowVisible(window) || GetWindow(window, GW_OWNER)) return TRUE;
*reinterpret_cast<HWND*>(parameter) = window;
return FALSE;
}
static void EnsureNewNativeSafePointTimer() {
if (!g_dispatchThread) g_dispatchThread = GetCurrentThreadId();
if (GetCurrentThreadId() != g_dispatchThread) return;
if (g_safePointWindow && IsWindow(g_safePointWindow)) return;
HWND window = nullptr;
EnumThreadWindows(g_dispatchThread, FindNewNativeThreadWindow,
reinterpret_cast<LPARAM>(&window));
if (!window) return;
if (SetTimer(window, kSafePointTimer, 100, NewNativeSafePointTimer)) {
g_safePointWindow = window;
if (g_captureLog) g_captureLog(
"new-native companion UI safe-point timer installed window=%p thread=%lu interval_ms=100",
window, g_dispatchThread);
}
}
struct NewNativeTownSnapshot {
const unsigned char* start = nullptr;
std::size_t bodyLength = 0;
std::size_t consumedMinimum = 0;
std::uint16_t localKey = 0;
};
struct NewNativeDungeonInfoSnapshot {
const unsigned char* start = nullptr;
std::size_t bodyLength = 0;
std::uint32_t dungeon = 0;
};
// Current class0 op28 is registered to 01FF5050 at 02001F73. This is a
// per-dungeon notification, while op29 is also used for ordinary room changes.
// Only the former opens a new run; no duplicate companion lifecycle is stored.
static NewNativeDungeonInfoSnapshot SnapshotNewNativeDungeonInfo(
int opcode, const unsigned char* header) {
NewNativeDungeonInfoSnapshot result{};
if (opcode != 28 || !g_captureBase || !header) return result;
__try {
const std::uint32_t total = dnf90_companion_projection::Read32(header + 3);
if (total < 52 || total > kCapturePacketCapacity) return result;
const NewNativeReaderState reader = *reinterpret_cast<const NewNativeReaderState*>(
g_captureBase + kReaderStateRva);
const std::size_t bodyLength = total - 16;
if (!reader.cursor || reader.remaining < 0 ||
static_cast<std::size_t>(reader.remaining) < bodyLength) return result;
const std::uint32_t dungeon = dnf90_companion_projection::Read32(reader.cursor);
if (!dungeon) return result;
result.start = reader.cursor;
result.bodyLength = bodyLength;
result.dungeon = dungeon;
} __except (EXCEPTION_EXECUTE_HANDLER) {
result = {};
}
return result;
}
static bool NewNativeBodyWasConsumed(
const unsigned char* startPointer, std::size_t bodyLength, std::size_t minimum) {
if (!startPointer || !g_captureBase) return false;
__try {
const NewNativeReaderState reader = *reinterpret_cast<const NewNativeReaderState*>(
g_captureBase + kReaderStateRva);
const std::uintptr_t start = reinterpret_cast<std::uintptr_t>(startPointer);
const std::uintptr_t cursor = reinterpret_cast<std::uintptr_t>(reader.cursor);
return reader.remaining >= 0 && cursor >= start + minimum &&
cursor <= start + bodyLength;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
// DB4 class0 initializer 02524320 calls 020460E0, which registers op24 to
// 02042900 at 020461A3. Its row reads at 020432C2..02043312 are 3 WORDs and
// 3 BYTEs (nine bytes, versus eight in the old client). Early-return gates at
// 02042948/02042956 run before any reader consumption.
static NewNativeTownSnapshot SnapshotNewNativeTown(int opcode, const unsigned char* header) {
NewNativeTownSnapshot result{};
if (opcode != 24 || !g_captureBase || !header) return result;
__try {
const std::uint32_t total = dnf90_companion_projection::Read32(header + 3);
if (total < 20 || total > kCapturePacketCapacity) return result;
const NewNativeReaderState reader = *reinterpret_cast<const NewNativeReaderState*>(
g_captureBase + kReaderStateRva);
const std::size_t bodyLength = total - 16;
if (!reader.cursor || reader.remaining < 0 ||
static_cast<std::size_t>(reader.remaining) < bodyLength) return result;
const std::size_t rows = dnf90_companion_projection::Read16(reader.cursor + 2);
if (!rows || rows > (bodyLength - 4) / 9) return result;
const std::uint16_t key = dnf90_companion_projection::Read16(
reader.cursor + 4 + (rows - 1) * 9);
if (!key) return result;
result.start = reader.cursor;
result.bodyLength = bodyLength;
result.consumedMinimum = 4 + rows * 9;
result.localKey = key;
} __except (EXCEPTION_EXECUTE_HANDLER) {
result = {};
}
return result;
}
static bool NewNativeTownWasAccepted(const NewNativeTownSnapshot& snapshot) {
if (!snapshot.start || !snapshot.localKey || !g_captureBase) return false;
__try {
if (!NewNativeBodyWasConsumed(snapshot.start, snapshot.bodyLength,
snapshot.consumedMinimum)) return false;
using CurrentActor = void* (__cdecl*)();
using ActorByKey = void* (__cdecl*)(unsigned short);
void* current = reinterpret_cast<CurrentActor>(g_captureBase + 0x025610E0)();
void* selected = reinterpret_cast<ActorByKey>(g_captureBase + 0x0256D730)(snapshot.localKey);
return current && current == selected;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
static void ConfigureNewNativeCaptureResume(
std::uintptr_t base,
NewNativeCaptureLogFn logger = nullptr)
{
g_captureBase = base;
g_captureResume = base ? base + 0x02123C94 : 0;
g_captureConsumed = base ? base + 0x02123F50 : 0;
g_captureLog = logger;
}
static void SetOriginalNewNativeClass0Dispatch(void* original) {
g_originalClass0Dispatch =
reinterpret_cast<NewNativeClass0DispatchFn>(original);
}
// Called only at VA 02523C8A, after native decoding/decompression and reader
// binding, before admission is enforced. EDI may still be the protected outer
// packet, so its header is copied but plaintext always comes from reader.cursor.
// The native decompressor at 02523820 updates header+3 to decoded body size+16;
// the enclosing router then binds that body size as reader.remaining. Using the
// original socket receive length here would truncate compressed private frames.
// Return true for every recognized private frame, including malformed frames:
// the native router must never index its opcode tables with private opcode 7F41.
static bool __cdecl CaptureNewNativeDecodedProjection(
int sourceContext,
const unsigned char* header)
{
bool privatePacket = false;
bool captured = false;
unsigned char* ownedFrame = nullptr;
std::uint32_t packetLength = 0;
int remaining = -1;
unsigned long exceptionCode = 0;
__try {
do {
if (!g_captureBase || !header || header[0] != 0 ||
dnf90_companion_projection::Read16(header + 1) !=
dnf90_companion_projection::kPrivateOpcode) {
break;
}
privatePacket = true;
packetLength = dnf90_companion_projection::Read32(header + 3);
if (packetLength <
dnf90_companion_projection::kPacketHeaderSize +
dnf90_companion_projection::kBodyHeaderSize ||
packetLength > kCapturePacketCapacity) {
break;
}
const NewNativeReaderState reader =
*reinterpret_cast<const NewNativeReaderState*>(
g_captureBase + kReaderStateRva);
remaining = reader.remaining;
const std::size_t bodyLength = packetLength -
dnf90_companion_projection::kPacketHeaderSize;
if (!reader.cursor || reader.remaining < 0 ||
static_cast<std::size_t>(reader.remaining) < bodyLength) {
break;
}
ownedFrame = static_cast<unsigned char*>(
HeapAlloc(GetProcessHeap(), 0, packetLength));
if (!ownedFrame) break;
std::memcpy(ownedFrame, header,
dnf90_companion_projection::kPacketHeaderSize);
std::memcpy(ownedFrame +
dnf90_companion_projection::kPacketHeaderSize,
reader.cursor, bodyLength);
// Lifecycle work can reenter native readers. The complete private
// frame is owned before calling it, so no borrowed body survives.
dnf90_companion_p0::NewNativeSafePoint(-1);
captured = dnf90_companion_p0::CaptureProjectionClass0Packet(
sourceContext, reinterpret_cast<int>(ownedFrame));
if (captured) {
dnf90_companion_p0::NewNativeSafePoint(
dnf90_companion_projection::kPrivateOpcode);
} else {
dnf90_companion_p0::LogProjectionDecodeRejected(
ownedFrame, packetLength, "new-native-decoded-boundary");
}
} while (false);
} __except (EXCEPTION_EXECUTE_HANDLER) {
exceptionCode = GetExceptionCode();
}
if (ownedFrame) HeapFree(GetProcessHeap(), 0, ownedFrame);
if (privatePacket && g_captureLog) {
g_captureLog("new-native companion decoded capture context=%d length=%u remaining=%d captured=%u exception=0x%08lX",
sourceContext, static_cast<unsigned int>(packetLength), remaining,
captured ? 1u : 0u, exceptionCode);
}
return privatePacket;
}
// The displaced 10 bytes contain no relative instructions:
// 8A 45 A3 mov al,[ebp-5D]
// C7 45 AC 01 00 00 00 mov dword ptr [ebp-54],1
// Resuming at 02523C94 preserves admission for all non-private packets. The
// private branch enters the router's normal consumed epilogue at 02523F50.
// That epilogue writes EAX=1 itself and never reads [EBP-54], so the private
// branch deliberately does not initialize the displaced dispatch-result local.
__declspec(naked) static void ProxyNewNativeProjectionCapture() {
__asm {
pushfd
pushad
push edi
push dword ptr [ebp - 70h]
call CaptureNewNativeDecodedProjection
add esp, 8
test al, al
jnz private_consumed
popad
popfd
mov al, byte ptr [ebp - 5Dh]
mov dword ptr [ebp - 54h], 1
jmp dword ptr [g_captureResume]
private_consumed:
popad
popfd
jmp dword ptr [g_captureConsumed]
}
}
// Current VA 0251E340 has the same two-argument stdcall ABI as the shared
// legacy owner. Its callbacks consume global reader state before this point.
static int __stdcall ProxyNewNativeClass0Dispatch(int sourceContext, int packet) {
NewNativeClass0DispatchFn original = g_originalClass0Dispatch;
if (!original) return 1;
int opcode = -1;
__try {
const unsigned char* bytes =
reinterpret_cast<const unsigned char*>(packet);
if (bytes && bytes[0] == 0) {
opcode = dnf90_companion_projection::Read16(bytes + 1);
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
opcode = -1;
}
const NewNativeTownSnapshot town = SnapshotNewNativeTown(opcode,
reinterpret_cast<const unsigned char*>(packet));
const NewNativeDungeonInfoSnapshot dungeon = SnapshotNewNativeDungeonInfo(opcode,
reinterpret_cast<const unsigned char*>(packet));
const int result = original(sourceContext, packet);
EnsureNewNativeSafePointTimer();
if (NewNativeTownWasAccepted(town)) {
dnf90_companion_p0::CancelFixedApc5405();
if (g_captureLog) g_captureLog(
"new-native companion lifecycle cancelled reason=accepted-town opcode=24 key=%u consumed_min=%zu",
static_cast<unsigned int>(town.localKey), town.consumedMinimum);
return result;
}
if (dungeon.dungeon && NewNativeBodyWasConsumed(dungeon.start, dungeon.bodyLength, 36)) {
dnf90_companion_p0::CancelFixedApc5405();
if (g_captureLog) g_captureLog(
"new-native companion lifecycle reset reason=accepted-dungeon-info opcode=28 dungeon=%u",
static_cast<unsigned int>(dungeon.dungeon));
}
if (opcode >= 0) {
dnf90_companion_p0::NewNativeSafePoint(opcode);
}
return result;
}
} // namespace dnf90_companion_new_native