diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-16 19:51:40 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-16 19:51:40 -0500 |
| commit | 549b84b0f8815737c93913b200dde3821a062cb0 (patch) | |
| tree | f6bfd96729016847438beda9416c93f447838077 /libs/basic_games | |
| parent | a0747cabb420d699be3a1dd4c63f47c5153bd7b9 (diff) | |
mo2: catch up to upstream 2.5.3 Betas 3-11
Multi-area sync against the upstream MO2 2.5.3 beta line. Roughly three
logical sections share the diff:
Catch-up (Betas 3-11):
- nxmaccessmanager: drop "supporter" from validRoles so non-premium
supporters get the right download path (Beta 10)
- mainwindow: disable tutorial triggers at the trigger sites; Qt 6.11
lockup on child windows (Beta 4)
- basic_games: add Kingdom Come Deliverance 2, Slay the Spire 2
(Betas 11, 6)
- uibase: add NXM collections parsing fields/accessors + (?:nxm|modl)
scheme support in NXMUrl (Beta 9)
- Starfield: blueprintPrefix() on IPluginGame; plugins.txt write
suppression for blueprint plugins; hasInvalidBlueprint /
hasUnpairedBlueprint diagnoses; Title-keyed content catalog
consolidation (Betas 3-5)
- downloadmanager + systemtraymanager + organizercore + iuserinterface
+ settings: add Beta 11 "show notifications when downloads complete
or fail" toggle and the showNotification plumbing
- nxmaccessmanager: restore upstream re-entry guard for OAuth refresh
and the styled OAuth response page (Beta 11)
- organizer_en.ts and game-bethesda *_en.ts: pull upstream HEAD strings
Workarounds tab removed:
- Delete settingsdialogworkarounds.{h,cpp}; drop the tab in
settingsdialog.ui; move "Enable archives parsing" into General
- Hardcode GameSettings::forceEnableCoreFiles() to true so the
primary-master toggle-off bug (DLCs unchecked on tab refresh) can't
recur; setter becomes a no-op
- No-op stubs for offlineMode, useProxy, useCustomBrowser, Steam
appID/credentials, executablesBlacklist, skipFileSuffixes,
skipDirectories — UI gone, accessors keep call sites green
- settingsdialognexus drops the custom-browser wiring; spawn drops the
"Change the blacklist" Retry path
Beta 9 download manager port:
- fileID-first match in nxmFilesAvailable and
nxmFileInfoFromMd5Available; filename remains the fallback
- NexusInterface::isActiveFileStatus() helper; rewrite of
nxmUpdatesAvailable to use pickNewestVersion /
findUpdateChainSuccessors / resolveInstalledFileId. ARCHIVED_HIDDEN
now correctly treated as inactive
- std::optional<unsigned int> reservedID on DownloadInfo factories and
on the three addDownload overloads; startDownloadURLs and
startDownloadURLWithMeta return the canonical DownloadID instead of
m_ActiveDownloads.size()-1 (which was the index, not an ID)
- QHash<unsigned int, DownloadInfo*> m_ByID maintained alongside
m_ActiveDownloads at every mutation site; downloadInfoByID is now
O(1) via m_ByID.value(id, nullptr)
LOOT remains intentionally stripped (lootcli build cruft cleaned up
separately). BSA wide-path skipped — Linux UTF-8 paths already work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs/basic_games')
| -rw-r--r-- | libs/basic_games/games/game_kingdomcomedeliverance2.py | 104 | ||||
| -rw-r--r-- | libs/basic_games/games/game_sts2.py | 81 |
2 files changed, 185 insertions, 0 deletions
diff --git a/libs/basic_games/games/game_kingdomcomedeliverance2.py b/libs/basic_games/games/game_kingdomcomedeliverance2.py new file mode 100644 index 0000000..cef7e23 --- /dev/null +++ b/libs/basic_games/games/game_kingdomcomedeliverance2.py @@ -0,0 +1,104 @@ +import os +import xml.etree.ElementTree as ET +from pathlib import Path + +from PyQt6.QtCore import QDir, qInfo, qWarning + +import mobase + +from ..basic_features import BasicModDataChecker, GlobPatterns +from ..basic_game import BasicGame + + +class KingdomComeDeliverance2Game(BasicGame): + Name = "Kingdom Come: Deliverance 2 Support Plugin" + Author = "TheForgotten69" + Version = "1.0.0" + + GameName = "Kingdom Come: Deliverance II" + GameShortName = "kingdomcomedeliverance2" + GameNexusName = "kingdomcomedeliverance2" + GameNexusId = 7286 + GameSteamId = [1771300] + GameBinary = "bin/Win64MasterMasterSteamPGO/KingdomCome.exe" + GameDataPath = "Mods" + GameSaveExtension = "whs" + GameDocumentsDirectory = "%GAME_PATH%" + GameSavesDirectory = "%USERPROFILE%/Saved Games/kingdomcome2/saves" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(BasicModDataChecker(GlobPatterns(valid=["*"]))) + organizer.onAboutToRun(self._write_mod_order) + return True + + @staticmethod + def _get_mod_id(mod_path: Path) -> str | None: + """Return the mod ID for a KCD2 mod. + + Checks mod.manifest for <modid> (preferred) or <name>, falling back + to the game mod folder name (the subfolder inside the MO2 mod directory). + """ + for manifest in mod_path.rglob("mod.manifest"): + try: + root = ET.parse(manifest).getroot() + for tag in ("modid", "name"): + elem = root.find(f".//{tag}") + if elem is not None and elem.text and elem.text.strip(): + return elem.text.strip() + except ET.ParseError: + qWarning(f"KCD2: failed to parse {manifest}") + # Fall back to the folder that contains mod.manifest + return manifest.parent.name + # No manifest — use first subdirectory name (the game mod folder) + for subdir in mod_path.iterdir(): + if subdir.is_dir(): + return subdir.name + return None + + def _write_mod_order(self, app_path: str, wd: QDir, args: str) -> bool: + if not self.isActive(): + return True + + modlist = self._organizer.modList() + mods_path = Path(self._organizer.modsPath()) + mod_order_path = Path(self._organizer.overwritePath()) / "mod_order.txt" + + mod_ids: list[str] = [] + for mod_name in modlist.allModsByProfilePriority(): + if not (modlist.state(mod_name) & mobase.ModState.ACTIVE): + continue + mod_id = self._get_mod_id(mods_path / mod_name) + if mod_id: + qInfo(f"KCD2: mod '{mod_name}' -> id '{mod_id}'") + mod_ids.append(mod_id) + else: + qWarning(f"KCD2: could not resolve id for mod '{mod_name}', skipping") + + if not mod_ids: + mod_order_path.unlink(missing_ok=True) + return True + + # MO2 priority 1 = top = highest priority. KCD2 is last-loaded-wins, + # so highest priority must be last in the file. + mod_ids.reverse() + mod_order_path.parent.mkdir(parents=True, exist_ok=True) + mod_order_path.write_text("\n".join(mod_ids)) + qInfo(f"KCD2: wrote mod_order.txt with {len(mod_ids)} mods: {mod_ids}") + return True + + def iniFiles(self): + return ["custom.cfg", "system.cfg", "user.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + for iniFile in self.iniFiles(): + iniPath = self.documentsDirectory().absoluteFilePath(iniFile) + if not os.path.exists(iniPath): + with open(iniPath, "w") as _: + pass + + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + + super().initializeProfile(directory, settings) diff --git a/libs/basic_games/games/game_sts2.py b/libs/basic_games/games/game_sts2.py new file mode 100644 index 0000000..fe992e8 --- /dev/null +++ b/libs/basic_games/games/game_sts2.py @@ -0,0 +1,81 @@ +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_features import BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame + + +class SlayTheSpire2ModDataChecker(BasicModDataChecker): + def __init__(self): + super().__init__( + GlobPatterns( + valid=[ + "*.pck", + "*.dll", + "*.json", + ], + move={ + "*/*.pck": "", + "*/*.dll": "", + "*/*.json": "", + }, + ) + ) + + +class SlayTheSpire2Game(BasicGame): + Name = "Slay the Spire 2 Support Plugin" + Author = "Azlle" + Version = "1.0.1" + + GameName = "Slay the Spire 2" + GameShortName = "slaythespire2" + GameNexusName = "slaythespire2" + GameNexusId = 8916 + GameSteamId = 2868840 + GameBinary = "SlayTheSpire2.exe" + GameDataPath = "mods" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Roaming/SlayTheSpire2" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(SlayTheSpire2ModDataChecker()) + return True + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + mods_path = Path(self.dataDirectory().absolutePath()) + mods_path.mkdir(exist_ok=True) + super().initializeProfile(directory, settings) + + 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])) + return QDir(str(steam_dir)) + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + base = Path(folder.absolutePath()) + return [ + BasicGameSaveGame(save) + for save in base.rglob("*") + if save.is_file() and save.suffix in (".save", ".run") + ] + + def executables(self): + return [ + mobase.ExecutableInfo( + "Slay the Spire 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Slay the Spire 2 (OpenGL)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("--rendering-driver opengl3"), + ] |
