Files
90CNServer/client-patch/90CN_web_redirect.cpp
T
hanfuit e9a3952a9d GM 页:修复内嵌 IE 输入法 + 物品分类浏览
- 90CN.dll:给 IE 控件窗口链重新关联 IME 上下文,中文输入不再只能打字母
- gmweb:页面加 ime-mode:active,focus 时再主动要一次
- 物品页加分组/类型/等级筛选,不依赖中文输入也能定位物品
- 服务端指纹回绑新 DLL(1223680 / 26633404…)
2026-11-05 12:37:03 +08:00

341 lines
12 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 <imm.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;
}
// ---------------------------------------------------------------- 输入法
//
// 症状:页面里的输入框只能打字母,切不到中文。
// 原因:IME 上下文是窗口级的,DNF 主窗口把它握在自己手里,内嵌 IE 控件
// 这条窗口链上没有上下文,输入法候选项压根不会出现。
// 做法:给控件窗口及其子窗口重新关联线程默认 IME 上下文。这个调用是幂等
// 的,每次轮询顺手做一遍,防止游戏在切图/进出聊天时把它重置掉。
bool AttachIme(HWND hwnd)
{
if (!hwnd) return false;
if (ImmAssociateContextEx(hwnd, nullptr, IACE_DEFAULT | IACE_CHILDREN)) return true;
// 退化路径:某些环境不给默认上下文,那就自己建一个挂上去。
HIMC context = ImmCreateContext();
if (!context) return false;
if (ImmAssociateContext(hwnd, context)) return true;
ImmDestroyContext(context);
return false;
}
bool EnsureImeChain(HWND hwnd)
{
if (!hwnd) return false;
// 只给 IE_Server 挂还不够:焦点落在父窗口(Browser Example)时,
// 输入法又会被游戏主窗口抢回去,所以父链一起处理。
HWND chain[3] = { hwnd, GetParent(hwnd), nullptr };
if (chain[1]) chain[2] = GetParent(chain[1]);
bool any = false;
for (int i = 0; i < 3; i++) {
if (chain[i] && AttachIme(chain[i])) any = true;
}
return any;
}
// 取文档对象。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;
HWND imeHwnd = nullptr;
for (;;) {
if (InterlockedCompareExchange(&g_stop, 0, 0) != 0) break;
HWND hwnd = FindIeServerWindow();
if (hwnd) {
// 每轮都重挂一次输入法:游戏在切图、进出聊天时会把上下文收回去。
// 日志只在窗口变化时写,避免把 trace 刷满。
const bool imeOk = EnsureImeChain(hwnd);
if (hwnd != imeHwnd) {
LogLine("web ime: attach %s hwnd=%p", imeOk ? "ok" : "failed", (void*)hwnd);
imeHwnd = hwnd;
}
}
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;
imeHwnd = 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());
}