diff --git a/games/game_sts2.py b/games/game_sts2.py index fe992e8..6f3ecef 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -1,36 +1,50 @@ +import json +import re from pathlib import Path +from typing import cast -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo, qInfo, qWarning import mobase -from ..basic_features import BasicModDataChecker, GlobPatterns from ..basic_features.basic_save_game_info import BasicGameSaveGame from ..basic_game import BasicGame +from ..steam_utils import find_steam_path -class SlayTheSpire2ModDataChecker(BasicModDataChecker): - def __init__(self): - super().__init__( - GlobPatterns( - valid=[ - "*.pck", - "*.dll", - "*.json", - ], - move={ - "*/*.pck": "", - "*/*.dll": "", - "*/*.json": "", - }, - ) +class SlayTheSpire2ModDataChecker(mobase.ModDataChecker): + _VALID_EXTENSIONS = (".pck", ".dll", ".json") + + def _has_mod_files(self, filetree: mobase.IFileTree) -> bool: + return any( + entry.isFile() and entry.name().endswith(self._VALID_EXTENSIONS) + for entry in filetree ) + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + if self._has_mod_files(filetree): + return mobase.ModDataChecker.VALID + for entry in filetree: + if entry.isDir() and self._has_mod_files(cast(mobase.IFileTree, entry)): + return mobase.ModDataChecker.FIXABLE + return mobase.ModDataChecker.INVALID + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + for entry in list(filetree): + if entry.isDir(): + tree = cast(mobase.IFileTree, entry) + if self._has_mod_files(tree): + filetree.merge(tree) + entry.detach() + return filetree + class SlayTheSpire2Game(BasicGame): Name = "Slay the Spire 2 Support Plugin" Author = "Azlle" - Version = "1.0.1" + Version = "1.1.0" GameName = "Slay the Spire 2" GameShortName = "slaythespire2" @@ -41,23 +55,82 @@ class SlayTheSpire2Game(BasicGame): GameDataPath = "mods" GameDocumentsDirectory = "%USERPROFILE%/AppData/Roaming/SlayTheSpire2" + _last_save_dir: str = "" + def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self._register_feature(SlayTheSpire2ModDataChecker()) + organizer.modList().onModInstalled(self._on_mod_installed) return True def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): mods_path = Path(self.dataDirectory().absolutePath()) - mods_path.mkdir(exist_ok=True) + if not mods_path.exists(): + qInfo(f"Creating mods directory: {mods_path}") + mods_path.mkdir() super().initializeProfile(directory, settings) + def _on_mod_installed(self, mod: mobase.IModInterface): + mod_name = mod.name() + self._organizer.onNextRefresh( + lambda: self._apply_version(self._organizer.modList().getMod(mod_name)), + True, + ) + + def _apply_version(self, mod: mobase.IModInterface | None): + if mod is None: + return + mod_path = Path(mod.absolutePath()) + for json_file in mod_path.glob("*.json"): + try: + with open(json_file, encoding="utf-8-sig") as f: + data = json.load(f) + if version := data.get("version"): + version = version.lstrip("v") + meta_ini = mod_path / "meta.ini" + raw = meta_ini.read_bytes() + raw = re.sub( + rb"^\s*version\s*=\s*[^\r\n]*", + f"version={version}".encode(), + raw, + flags=re.MULTILINE, + ) + meta_ini.write_bytes(raw) + qInfo( + f"Set version of {mod_path.name} to {version} using {json_file.name}" + ) + self._organizer.modDataChanged(mod) + break + except (json.JSONDecodeError, OSError) as e: + qWarning( + f"Failed to apply version for {mod_path.name} via {json_file.name}: {e}" + ) + continue + def savesDirectory(self) -> QDir: - docs = QDir(self.documentsDirectory()) - steam_dir = Path(docs.absoluteFilePath("steam")) - if steam_dir.exists(): - entries = [e for e in steam_dir.iterdir() if e.is_dir()] - if entries: - return QDir(str(entries[0])) + steam_dir = Path(self.documentsDirectory().absolutePath()) / "steam" + candidates = [] + is_fallback = False + steam_path = find_steam_path() + if steam_path is not None: + userdata = steam_path / "userdata" + if userdata.exists(): + candidates = [ + child / "2868840" / "remote" + for child in userdata.iterdir() + if child.is_dir() and (child / "2868840" / "remote").exists() + ] + if not candidates: + is_fallback = True + if steam_dir.exists(): + candidates = [child for child in steam_dir.iterdir() if child.is_dir()] + if candidates: + save_dir = max(candidates, key=lambda p: p.stat().st_mtime) + if (s := str(save_dir)) != self._last_save_dir: + status = "not found, using AppData" if is_fallback else "found" + qInfo(f"Steam save directory {status}: {save_dir}") + self.__class__._last_save_dir = s + return QDir(s) return QDir(str(steam_dir)) def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: @@ -65,17 +138,17 @@ def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: return [ BasicGameSaveGame(save) for save in base.rglob("*") - if save.is_file() and save.suffix in (".save", ".run") + if save.is_file() and save.suffix in (".save", ".run", ".backup") ] def executables(self): return [ mobase.ExecutableInfo( - "Slay the Spire 2", + self.GameName, QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ), mobase.ExecutableInfo( - "Slay the Spire 2 (OpenGL)", + f"{self.GameName} (OpenGL)", QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ).withArgument("--rendering-driver opengl3"), ]