feat: TurboSu initial release - racing telemetry dashboard

This commit is contained in:
Yei.J. (AskaEth)
2026-07-25 15:23:40 +08:00
commit d61bb61a83
61 changed files with 5270 additions and 0 deletions
+191
View File
@@ -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()