From e9a3952a9da7d01109a3fc11b9f6cadd02ffc610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=AE=9D=E4=BB=99=E5=B0=8A?= Date: Thu, 5 Nov 2026 12:37:03 +0800 Subject: [PATCH] =?UTF-8?q?GM=20=E9=A1=B5=EF=BC=9A=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=86=85=E5=B5=8C=20IE=20=E8=BE=93=E5=85=A5=E6=B3=95=20+=20?= =?UTF-8?q?=E7=89=A9=E5=93=81=E5=88=86=E7=B1=BB=E6=B5=8F=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 90CN.dll:给 IE 控件窗口链重新关联 IME 上下文,中文输入不再只能打字母 - gmweb:页面加 ime-mode:active,focus 时再主动要一次 - 物品页加分组/类型/等级筛选,不依赖中文输入也能定位物品 - 服务端指纹回绑新 DLL(1223680 / 26633404…) --- client-patch/90CN_web_redirect.cpp | 50 +++++++- gmweb/app.js | 107 ++++++++++++++++-- gmweb/index.html | 25 +++- gmweb/style.css | 7 +- .../services/dnfbridge/client_build_id.go | 4 +- 5 files changed, 176 insertions(+), 17 deletions(-) diff --git a/client-patch/90CN_web_redirect.cpp b/client-patch/90CN_web_redirect.cpp index 3eb2425..056ea51 100644 --- a/client-patch/90CN_web_redirect.cpp +++ b/client-patch/90CN_web_redirect.cpp @@ -25,6 +25,7 @@ // ============================================================================ #include +#include #include #include #include @@ -159,6 +160,40 @@ HWND FindIeServerWindow() 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) { @@ -216,10 +251,20 @@ 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。 @@ -257,7 +302,10 @@ DWORD WINAPI WorkerThread(void*) } } - if (!hwnd) lastHandled = nullptr; // 窗口被销毁后允许下次重新接管 + if (!hwnd) { // 窗口被销毁后允许下次重新接管 + lastHandled = nullptr; + imeHwnd = nullptr; + } for (int waited = 0; waited < 700 && !g_stop; waited += 100) { Sleep(100); } diff --git a/gmweb/app.js b/gmweb/app.js index f11cf58..7880c39 100644 --- a/gmweb/app.js +++ b/gmweb/app.js @@ -257,23 +257,79 @@ log('已选物品:' + it.name + '(id ' + it.item_id + ')', 'tip'); } + /* 分组 -> 类型选项。装备/时装/宠物走 tag(PVF 的 equipment type), + 道具走 segment(服务端按 stackable type 归类成中文段)。 + 有了这两级下拉,不敲键盘也能把范围缩到十几页。 */ + var TYPE_OPTIONS = { + equipment: [ + ['tag:weapon', '武器'], ['tag:coat', '上衣'], ['tag:shoulder', '头肩'], + ['tag:pants', '下装'], ['tag:shoes', '鞋'], ['tag:waist', '腰带'], + ['tag:amulet', '项链'], ['tag:wrist', '手镯'], ['tag:ring', '戒指'], + ['tag:support', '辅助装备'], ['tag:magic stone', '魔法石'], + ['tag:earring', '耳环'], ['tag:support weapon', '辅助武器'], + ['tag:title name', '称号'], ['tag:name tag', '名牌'] + ], + avatar: [ + ['tag:hat avatar', '帽子'], ['tag:hair avatar', '头发'], ['tag:face avatar', '脸部'], + ['tag:coat avatar', '上衣'], ['tag:breast avatar', '胸部'], ['tag:waist avatar', '腰部'], + ['tag:pants avatar', '下装'], ['tag:shoes avatar', '鞋'], ['tag:skin avatar', '皮肤'], + ['tag:aurora avatar', '光环'], ['tag:weapon avatar', '武器装扮'] + ], + pet: [ + ['tag:creature', '宠物'], ['tag:artifact red', '神器·红'], + ['tag:artifact blue', '神器·蓝'], ['tag:artifact green', '神器·绿'] + ], + stackable: [ + ['seg:消耗品', '消耗品'], ['seg:材料', '材料'], ['seg:特殊材料', '特殊材料'], + ['seg:任务品', '任务品'], ['seg:徽章', '徽章'], ['seg:副职业材料', '副职业材料'] + ] + }; + + function fillTypeOptions() { + var opts = TYPE_OPTIONS[$('in-group').value] || []; + var h = ['']; + for (var i = 0; i < opts.length; i++) { + h.push(''); + } + $('in-tag').innerHTML = h.join(''); + } + function loadItems(page) { var q = $('q-item').value; var size = parseInt($('in-page-size').value, 10) || 20; var p = page || 1; if (p < 1) { p = 1; } S.pageSize = size; - var offset = (p - 1) * size; - api('/dnf/gm/items/page?limit=' + size + '&offset=' + offset + - '&q=' + encodeURIComponent(q || ''), 'GET', null, function (d) { - if (!d) { return; } - var pageData = d.data || {}; - S.items = pageData.items || []; - S.total = pageData.total || 0; - S.page = p; - renderItems(); - log('物品 ' + S.items.length + ' 条 / 共 ' + S.total + ' 条'); - }); + + var group = $('in-group').value; + var picked = $('in-tag').value; + var minLv = parseInt($('in-lv-min').value, 10); + var maxLv = parseInt($('in-lv-max').value, 10); + if (isNaN(minLv)) { minLv = 0; } + if (isNaN(maxLv)) { maxLv = 0; } + + var qs = 'limit=' + size + '&offset=' + ((p - 1) * size) + + '&q=' + encodeURIComponent(q || ''); + if (group) { qs += '&group=' + encodeURIComponent(group); } + if (picked) { + if (picked.indexOf('tag:') === 0) { + qs += '&tag=' + encodeURIComponent(picked.substring(4)); + } else if (picked.indexOf('seg:') === 0) { + qs += '&segment=' + encodeURIComponent(picked.substring(4)); + } + } + if (minLv > 0) { qs += '&min_level=' + minLv; } + if (maxLv > 0) { qs += '&max_level=' + maxLv; } + + api('/dnf/gm/items/page?' + qs, 'GET', null, function (d) { + if (!d) { return; } + var pageData = d.data || {}; + S.items = pageData.items || []; + S.total = pageData.total || 0; + S.page = p; + renderItems(); + log('物品 ' + S.items.length + ' 条 / 共 ' + S.total + ' 条'); + }); } /* 组装强化/增幅/锻造/品质:增幅优先,红字装不再叠加白字强化。 */ @@ -368,6 +424,17 @@ if (el) { el.onclick = fn; } } + /* 内嵌 IE 控件的输入法常被游戏主窗口占着,focus 时再主动要一次。 + ime-mode:active 是 IE 私有属性,别的浏览器会忽略,不影响兼容。 */ + function forceIME() { + var els = document.getElementsByTagName('input'); + for (var i = 0; i < els.length; i++) { + els[i].onfocus = function () { + try { this.style.imeMode = 'active'; } catch (e) { } + }; + } + } + function init() { bindTabs(); @@ -389,11 +456,29 @@ bind('btn-mail', sendMail); $('in-page-size').onchange = function () { loadItems(1); }; + $('in-group').onchange = function () { fillTypeOptions(); loadItems(1); }; + $('in-tag').onchange = function () { loadItems(1); }; + + /* 等级快捷档:点一下就把区间填好并重新查,省得手敲。 */ + var lvBtns = document.getElementsByTagName('button'); + for (var b = 0; b < lvBtns.length; b++) { + var spec = lvBtns[b].getAttribute('data-lv'); + if (!spec) { continue; } + lvBtns[b].onclick = function () { + var pair = this.getAttribute('data-lv').split('-'); + $('in-lv-min').value = pair[0]; + $('in-lv-max').value = pair[1]; + loadItems(1); + }; + } $('q-char').onkeydown = function (e) { if ((e || window.event).keyCode === 13) { loadChars(); } }; $('q-item').onkeydown = function (e) { if ((e || window.event).keyCode === 13) { loadItems(1); } }; + fillTypeOptions(); + renderPicked(); + forceIME(); log('GM 控制台已加载', 'tip'); loadStatus(); loadChars(); diff --git a/gmweb/index.html b/gmweb/index.html index caf9c14..c88661f 100644 --- a/gmweb/index.html +++ b/gmweb/index.html @@ -56,7 +56,30 @@
- + + + + +
+
+ + + ~ + + + + + +
+
+