小助手内嵌 GM 页:网页重定向 + 物品分页 + 装备强化增幅发放

- 90CN.dll 新增 IE 控件接管,把腾讯小助手页面重定向到本地 GM 页
- gmweb:物品列表改分页(每页 10/20/50,列表固定 6 行高)
- 发放走 grant-options,支持强化 0-31、增幅属性+等级、锻造 0-8、品质
- 服务端补齐 amplify_level:等级统一写入 ext_data0/packed_flag_byte/reinforce/upgrade_level
- 增幅与强化互斥校验,避免写出客户端无法创建的装备
This commit is contained in:
hanfuit committed 2026-11-05 12:20:46 +08:00
1 parent 6e570fe820
commit 18e98ca7c9
13 files changed
+1206 -11

No files matched your search

+16
View File
@@ -38,6 +38,7 @@
#include <algorithm>
extern "C" void InstallRaidProbe();
extern "C" void InstallWebRedirect();
namespace
{
@@ -15486,6 +15487,9 @@ DWORD WINAPI InstallTransportWorkerInner(void*)
InstallRaidProbe();
LogLine("raid probe integrated normal/single mode compatibility");
// 内嵌网页窗口(小助手/小井盖)跳到本地 GM 页。只起一个轮询线程,不碰游戏逻辑。
InstallWebRedirect();
LogLine("transport DLL installed codec=1 decode=1 route=%d dproto=%d party_ui=%d town_copresence=%d dungeon_pickup=%d creature_rename=%d rare_avatar_ticket=%d premium_state=%d contract_use=%d wait_ms=%d",
enableSceneRouteCompat ? 1 : 0,
enableDprotoCompat ? 1 : 0,
@@ -15512,6 +15516,18 @@ DWORD WINAPI InstallWorker(void*)
}
} // namespace
// 其它翻译单元(如 90CN_web_redirect.cpp)要写同一条 90CN_trace.log,但 LogLine
// 在匿名 namespace 里只有内部链接,所以这里开一个 C 链接的转发出口。
extern "C" void __cdecl CnmLogLine(const char* format, ...)
{
char buffer[1024];
va_list args;
va_start(args, format);
_vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, format, args);
va_end(args);
LogLine("%s", buffer);
}
#pragma comment(linker, "/EXPORT:Queue90CNClientNotice=_Queue90CNClientNotice@8")
#pragma comment(linker, "/EXPORT:Dequeue90CNClientEvent=_Dequeue90CNClientEvent@8")
#pragma comment(linker, "/EXPORT:Query90CNCharacterStatSnapshot=_Query90CNCharacterStatSnapshot@8")
+9
View File
@@ -0,0 +1,9 @@
[web]
; 1 = redirect the embedded web window to the page below, 0 = leave it alone
enabled=1
; target page. Must be plain http on 127.0.0.1 to avoid IE certificate prompts.
url=http://127.0.0.1:18112/
; 1 = if the document navigates away (native tabs), pull it back to url
force=0
; ms to wait after the IE_Server window appears, before navigating
delay=1200
+292
View File
@@ -0,0 +1,292 @@
// ============================================================================
// 90CN_web_redirect.cpp -- 把客户端内嵌网页窗口跳到本地 GM 页面
//
// 背景:DNF 客户端里那个「小井盖 / 小助手」窗口是 IE 内核(窗口类链为
// Browser Example -> Shell DocObject View -> Internet Explorer_Server),
// 地址在运行时拼接,客户端二进制与 PVF 里都没有明文域名,硬改字符串
// 不可行。所以这里不碰原地址,而是等控件创建出来后,直接把文档导航
// 到我们自己的页面。
//
// 做法:起一个后台线程轮询本进程内的 Internet Explorer_Server 窗口;一旦
// 发现新窗口,等原生导航落定,再用 WM_HTML_GETOBJECT + ObjectFromLresult
// 取到 IHTMLDocument2,经 IHTMLWindow2::execScript 执行
// location.replace(...) 完成跳转。同一窗口只跳一次(force=1 时若发现
// 文档又跑到别的域名,会再次拉回)。
//
// 配置:DLL 同目录的 90CN_web.ini
// [web]
// enabled=1
// url=http://127.0.0.1:18112/
// force=0
// delay=1200
// 没有配置文件时用上面的默认值(enabled=1)。
//
// 失败不影响游戏:任何一步取不到对象都只写日志,不会终止进程。
// ============================================================================
#include <windows.h>
#include <ole2.h>
#include <oleacc.h>
#include <mshtml.h>
#include <string>
// 90CN.cpp 里的 LogLine 在匿名 namespace 内(内部链接),所以走它导出的
// C 链接转发口,保证本模块的日志和安装流程写进同一个 90CN_trace.log。
extern "C" void __cdecl CnmLogLine(const char* format, ...);
#define LogLine CnmLogLine
namespace {
const wchar_t kIniName[] = L"90CN_web.ini";
const wchar_t kIeServerClass[] = L"Internet Explorer_Server";
struct WebConfig {
bool enabled;
bool force;
int delayMs;
std::wstring url;
};
WebConfig g_cfg = { true, false, 1200, L"http://127.0.0.1:18112/" };
DWORD g_threadId = 0;
volatile LONG g_stop = 0;
std::wstring WideFromAnsi(const char* s)
{
if (!s || !*s) return std::wstring();
int need = MultiByteToWideChar(CP_UTF8, 0, s, -1, nullptr, 0);
if (need <= 1) {
need = MultiByteToWideChar(CP_ACP, 0, s, -1, nullptr, 0);
if (need <= 1) return std::wstring();
std::wstring out(static_cast<size_t>(need - 1), L'\0');
MultiByteToWideChar(CP_ACP, 0, s, -1, &out[0], need);
return out;
}
std::wstring out(static_cast<size_t>(need - 1), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s, -1, &out[0], need);
return out;
}
std::wstring ModuleDirectory()
{
wchar_t path[MAX_PATH] = {};
HMODULE self = nullptr;
// 用数据地址反查自身模块:函数地址在某些加载方式下解析不到。
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&g_cfg), &self) ||
!self) {
return std::wstring();
}
if (!GetModuleFileNameW(self, path, MAX_PATH)) return std::wstring();
std::wstring full(path);
const size_t slash = full.find_last_of(L"\\/");
return slash == std::wstring::npos ? std::wstring() : full.substr(0, slash + 1);
}
void LoadConfig()
{
const std::wstring dir = ModuleDirectory();
if (dir.empty()) {
LogLine("web redirect: module dir unresolved, using built-in defaults");
return;
}
const std::wstring ini = dir + kIniName;
if (GetFileAttributesW(ini.c_str()) == INVALID_FILE_ATTRIBUTES) {
LogLine("web redirect: %ls not found, defaults enabled=1 url=%ls", ini.c_str(), g_cfg.url.c_str());
return;
}
const UINT enabled = GetPrivateProfileIntW(L"web", L"enabled", 1, ini.c_str());
const UINT force = GetPrivateProfileIntW(L"web", L"force", 0, ini.c_str());
const UINT delay = GetPrivateProfileIntW(L"web", L"delay", 1200, ini.c_str());
wchar_t buf[1024] = {};
GetPrivateProfileStringW(L"web", L"url", L"http://127.0.0.1:18112/", buf,
static_cast<DWORD>(sizeof(buf) / sizeof(buf[0])), ini.c_str());
g_cfg.enabled = enabled != 0;
g_cfg.force = force != 0;
g_cfg.delayMs = delay > 200 ? static_cast<int>(delay) : 200;
if (buf[0]) g_cfg.url = buf;
LogLine("web redirect: config enabled=%d force=%d delay=%d url=%ls",
g_cfg.enabled ? 1 : 0, g_cfg.force ? 1 : 0, g_cfg.delayMs, g_cfg.url.c_str());
}
bool IsLocalUrl(const std::wstring& url)
{
// 只认 "http://127.0.0.1:<port>/" 前缀,用来判断当前文档是否已经在我们的页面上。
if (g_cfg.url.empty() || url.size() < g_cfg.url.size()) return false;
return _wcsnicmp(url.c_str(), g_cfg.url.c_str(), g_cfg.url.size()) == 0;
}
struct FindContext {
DWORD pid;
HWND found;
};
BOOL CALLBACK OnChildWindow(HWND hwnd, LPARAM param)
{
FindContext* ctx = reinterpret_cast<FindContext*>(param);
wchar_t cls[128] = {};
if (GetClassNameW(hwnd, cls, 128) && _wcsicmp(cls, kIeServerClass) == 0) {
DWORD pid = 0;
GetWindowThreadProcessId(hwnd, &pid);
if (pid == ctx->pid) {
ctx->found = hwnd;
return FALSE;
}
}
return TRUE;
}
BOOL CALLBACK OnTopWindow(HWND hwnd, LPARAM param)
{
FindContext* ctx = reinterpret_cast<FindContext*>(param);
DWORD pid = 0;
GetWindowThreadProcessId(hwnd, &pid);
if (pid != ctx->pid) return TRUE;
EnumChildWindows(hwnd, OnChildWindow, param);
return ctx->found ? FALSE : TRUE;
}
HWND FindIeServerWindow()
{
FindContext ctx = { GetCurrentProcessId(), nullptr };
EnumWindows(OnTopWindow, reinterpret_cast<LPARAM>(&ctx));
return ctx.found;
}
// 取文档对象。WM_HTML_GETOBJECT 的返回值经 ObjectFromLresult 还原成接口指针。
IHTMLDocument2* GetDocument(HWND hwnd)
{
static const UINT msg = RegisterWindowMessageW(L"WM_HTML_GETOBJECT");
if (!msg) return nullptr;
DWORD_PTR result = 0;
const LRESULT ok = SendMessageTimeoutW(hwnd, msg, 0, 0,
SMTO_ABORTIFHUNG | SMTO_BLOCK, 4000, &result);
if (!ok || !result) return nullptr;
IHTMLDocument2* doc = nullptr;
if (FAILED(ObjectFromLresult(static_cast<LRESULT>(result), IID_IHTMLDocument2, 0,
reinterpret_cast<void**>(&doc)))) {
return nullptr;
}
return doc;
}
std::wstring DocumentUrl(IHTMLDocument2* doc)
{
BSTR url = nullptr;
std::wstring out;
if (doc && SUCCEEDED(doc->get_URL(&url)) && url) {
out.assign(url, SysStringLen(url));
}
if (url) SysFreeString(url);
return out;
}
bool NavigateDocument(IHTMLDocument2* doc)
{
IHTMLWindow2* win = nullptr;
if (FAILED(doc->get_parentWindow(&win)) || !win) return false;
// 用 location.replace 而不是 href:不往历史里塞记录,避免用户按返回键回到原页。
std::wstring script = L"location.replace('";
script += g_cfg.url;
script += L"')";
BSTR code = SysAllocString(script.c_str());
BSTR lang = SysAllocString(L"JavaScript");
VARIANT ret;
VariantInit(&ret);
HRESULT hr = win->execScript(code, lang, &ret);
VariantClear(&ret);
SysFreeString(code);
SysFreeString(lang);
win->Release();
return SUCCEEDED(hr);
}
DWORD WINAPI WorkerThread(void*)
{
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
HWND lastHandled = nullptr;
for (;;) {
if (InterlockedCompareExchange(&g_stop, 0, 0) != 0) break;
HWND hwnd = FindIeServerWindow();
if (hwnd && hwnd != lastHandled) {
LogLine("web redirect: IE_Server window found hwnd=%p, waiting %dms", (void*)hwnd, g_cfg.delayMs);
// 等原生导航把文档对象建好,太早拿会拿到 about:blank。
for (int waited = 0; waited < g_cfg.delayMs && !g_stop; waited += 100) {
Sleep(100);
}
IHTMLDocument2* doc = GetDocument(hwnd);
if (!doc) {
LogLine("web redirect: document object unavailable hwnd=%p", (void*)hwnd);
lastHandled = hwnd; // 失败也不无限重试同一窗口
} else {
const std::wstring current = DocumentUrl(doc);
LogLine("web redirect: current url=%ls", current.c_str());
if (IsLocalUrl(current)) {
LogLine("web redirect: already on local page, skip");
lastHandled = hwnd;
} else if (NavigateDocument(doc)) {
LogLine("web redirect: navigated to %ls", g_cfg.url.c_str());
lastHandled = hwnd;
} else {
LogLine("web redirect: execScript failed hwnd=%p", (void*)hwnd);
lastHandled = hwnd;
}
doc->Release();
}
} else if (hwnd && hwnd == lastHandled && g_cfg.force) {
// force 模式下防止用户点原生标签页跳走。
IHTMLDocument2* doc = GetDocument(hwnd);
if (doc) {
if (!IsLocalUrl(DocumentUrl(doc))) {
if (NavigateDocument(doc)) LogLine("web redirect: pulled back to %ls", g_cfg.url.c_str());
}
doc->Release();
}
}
if (!hwnd) lastHandled = nullptr; // 窗口被销毁后允许下次重新接管
for (int waited = 0; waited < 700 && !g_stop; waited += 100) {
Sleep(100);
}
}
CoUninitialize();
return 0;
}
} // namespace
extern "C" void InstallWebRedirect()
{
LoadConfig();
if (!g_cfg.enabled) {
LogLine("web redirect: disabled by config");
return;
}
if (g_cfg.url.find(L'\'') != std::wstring::npos) {
LogLine("web redirect: url contains a quote, refused");
return;
}
InterlockedExchange(&g_stop, 0);
HANDLE thread = CreateThread(nullptr, 0, WorkerThread, nullptr, 0, &g_threadId);
if (!thread) {
LogLine("web redirect: CreateThread failed err=%u", GetLastError());
return;
}
CloseHandle(thread);
LogLine("web redirect: worker started tid=%u target=%ls", g_threadId, g_cfg.url.c_str());
}