Skip to content

Commit 85a70a3

Browse files
committed
feat: 添加历史记录插件和录像保存功能
1 parent 3820caf commit 85a70a3

File tree

9 files changed

+1106
-42
lines changed

9 files changed

+1106
-42
lines changed

build.bat

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ if exist "%OUT%\plugin_manager" rmdir /s /q "%OUT%\plugin_manager"
1010

1111
echo.
1212
echo [1/3] metaminsweeper.exe
13-
pyinstaller --noconfirm --name metaminsweeper --windowed --distpath %OUT% src\main.py
13+
pyinstaller --noconfirm --name metaminsweeper --windowed --distpath %OUT% ^
14+
--icon src/media/cat.ico ^
15+
--clean ^
16+
--paths src ^
17+
--add-data "src/media;media" ^
18+
src\main.py
1419

1520
echo.
1621
echo [2/3] plugin_manager.exe
17-
pyinstaller --noconfirm --name plugin_manager --windowed --hidden-import code --hidden-import xmlrpc --hidden-import xmlrpc.server --hidden-import xmlrpc.client --hidden-import http.server --hidden-import socketserver --hidden-import email --hidden-import email.utils --distpath %OUT% src\plugin_manager\_run.py
22+
pyinstaller --noconfirm --name plugin_manager --windowed --hidden-import sqlite3 --hidden-import code --hidden-import xmlrpc --hidden-import xmlrpc.server --hidden-import xmlrpc.client --hidden-import http.server --hidden-import socketserver --hidden-import email --hidden-import email.utils --distpath %OUT% src\plugin_manager\_run.py
1823

1924
echo.
2025
echo [3/3] Copy resources to metaminsweeper\

src/lib_zmq_plugins/client/zmq_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,11 @@ def _handle_sub_message(self) -> None:
283283
topic = msg[0].decode("utf-8", errors="replace")
284284
try:
285285
event = self._serializer.decode_event(msg[1])
286-
except Exception:
287-
self._log.warning("Failed to decode event for topic: %s", topic, exc_info=True)
286+
except Exception as e:
287+
self._log.warning(
288+
f"Failed to decode event for topic: {topic}, Exception: {e}",
289+
exc_info=True,
290+
)
288291
return
289292
self._notify_subscribers(topic, event)
290293

src/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def cli_check_file(file_path: str) -> int:
156156

157157
# ── 启动 ZMQ Server + 插件管理器 ──
158158
game_server = GameServerBridge(ui)
159+
ui.gameServerBridge = game_server
159160

160161
# 打包后直接调用 plugin_manager.exe,开发模式用 python -m
161162
if getattr(sys, 'frozen', False):

src/mineSweeperGUI.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
# from PyQt5.QtWidgets import QLineEdit, QInputDialog, QShortcut
77
# from PyQt5.QtWidgets import QApplication, QFileDialog, QWidget
88
import gameDefinedParameter
9+
from plugin_manager.server_bridge import GameServerBridge
10+
from shared_types.events import VideoSaveEvent
911
import superGUI
1012
import gameAbout
1113
import gameSettings
@@ -146,6 +148,7 @@ def save_evf_file_integrated():
146148
# 不带后缀、有绝对路径的、不含最后次数的文件名
147149
# C:/path/zhangsan_20251111_190114_
148150
self.old_evfs_filename = ""
151+
self.gameServerBridge: GameServerBridge = None
149152

150153
@property
151154
def pixSize(self):
@@ -557,6 +560,16 @@ def gameFinished(self):
557560
status = utils.GameBoardState(ms_board.game_board_state)
558561
if status == utils.GameBoardState.Win:
559562
self.dump_evf_file_data()
563+
event = VideoSaveEvent()
564+
data = msgspec.structs.asdict(event)
565+
for key in data:
566+
if hasattr(ms_board, key):
567+
if key == "raw_data":
568+
data[key] = base64.b64encode(ms_board.raw_data).decode("utf-8")
569+
continue
570+
data[key] = getattr(ms_board, key)
571+
event = VideoSaveEvent(**data)
572+
self.gameServerBridge._server.publish(VideoSaveEvent, event)
560573

561574
def gameWin(self): # 成功后改脸和状态变量,停时间
562575
self.timer_10ms.stop()

src/plugin_manager/app_paths.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,32 @@ def get_all_plugin_dirs() -> list[Path]:
112112
return get_builtin_plugin_dirs() + get_user_plugin_dirs()
113113

