- 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 - 增幅与强化互斥校验,避免写出客户端无法创建的装备
293 lines
10 KiB
C++
293 lines
10 KiB
C++
// ============================================================================
|
|
// 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());
|
|
}
|