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 @@
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,