114114

115+
def get_plugin_data_dir(plugin_class: type | str) -> Path:
116+
"""
117+
获取指定插件的专属数据目录(可写)
118+
119+
根据传入的插件类或名称,在 data/plugin_data/ 下创建对应子目录。
120+
每个插件拥有独立的数据空间,互不干扰。
121+
122+
- 开发模式: <project>/data/plugin_data/<plugin_name>/
123+
- 打包模式: <exe所在目录>/data/plugin_data/<plugin_name>/
124+
125+
Args:
126+
plugin_class: 插件类或插件名称字符串
127+
128+
Returns:
129+
插件的专属数据目录路径
130+
"""
131+
if isinstance(plugin_class, type):
132+
name = plugin_class.__name__
133+
else:
134+
name = str(plugin_class)
135+
136+
plugin_data_dir = get_data_dir() / "plugin_data" / name
137+
plugin_data_dir.mkdir(parents=True, exist_ok=True)
138+
return plugin_data_dir
139+
140+
115141
# ── 环境变量补丁(给子进程使用) ───────────────────────
116142

117143
def patch_sys_path_for_frozen() -> None:

src/plugin_manager/plugin_base.py

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
if TYPE_CHECKING:
1919
from PyQt5.QtGui import QIcon
2020

21-
from PyQt5.QtCore import Qt
21+
from PyQt5.QtCore import Qt, QObject
2222
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QPen, QColor, QBrush, QFont
2323

2424
from lib_zmq_plugins.shared.base import BaseEvent, get_event_tag
@@ -169,31 +169,41 @@ def __init__(self, info: PluginInfo):
169169
log_config=info.log_config, # 插件可自定义轮转策略
170170
)
171171
self._log_level: LogLevel = info.log_level # 当前日志级别
172-
172+
173173
# ═══════════════════════════════════════════════════════════════════
174174
# 属性
175175
# ═══════════════════════════════════════════════════════════════════
176-
176+
177177
@property
178178
def info(self) -> PluginInfo:
179179
return self._info
180-
180+
181181
@property
182182
def name(self) -> str:
183183
return self._info.name
184-
184+
185185
@property
186186
def is_enabled(self) -> bool:
187187
return self._info.enabled
188-
188+
189189
@property
190190
def widget(self) -> QWidget | None:
191191
return self._widget
192-
192+
193193
@property
194194
def client(self) -> ZMQClient | None:
195195
return self._client
196196

