Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ if exist "%OUT%\plugin_manager" rmdir /s /q "%OUT%\plugin_manager"

echo.
echo [1/3] metaminsweeper.exe
pyinstaller --noconfirm --name metaminsweeper --windowed --distpath %OUT% src\main.py
pyinstaller --noconfirm --name metaminsweeper --windowed --distpath %OUT% ^
--icon src/media/cat.ico ^
--clean ^
--paths src ^
--add-data "src/media;media" ^
src\main.py

echo.
echo [2/3] plugin_manager.exe
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
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

echo.
echo [3/3] Copy resources to metaminsweeper\
Expand Down
7 changes: 5 additions & 2 deletions src/lib_zmq_plugins/client/zmq_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,11 @@ def _handle_sub_message(self) -> None:
topic = msg[0].decode("utf-8", errors="replace")
try:
event = self._serializer.decode_event(msg[1])
except Exception:
self._log.warning("Failed to decode event for topic: %s", topic, exc_info=True)
except Exception as e:
self._log.warning(
f"Failed to decode event for topic: {topic}, Exception: {e}",
exc_info=True,
)
return
self._notify_subscribers(topic, event)

Expand Down
1 change: 1 addition & 0 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ def cli_check_file(file_path: str) -> int:

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

# 打包后直接调用 plugin_manager.exe,开发模式用 python -m
if getattr(sys, 'frozen', False):
Expand Down
13 changes: 13 additions & 0 deletions src/mineSweeperGUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
# from PyQt5.QtWidgets import QLineEdit, QInputDialog, QShortcut
# from PyQt5.QtWidgets import QApplication, QFileDialog, QWidget
import gameDefinedParameter
from plugin_manager.server_bridge import GameServerBridge
from shared_types.events import VideoSaveEvent
import superGUI
import gameAbout
import gameSettings
Expand Down Expand Up @@ -146,6 +148,7 @@ def save_evf_file_integrated():
# 不带后缀、有绝对路径的、不含最后次数的文件名
# C:/path/zhangsan_20251111_190114_
self.old_evfs_filename = ""
self.gameServerBridge: GameServerBridge = None

@property
def pixSize(self):
Expand Down Expand Up @@ -557,6 +560,16 @@ def gameFinished(self):
status = utils.GameBoardState(ms_board.game_board_state)
if status == utils.GameBoardState.Win:
self.dump_evf_file_data()
event = VideoSaveEvent()
data = msgspec.structs.asdict(event)
for key in data:
if hasattr(ms_board, key):
if key == "raw_data":
data[key] = base64.b64encode(ms_board.raw_data).decode("utf-8")
continue
data[key] = getattr(ms_board, key)
event = VideoSaveEvent(**data)
self.gameServerBridge._server.publish(VideoSaveEvent, event)

def gameWin(self): # 成功后改脸和状态变量,停时间
self.timer_10ms.stop()
Expand Down
32 changes: 29 additions & 3 deletions src/plugin_manager/app_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def get_builtin_plugin_dirs() -> list[Path]:
plugin_dir = bundle / "plugins"
if plugin_dir.is_dir():
return [plugin_dir]
logger.warning("内置插件目录不存在: %s", plugin_dir)
logger.warning(f"内置插件目录不存在: {plugin_dir}")
return []


Expand All @@ -112,6 +112,32 @@ def get_all_plugin_dirs() -> list[Path]:
return get_builtin_plugin_dirs() + get_user_plugin_dirs()


def get_plugin_data_dir(plugin_class: type | str) -> Path:
"""
获取指定插件的专属数据目录(可写)

根据传入的插件类或名称,在 data/plugin_data/ 下创建对应子目录。
每个插件拥有独立的数据空间,互不干扰。

- 开发模式: <project>/data/plugin_data/<plugin_name>/
- 打包模式: <exe所在目录>/data/plugin_data/<plugin_name>/

Args:
plugin_class: 插件类或插件名称字符串

Returns:
插件的专属数据目录路径
"""
if isinstance(plugin_class, type):
name = plugin_class.__name__
else:
name = str(plugin_class)

plugin_data_dir = get_data_dir() / "plugin_data" / name
plugin_data_dir.mkdir(parents=True, exist_ok=True)
return plugin_data_dir


# ── 环境变量补丁(给子进程使用) ───────────────────────

def patch_sys_path_for_frozen() -> None:
Expand All @@ -127,7 +153,7 @@ def patch_sys_path_for_frozen() -> None:
bundle = str(get_bundle_dir())
if bundle not in sys.path:
sys.path.insert(0, bundle)
logger.debug("已将 bundle 目录加入 sys.path: %s", bundle)
logger.debug(f"已将 bundle 目录加入 sys.path: {bundle}")


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

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


Expand Down
8 changes: 4 additions & 4 deletions src/plugin_manager/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ def _open_plugin_log(self, name: str) -> None:
else:
subprocess.Popen(["xdg-open", str(log_file)])
except Exception as e:
logger.warning("Failed to open log file %s: %s", log_file, e)
logger.warning(f"Failed to open log file {log_file}: {e}")

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

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

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

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

