from __future__ import annotations import json import importlib.util import sys from dataclasses import dataclass, field, asdict from pathlib import Path from typing import Any from utils.logger import get_logger logger = get_logger(__name__) GAMES_DIR = Path(__file__).resolve().parent.parent / "games" BUILTIN_DIR = GAMES_DIR / "builtin" USER_DIR = GAMES_DIR / "user" @dataclass class GamePlugin: id: str = "" name: str = "" parser_type: str = "forza" telemetry_format: str = "" description: str = "" author: str = "" version: str = "1.0.0" default_port: int = 20777 icon: str = "" is_builtin: bool = True enabled: bool = True manifest_path: str = "" def to_dict(self) -> dict[str, Any]: return asdict(self) @classmethod def from_manifest(cls, path: Path, is_builtin: bool = True) -> GamePlugin | None: try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) gp = cls( id=data.get("id", path.parent.name), name=data.get("name", path.parent.name), parser_type=data.get("parser_type", "forza"), telemetry_format=data.get("telemetry_format", ""), description=data.get("description", ""), author=data.get("author", ""), version=data.get("version", "1.0.0"), default_port=data.get("default_port", 20777), icon=data.get("icon", ""), is_builtin=is_builtin, enabled=True, manifest_path=str(path.parent), ) return gp except Exception as e: logger.error("Failed to load game plugin manifest %s: %s", path, e) return None class GamePluginManager: def __init__(self): BUILTIN_DIR.mkdir(parents=True, exist_ok=True) USER_DIR.mkdir(parents=True, exist_ok=True) self._plugins: dict[str, GamePlugin] = {} self._parser_cache: dict[str, Any] = {} self._discover() def _discover(self): self._plugins.clear() for d in [BUILTIN_DIR, USER_DIR]: if not d.exists(): continue is_builtin = (d == BUILTIN_DIR) for item in d.iterdir(): if item.is_dir(): manifest = item / "manifest.json" if manifest.exists(): gp = GamePlugin.from_manifest(manifest, is_builtin) if gp: self._plugins[gp.id] = gp logger.debug("Discovered game plugin: %s", gp.id) def list_all(self) -> list[dict[str, Any]]: self._discover() return [p.to_dict() for p in self._plugins.values()] def get(self, plugin_id: str) -> GamePlugin | None: self._discover() return self._plugins.get(plugin_id) def get_parser(self, plugin_id: str) -> Any | None: self._discover() gp = self._plugins.get(plugin_id) if not gp: return None cache_key = gp.id if cache_key in self._parser_cache: return self._parser_cache[cache_key] parser_dir = Path(gp.manifest_path) parser_file = parser_dir / "parser.py" if not parser_file.exists(): return None try: module_name = f"turbosu_game_{gp.id}" spec = importlib.util.spec_from_file_location(module_name, parser_file) if spec and spec.loader: module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) if hasattr(module, "get_parser"): parser = module.get_parser() self._parser_cache[cache_key] = parser logger.info("Loaded parser for game: %s", gp.name) return parser except Exception as e: logger.error("Failed to load parser for %s: %s", gp.id, e) return None def reload(self): self._discover() self._parser_cache.clear() def install_plugin(self, data: dict[str, Any]) -> bool: plugin_id = data.get("id", "") if not plugin_id: return False dest_dir = USER_DIR / plugin_id dest_dir.mkdir(parents=True, exist_ok=True) manifest = data.get("manifest", {}) with open(dest_dir / "manifest.json", "w", encoding="utf-8") as f: json.dump(manifest, f, indent=2, ensure_ascii=False) if data.get("parser_code"): with open(dest_dir / "parser.py", "w", encoding="utf-8") as f: f.write(data["parser_code"]) self._discover() logger.info("Plugin installed: %s", plugin_id) return True def remove_plugin(self, plugin_id: str) -> bool: gp = self._plugins.get(plugin_id) if gp and gp.is_builtin: logger.warning("Cannot remove builtin plugin: %s", plugin_id) return False import shutil 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 export_plugin(self, plugin_id: str) -> dict[str, Any] | None: gp = self._plugins.get(plugin_id) if not gp: return None parser_dir = Path(gp.manifest_path) manifest_file = parser_dir / "manifest.json" parser_file = parser_dir / "parser.py" result: dict[str, Any] = { "type": "game_plugin", "version": "1.0", "manifest": {}, "parser_code": "", } if manifest_file.exists(): with open(manifest_file, "r", encoding="utf-8") as f: result["manifest"] = json.load(f) if parser_file.exists(): with open(parser_file, "r", encoding="utf-8") as f: result["parser_code"] = f.read() return result game_plugin_manager = GamePluginManager()