Skip to content

Commit e276770

Browse files
authored
Merge pull request #87 from ljzloser/master
添加历史记录插件和录像保存功能
2 parents 5971cc8 + 9f82f47 commit e276770

File tree

11 files changed

+1120
-56
lines changed

11 files changed

+1120
-56
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: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def get_builtin_plugin_dirs() -> list[Path]:
8787
plugin_dir = bundle / "plugins"
8888
if plugin_dir.is_dir():
8989
return [plugin_dir]
90-
logger.warning("内置插件目录不存在: %s", plugin_dir)
90+
logger.warning(f"内置插件目录不存在: {plugin_dir}")
9191
return []
9292

9393

@@ -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:
@@ -127,7 +153,7 @@ def patch_sys_path_for_frozen() -> None:
127153
bundle = str(get_bundle_dir())
128154
if bundle not in sys.path:
129155
sys.path.insert(0, bundle)
130-
logger.debug("已将 bundle 目录加入 sys.path: %s", bundle)
156+
logger.debug(f"已将 bundle 目录加入 sys.path: {bundle}")
131157

132158

133159
def get_env_for_subprocess(env: dict | None = None) -> dict:
@@ -149,7 +175,7 @@ def get_env_for_subprocess(env: dict | None = None) -> dict:
149175
paths.append(existing)
150176
env["PYTHONPATH"] = os.pathsep.join(paths)
151177

152-
logger.debug("子进程环境: PYTHONPATH=%s", env.get("PYTHONPATH"))
178+
logger.debug(f"子进程环境: PYTHONPATH={env.get('PYTHONPATH')}")
153179
return env
154180

155181

src/plugin_manager/main_window.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,7 @@ def _open_plugin_log(self, name: str) -> None:
889889
else:
890890
subprocess.Popen(["xdg-open", str(log_file)])
891891
except Exception as e:
892-
logger.warning("Failed to open log file %s: %s", log_file, e)
892+
logger.warning(f"Failed to open log file {log_file}: {e}")
893893

894894
def _open_plugin_settings(self, name: str) -> None:
895895
"""打开插件设置对话框"""
@@ -977,13 +977,13 @@ def _on_list_double_clicked(self, item) -> None:
977977
# ── 标签页弹出/嵌回 ─────────────────────────────────
978978

979979
def _on_tab_detached(self, name: str) -> None:
980-
logger.debug("Tab detached: %s", name)
980+
logger.debug(f"Tab detached: {name}")
981981

982982
def _on_tab_attached(self, name: str) -> None:
983-
logger.debug("Tab attached back: %s", name)
983+
logger.debug(f"Tab attached back: {name}")
984984

985985
def _on_tab_closed(self, name: str) -> None:
986-
logger.debug("Tab closed: %s", name)
986+
logger.debug(f"Tab closed: {name}")
987987
self._closed_plugins.add(name)
988988

989989
# ── 窗口事件 ────────────────────────────────────────

src/plugin_manager/plugin_base.py

Lines changed: 44 additions & 34 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
"""当前日志级别"""
@@ -211,55 +221,55 @@ def set_log_level(self, level: LogLevel | str) -> None:
211221
level = LogLevel(level.upper())
212222
self._log_level = level
213223
set_plugin_log_level(self._log_sink_id, level)
214-
self.logger.debug("Log level changed to %s", level)
224+
self.logger.debug(f"Log level changed to {level}")
215225

216226
@property
217227
def plugin_icon(self) -> QIcon:
218228
"""返回插件图标(使用 PluginInfo.icon,未设置则生成默认图标)"""
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}>"

0 commit comments

Comments
 (0)