GM 页:修复内嵌 IE 输入法 + 物品分类浏览

- 90CN.dll:给 IE 控件窗口链重新关联 IME 上下文,中文输入不再只能打字母
- gmweb:页面加 ime-mode:active,focus 时再主动要一次
- 物品页加分组/类型/等级筛选,不依赖中文输入也能定位物品
- 服务端指纹回绑新 DLL(1223680 / 26633404…)
This commit is contained in:
hanfuit committed 2026-11-05 12:37:03 +08:00
1 parent c4ffa57bf2
commit e9a3952a9d
5 files changed
+168 -9

No files matched your search

+49 -1
View File
@@ -25,6 +25,7 @@
// ============================================================================ // ============================================================================
#include <windows.h> #include <windows.h>
#include <imm.h>
#include <ole2.h> #include <ole2.h>
#include <oleacc.h> #include <oleacc.h>
#include <mshtml.h> #include <mshtml.h>
@@ -159,6 +160,40 @@ HWND FindIeServerWindow()
return ctx.found; 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 还原成接口指针。 // 取文档对象。WM_HTML_GETOBJECT 的返回值经 ObjectFromLresult 还原成接口指针。
IHTMLDocument2* GetDocument(HWND hwnd) IHTMLDocument2* GetDocument(HWND hwnd)
{ {
@@ -216,10 +251,20 @@ DWORD WINAPI WorkerThread(void*)
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
HWND lastHandled = nullptr; HWND lastHandled = nullptr;
HWND imeHwnd = nullptr;
for (;;) { for (;;) {
if (InterlockedCompareExchange(&g_stop, 0, 0) != 0) break; if (InterlockedCompareExchange(&g_stop, 0, 0) != 0) break;
HWND hwnd = FindIeServerWindow(); 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) { if (hwnd && hwnd != lastHandled) {
LogLine("web redirect: IE_Server window found hwnd=%p, waiting %dms", (void*)hwnd, g_cfg.delayMs); LogLine("web redirect: IE_Server window found hwnd=%p, waiting %dms", (void*)hwnd, g_cfg.delayMs);
// 等原生导航把文档对象建好,太早拿会拿到 about:blank。 // 等原生导航把文档对象建好,太早拿会拿到 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) { for (int waited = 0; waited < 700 && !g_stop; waited += 100) {
Sleep(100); Sleep(100);
} }
+88 -3
View File
@@ -257,15 +257,71 @@
log('已选物品:' + it.name + '(id ' + it.item_id + ')', 'tip'); 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 = ['<option value="">不限</option>'];
for (var i = 0; i < opts.length; i++) {
h.push('<option value="' + esc(opts[i][0]) + '">' + esc(opts[i][1]) + '</option>');
}
$('in-tag').innerHTML = h.join('');
}
function loadItems(page) { function loadItems(page) {
var q = $('q-item').value; var q = $('q-item').value;
var size = parseInt($('in-page-size').value, 10) || 20; var size = parseInt($('in-page-size').value, 10) || 20;
var p = page || 1; var p = page || 1;
if (p < 1) { p = 1; } if (p < 1) { p = 1; }
S.pageSize = size; S.pageSize = size;
var offset = (p - 1) * size;
api('/dnf/gm/items/page?limit=' + size + '&offset=' + offset + var group = $('in-group').value;
'&q=' + encodeURIComponent(q || ''), 'GET', null, function (d) { 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; } if (!d) { return; }
var pageData = d.data || {}; var pageData = d.data || {};
S.items = pageData.items || []; S.items = pageData.items || [];
@@ -368,6 +424,17 @@
if (el) { el.onclick = fn; } 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() { function init() {
bindTabs(); bindTabs();
@@ -389,11 +456,29 @@
bind('btn-mail', sendMail); bind('btn-mail', sendMail);
$('in-page-size').onchange = function () { loadItems(1); }; $('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-char').onkeydown = function (e) { if ((e || window.event).keyCode === 13) { loadChars(); } };
$('q-item').onkeydown = function (e) { if ((e || window.event).keyCode === 13) { loadItems(1); } }; $('q-item').onkeydown = function (e) { if ((e || window.event).keyCode === 13) { loadItems(1); } };
fillTypeOptions();
renderPicked(); renderPicked();
forceIME();
log('GM 控制台已加载', 'tip'); log('GM 控制台已加载', 'tip');
loadStatus(); loadStatus();
loadChars(); loadChars();
+24 -1
View File
@@ -56,7 +56,30 @@
<div class="pane" id="pane-item"> <div class="pane" id="pane-item">
<div class="row"> <div class="row">
<input id="q-item" class="inp" placeholder="物品名关键字,如 太刀 / 强化"> <label class="lb">分组</label>
<select id="in-group" class="inp sm">
<option value="">全部</option>
<option value="equipment">装备</option>
<option value="avatar">时装</option>
<option value="pet">宠物 / 神器</option>
<option value="stackable">道具</option>
<option value="other">其他</option>
</select>
<label class="lb">类型</label>
<select id="in-tag" class="inp sm"><option value="">不限</option></select>
</div>
<div class="row">
<label class="lb">等级</label>
<input id="in-lv-min" class="inp xs" value="">
<span class="dim">~</span>
<input id="in-lv-max" class="inp xs" value="">
<button class="btn sm" data-lv="1-20">1-20</button>
<button class="btn sm" data-lv="21-40">21-40</button>
<button class="btn sm" data-lv="41-60">41-60</button>
<button class="btn sm" data-lv="61-90">61-90</button>
</div>
<div class="row">
<input id="q-item" class="inp" placeholder="物品名关键字(可留空,用上面分类浏览)">
<select id="in-page-size" class="inp sm"> <select id="in-page-size" class="inp sm">
<option value="10">每页 10 行</option> <option value="10">每页 10 行</option>
<option value="20" selected>每页 20 行</option> <option value="20" selected>每页 20 行</option>
+5 -2
View File
@@ -48,8 +48,11 @@ table.kv td { padding: 4px 0; border-bottom: 1px solid #232734; color: #dfe3ea;
background: #0f1116; border: 1px solid #2f3547; color: #dfe3ea; background: #0f1116; border: 1px solid #2f3547; color: #dfe3ea;
padding: 4px 6px; font: 12px "Microsoft YaHei", Tahoma, sans-serif; padding: 4px 6px; font: 12px "Microsoft YaHei", Tahoma, sans-serif;
width: 240px; outline: none; width: 240px; outline: none;
/* 内嵌 IE 控件里输入法默认是关的,这里强制激活,否则只能打英文 */
ime-mode: active;
} }
.inp:focus { border-color: #e8c979; } .inp:focus { border-color: #e8c979; ime-mode: active; }
input[type="text"], textarea { ime-mode: active; }
.inp.sm { width: 110px; } .inp.sm { width: 110px; }
.inp.xs { width: 60px; } .inp.xs { width: 60px; }
.inp.xs2 { width: 84px; } .inp.xs2 { width: 84px; }
@@ -83,7 +86,7 @@ select.inp { padding: 3px 2px; }
.empty { padding: 12px; color: #666e80; text-align: center; } .empty { padding: 12px; color: #666e80; text-align: center; }
.dim { color: #7c8496; } .dim { color: #7c8496; }
.pager { margin: 6px 0 2px; } .pager { margin: 6px 0 2px; }
.pager .btn.sm { padding: 3px 10px; } .btn.sm { padding: 3px 10px; font-size: 11px; }
.pg { color: #8d94a6; font-size: 11px; margin: 0 8px; } .pg { color: #8d94a6; font-size: 11px; margin: 0 8px; }
#log { #log {
@@ -10,8 +10,8 @@ const (
boundMovieSHA256 = "D62E3623C39CD9FCED5F6C4C7F5E6E18BEB69EB269ABF5D713EDE08EE8C926A1" boundMovieSHA256 = "D62E3623C39CD9FCED5F6C4C7F5E6E18BEB69EB269ABF5D713EDE08EE8C926A1"
boundMovieBytes = int64(22846532) boundMovieBytes = int64(22846532)
boundDllSHA256 = "E947A0C2E36EA24C3F2F557E7D3207C76F9B6E63192E2555B6D4B73593EE17F1" boundDllSHA256 = "266334040E714A429F36AB4CB69BB6824F2D7DD39AAB97D07AFBCF73545E5640"
boundDllBytes = int64(1223168) boundDllBytes = int64(1223680)
) )