From 1e936bfb844c78a60f42a6be0be7ec58e32bc264 Mon Sep 17 00:00:00 2001 From: 90CN <90cn@local> Date: Thu, 5 Nov 2026 21:08:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E9=94=AE=E6=8D=A2=E8=A3=85=E5=86=85?= =?UTF-8?q?=E5=B5=8C=20gmweb=EF=BC=9A=E5=8F=96=E6=B6=88=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=20exe=20=E5=90=AF=E5=8A=A8=E5=B9=B6=E6=94=AF=E6=8C=81=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=83=AD=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gmweb/index.html | 10 ++++ gmweb/loadout.js | 133 +++++++++++++++++++++++++++++++++++++++++++++ launcher/server.py | 29 ++++------ 3 files changed, 153 insertions(+), 19 deletions(-) diff --git a/gmweb/index.html b/gmweb/index.html index e9a02c4..ca74af9 100644 --- a/gmweb/index.html +++ b/gmweb/index.html @@ -151,6 +151,11 @@
+
+ + + +
保存后会给当前角色创建一套换装方案。
@@ -158,6 +163,11 @@
方案详情
未选择方案
+
+ + + +
diff --git a/gmweb/loadout.js b/gmweb/loadout.js index a697118..7bbff74 100644 --- a/gmweb/loadout.js +++ b/gmweb/loadout.js @@ -200,10 +200,14 @@ if (!sc) { detail.innerHTML = '未选择方案'; slotsBox.innerHTML = ''; + var emptyHotkey = $('lo-hotkey-text'); + if (emptyHotkey) { emptyHotkey.innerHTML = ''; } return; } detail.innerHTML = '' + esc(sc.Name || '') + '' + '' + esc(sc.CharacterName || '未绑定角色') + ''; + var hotkeyTextEl = $('lo-hotkey-text'); + if (hotkeyTextEl) { hotkeyTextEl.innerHTML = '快捷键:' + esc(hotkeyText(sc)); } var slots = sc.Slots || []; if (!slots.length) { slotsBox.innerHTML = '
该方案没有保存装备
'; @@ -217,6 +221,123 @@ slotsBox.innerHTML = h.join(''); } + function hotkeyText(sc) { + if (!sc || !sc.KeyCode) { return '未设置'; } + var parts = modText(sc.Modifiers || 0); + parts.push(keyText(sc.KeyCode)); + return parts.join('+'); + } + + function currentScheme() { + var page = currentPage(); + var schemes = (page && page.Schemes) || []; + return schemes[S.scheme] || null; + } + + function setHotkey() { + var sc = currentScheme(); + if (!sc) { msg('请先选择方案'); return; } + S.capture = true; + msg('请按下快捷键,Esc 取消'); + var handler = function (ev) { + ev = ev || window.event; + var code = ev.keyCode || ev.which; + if (code === 27) { + S.capture = false; + document.removeEventListener('keydown', handler, true); + msg('已取消设置快捷键'); + return false; + } + if (!code || code === 16 || code === 17 || code === 18) { + return false; + } + var mods = 0; + if (ev.ctrlKey) { mods |= 2; } + if (ev.altKey) { mods |= 1; } + if (ev.shiftKey) { mods |= 4; } + sc.KeyCode = code; + sc.Modifiers = mods; + S.capture = false; + document.removeEventListener('keydown', handler, true); + saveConfig(function (ok) { + if (ok) { + renderDetail(); + loadHotkeys(); + msg('快捷键已保存:' + hotkeyText(sc)); + } + }); + if (ev.preventDefault) { ev.preventDefault(); } + return false; + }; + document.addEventListener('keydown', handler, true); + } + + function clearHotkey() { + var sc = currentScheme(); + if (!sc) { msg('请先选择方案'); return; } + sc.KeyCode = 0; + sc.Modifiers = 0; + saveConfig(function (ok) { + if (ok) { + renderDetail(); + loadHotkeys(); + msg('快捷键已清除'); + } + }); + } + + function addScheme() { + var page = currentPage(); + if (!page) { return; } + var name = window.prompt('方案名称', '方案' + ((page.Schemes || []).length + 1)); + if (!name) { return; } + var character = window.prompt('绑定角色游戏内名称', S.charName || '') || ''; + page.Schemes = page.Schemes || []; + page.Schemes.push({ + Name: String(name), + CharacterName: String(character), + Modifiers: 0, + KeyCode: 0, + Slots: [] + }); + S.scheme = page.Schemes.length - 1; + saveConfig(function (ok) { + if (ok) { + renderSchemes(); + msg('已新增方案:' + name); + } + }); + } + + function renameScheme() { + var sc = currentScheme(); + if (!sc) { msg('请先选择方案'); return; } + var name = window.prompt('重命名方案', sc.Name || ''); + if (!name) { return; } + sc.Name = String(name); + saveConfig(function (ok) { + if (ok) { + renderSchemes(); + msg('方案已重命名'); + } + }); + } + + function deleteScheme() { + var page = currentPage(); + var sc = currentScheme(); + if (!page || !sc) { msg('请先选择方案'); return; } + if (!window.confirm('删除方案“' + (sc.Name || '') + '”?')) { return; } + page.Schemes.splice(S.scheme, 1); + S.scheme = -1; + saveConfig(function (ok) { + if (ok) { + renderSchemes(); + msg('方案已删除'); + } + }); + } + function loadChars() { api('/loadout/characters', 'GET', null, function (d) { if (!d) { msg('角色读取失败,确认服务端已启动'); return; } @@ -347,8 +468,15 @@ sel.onchange = function () { S.page = parseInt(this.value, 10) || 0; S.scheme = -1; + if (S.cfg) { S.cfg.ActivePage = S.page; } renderSchemes(); loadHotkeys(); + // The launcher hotkey worker reads ActivePage from config.json. Persist + // the page switch here, otherwise a hotkey assigned on this page never + // becomes the active page's hotkey. + saveConfig(function (ok) { + if (ok) { loadHotkeys(); } + }); }; } var ch = $('lo-char'); @@ -360,6 +488,11 @@ if ((b = $('lo-page-add'))) { b.onclick = addPage; } if ((b = $('lo-page-ren'))) { b.onclick = renamePage; } if ((b = $('lo-page-del'))) { b.onclick = deletePage; } + if ((b = $('lo-scheme-add'))) { b.onclick = addScheme; } + if ((b = $('lo-scheme-ren'))) { b.onclick = renameScheme; } + if ((b = $('lo-scheme-del'))) { b.onclick = deleteScheme; } + if ((b = $('lo-hotkey'))) { b.onclick = setHotkey; } + if ((b = $('lo-hotkey-clear'))) { b.onclick = clearHotkey; } } function loadTab() { diff --git a/launcher/server.py b/launcher/server.py index 98f0a32..c9d22e8 100644 --- a/launcher/server.py +++ b/launcher/server.py @@ -134,9 +134,6 @@ PVF_PATH = os.path.join(CLIENT_DIR, "Script.pvf") # 一键换装工具(独立工具,放在服务端根目录的 LoadoutTool 文件夹下) LOADOUT_DIR = os.path.join(SERVER_ROOT, "LoadoutTool") -LOADOUT_EXE = os.path.join(LOADOUT_DIR, "换装工具.exe") -if not os.path.exists(LOADOUT_EXE): - LOADOUT_EXE = os.path.join(LOADOUT_DIR, "DNFLoadout.exe") # 一键换装的持久化配置路径 LOADOUT_CONFIG = os.path.join(LOADOUT_DIR, "config.json") @@ -767,25 +764,19 @@ def flow_stop_server(): def flow_launch_loadout(): - """一键换装:启动 DNFLoadout.exe。""" - log("== 一键换装 ==") - if not os.path.exists(LOADOUT_EXE): - log("[错误] 找不到换装工具:%s" % LOADOUT_EXE) - return + """One-click loadout: open the embedded gmweb loadout page.""" + log("== One-click loadout ==") if not server_running(): - log("[提示] 服务端未运行,换装工具可能无法连接。建议先「开始游戏」。") - log("正在启动换装工具…") + log("[hint] server is not running; loadout may not read characters/equipment") + url = "http://127.0.0.1:%d/#loadout" % GM_WEB_PORT + log("opening loadout page: %s" % url) try: - subprocess.Popen( - [LOADOUT_EXE], cwd=LOADOUT_DIR, - creationflags=DETACHED_PROCESS | CREATE_NO_WINDOW, - close_fds=True) - log("换装工具已启动。") + webbrowser.open(url) + log("loadout page opened; the in-game GM window has the same tab") except Exception as e: - log("[错误] 启动失败:%s" % repr(e)) + log("[error] open failed: %s" % repr(e)) - -# ------------------------------------------------------------------ 数据库 / Native 客户端初始化 +# ------------------------------------------------------------------ Database / Native client initialization def load_instance(path): if not path or not os.path.exists(path): return {} @@ -1074,7 +1065,7 @@ class Handler(BaseHTTPRequestHandler): "client_dir_exists": os.path.isdir(client_dir), "control_exists": os.path.exists(CONTROL), "sponsor_exists": os.path.exists(SPONSOR_IMG), - "loadout_exists": os.path.exists(LOADOUT_EXE), + "loadout_exists": True, "db": DB_PATH, "db_exists": bool(DB_PATH) and os.path.exists(DB_PATH), "native_available": NATIVE is not None,