From 0e5de57e45479768542ebe3610c11203ca42db1e Mon Sep 17 00:00:00 2001 From: Azlle Date: Fri, 24 Apr 2026 14:27:24 +0900 Subject: [PATCH 01/11] feat: read version from mod JSON and apply to meta.ini --- games/game_sts2.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index fe992e8..5be2001 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -1,6 +1,8 @@ +import json +import re from pathlib import Path -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo, qInfo, qWarning import mobase @@ -44,6 +46,7 @@ class SlayTheSpire2Game(BasicGame): 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): @@ -51,6 +54,32 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): mods_path.mkdir(exist_ok=True) 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")) From 22995c569fdbde77140b98cc21033396c54a6115 Mon Sep 17 00:00:00 2001 From: Azlle Date: Fri, 24 Apr 2026 14:31:40 +0900 Subject: [PATCH 02/11] feat: detect saves from userdata or AppData with mtime selection --- games/game_sts2.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index 5be2001..b42e34d 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -9,6 +9,7 @@ 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): @@ -43,6 +44,8 @@ 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()) @@ -80,13 +83,31 @@ def _apply_version(self, mod: mobase.IModInterface | None): 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]: From 85df410b9e8a7ff99c3b288c35b567c2df82f7f4 Mon Sep 17 00:00:00 2001 From: Azlle Date: Fri, 24 Apr 2026 14:51:50 +0900 Subject: [PATCH 03/11] fix: add existence check and qInfo log when creating mods directory --- games/game_sts2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index b42e34d..86e1be7 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -54,7 +54,9 @@ def init(self, organizer: mobase.IOrganizer) -> bool: 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): From 359c6c4c34da81af7e6272fb9d91345683c9b7cb Mon Sep 17 00:00:00 2001 From: Azlle Date: Fri, 24 Apr 2026 14:42:56 +0900 Subject: [PATCH 04/11] feat: add .backup to recognized save extensions --- games/game_sts2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index 86e1be7..e689612 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -117,7 +117,7 @@ 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): From 4098c0d313c9283724ca298a04c9aa60689ba66f Mon Sep 17 00:00:00 2001 From: Azlle Date: Fri, 24 Apr 2026 14:45:27 +0900 Subject: [PATCH 05/11] refactor: use GameName attribute for executable titles --- games/game_sts2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index e689612..c5ab860 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -123,11 +123,11 @@ def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: 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"), ] From 17d7b401b841782b12c3bf55a19bb79373d004b8 Mon Sep 17 00:00:00 2001 From: Azlle Date: Fri, 24 Apr 2026 14:20:48 +0900 Subject: [PATCH 06/11] chore: bump version to 1.1.0 --- games/game_sts2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index c5ab860..e1009d1 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -33,7 +33,7 @@ def __init__(self): 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" From 8d84709c9140602c1b275ee0315f67c11589dca4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 06:49:53 +0000 Subject: [PATCH 07/11] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- games/game_sts2.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index e1009d1..9060022 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -62,7 +62,8 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): 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 + lambda: self._apply_version(self._organizer.modList().getMod(mod_name)), + True, ) def _apply_version(self, mod: mobase.IModInterface | None): @@ -77,13 +78,22 @@ def _apply_version(self, mod: mobase.IModInterface | None): 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) + 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}") + 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}") + qWarning( + f"Failed to apply version for {mod_path.name} via {json_file.name}: {e}" + ) continue def savesDirectory(self) -> QDir: From cd3edafad666e3e6871a78afdefebaac6fd4a68f Mon Sep 17 00:00:00 2001 From: Azlle Date: Sat, 25 Apr 2026 14:21:02 +0900 Subject: [PATCH 08/11] feat: improve mod data checker to handle nested mod structures --- games/game_sts2.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index 9060022..fe420aa 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -6,29 +6,34 @@ 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.FileTreeEntry) -> 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 + if any(entry.isDir() and self._has_mod_files(entry) for entry in filetree): + return mobase.ModDataChecker.FIXABLE + return mobase.ModDataChecker.INVALID + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + for entry in list(filetree): + if entry.isDir() and self._has_mod_files(entry): + filetree.merge(entry) + entry.detach() + return filetree + class SlayTheSpire2Game(BasicGame): Name = "Slay the Spire 2 Support Plugin" From a4aeeaaf7fb3bb1c1415a42eb8f9a9ee0171711a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 05:21:49 +0000 Subject: [PATCH 09/11] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- games/game_sts2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index fe420aa..369029f 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -20,7 +20,9 @@ def _has_mod_files(self, filetree: mobase.FileTreeEntry) -> bool: for entry in filetree ) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if self._has_mod_files(filetree): return mobase.ModDataChecker.VALID if any(entry.isDir() and self._has_mod_files(entry) for entry in filetree): From 417e874c8aeb03ddb7a358530daeda5ad6874fc1 Mon Sep 17 00:00:00 2001 From: Azlle Date: Sat, 25 Apr 2026 14:31:24 +0900 Subject: [PATCH 10/11] fix: resolve Pyright type errors in mod data checker --- games/game_sts2.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index 369029f..9afb0cc 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -9,12 +9,13 @@ from ..basic_features.basic_save_game_info import BasicGameSaveGame from ..basic_game import BasicGame from ..steam_utils import find_steam_path +from typing import cast class SlayTheSpire2ModDataChecker(mobase.ModDataChecker): _VALID_EXTENSIONS = (".pck", ".dll", ".json") - def _has_mod_files(self, filetree: mobase.FileTreeEntry) -> bool: + def _has_mod_files(self, filetree: mobase.IFileTree) -> bool: return any( entry.isFile() and entry.name().endswith(self._VALID_EXTENSIONS) for entry in filetree @@ -25,15 +26,18 @@ def dataLooksValid( ) -> mobase.ModDataChecker.CheckReturn: if self._has_mod_files(filetree): return mobase.ModDataChecker.VALID - if any(entry.isDir() and self._has_mod_files(entry) for entry in filetree): - return mobase.ModDataChecker.FIXABLE + 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() and self._has_mod_files(entry): - filetree.merge(entry) - entry.detach() + if entry.isDir(): + tree = cast(mobase.IFileTree, entry) + if self._has_mod_files(tree): + filetree.merge(tree) + entry.detach() return filetree From 457b8b8f14f1947e112ef95c9eb281980fcad12e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 05:32:27 +0000 Subject: [PATCH 11/11] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- games/game_sts2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_sts2.py b/games/game_sts2.py index 9afb0cc..6f3ecef 100644 --- a/games/game_sts2.py +++ b/games/game_sts2.py @@ -1,6 +1,7 @@ import json import re from pathlib import Path +from typing import cast from PyQt6.QtCore import QDir, QFileInfo, qInfo, qWarning @@ -9,7 +10,6 @@ from ..basic_features.basic_save_game_info import BasicGameSaveGame from ..basic_game import BasicGame from ..steam_utils import find_steam_path -from typing import cast class SlayTheSpire2ModDataChecker(mobase.ModDataChecker):