192 lines
6.1 KiB
Python
192 lines
6.1 KiB
Python
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()
|