diff --git a/.gitignore b/.gitignore index 380d827..568a6bc 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,5 @@ toollib/frame.csv old/ 发布流程.txt src/plugins/*/*.json +src/plugins/*/*.db +.vscode/ diff --git a/src/CheckUpdateGui.py b/src/CheckUpdateGui.py index 702acbb..7bd66d0 100644 --- a/src/CheckUpdateGui.py +++ b/src/CheckUpdateGui.py @@ -89,11 +89,9 @@ def initUi(self): row1.addWidget(QLabel(self.release.tag_name)) row1.addItem(QSpacerItem( 20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) - self.dateTimeLabel.hide() if self.release.assets_created_at != "": self.dateTimeLabel.setText(QDateTime.fromString( self.release.assets_created_at, "yyyy-MM-ddThh:mm:ssZ").toString("yyyy-MM-dd hh:mm:ss")) - self.dateTimeLabel.show() row1.addWidget(self.dateTimeLabel) row1.addItem(QSpacerItem( 20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) @@ -337,11 +335,11 @@ def checkUpdate(self, releases: list[ReleaseInfo]): layout.setSpacing(2) for release in releases: frame = ReleaseFrame( - release, self.github.compareVersion(release.tag_name), r_path=self.r_path) + release, self.github.compareVersion(release.tag_name), r_path=self.r_path, parent=self) layout.addWidget(frame) frame.downLoadFile.connect(self.github.downloadRelease) # 底部加一个空白区域 - panel = QWidget() + panel = QWidget(self) panel.setContentsMargins(0, 0, 0, 0) panel.setFixedHeight(100) layout.addWidget(panel) diff --git a/src/gen_plugin.py b/src/gen_plugin.py new file mode 100644 index 0000000..7fe1b55 --- /dev/null +++ b/src/gen_plugin.py @@ -0,0 +1,104 @@ +import os +import sys +from pathlib import Path + + +def is_valid_class_name(name: str) -> bool: + """检查是否为有效的Python类名""" + if not name or not isinstance(name, str): + return False + if not name.isidentifier(): + return False + if name[0].islower(): + return False + return True + + +def build_py_file_content(name: str) -> str: + """生成Python文件内容""" + template = f''' +import sys +import os +import msgspec +import zmq + +if getattr(sys, "frozen", False): # 检查是否为pyInstaller生成的EXE + application_path = os.path.dirname(sys.executable) + sys.path.append(application_path + "/../../") + print(application_path + "/../../") +else: + sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../") +from mp_plugins import BasePlugin, BaseConfig +from mp_plugins.base.config import * +from mp_plugins.context import AppContext +from mp_plugins.events import * + +class {name}Config(BaseConfig): + pass + + +class {name}(BasePlugin): + def __init__( + self, + ) -> None: + super().__init__() + self._context: AppContext + self._config = {name}Config() + + def build_plugin_context(self) -> None: + self._plugin_context.name = "{name}" + self._plugin_context.display_name = "{name}" + self._plugin_context.version = "1.0.0" + self._plugin_context.description = "{name}" + self._plugin_context.author = "" + + def initialize(self) -> None: + return super().initialize() + + def shutdown(self) -> None: + return super().shutdown() + + + +if __name__ == "__main__": + try: + import sys + + args = sys.argv[1:] + host = args[0] + port = int(args[1]) + plugin = {name}() + # 捕获退出信号,优雅关闭 + import signal + + def signal_handler(sig, frame): + plugin.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + plugin.run(host, port) + except Exception: + pass +''' + return template + + +if __name__ == "__main__": + print("Building plugin...") + args = sys.argv[1:] + if len(args) != 1: + print("Usage: gen_plugin.py ") + exit(1) + name = args[0] + if not is_valid_class_name(name): + print("Invalid plugin name") + exit(1) + current_path = os.path.dirname(os.path.abspath(__file__)) + plugin_path = Path(current_path) / "plugins" / name + plugin_path.mkdir(parents=True, exist_ok=True) + plugin_file = plugin_path / f"{name}.py" + with open(plugin_file, "w", encoding="utf-8") as f: + context = build_py_file_content(name) + f.write(context) + print("gen py file success") diff --git a/src/main.py b/src/main.py index fefa9cf..d4a6049 100644 --- a/src/main.py +++ b/src/main.py @@ -74,7 +74,6 @@ def on_ready_read(socket: QLocalSocket): socket.disconnectFromServer() # 断开连接 - def cli_check_file(file_path: str) -> int: if not os.path.exists(file_path): print("ERROR: file not found") @@ -132,7 +131,8 @@ def cli_check_file(file_path: str) -> int: else: if videos.len() <= 0: evf_evfs_files[ide] = (e, 2) - checksum = ui.checksum_guard.get_checksum(videos[0].evf_video.raw_data) + checksum = ui.checksum_guard.get_checksum( + videos[0].evf_video.raw_data) if video.checksum != checksum: evf_evfs_files[ide] = (e, 1) continue @@ -160,7 +160,10 @@ def cli_check_file(file_path: str) -> int: exit_code = cli_check_file(args.check) sys.exit(exit_code) env = patch_env() - context = AppContext("Metasweeper", "1.0.0", "元扫雷") + context = AppContext(name="Metasweeper", version="1.0.0", display_name="元扫雷", + plugin_dir=(Path(get_paths()) / "plugins").as_posix(), + app_dir=get_paths() + ) PluginManager.instance().context = context PluginManager.instance().start(Path(get_paths()) / "plugins", env) @@ -195,9 +198,8 @@ def cli_check_file(file_path: str) -> int: hwnd, 0x00000011) else 1/0 ui.enable_screenshot = lambda: ... if SetWindowDisplayAffinity( hwnd, 0x00000000) else 1/0 - + app.aboutToQuit.connect(PluginManager.instance().stop) sys.exit(app.exec_()) - PluginManager.instance().stop() ... # except: # pass diff --git a/src/mineSweeperGUI.py b/src/mineSweeperGUI.py index 33895e0..2ce26e8 100644 --- a/src/mineSweeperGUI.py +++ b/src/mineSweeperGUI.py @@ -8,7 +8,9 @@ import gameAbout import gameSettings import gameSettingShortcuts -import captureScreen, mine_num_bar, gameRecordPop +import captureScreen +import mine_num_bar +import gameRecordPop from CheckUpdateGui import CheckUpdateGui from githubApi import GitHub, SourceManager import win32con @@ -32,6 +34,7 @@ from mp_plugins import PluginManager, PluginContext from mp_plugins.events import GameEndEvent + class MineSweeperGUI(MineSweeperVideoPlayer): def __init__(self, MainWindow: MainWindow, args): self.mainWindow = MainWindow @@ -80,7 +83,7 @@ def save_evf_file_integrated(): lambda: self.trans_language("pl_PL")) self.german_action.triggered.connect( lambda: self.trans_language("de_DE")) - + # 查看菜单 self.action_open_replay.triggered.connect( lambda: QDesktopServices.openUrl( @@ -253,7 +256,8 @@ def game_state(self, game_state: str): "column": self.column, "minenum": self.minenum, }) - self.score_board_manager.show(self.label.ms_board, index_type=1) + self.score_board_manager.show( + self.label.ms_board, index_type=1) case "study": self.num_bar_ui.QWidget.close() self._game_state = game_state @@ -292,7 +296,7 @@ def minenum(self, minenum): self._minenum = minenum def layMine(self, i, j): - + xx = self.row yy = self.column num = self.minenum @@ -301,13 +305,13 @@ def layMine(self, i, j): if self.gameMode == 5 or self.gameMode == 6 or self.gameMode == 9: # 根据模式生成局面 Board, _ = utils.laymine_solvable(self.board_constraint, - self.attempt_times_limit, (xx, yy, num, i, j)) + self.attempt_times_limit, (xx, yy, num, i, j)) elif self.gameMode == 0 or self.gameMode == 7 or self.gameMode == 8 or self.gameMode == 10: Board, _ = utils.laymine(self.board_constraint, - self.attempt_times_limit, (xx, yy, num, i, j)) + self.attempt_times_limit, (xx, yy, num, i, j)) elif self.gameMode == 4: Board, _ = utils.laymine_op(self.board_constraint, - self.attempt_times_limit, (xx, yy, num, i, j)) + self.attempt_times_limit, (xx, yy, num, i, j)) self.label.ms_board.board = Board @@ -362,7 +366,7 @@ def ai(self, i, j): self.label.ms_board.board = board elif code == 2: board, flag = utils.enumerateChangeBoard(self.label.ms_board.board, - self.label.ms_board.game_board, [(i, j)]) + self.label.ms_board.game_board, [(i, j)]) self.label.ms_board.board = board return elif self.gameMode == 8: @@ -370,14 +374,14 @@ def ai(self, i, j): self.label.ms_board.game_board, (i, j)) if code == 2: board, flag = utils.enumerateChangeBoard(self.label.ms_board.board, - self.label.ms_board.game_board, [(i, j)]) + self.label.ms_board.game_board, [(i, j)]) self.label.ms_board.board = board return elif self.gameMode == 9 or self.gameMode == 10: if self.label.ms_board.board[i][j] == -1: # 可猜调整的核心逻辑 board, flag = utils.enumerateChangeBoard(self.label.ms_board.board, - self.label.ms_board.game_board, [(i, j)]) + self.label.ms_board.game_board, [(i, j)]) self.label.ms_board.board = board return @@ -435,8 +439,8 @@ def chording_ai(self, i, j): break if must_guess: board, flag = utils.enumerateChangeBoard(board, - self.label.ms_board.game_board, - not_mine_round + is_mine_round) + self.label.ms_board.game_board, + not_mine_round + is_mine_round) self.label.ms_board.board = board else: for (x, y) in is_mine_round + not_mine_round: @@ -452,13 +456,13 @@ def chording_ai(self, i, j): break if must_guess: board, flag = utils.enumerateChangeBoard(board, - self.label.ms_board.game_board, - not_mine_round + is_mine_round) + self.label.ms_board.game_board, + not_mine_round + is_mine_round) self.label.ms_board.board = board elif self.gameMode == 9 or self.gameMode == 10: board, flag = utils.enumerateChangeBoard(board, - self.label.ms_board.game_board, - not_mine_round + is_mine_round) + self.label.ms_board.game_board, + not_mine_round + is_mine_round) self.label.ms_board.board = board def mineNumWheel(self, i): @@ -688,7 +692,8 @@ def save_evfs_file(self): if self.old_evfs_filename: file_name = self.old_evfs_filename + str(self.evfs.len()) self.evfs.save_evfs_file(file_name) - old_evfs_filename = self.old_evfs_filename + str(self.evfs.len() - 1) + ".evfs" + old_evfs_filename = self.old_evfs_filename + \ + str(self.evfs.len() - 1) + ".evfs" if os.path.exists(old_evfs_filename): # 进一步确认是文件而不是目录 if os.path.isfile(old_evfs_filename): @@ -928,13 +933,13 @@ def try_append_evfs(self, new_game_state): # self.evfs[0].checksum checksum = self.checksum_guard.get_checksum( self.label.ms_board.raw_data) - self.evfs.push(self.label.ms_board.raw_data, + self.evfs.push(self.label.ms_board.raw_data, self.cal_evf_filename(absolute=False), checksum) else: evfs_len = self.evfs.len() checksum = self.checksum_guard.get_checksum( self.label.ms_board.raw_data + self.evfs[evfs_len - 1].checksum) - self.evfs.push(self.label.ms_board.raw_data, + self.evfs.push(self.label.ms_board.raw_data, self.cal_evf_filename(absolute=False), checksum) self.evfs.generate_evfs_v0_raw_data() self.save_evfs_file() @@ -1479,6 +1484,5 @@ def closeEvent_(self): self.record_setting.sync() def action_OpenPluginDialog(self): - contexts = list(PluginManager.instance().plugin_contexts) - dialog = PluginManagerUI(contexts) + dialog = PluginManagerUI(PluginManager.instance().Get_Plugin_Names()) dialog.exec() diff --git a/src/mp_plugins/base/config.py b/src/mp_plugins/base/config.py index 9fbd271..0639a4a 100644 --- a/src/mp_plugins/base/config.py +++ b/src/mp_plugins/base/config.py @@ -37,7 +37,6 @@ class SelectSetting(BaseSetting): class BaseConfig(_BaseData): """ """ - pass diff --git a/src/mp_plugins/base/context.py b/src/mp_plugins/base/context.py index 178305c..b8e8823 100644 --- a/src/mp_plugins/base/context.py +++ b/src/mp_plugins/base/context.py @@ -11,6 +11,8 @@ class BaseContext(_BaseData): name: str = "" version: str = "" + plugin_dir: str = "" + app_dir: str = "" class PluginContext(BaseContext): diff --git a/src/mp_plugins/base/mode.py b/src/mp_plugins/base/mode.py index 0e92a59..d3819d4 100644 --- a/src/mp_plugins/base/mode.py +++ b/src/mp_plugins/base/mode.py @@ -7,6 +7,9 @@ def __eq__(self, value: object) -> bool: return self.value == value.value return self.value == value # 支持直接比较 value + def __str__(self) -> str: + return self.name + class PluginStatus(ValueEnum): """ diff --git a/src/mp_plugins/base/plugin.py b/src/mp_plugins/base/plugin.py index a7f6edc..0d5fc95 100644 --- a/src/mp_plugins/base/plugin.py +++ b/src/mp_plugins/base/plugin.py @@ -1,5 +1,8 @@ +import time from abc import ABC, abstractmethod +import ctypes import inspect +from pathlib import Path from msgspec import json from datetime import datetime, timedelta from .error import Error @@ -25,8 +28,13 @@ from .mode import PluginStatus from .config import BaseConfig, BaseSetting -P = ParamSpec("P") # 捕获参数 -R = TypeVar("R") # 捕获返回值 + +P = ParamSpec("P") +R = TypeVar("R") + +# 引入 Windows 多媒体计时器函数 +timeBeginPeriod = ctypes.windll.winmm.timeBeginPeriod +timeEndPeriod = ctypes.windll.winmm.timeEndPeriod class BasePlugin(ABC): @@ -72,16 +80,16 @@ def __init_subclass__(cls) -> None: def __init__(self) -> None: super().__init__() - context = zmq.Context() + context = zmq.Context(5) self.dealer = context.socket(zmq.DEALER) self.__message_queue: Queue[Message] = Queue() self.__heartbeat_time = datetime.now() + self._is_running: bool = False self.build_plugin_context() @abstractmethod def initialize(self) -> None: - self._plugin_context.status = PluginStatus.Running - self.refresh_context() + self.init_config(self._config) @abstractmethod def shutdown(self) -> None: @@ -89,31 +97,50 @@ def shutdown(self) -> None: self.refresh_context() def run(self, host: str, port: int) -> None: - self.initialize() - self.dealer.setsockopt_string(zmq.IDENTITY, str(self._plugin_context.pid)) + self.dealer.setsockopt_string( + zmq.IDENTITY, str(self._plugin_context.pid)) self.dealer.connect(f"tcp://{host}:{port}") + self._is_running = True + self._plugin_context.status = PluginStatus.Running self.refresh_context() - while datetime.now() - self.__heartbeat_time < timedelta(seconds=30): - while not self.__message_queue.empty(): - message = self.__message_queue.get() - self.dealer.send_multipart([b"", json.encode(message)]) - try: - data = self.dealer.recv(flags=zmq.NOBLOCK) - except zmq.Again: - continue - if data: + poller = zmq.Poller() + poller.register(self.dealer, zmq.POLLIN) + timeBeginPeriod(1) + while self._is_running: + # 轮询是否有数据,超时 1ms + events = dict(poller.poll(timeout=0)) + + # ---- 接收消息 ----------------------------------------------------- + if self.dealer in events: + data = self.dealer.recv() message = json.decode(data, type=Message) self.__message_dispatching(message) + + # ---- 发送消息 ----------------------------------------------------- + if not self.__message_queue.empty(): + msg = self.__message_queue.get() + self.dealer.send(json.encode(msg)) + + # ---- 心跳检查 ----------------------------------------------------- + if datetime.now() - self.__heartbeat_time > timedelta(seconds=10): + break + + time.sleep(0.001) + + # --------------------------------------------------------------------- + timeEndPeriod(1) self.shutdown() self.dealer.close() + # ------------------------------------------------------------------------- + def __message_dispatching(self, message: Message) -> None: """ 消息分发 """ - new_message = message.copy() - new_message.Source = self.__class__.__name__ + message.Source = self.__class__.__name__ self.__heartbeat_time = datetime.now() + if message.mode == MessageMode.Event: if isinstance(message.data, BaseEvent) and message.data is not None: if message.data.__class__ not in self._event_handlers: @@ -124,8 +151,8 @@ def __message_dispatching(self, message: Message) -> None: return for handler in self._event_handlers[message.data.__class__]: event = handler(self, message.data) - new_message.data = event - self.__message_queue.put(new_message) + message.data = event + self.__message_queue.put(message) else: self.send_error( type="Event Validation", @@ -133,13 +160,16 @@ def __message_dispatching(self, message: Message) -> None: ) elif message.mode == MessageMode.Context: if isinstance(message.data, BaseContext) and message.data is not None: - self._context = message.data + if hasattr(self, "_context"): + self._context = message.data + self.initialize() + self._context = message.class_name elif message.mode == MessageMode.Error: pass elif message.mode == MessageMode.Unknown: pass elif message.mode == MessageMode.Heartbeat: - self.__message_queue.put(new_message) + self.__message_queue.put(message) def refresh_context(self): self._plugin_context.heartbeat = datetime.now().timestamp() @@ -168,8 +198,8 @@ def send_error(self, type: str, error: str): def init_config(self, config: BaseConfig): self._config = config old_config = self.config - dir = os.path.dirname(inspect.getfile(self.__class__)) - config_path = os.path.join(dir, f"{self.__class__.__name__}.json") + config_path = self.context.plugin_dir + \ + f'/{self.__class__.__name__}/{self.__class__.__name__}.json' if old_config is None: with open(config_path, "w", encoding="utf-8") as f: b = json.encode(config) @@ -177,8 +207,8 @@ def init_config(self, config: BaseConfig): @property def config(self): - dir = os.path.dirname(inspect.getfile(self.__class__)) - config_path = os.path.join(dir, f"{self.__class__.__name__}.json") + config_path = self.context.plugin_dir + \ + f'/{self.__class__.__name__}/{self.__class__.__name__}.json' if os.path.exists(config_path): try: with open(config_path, "r", encoding="utf-8") as f: @@ -186,3 +216,12 @@ def config(self): return config except: return None + + @property + def path(self) -> Path: + if hasattr(self, "_context"): + return Path(self._context.plugin_dir) / self.__class__.__name__ + return Path(os.path.dirname(os.path.abspath(__file__))) + + def stop(self): + self._is_running = False diff --git a/src/mp_plugins/plugin_manager.py b/src/mp_plugins/plugin_manager.py index f4595eb..1dbfb6e 100644 --- a/src/mp_plugins/plugin_manager.py +++ b/src/mp_plugins/plugin_manager.py @@ -1,8 +1,17 @@ +import asyncio +import ctypes from datetime import datetime, timedelta import sys import threading from typing import Callable, Dict, List, Optional, TypeVar, Generic, Sequence from threading import RLock +import zmq +from queue import Queue +import time +import msgspec +import pathlib +import json + from .base import ( PluginContext, BaseContext, @@ -12,42 +21,46 @@ Error, PluginStatus, ) -import zmq -from .base._data import _BaseData -from queue import Queue -import time -import msgspec from .plugin_process import PluginProcess from .base.config import BaseConfig, BaseSetting, Get_settings -import pathlib -import json + _Event = TypeVar("_Event", bound=BaseEvent) +# 引入 Windows 多媒体计时器函数 +timeBeginPeriod = ctypes.windll.winmm.timeBeginPeriod +timeEndPeriod = ctypes.windll.winmm.timeEndPeriod + class PluginManager(object): __instance: Optional["PluginManager"] = None __lock = RLock() - __plugins_context: Dict[str, PluginContext] = {} - __plugins_process: Dict[str, PluginProcess] = {} - __plugins_config_path: Dict[str, pathlib.Path] = {} - __context: BaseContext - __event_dict: Dict[str, List[BaseEvent]] = {} - __message_queue: Queue[Message] = Queue() - __running: bool = False def __init__(self) -> None: - context = zmq.Context() + context = zmq.Context(3) self.__router = context.socket(zmq.ROUTER) - self.__port = self.__router.bind_to_random_port("tcp://127.0.0.1") + self.__poller = zmq.Poller() + self.__poller.register(self.__router, zmq.POLLIN) + + self.__port = self.__router.bind_to_random_port("tcp://*") + + self.__plugins_context: Dict[str, PluginContext] = {} + self.__plugins_process: Dict[str, PluginProcess] = {} + self.__plugins_config_path: Dict[str, pathlib.Path] = {} + + self.__event_dict: Dict[str, List[BaseEvent]] = {} + self.__message_queue: Queue[Message] = Queue() + self.__error_callback = None - @property - def port(self): - return self.__port + self.__running = False + self.__context: BaseContext = None + # ------------------------- + # 单例 + # ------------------------- @classmethod def instance(cls) -> "PluginManager": if cls.__instance is None: @@ -56,189 +69,224 @@ def instance(cls) -> "PluginManager": cls.__instance = cls() return cls.__instance + # ------------------------- + # 公开属性 + # ------------------------- + @property + def port(self): + return self.__port + + @property + def context(self) -> BaseContext: + return self.__context + + @context.setter + def context(self, context: BaseContext): + self.__context = context + + @property + def plugin_contexts(self): + for context in self.__plugins_context.values(): + yield context.copy() + + # ------------------------- + # 启动 / 停止 + # ------------------------- def start(self, plugin_dir: pathlib.Path, env: Dict[str, str]): - with self.__lock: - if self.__running: - return - self.__running = True + timeBeginPeriod(1) + if self.__running: + return + + self.__running = True + plugin_dir.mkdir(parents=True, exist_ok=True) + self._load_plugins(plugin_dir, env) + + # 后台线程执行单线程循环 + self.__loop_thread = threading.Thread( + target=self.run_loop, daemon=True) + self.__loop_thread.start() + + print("Plugin manager started (non-blocking)") + + def stop(self): + timeEndPeriod(1) + self.__running = False + if hasattr(self, "__loop_thread"): + self.__loop_thread.join(2) + for process in self.__plugins_process.values(): + process.stop() + print("Plugin manager stopped") + + # ------------------------- + # 加载插件(保持原逻辑) + # ------------------------- + def _load_plugins(self, plugin_dir, env): for plugin_path in plugin_dir.iterdir(): if plugin_path.is_dir(): if plugin_path.name.startswith("__"): continue + self.__plugins_config_path[plugin_path.name] = plugin_path / ( plugin_path.name + ".json" ) - # 先判断是否有exe + + # exe 优先 if (plugin_path / (plugin_path.name + ".exe")).exists(): - plugin_path = plugin_path / (plugin_path.name + ".exe") + path = plugin_path / (plugin_path.name + ".exe") else: - plugin_path = plugin_path / (plugin_path.name + ".py") - process = PluginProcess(plugin_path) - process.start("127.0.0.1", self.__port, env=env) + path = plugin_path / (plugin_path.name + ".py") + + process = PluginProcess(path) + process.start("localhost", self.__port, env=env) + self.__plugins_process[str(process.pid)] = process - t = threading.Thread(target=self.__message_dispatching, daemon=True) - self.__request_thread = t - t2 = threading.Thread(target=self.__async_send_message, daemon=True) - self.__response_thread = t2 - t3 = threading.Thread(target=self.__send_heartbeat, daemon=True) - self.__heartbeat_thread = t3 - t3.start() - t.start() - t2.start() - print("Plugin manager started") + # ------------------------- + # 单线程事件循环(核心) + # ------------------------- + def run_loop(self): + heartbeat_timer = time.time() - def stop(self): - with self.__lock: - if not self.__running: - return - self.__running = False - for process in self.__plugins_process.values(): - process.stop() - self.__request_thread.join(10) - self.__response_thread.join(10) - self.__heartbeat_thread.join(10) - self.__router.close() - print("Plugin manager stopped") + while self.__running: - @property - def plugin_contexts(self): - for context in self.__plugins_context.values(): - yield context.copy() + # 1. 收消息 + events = dict(self.__poller.poll(0)) + if self.__router in events: + identity, data = self.__router.recv_multipart() + self._handle_incoming(identity, data) - @property - def context(self) -> BaseContext: - return self.__context + # 2. 发消息 + if not self.__message_queue.empty(): + message = self.__message_queue.get() + self._send_message_internal(message) - @context.setter - def context(self, context: BaseContext): - self.__context = context + # 3. 心跳 + now = time.time() + if now - heartbeat_timer > 5: + self._send_heartbeat() + heartbeat_timer = now + time.sleep(0.001) + + # ------------------------- + # 消息解包逻辑 + # ------------------------- + def _handle_incoming(self, identity, data): + pid = identity.decode() + + if pid in self.__plugins_context: + self.__plugins_context[pid].heartbeat = time.time() + + message = msgspec.json.decode(data, type=Message) + + # Event + if message.mode == MessageMode.Event: + if message.id in self.__event_dict and isinstance(message.data, BaseEvent): + self.__event_dict[message.id].append(message.data) + # Error + elif message.mode == MessageMode.Error: + if self.__error_callback and isinstance(message.data, Error): + self.__error_callback(message.data) + + # 初次 Context + elif message.mode == MessageMode.Context: + if isinstance(message.data, PluginContext): + self.__plugins_context[pid] = message.data + # 把主 Context 返回插件 + self.__send_message( + Message( + data=self.context, + mode=MessageMode.Context, + class_name=self.context.__class__.__name__, + ) + ) + + # ------------------------- + # 事件发射 + # ------------------------- def send_event( self, event: _Event, timeout: int = 10, response_count: int = 1 ) -> Sequence[_Event]: + message = Message( - data=event, mode=MessageMode.Event, class_name=event.__class__.__name__ + data=event, + mode=MessageMode.Event, + class_name=event.__class__.__name__, ) + self.__event_dict[message.id] = [] + self.__send_message(message) - current_time = datetime.now() - while datetime.now() - current_time < timedelta(seconds=timeout) and ( + + expire = datetime.now() + timedelta(seconds=timeout) + + while datetime.now() < expire and ( len(self.__event_dict.get(message.id, [])) < response_count ): - time.sleep(0.1) - result = [] + time.sleep(0.0001) + + result = self.__event_dict.get(message.id, []) if message.id in self.__event_dict: - for response in self.__event_dict[message.id]: - result.append(response) - with self.__lock: del self.__event_dict[message.id] - return result - def __send_heartbeat(self): - while self.__running: - time.sleep(5) - for context in self.__plugins_context.values(): - if datetime.now().timestamp() - context.heartbeat > 3000: - context.status = PluginStatus.Dead - if context.status == PluginStatus.Running: - self.__send_message(Message(mode=MessageMode.Heartbeat)) + return result + # ------------------------- + # 发消息入口(入队) + # ------------------------- def __send_message(self, message: Message): self.__message_queue.put(message) - def bind_error(self, func: Callable[[Error], None]): - self.__error_callback = func + # ------------------------- + # 发消息逻辑(内部) + # ------------------------- + def _send_message_internal(self, message: Message): + if message.mode in ( + MessageMode.Event, + MessageMode.Context, + MessageMode.Heartbeat, + ): + for ctx in self.__plugins_context.values(): + if ctx.status == PluginStatus.Running: + if message.mode == MessageMode.Event: + if message.class_name not in ctx.subscribers: + continue + self.__router.send_multipart( + [ + str(ctx.pid).encode(), + msgspec.json.encode(message), + ] + ) - def __message_dispatching(self) -> None: - while self.__running: - time.sleep(0.1) - try: - identity, _, data = self.__router.recv_multipart(flags=zmq.NOBLOCK) - except zmq.Again: - continue - if not data: - continue - if identity.decode() in self.__plugins_context: - self.__plugins_context[identity.decode()].heartbeat = ( - datetime.now().timestamp() + # ------------------------- + # 心跳 + # ------------------------- + def _send_heartbeat(self): + msg = Message(mode=MessageMode.Heartbeat) + for ctx in self.__plugins_context.values(): + if ctx.status == PluginStatus.Running: + self.__router.send_multipart( + [str(ctx.pid).encode(), msgspec.json.encode(msg)] ) - message = msgspec.json.decode(data, type=Message) - if message.mode == MessageMode.Event: - with self.__lock: - if message.id in self.__event_dict and isinstance( - message.data, BaseEvent - ): - self.__event_dict[message.id].append(message.data) - elif message.mode == MessageMode.Error: - if self.__error_callback is not None and isinstance( - message.data, Error - ): - self.__error_callback(message.data) - elif message.mode == MessageMode.Heartbeat: - pass - elif message.mode == MessageMode.Context: - if isinstance(message.data, PluginContext): - with self.__lock: - self.__plugins_context[identity.decode()] = message.data - self.__send_message( - Message( - data=self.context, - mode=MessageMode.Context, - class_name=self.context.__class__.__name__, - ) - ) - - def __async_send_message(self): - while self.__running: - time.sleep(0.1) - if self.__message_queue.empty(): - continue - message = self.__message_queue.get() - if message.mode == MessageMode.Event: - for context in self.__plugins_context.values(): - event_name = message.data.__class__.__name__ - if ( - event_name in context.subscribers - and context.status == PluginStatus.Running - ): - self.__router.send_multipart( - [ - str(context.pid).encode(), - b"", - msgspec.json.encode(message), - ] - ) - elif message.mode == MessageMode.Heartbeat: - for context in self.__plugins_context.values(): - if context.status == PluginStatus.Running: - self.__router.send_multipart( - [ - str(context.pid).encode(), - b"", - msgspec.json.encode(message), - ] - ) - elif message.mode == MessageMode.Context: - for context in self.__plugins_context.values(): - if context.status == PluginStatus.Running: - self.__router.send_multipart( - [ - str(context.pid).encode(), - b"", - msgspec.json.encode(message), - ] - ) + # ------------------------- + # 错误回调 + # ------------------------- + def bind_error(self, func: Callable[[Error], None]): + self.__error_callback = func + + # ------------------------- + # 配置操作(原样) + # ------------------------- def Get_Settings(self, plugin_name: str): if plugin_name not in self.__plugins_config_path: return {} if not self.__plugins_config_path[plugin_name].exists(): return {} - data = json.loads(self.__plugins_config_path[plugin_name].read_text("utf-8")) - + data = json.loads( + self.__plugins_config_path[plugin_name].read_text("utf-8")) return Get_settings(data) def Set_Settings(self, plugin_name: str, name: str, setting: BaseSetting): @@ -253,3 +301,15 @@ def Set_Settings(self, plugin_name: str, name: str, setting: BaseSetting): self.__plugins_config_path[plugin_name].write_text( json.dumps(new_data, indent=4) ) + + # ------------------------- + # 查找插件 Context + # ------------------------- + def Get_Context_By_Name(self, plugin_name: str) -> Optional[PluginContext]: + for ctx in self.__plugins_context.values(): + if ctx.name == plugin_name: + return ctx.copy() + return None + + def Get_Plugin_Names(self) -> List[str]: + return [ctx.name for ctx in self.__plugins_context.values()] diff --git a/src/mp_plugins/plugin_process.py b/src/mp_plugins/plugin_process.py index 287798f..1d60917 100644 --- a/src/mp_plugins/plugin_process.py +++ b/src/mp_plugins/plugin_process.py @@ -3,7 +3,7 @@ import os from pathlib import Path from typing import List, Optional, Dict, Any - +import signal class PluginProcess(object): @@ -30,13 +30,10 @@ def start(self, host: str, port: int, env: Dict[str, str]): self._process = subprocess.Popen( [ sys.executable, - "-m", - module, + str(self.__plugin_path), host, str(port), ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=env, ) else: @@ -46,8 +43,6 @@ def start(self, host: str, port: int, env: Dict[str, str]): host, str(port), ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, ) self.__pid = self._process.pid @@ -55,4 +50,11 @@ def stop(self): if self._process is None: return if self._process.poll() is None: - self._process.terminate() + if sys.platform == "win32": + self._process.kill() + else: + self._process.terminate() + try: + self._process.wait(timeout=2) + except subprocess.TimeoutExpired: + self._process.kill() diff --git a/src/pluginDialog.py b/src/pluginDialog.py index e2250aa..80c9c06 100644 --- a/src/pluginDialog.py +++ b/src/pluginDialog.py @@ -16,9 +16,11 @@ QCheckBox, QComboBox, ) -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QTimer import sys from typing import Dict + +import msgspec from mp_plugins import PluginContext # 你的 Setting 类 @@ -30,11 +32,14 @@ SelectSetting, ) from mp_plugins import PluginManager +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate class PluginManagerUI(QDialog): - def __init__(self, plugin_contexts: list[PluginContext]): + def __init__(self, plugin_names: list[str]): """ plugin_contexts: 你传入的 plugin 列表 get_settings_func(plugin_ctx) -> Dict[str, BaseSetting] @@ -42,18 +47,24 @@ def __init__(self, plugin_contexts: list[PluginContext]): """ super().__init__() - self.plugin_contexts = plugin_contexts + self.plugin_names = plugin_names self.current_settings_widgets: Dict[str, QWidget] = {} + self.timer = QTimer(self) + self.timer.timeout.connect( + lambda: self.update_context(self.list_widget.currentRow()) + ) + self.timer.start(3000) # 每秒更新一次 - self.setWindowTitle("插件管理") + self.setWindowTitle(_translate("Form", "插件管理")) self.resize(1000, 650) root_layout = QHBoxLayout(self) # ================= 左侧插件列表 ================= self.list_widget = QListWidget() - for ctx in self.plugin_contexts: - self.list_widget.addItem(ctx.display_name or ctx.name) + for name in self.plugin_names: + self.list_widget.addItem( + PluginManager.instance().Get_Context_By_Name(name).display_name) self.list_widget.currentRowChanged.connect(self.on_plugin_selected) root_layout.addWidget(self.list_widget, 1) @@ -62,31 +73,53 @@ def __init__(self, plugin_contexts: list[PluginContext]): right_layout = QVBoxLayout() # -------- 插件详情 -------- - details_group = QGroupBox("插件详情") + details_group = QGroupBox(_translate("Form", "插件详情")) self.details_layout = QFormLayout() - + pid_label = QLabel() + name_label = QLabel() + display_name_label = QLabel() + description_label = QLabel() + version_label = QLabel() + author_label = QLabel() + email_label = QLabel() + url_label = QLabel() + status_label = QLabel() + heartbeat_label = QLabel() + subscribed_events_label = QLabel() + pid_label.setObjectName("pid") + name_label.setObjectName("name") + display_name_label.setObjectName("display_name") + description_label.setObjectName("description") + version_label.setObjectName("version") + author_label.setObjectName("author") + email_label.setObjectName("author_email") + url_label.setObjectName("url") + status_label.setObjectName("status") + heartbeat_label.setObjectName("heartbeat") + subscribed_events_label.setObjectName("subscribers") self.detail_labels = { - "pid": QLabel(), - "name": QLabel(), - "display_name": QLabel(), - "description": QLabel(), - "version": QLabel(), - "author": QLabel(), - "author_email": QLabel(), - "url": QLabel(), - "status": QLabel(), - "heartbeat": QLabel(), - "subscribers": QLabel(), + _translate("Form", "进程ID"): pid_label, + _translate("Form", "插件名称"): name_label, + _translate("Form", "插件显示名称"): display_name_label, + _translate("Form", "插件描述"): description_label, + _translate("Form", "插件版本"): version_label, + _translate("Form", "作者"): author_label, + _translate("Form", "作者邮箱"): email_label, + _translate("Form", "插件URL"): url_label, + _translate("Form", "插件状态"): status_label, + _translate("Form", "心跳时间"): heartbeat_label, + _translate("Form", "订阅事件"): subscribed_events_label } for key, widget in self.detail_labels.items(): - self.details_layout.addRow(key.replace("_", " ").title() + ":", widget) + self.details_layout.addRow( + key.replace("_", " ").title() + ":", widget) details_group.setLayout(self.details_layout) right_layout.addWidget(details_group, 1) # -------- 设置面板(滚动) -------- - settings_group = QGroupBox("设置") + settings_group = QGroupBox(_translate("Form", "设置")) vbox = QVBoxLayout() self.scroll_area = QScrollArea() @@ -99,7 +132,7 @@ def __init__(self, plugin_contexts: list[PluginContext]): vbox.addWidget(self.scroll_area, 1) # 保存按钮 - self.btn_save = QPushButton("保存") + self.btn_save = QPushButton(_translate("Form", "保存")) self.btn_save.clicked.connect(self.on_save_clicked) vbox.addWidget(self.btn_save) @@ -108,7 +141,7 @@ def __init__(self, plugin_contexts: list[PluginContext]): root_layout.addLayout(right_layout, 2) - if plugin_contexts: + if self.plugin_names: self.list_widget.setCurrentRow(0) # =================================================================== @@ -118,21 +151,31 @@ def on_plugin_selected(self, index: int): if index < 0: return - ctx = self.plugin_contexts[index] - - # --- PluginContext 原样填充 --- - for key, label in self.detail_labels.items(): - value = getattr(ctx, key) - if isinstance(value, list): - value = ", ".join(value) - label.setText(str(value)) + self.update_context(index) # --- 你自己的获取 settings 的函数 --- - settings_dict = PluginManager.instance().Get_Settings(ctx.name) + settings_dict = PluginManager.instance( + ).Get_Settings(self.plugin_names[index]) # --- 动态加载设置控件 --- self.load_settings(settings_dict) + def update_context(self, index: int): + if index < 0: + return + + ctx = PluginManager.instance().Get_Context_By_Name( + self.plugin_names[index]) + + # --- PluginContext 原样填充 --- + for key, value in msgspec.structs.asdict(ctx).items(): + label = self.findChild(QLabel, key) + if label is None: + continue + if isinstance(value, list): + value = ", ".join(value) + label.setText(str(value)) + # =================================================================== # 加载 settings # =================================================================== @@ -201,4 +244,5 @@ def on_save_clicked(self): elif isinstance(widget, QComboBox) and isinstance(setting, SelectSetting): setting.value = widget.currentText() PluginManager.instance().Set_Settings(ctx.name, key, setting) - QMessageBox.information(self, "保存成功", "设置已保存") + QMessageBox.information( + self, _translate("Form", "保存成功"), _translate("Form", "设置已保存")) diff --git a/src/plugins/History/History.py b/src/plugins/History/History.py new file mode 100644 index 0000000..dafa66f --- /dev/null +++ b/src/plugins/History/History.py @@ -0,0 +1,104 @@ + +import sys +import os +import msgspec +import zmq +import sqlite3 + + +if getattr(sys, "frozen", False): # 检查是否为pyInstaller生成的EXE + application_path = os.path.dirname(sys.executable) + sys.path.append(application_path + "/../../") + print(application_path + "/../../") +else: + sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../") +from mp_plugins import BasePlugin, BaseConfig +from mp_plugins.base.config import * +from mp_plugins.context import AppContext +from mp_plugins.events import * + + +class HistoryConfig(BaseConfig): + pass + + +class History(BasePlugin): + def __init__( + self, + ) -> None: + super().__init__() + self._context: AppContext + self._config = HistoryConfig() + + def build_plugin_context(self) -> None: + self._plugin_context.name = "History" + self._plugin_context.display_name = "历史记录" + self._plugin_context.version = "1.0.0" + self._plugin_context.description = "History" + self._plugin_context.author = "ljzloser" + + def initialize(self) -> None: + db_path = self.path / "history.db" + if not db_path.exists(): + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute( + """ +create table main.history +( + replay_id INTEGER + primary key, + termination_type INTEGER not null, + player_nick TEXT not null, + level TEXT not null, + nf BOOLEAN not null, + timeth INTEGER not null, + bbbv_board INTEGER not null, + bbbv_completed INTEGER not null +); + """ + ) + conn.commit() + cursor.execute( + """ +create table main.replays +( + id INTEGER + primary key, + date DATETIME not null, + replay BLOB not null, + processed BOOLEAN default FALSE not null +); + """ + ) + conn.close() + return super().initialize() + + def shutdown(self) -> None: + return super().shutdown() + + @BasePlugin.event_handler(GameEndEvent) + def on_game_end(self, event: GameEndEvent) -> None: + pass + + +if __name__ == "__main__": + try: + import sys + + args = sys.argv[1:] + host = args[0] + port = int(args[1]) + plugin = History() + # 捕获退出信号,优雅关闭 + import signal + + def signal_handler(sig, frame): + plugin.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + plugin.run(host, port) + except Exception: + pass diff --git a/src/plugins/UpLoadVideo/UpLoadVideo.py b/src/plugins/UpLoadVideo/UpLoadVideo.py index 9808c89..09ff2c6 100644 --- a/src/plugins/UpLoadVideo/UpLoadVideo.py +++ b/src/plugins/UpLoadVideo/UpLoadVideo.py @@ -1,5 +1,6 @@ import sys import os +import time import msgspec import zmq @@ -32,11 +33,11 @@ def __init__( self._config = UpLoadVideoConfig( TextSetting("用户名", "user"), TextSetting("密码", "passwd"), - NumberSetting(name="上传周期", value=0, min_value=1, max_value=10, step=1), + NumberSetting(name="上传周期", value=0, min_value=1, + max_value=10, step=1), BoolSetting("自动上传", True), SelectSetting("上传类型", "自动上传", options=["自动上传", "手动上传"]), ) - self.init_config(self._config) def build_plugin_context(self) -> None: self._plugin_context.name = "UpLoadVideo" @@ -51,10 +52,6 @@ def initialize(self) -> None: def shutdown(self) -> None: return super().shutdown() - @BasePlugin.event_handler(GameEndEvent) - def on_game_end(self, event: GameEndEvent) -> GameEndEvent: - return event - if __name__ == "__main__": try: @@ -64,7 +61,15 @@ def on_game_end(self, event: GameEndEvent) -> GameEndEvent: host = args[0] port = int(args[1]) plugin = UpLoadVideo() + # 捕获退出信号,优雅关闭 + import signal + + def signal_handler(sig, frame): + plugin.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) plugin.run(host, port) - except Exception as e: - with open("UpLoadVideo_error.log", "w", encoding="utf-8") as f: - f.write(str(e)) + except Exception: + pass diff --git a/src/ui/de_DE.ts b/src/ui/de_DE.ts index 9ce65bc..5149f2c 100644 --- a/src/ui/de_DE.ts +++ b/src/ui/de_DE.ts @@ -14,32 +14,32 @@ Abbrechen - + 结束后标雷 Minen markieren nach Spielende - + 标识: Markierungen: - + 永远使用筛选法埋雷(不推荐) Immer Minen nach Zufallsprinzip platzieren (nicht empfohlen) - + 自动保存录像(推荐) Automatisches Speichern von Videos (empfohlen) - + 游戏模式: Spielmodus: - + 方格边长: Quadratische Seitenlängen: @@ -94,12 +94,12 @@ Situative Zwänge: - + 自动重开: Automatische Wiedereröffnung: - + 国家或地区: Land oder Region: @@ -251,42 +251,42 @@ Experte - + 勾选后永远使用筛选法埋雷,否则会适时改用调整法 Wenn diese Option aktiviert ist, wird beim Verlegen von Minen immer die Screening-Methode verwendet. Andernfalls wird gegebenenfalls auf die Anpassungsmethode umgeschaltet - + 完成后自动将录像保存到replay文件夹下 Aufzeichnung nach Abschluss automatisch im Ordner "Replay" speichern - + 允许纪录弹窗(推荐) Popup für Aufzeichnung zulassen (empfohlen) - + 用于参加比赛 Zur Turnierteilnahme - + 比赛标识: Turnierkennung: - + 光标不能超出边框 Der Cursor darf nicht über den Rand hinausragen - + 用于与其他人相区分,但不希望排名网站和软件展示出来 Dient dazu, sich von anderen zu unterscheiden, ist jedoch nicht für die Anzeige durch Ranking-Websites und Software vorgesehen - + 个性标识: Personalisierte Identifizierung: @@ -306,7 +306,7 @@ Scrollrad zum Ändern der Abspielgeschwindigkeit - + 自动保存录像集 "evfs" automatisch speichern @@ -315,103 +315,218 @@ Return Return + + + 插件管理 + + + + + 插件详情 + + + + + 设置 + Einstellungen + + + + 保存 + Speichern + + + + 保存成功 + + + + + 设置已保存 + + + + + 进程ID + + + + + 插件名称 + + + + + 插件显示名称 + + + + + 插件描述 + + + + + 插件版本 + + + + + 作者 + + + + + 作者邮箱 + + + + + 插件URL + + + + + 插件状态 + + + + + 心跳时间 + + + + + 订阅事件 + + MainWindow - + 元扫雷 Meta Minesweeper - + 游戏 Spiele - + 设置 Einstellungen - + 语言设置 Spracheinstellungen - + 帮助 Hilfe - + 新游戏 Neue Spiele - + 初级 Junior - + 中级 Zwischenbericht - + 高级 Fortgeschrittene - + 自定义 Personalisierung - + 退出 Rücknahme - + 游戏设置 Spiel-Einstellungen - + 关于 Über - + 快捷键设置 Shortcut-Einstellungen - + 打开 Öffnen Sie - + 鼠标设置 Maus-Einstellungen - + 保存 Speichern - + 回放 Wiederholung - + 检查更新 Auf Updates prüfen + + + 查看 + + + + + 录像所在位置 + + + + + 设置所在位置 + + + + + 成就 + + + + + 个人纪录 + + + + + 插件 + + diff --git a/src/ui/en_US.ts b/src/ui/en_US.ts index 1c49afd..973a9c2 100644 --- a/src/ui/en_US.ts +++ b/src/ui/en_US.ts @@ -14,32 +14,32 @@ Cancel - + 结束后标雷 Fill Finished Board With Flags - + 标识: User identifier: - + 永远使用筛选法埋雷(不推荐) Use filtering algorithm (not recommended) - + 自动保存录像(推荐) Auto save video (recommended) - + 游戏模式: Mode: - + 方格边长: Zoom: @@ -84,12 +84,12 @@ Lucky mode - + 自动重开: Auto restart: - + 允许纪录弹窗(推荐) Allow popovers for best scores (recommended) @@ -186,7 +186,7 @@ ②Anyone can use any part of the project source code in any project, and you are welcome to make valuable comments on the project home page. - + 国家或地区: Country or region: @@ -221,12 +221,12 @@ Expert - + 勾选后永远使用筛选法埋雷,否则会适时改用调整法 Tick the box to always use the filtering method to lay mines, otherwise the adjustment method will be used at the right time - + 完成后自动将录像保存到replay文件夹下 After completion, automatically save the recording to the replay folder @@ -266,27 +266,27 @@ RQP score! - + 用于参加比赛 The credential for participating in the competition - + 比赛标识: Championship identifier: - + 光标不能超出边框 The cursor cannot move out of the border - + 用于与其他人相区分,但不希望排名网站和软件展示出来 Used to distinguish from others, but not intended to be displayed on ranking websites and software - + 个性标识: Unique identifier: @@ -306,7 +306,7 @@ Scroll the mouse wheel to adjust the playing speed - + 自动保存录像集 Auto save evfs @@ -315,103 +315,218 @@ Return Return + + + 插件管理 + Plugin Management + + + + 插件详情 + Plugin details + + + + 设置 + Options + + + + 保存 + Save + + + + 保存成功 + Saved successfully + + + + 设置已保存 + Settings saved + + + + 进程ID + PID + + + + 插件名称 + Name + + + + 插件显示名称 + Display Name + + + + 插件描述 + Description + + + + 插件版本 + Version + + + + 作者 + Author + + + + 作者邮箱 + Author Email + + + + 插件URL + URL + + + + 插件状态 + Status + + + + 心跳时间 + Heartbeat Time + + + + 订阅事件 + Subscription Events + MainWindow - + 游戏 Game - + 设置 Options - + 帮助 Help - + 新游戏 New Game - + 初级 Beginner - + 中级 Intermediate - + 高级 Expert - + 自定义 Custom - + 退出 Exit - + 游戏设置 Game Settings - + 快捷键设置 Shortcut Settings - + 打开 Open - + 语言设置 Language - + 元扫雷 Metasweeper - + 关于 About - + 鼠标设置 Mouse Settings - + 保存 Save - + 回放 Replay - + 检查更新 Check update + + + 查看 + + + + + 录像所在位置 + + + + + 设置所在位置 + + + + + 成就 + + + + + 个人纪录 + + + + + 插件 + + diff --git a/src/ui/pl_PL.ts b/src/ui/pl_PL.ts index 5e1003b..bee306a 100644 --- a/src/ui/pl_PL.ts +++ b/src/ui/pl_PL.ts @@ -14,32 +14,32 @@ Anuluj - + 结束后标雷 Po zakończeniu znaku - + 标识: Logotyp: - + 永远使用筛选法埋雷(不推荐) Zawsze używaj metody przesiewowej do zakopywania min (niezalecane) - + 自动保存录像(推荐) Automatyczne zapisywanie nagrań (zalecane) - + 游戏模式: Tryb gry: - + 方格边长: Długość boku siatki: @@ -94,12 +94,12 @@ Ograniczenia sytuacyjne: - + 自动重开: Automatyczne ponowne otwarcie: - + 国家或地区: Kraj lub region: @@ -251,42 +251,42 @@ Starszy - + 勾选后永远使用筛选法埋雷,否则会适时改用调整法 Po zaznaczeniu tej opcji należy zawsze stosować metodę przesiewania do zakopywania min; w przeciwnym razie należy odpowiednio przełączyć się na metodę regulacji - + 完成后自动将录像保存到replay文件夹下 Po zakończeniu nagranie zostanie automatycznie zapisane w folderze „replay” - + 允许纪录弹窗(推荐) Zezwól na powiadomienia wyskakujące (zalecane) - + 用于参加比赛 Do użytku podczas zawodów - + 比赛标识: Logo konkursu: - + 光标不能超出边框 Kursor nie może wychodzić poza granice - + 用于与其他人相区分,但不希望排名网站和软件展示出来 Służy do odróżnienia się od innych, ale nie jest przeznaczony do wyświetlania przez strony internetowe i oprogramowanie zajmujące się rankingami - + 个性标识: Identyfikacja spersonalizowana: @@ -306,7 +306,7 @@ Reguluj prędkość odtwarzania, przesuwając pokrętło - + 自动保存录像集 Automatycznie zapisywana kolekcja filmów @@ -315,103 +315,218 @@ Return Return + + + 插件管理 + + + + + 插件详情 + + + + + 设置 + Zakładać + + + + 保存 + Zapisz + + + + 保存成功 + + + + + 设置已保存 + + + + + 进程ID + + + + + 插件名称 + + + + + 插件显示名称 + + + + + 插件描述 + + + + + 插件版本 + + + + + 作者 + + + + + 作者邮箱 + + + + + 插件URL + + + + + 插件状态 + + + + + 心跳时间 + + + + + 订阅事件 + + MainWindow - + 元扫雷 Trałowiec Meta - + 游戏 Gra - + 设置 Zakładać - + 语言设置 Ustawienia językowe - + 帮助 Pomoc - + 新游戏 Nowa gra - + 初级 młodszy - + 中级 pośredni - + 高级 Starszy - + 自定义 Dostosowywania - + 退出 kończyć - + 游戏设置 Ustawienia gry - + 关于 o - + 快捷键设置 Ustawienia skrótu - + 打开 Otwórz go - + 鼠标设置 Ustawienia myszy - + 保存 Zapisz - + 回放 Powtórka - + 检查更新 Sprawdź dostępność aktualizacji + + + 查看 + + + + + 录像所在位置 + + + + + 设置所在位置 + + + + + 成就 + + + + + 个人纪录 + + + + + 插件 + + diff --git "a/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" "b/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" index dcb9998..8726d36 100644 --- "a/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" +++ "b/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" @@ -1,5 +1,5 @@ -pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py -ts en_US.ts -noobsolete +pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts en_US.ts -noobsolete -pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py -ts pl_PL.ts -noobsolete +pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts pl_PL.ts -noobsolete -pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py -ts de_DE.ts -noobsolete \ No newline at end of file +pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts de_DE.ts -noobsolete