feat: TurboSu initial release - racing telemetry dashboard
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user