diff --git a/app.py b/app.py index 78ad8a9..8b83b18 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,7 @@ import os from contextlib import asynccontextmanager from pathlib import Path -from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, FileResponse diff --git a/config/settings.py b/config/settings.py index 6bc0a2c..6ecbed8 100644 --- a/config/settings.py +++ b/config/settings.py @@ -17,6 +17,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "theme": "dark", "sidebar_collapsed": False, "telemetry_port": 20777, + "use_unified_port": True, "telemetry_host": "0.0.0.0", "server_host": "0.0.0.0", "server_port": 9527, diff --git a/server/game_manager.py b/server/game_manager.py index 613ca56..487b0cb 100644 --- a/server/game_manager.py +++ b/server/game_manager.py @@ -130,6 +130,20 @@ class GamePluginManager: self._discover() self._parser_cache.clear() + def remove_plugin(self, plugin_id: str) -> bool: + self._discover() + gp = self._plugins.get(plugin_id) + if gp and gp.is_builtin: + logger.warning("Cannot remove builtin plugin: %s", plugin_id) + return False + dest_dir = USER_DIR / plugin_id + if dest_dir.exists(): + shutil.rmtree(dest_dir) + self._discover() + logger.info("Plugin removed: %s", plugin_id) + return True + return False + def install_plugin_zip(self, zip_data: bytes) -> bool: try: with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf: diff --git a/server/telemetry/listener.py b/server/telemetry/listener.py index 8ad1a82..4ae52b7 100644 --- a/server/telemetry/listener.py +++ b/server/telemetry/listener.py @@ -115,6 +115,9 @@ class TelemetryListener: if self._transport: self._transport.close() self._transport = None + if self.forwarder._sock: + self.forwarder._sock.close() + self.forwarder._sock = None logger.info("Telemetry listener stopped") def _get_parser(self, key: str) -> BaseParser: diff --git a/static/js/api.js b/static/js/api.js index b141d73..afa576f 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -1,33 +1,24 @@ const API = { _base: '/api', - async get(path) { - const res = await fetch(`${this._base}${path}`); + async _req(method, path, body) { + const opts = { method }; + if (body) { + opts.headers = { 'Content-Type': 'application/json' }; + opts.body = JSON.stringify(body); + } + const res = await fetch(`${this._base}${path}`, opts); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`${method} ${path} ${res.status}: ${text.slice(0, 200)}`); + } return res.json(); }, - async post(path, data) { - const res = await fetch(`${this._base}${path}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); - return res.json(); - }, - - async put(path, data) { - const res = await fetch(`${this._base}${path}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); - return res.json(); - }, - - async del(path) { - const res = await fetch(`${this._base}${path}`, { method: 'DELETE' }); - return res.json(); - }, + async get(path) { return this._req('GET', path); }, + async post(path, data) { return this._req('POST', path, data); }, + async put(path, data) { return this._req('PUT', path, data); }, + async del(path) { return this._req('DELETE', path); }, async getStatus() { return this.get('/status'); }, async getConfig() { return this.get('/config'); }, diff --git a/static/js/components/sidebar.js b/static/js/components/sidebar.js index 9304094..f008f39 100644 --- a/static/js/components/sidebar.js +++ b/static/js/components/sidebar.js @@ -3,6 +3,7 @@ const Sidebar = { _gameListEl: null, _gameToggleEl: null, _gameLabelEl: null, + _gameData: [], init() { this._el = document.getElementById('sidebar'); @@ -44,6 +45,7 @@ const Sidebar = { async _loadGames() { const games = await API.getGames(); + this._gameData = games; this._gameListEl.innerHTML = ''; if (!Array.isArray(games) || games.length === 0) { @@ -80,11 +82,17 @@ const Sidebar = { async _selectGame(gameId) { await API.updateConfig({ selected_game_id: gameId }); + const cfg = await API.getConfig(); + const game = this._gameData.find(g => g.id === gameId); + if (game && !cfg.use_unified_port && game.default_port) { + await API.updateConfig({ telemetry_port: game.default_port }); + } + await API.stopTelemetry(); await API.startTelemetry(); this._gameListEl.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); const items = this._gameListEl.querySelectorAll('.nav-item'); - items.forEach(item => { - if (item.textContent.trim()) item.classList.add('active'); + items.forEach((item, idx) => { + if (this._gameData[idx] && this._gameData[idx].id === gameId) item.classList.add('active'); }); const games = await API.getGames(); this._updateGameLabel(gameId, games); diff --git a/static/js/pages/settings.js b/static/js/pages/settings.js index d6778de..e91eab7 100644 --- a/static/js/pages/settings.js +++ b/static/js/pages/settings.js @@ -25,7 +25,12 @@ const PageSettings = { _bindEvents() { document.getElementById('settings-telemetry-port')?.addEventListener('change', async (e) => { await API.updateConfig({ telemetry_port: parseInt(e.target.value) || 20777 }); - Toast.show('遥测端口已更新,重启监听后生效', 'info'); + Toast.show('端口已更新,重启后生效', 'info'); + }); + + document.getElementById('settings-unified-port')?.addEventListener('change', async (e) => { + await API.updateConfig({ use_unified_port: e.target.checked }); + Toast.show('已更新', 'info'); }); document.getElementById('settings-restart-telemetry')?.addEventListener('click', async () => { @@ -119,7 +124,7 @@ const PageSettings = { '

数据转发

将收到的游戏原始 UDP 数据包完整转发到下游物理外设

' + this._forwardListHtml(forwards) + '
' + - '

连接设置

遥测监听端口
游戏内 UDP 输出端口 (默认 20777)
重启遥测监听
修改端口或切换游戏后
' + + '

连接设置

遥测监听端口
全局统一端口
使用游戏独立端口
关闭后切游戏自动切换到各游戏的默认端口
重启遥测监听
手动重启
' + '

仪表盘管理

右键卡片导出
' + diff --git a/templates/scene.html b/templates/scene.html index 62a224c..78538ae 100644 --- a/templates/scene.html +++ b/templates/scene.html @@ -124,8 +124,6 @@ } window.addEventListener('resize', () => { - const cw = parseInt(sceneCanvas.style.width) / (parseInt(sceneCanvas.style.width) / 1920) || 1920; - const ch = parseInt(sceneCanvas.style.height) / (parseInt(sceneCanvas.style.height) / 1080) || 1080; const vw = window.innerWidth; const vh = window.innerHeight; const scale = Math.min(vw / cw, vh / ch);