197+
@property
198+
def data_dir(self) -> "Path":
199+
"""插件专属数据目录(可写),自动根据插件类名创建"""
200+
from pathlib import Path
201+
from .app_paths import get_plugin_data_dir
202+
203+
if not hasattr(self, "_data_dir"):
204+
self._data_dir = get_plugin_data_dir(type(self))
205+
return self._data_dir
206+
197207
@property
198208
def log_level(self) -> LogLevel:
199209
"""当前日志级别"""
@@ -219,47 +229,47 @@ def plugin_icon(self) -> QIcon:
219229
if self._info.icon:
220230
return self._info.icon
221231
return make_plugin_icon()
222-
232+
223233
# ═══════════════════════════════════════════════════════════════════
224234
# 生命周期
225235
# ═══════════════════════════════════════════════════════════════════
226-
236+
227237
def set_client(self, client: ZMQClient) -> None:
228238
self._client = client
229-
239+
230240
def set_event_dispatcher(self, dispatcher: EventDispatcher) -> None:
231241
self._event_dispatcher = dispatcher
232-
242+
233243
def initialize(self) -> None:
234244
"""初始化插件"""
235245
if self._initialized:
236246
return
237-
247+
238248
self._setup_subscriptions()
239249
self._widget = self._create_widget()
240250
self._initialized = True
241251
self.on_initialized()
242-
252+
243253
def shutdown(self) -> None:
244254
"""关闭插件"""
245255
if not self._initialized:
246256
return
247-
257+
248258
self.on_shutdown()
249-
259+
250260
if self._event_dispatcher:
251261
self._event_dispatcher.unsubscribe_all(self)
252-
262+
253263
if self._widget:
254264
self._widget.deleteLater()
255265
self._widget = None
256-
266+
257267
self._initialized = False
258-
268+
259269
# ═══════════════════════════════════════════════════════════════════
260270
# 抽象方法
261271
# ═══════════════════════════════════════════════════════════════════
262-
272+
263273
@abstractmethod
264274
def _setup_subscriptions(self) -> None:
265275
"""
@@ -270,27 +280,27 @@ def _setup_subscriptions(self) -> None:
270280
self.subscribe(BoardUpdateEvent, self._on_board_update)
271281
"""
272282
pass
273-
283+
274284
# ═══════════════════════════════════════════════════════════════════
275285
# 可选重写
276286
# ═══════════════════════════════════════════════════════════════════
277-
287+
278288
def _create_widget(self) -> QWidget | None:
279289
"""创建界面组件,返回 None 表示无界面"""
280290
return None
281-
291+
282292
def on_initialized(self) -> None:
283293
"""插件初始化完成回调"""
284294
pass
285-
295+
286296
def on_shutdown(self) -> None:
287297
"""插件关闭前回调"""
288298
pass
289-
299+
290300
# ═══════════════════════════════════════════════════════════════════
291301
# 事件订阅(使用事件类)
292302
# ═══════════════════════════════════════════════════════════════════
293-
303+
294304
def subscribe(
295305
self,
296306
event_class: type[_E],
@@ -306,43 +316,43 @@ def subscribe(
306316
if self._event_dispatcher:
307317
tag = get_event_tag(event_class)
308318
self._event_dispatcher.subscribe(tag, handler, self._info.priority, self)
309-
319+
310320
def unsubscribe(self, event_class: type[BaseEvent]) -> None:
311321
"""取消订阅事件"""
312322
if self._event_dispatcher:
313323
tag = get_event_tag(event_class)
314324
self._event_dispatcher.unsubscribe(tag, self)
315-
325+
316326
# ═══════════════════════════════════════════════════════════════════
317327
# 指令发送
318328
# ═══════════════════════════════════════════════════════════════════
319-
329+
320330
def send_command(self, command: Any) -> None:
321331
"""发送控制指令到主进程(异步)"""
322332
if self._client:
323333
self._client.send_command(command)
324-
334+
325335
def request(self, command: Any, timeout: float = 5.0) -> Any:
326336
"""发送请求并等待响应(同步)"""
327337
if self._client:
328338
return self._client.request(command, timeout)
329339
return None
330-
340+
331341
# ═══════════════════════════════════════════════════════════════════
332342
# 辅助
333343
# ═══════════════════════════════════════════════════════════════════
334-
344+
335345
def enable(self) -> None:
336346
"""启用插件"""
337347
self._info.enabled = True
338348
if not self._initialized:
339349
self.initialize()
340-
350+
341351
def disable(self) -> None:
342352
"""禁用插件"""
343353
self._info.enabled = False
344354
if self._initialized:
345355
self.shutdown()
346-
356+
347357
def __repr__(self) -> str:
348358
return f"<Plugin {self._info.name} v{self._info.version}>"

src/plugin_manager/plugin_manager.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@
2828
logger = loguru.logger.bind(name="PluginManager")
2929

3030

31+
class _LogHandler(LogHandler):
32+
def debug(self, msg: str, /, *args: object, **kwargs: object) -> None:
33+
logger.debug(msg, *args, **kwargs)
34+
35+
def info(self, msg: str, /, *args: object, **kwargs: object) -> None:
36+
logger.info(msg, *args, **kwargs)
37+
38+
def warning(self, msg: str, /, *args: object, **kwargs: object) -> None:
39+
logger.warning(msg, *args, **kwargs)
40+
41+
def error(self, msg: str, /, *args: object, **kwargs: object) -> None:
42+
logger.error(msg, *args, **kwargs)
43+
44+
def critical(self, msg: str, /, *args: object, **kwargs: object) -> None:
45+
logger.critical(msg, *args, **kwargs)
46+
47+
3148
class PluginManager:
3249
"""
3350
插件管理器
@@ -307,10 +324,9 @@ def run_plugin_manager_process(
307324
退出代码
308325
"""
309326
manager = PluginManager(
310-
endpoint=endpoint,
311-
plugin_dirs=plugin_dirs,
327+
endpoint=endpoint, plugin_dirs=plugin_dirs, log_handler=_LogHandler()
312328
)
313-
329+
314330
try:
315331
if with_gui:
316332
return manager.exec_gui(show_main_window=show_main_window)
@@ -323,5 +339,5 @@ def run_plugin_manager_process(
323339
pass
324340
finally:
325341
manager.stop()
326-
342+
327343
return 0

0 commit comments

Comments
 (0)