# ── 窗口事件 ────────────────────────────────────────
Expand Down
78 changes: 44 additions & 34 deletions src/plugin_manager/plugin_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
if TYPE_CHECKING:
from PyQt5.QtGui import QIcon

from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QObject
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QPen, QColor, QBrush, QFont

from lib_zmq_plugins.shared.base import BaseEvent, get_event_tag
Expand Down Expand Up @@ -169,31 +169,41 @@ def __init__(self, info: PluginInfo):
log_config=info.log_config, # 插件可自定义轮转策略
)
self._log_level: LogLevel = info.log_level # 当前日志级别

# ═══════════════════════════════════════════════════════════════════
# 属性
# ═══════════════════════════════════════════════════════════════════

@property
def info(self) -> PluginInfo:
return self._info

@property
def name(self) -> str:
return self._info.name

@property
def is_enabled(self) -> bool:
return self._info.enabled

@property
def widget(self) -> QWidget | None:
return self._widget

@property
def client(self) -> ZMQClient | None:
return self._client

@property
def data_dir(self) -> "Path":
"""插件专属数据目录(可写),自动根据插件类名创建"""
from pathlib import Path
from .app_paths import get_plugin_data_dir

if not hasattr(self, "_data_dir"):
self._data_dir = get_plugin_data_dir(type(self))
return self._data_dir

@property
def log_level(self) -> LogLevel:
"""当前日志级别"""
Expand All @@ -211,55 +221,55 @@ def set_log_level(self, level: LogLevel | str) -> None:
level = LogLevel(level.upper())
self._log_level = level
set_plugin_log_level(self._log_sink_id, level)
self.logger.debug("Log level changed to %s", level)
self.logger.debug(f"Log level changed to {level}")

@property
def plugin_icon(self) -> QIcon:
"""返回插件图标(使用 PluginInfo.icon,未设置则生成默认图标)"""
if self._info.icon:
return self._info.icon
return make_plugin_icon()

# ═══════════════════════════════════════════════════════════════════
# 生命周期
# ═══════════════════════════════════════════════════════════════════

def set_client(self, client: ZMQClient) -> None:
self._client = client

def set_event_dispatcher(self, dispatcher: EventDispatcher) -> None:
self._event_dispatcher = dispatcher

def initialize(self) -> None:
"""初始化插件"""
if self._initialized:
return

self._setup_subscriptions()
self._widget = self._create_widget()
self._initialized = True
self.on_initialized()

def shutdown(self) -> None:
"""关闭插件"""
if not self._initialized:
return

self.on_shutdown()

if self._event_dispatcher:
self._event_dispatcher.unsubscribe_all(self)

if self._widget:
self._widget.deleteLater()
self._widget = None

self._initialized = False

# ═══════════════════════════════════════════════════════════════════
# 抽象方法
# ═══════════════════════════════════════════════════════════════════

@abstractmethod
def _setup_subscriptions(self) -> None:
"""
Expand All @@ -270,27 +280,27 @@ def _setup_subscriptions(self) -> None:
self.subscribe(BoardUpdateEvent, self._on_board_update)
"""
pass

# ═══════════════════════════════════════════════════════════════════
# 可选重写
# ═══════════════════════════════════════════════════════════════════

def _create_widget(self) -> QWidget | None:
"""创建界面组件,返回 None 表示无界面"""
return None

def on_initialized(self) -> None:
"""插件初始化完成回调"""
pass

def on_shutdown(self) -> None:
"""插件关闭前回调"""
pass

# ═══════════════════════════════════════════════════════════════════
# 事件订阅(使用事件类)
# ═══════════════════════════════════════════════════════════════════

def subscribe(
self,
event_class: type[_E],
Expand All @@ -306,43 +316,43 @@ def subscribe(
if self._event_dispatcher:
tag = get_event_tag(event_class)
self._event_dispatcher.subscribe(tag, handler, self._info.priority, self)

def unsubscribe(self, event_class: type[BaseEvent]) -> None:
"""取消订阅事件"""
if self._event_dispatcher:
tag = get_event_tag(event_class)
self._event_dispatcher.unsubscribe(tag, self)

# ═══════════════════════════════════════════════════════════════════
# 指令发送
# ═══════════════════════════════════════════════════════════════════

def send_command(self, command: Any) -> None:
"""发送控制指令到主进程(异步)"""
if self._client:
self._client.send_command(command)

def request(self, command: Any, timeout: float = 5.0) -> Any:
"""发送请求并等待响应(同步)"""
if self._client:
return self._client.request(command, timeout)
return None

# ═══════════════════════════════════════════════════════════════════
# 辅助
# ═══════════════════════════════════════════════════════════════════

def enable(self) -> None:
"""启用插件"""
self._info.enabled = True
if not self._initialized:
self.initialize()

def disable(self) -> None:
"""禁用插件"""
self._info.enabled = False
if self._initialized:
self.shutdown()

def __repr__(self) -> str:
return f"<Plugin {self._info.name} v{self._info.version}>"
Loading
Loading