Merge remote-tracking branch 'origin/master' into dev/vcpkg

This commit is contained in:
Mikaël Capelle
2025-05-25 16:27:06 +02:00
71 changed files with 3042 additions and 475 deletions
+25
View File
@@ -0,0 +1,25 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-merge-conflict
- id: mixed-line-ending
args: [--fix=lf]
- id: check-case-conflict
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.7 # must match pyproject.toml
hooks:
- id: ruff
args: [--extend-select, I, --fix]
- id: ruff-format
ci:
autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks."
autofix_prs: true
autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate."
autoupdate_schedule: quarterly
submodules: false
+1 -1
View File
@@ -4,4 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+2 -1
View File
@@ -1,6 +1,7 @@
import mobase
from PyQt6.QtCore import QDir
import mobase
class BasicLocalSavegames(mobase.LocalSavegames):
def __init__(self, game_save_dir: QDir):
+1 -1
View File
@@ -78,7 +78,7 @@ class GlobPatterns:
unfold: list[str] | None = None
valid: list[str] | None = None
delete: list[str] | None = None
move: dict[str, str] = field(default_factory=dict)
move: dict[str, str] = field(default_factory=dict[str, str])
def merge(
self, other: GlobPatterns, mode: Literal["merge", "replace"] = "replace"
+2 -1
View File
@@ -6,11 +6,12 @@ from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Self, Sequence
import mobase
from PyQt6.QtCore import QDateTime, QLocale, Qt
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtWidgets import QFormLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget
import mobase
def format_date(date_time: QDateTime | datetime | str, format_str: str | None = None):
"""Default format for date and time in the `BasicGameSaveGameInfoWidget`.
+16 -3
View File
@@ -5,9 +5,11 @@ import sys
from pathlib import Path
from typing import Callable, Generic, TypeVar
import mobase
from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMessageBox
import mobase
from .basic_features.basic_save_game_info import (
BasicGameSaveGame,
@@ -380,11 +382,22 @@ class BasicGame(mobase.IPluginGame):
from .origin_utils import find_games as find_origin_games
from .steam_utils import find_games as find_steam_games
errors: list[tuple[str, Exception]] = []
BasicGame.steam_games = find_steam_games()
BasicGame.gog_games = find_gog_games()
BasicGame.origin_games = find_origin_games()
BasicGame.epic_games = find_epic_games()
BasicGame.eadesktop_games = find_eadesktop_games()
BasicGame.epic_games = find_epic_games(errors)
BasicGame.eadesktop_games = find_eadesktop_games(errors)
if errors:
QMessageBox.critical(
None,
"Errors loading game list",
(
"The following errors occurred while loading the list of available games:\n"
f"\n- {'\n\n- '.join('\n '.join(str(e) for e in messageError) for messageError in errors)}"
),
)
# File containing the plugin:
_fromName: str
+13 -2
View File
@@ -2,13 +2,14 @@
import configparser
import os
import sys
import xml.etree.ElementTree as et
from configparser import NoOptionError
from pathlib import Path
from typing import Dict
def find_games() -> Dict[str, Path]:
def find_games(errors: list[tuple[str, Exception]] | None = None) -> Dict[str, Path]:
"""
Find the list of EA Desktop games installed.
@@ -37,7 +38,17 @@ def find_games() -> Dict[str, Path]:
ini_content = "[mod_organizer]\n" + f.read()
config = configparser.ConfigParser()
config.read_string(ini_content)
try:
config.read_string(ini_content)
except configparser.ParsingError as e:
error_message = (
f'Failed to parse EA Desktop games list file "{user_ini}",\n'
" Try to run the launcher to recreate it."
)
print(error_message, e, file=sys.stderr)
if errors is not None:
errors.append((error_message, e))
return games
try:
install_path = Path(config.get("mod_organizer", "user.downloadinplacedir"))
+36 -12
View File
@@ -9,8 +9,12 @@ import winreg
from collections.abc import Iterable
from pathlib import Path
ErrorList = list[tuple[str, Exception]]
def find_epic_games() -> Iterable[tuple[str, Path]]:
def find_epic_games(
errors: ErrorList | None = None,
) -> Iterable[tuple[str, Path]]:
try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
@@ -30,15 +34,23 @@ def find_epic_games() -> Iterable[tuple[str, Path]]:
manifest_file_data["AppName"],
Path(manifest_file_data["InstallLocation"]),
)
except (json.JSONDecodeError, KeyError):
except (json.JSONDecodeError, KeyError) as e:
error_message = (
f'Unable to parse Epic Games manifest file: "{manifest_file_path}"\n'
" Try to run the launcher recreate it."
)
print(
"Unable to parse Epic Games manifest file",
manifest_file_path,
error_message,
e,
file=sys.stderr,
)
if errors is not None:
errors.append((error_message, e))
def find_legendary_games(config_path: str | None = None) -> Iterable[tuple[str, Path]]:
def find_legendary_games(
config_path: str | None = None, errors: ErrorList | None = None
) -> Iterable[tuple[str, Path]]:
# Based on legendary source:
# https://github.com/derrod/legendary/blob/master/legendary/lfs/lgndry.py
if config_path := config_path or os.environ.get("XDG_CONFIG_HOME"):
@@ -53,21 +65,33 @@ def find_legendary_games(config_path: str | None = None) -> Iterable[tuple[str,
installed_games = json.load(installed_file)
for game in installed_games.values():
yield game["app_name"], Path(game["install_path"])
except (json.JSONDecodeError, AttributeError, KeyError):
except (json.JSONDecodeError, AttributeError, KeyError) as e:
error_message = (
f'Unable to parse installed games from Legendary/Heroic launcher: "{installed_path}"\n'
" Try to run the launcher to recrated the file."
)
print(
"Unable to parse installed games from Legendary",
installed_path,
error_message,
e,
file=sys.stderr,
)
if errors is not None:
errors.append((error_message, e))
def find_heroic_games():
return find_legendary_games(os.path.expandvars(r"%AppData%\heroic\legendaryConfig"))
def find_heroic_games(errors: ErrorList | None = None):
return find_legendary_games(
os.path.expandvars(r"%AppData%\heroic\legendaryConfig"), errors
)
def find_games() -> dict[str, Path]:
def find_games(errors: ErrorList | None = None) -> dict[str, Path]:
return dict(
itertools.chain(find_epic_games(), find_legendary_games(), find_heroic_games())
itertools.chain(
find_epic_games(errors=errors),
find_legendary_games(errors=errors),
find_heroic_games(errors=errors),
)
)
+87 -86
View File
@@ -1,86 +1,87 @@
import mobase
from PyQt6.QtCore import QDir, QFileInfo
from ..basic_features import BasicLocalSavegames
from ..basic_game import BasicGame
from ..steam_utils import find_steam_path
# Lifted from https://github.com/ModOrganizer2/modorganizer-basic_games/blob/71dbb8c557d43cba9d290674a332e7ecd1650261/games/game_darkestdungeon.py
class ArkhamCityModDataChecker(mobase.ModDataChecker):
def __init__(self):
super().__init__()
self.validDirNames = [
"config",
"cookedpcconsole",
"localization",
"movies",
"moviesstereo",
"splash",
]
def dataLooksValid(
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
for entry in filetree:
if not entry.isDir():
continue
if entry.name().casefold() in self.validDirNames:
return mobase.ModDataChecker.VALID
return mobase.ModDataChecker.INVALID
class ArkhamCityGame(BasicGame):
Name = "Batman: Arkham City Plugin"
Author = "Paynamia"
Version = "0.5.3"
GameName = "Batman: Arkham City"
GameShortName = "batmanarkhamcity"
GameNexusId = 372
GameSteamId = 200260
GameGogId = 1260066469
GameEpicId = "Egret"
GameBinary = "Binaries/Win32/BatmanAC.exe"
GameLauncher = "Binaries/Win32/BmLauncher.exe"
GameDataPath = "BmGame"
GameDocumentsDirectory = (
"%DOCUMENTS%/WB Games/Batman Arkham City GOTY/BmGame/Config"
)
GameIniFiles = ["UserEngine.ini", "UserGame.ini", "UserInput.ini"]
GameSaveExtension = "sgd"
# This will only detect saves from the earliest-created Steam profile on the user's PC.
def savesDirectory(self) -> QDir:
docSaves = QDir(self.documentsDirectory().cleanPath("../../SaveData"))
if self.is_steam():
if (steamDir := find_steam_path()) is None:
return docSaves
for child in steamDir.joinpath("userdata").iterdir():
if not child.is_dir() or child.name == "0":
continue
steamSaves = child.joinpath("200260", "remote")
if steamSaves.is_dir():
return QDir(str(steamSaves))
else:
return docSaves
else:
return docSaves
def init(self, organizer: mobase.IOrganizer) -> bool:
super().init(organizer)
self._register_feature(ArkhamCityModDataChecker())
self._register_feature(BasicLocalSavegames(self.savesDirectory()))
return True
def executables(self):
return [
mobase.ExecutableInfo(
"Batman: Arkham City",
QFileInfo(self.gameDirectory(), "Binaries/Win32/BatmanAC.exe"),
),
mobase.ExecutableInfo(
"Arkham City Launcher",
QFileInfo(self.gameDirectory(), "Binaries/Win32/BmLauncher.exe"),
),
]
from PyQt6.QtCore import QDir, QFileInfo
import mobase
from ..basic_features import BasicLocalSavegames
from ..basic_game import BasicGame
from ..steam_utils import find_steam_path
# Lifted from https://github.com/ModOrganizer2/modorganizer-basic_games/blob/71dbb8c557d43cba9d290674a332e7ecd1650261/games/game_darkestdungeon.py
class ArkhamCityModDataChecker(mobase.ModDataChecker):
def __init__(self):
super().__init__()
self.validDirNames = [
"config",
"cookedpcconsole",
"localization",
"movies",
"moviesstereo",
"splash",
]
def dataLooksValid(
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
for entry in filetree:
if not entry.isDir():
continue
if entry.name().casefold() in self.validDirNames:
return mobase.ModDataChecker.VALID
return mobase.ModDataChecker.INVALID
class ArkhamCityGame(BasicGame):
Name = "Batman: Arkham City Plugin"
Author = "Paynamia"
Version = "0.5.3"
GameName = "Batman: Arkham City"
GameShortName = "batmanarkhamcity"
GameNexusId = 372
GameSteamId = 200260
GameGogId = 1260066469
GameEpicId = "Egret"
GameBinary = "Binaries/Win32/BatmanAC.exe"
GameLauncher = "Binaries/Win32/BmLauncher.exe"
GameDataPath = "BmGame"
GameDocumentsDirectory = (
"%DOCUMENTS%/WB Games/Batman Arkham City GOTY/BmGame/Config"
)
GameIniFiles = ["UserEngine.ini", "UserGame.ini", "UserInput.ini"]
GameSaveExtension = "sgd"
# This will only detect saves from the earliest-created Steam profile on the user's PC.
def savesDirectory(self) -> QDir:
docSaves = QDir(self.documentsDirectory().cleanPath("../../SaveData"))
if self.is_steam():
if (steamDir := find_steam_path()) is None:
return docSaves
for child in steamDir.joinpath("userdata").iterdir():
if not child.is_dir() or child.name == "0":
continue
steamSaves = child.joinpath("200260", "remote")
if steamSaves.is_dir():
return QDir(str(steamSaves))
else:
return docSaves
else:
return docSaves
def init(self, organizer: mobase.IOrganizer) -> bool:
super().init(organizer)
self._register_feature(ArkhamCityModDataChecker())
self._register_feature(BasicLocalSavegames(self.savesDirectory()))
return True
def executables(self):
return [
mobase.ExecutableInfo(
"Batman: Arkham City",
QFileInfo(self.gameDirectory(), "Binaries/Win32/BatmanAC.exe"),
),
mobase.ExecutableInfo(
"Arkham City Launcher",
QFileInfo(self.gameDirectory(), "Binaries/Win32/BmLauncher.exe"),
),
]
+2 -1
View File
@@ -6,9 +6,10 @@ from collections.abc import Mapping
from pathlib import Path
from typing import BinaryIO
import mobase
from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo
import mobase
from ..basic_features import BasicLocalSavegames
from ..basic_features.basic_save_game_info import (
BasicGameSaveGame,
+2 -1
View File
@@ -2,9 +2,10 @@ import json
from collections.abc import Mapping
from pathlib import Path
import mobase
from PyQt6.QtCore import QDateTime, QDir
import mobase
from ..basic_features.basic_save_game_info import (
BasicGameSaveGame,
BasicGameSaveGameInfo,
+2 -1
View File
@@ -1,8 +1,9 @@
from __future__ import annotations
import mobase
from PyQt6.QtCore import QFileInfo
import mobase
from ..basic_game import BasicGame
+4 -3
View File
@@ -10,7 +10,6 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal, TypeVar
import mobase
from PyQt6.QtCore import QDateTime, QDir, Qt, qCritical, qInfo, qWarning
from PyQt6.QtWidgets import (
QCheckBox,
@@ -20,6 +19,8 @@ from PyQt6.QtWidgets import (
QWidget,
)
import mobase
from ..basic_features import BasicLocalSavegames, BasicModDataChecker, GlobPatterns
from ..basic_features.basic_save_game_info import (
BasicGameSaveGame,
@@ -74,7 +75,7 @@ def parse_cyberpunk_save_metadata(save_path: Path, save: mobase.ISaveGame):
"Street Cred": int(meta_data["streetCred"]),
"Life Path": meta_data["lifePath"],
"Difficulty": meta_data["difficulty"],
"Gender": f'{meta_data["bodyGender"]} / {meta_data["brainGender"]}',
"Gender": f"{meta_data['bodyGender']} / {meta_data['brainGender']}",
"Game version": meta_data["buildPatch"],
}
except (FileNotFoundError, json.JSONDecodeError):
@@ -487,7 +488,7 @@ class Cyberpunk2077Game(BasicGame):
_,
) = self._modlist_files.update_modlist("redmod")
modlist_param = f'-modlist="{modlist_path}"' if modlist else ""
args = f"{args[:m.start()]}{modlist_param}{args[m.end():]}"
args = f"{args[: m.start()]}{modlist_param}{args[m.end() :]}"
qInfo(f"Manual modlist deployment: replacing {m[0]}, new args = {args}")
self._check_redmod_result(
self._organizer.waitForApplication(
+56 -56
View File
@@ -1,56 +1,56 @@
import mobase
from ..basic_game import BasicGame
class DaggerfallUnityModDataChecker(mobase.ModDataChecker):
def __init__(self):
super().__init__()
self.validDirNames = [
"biogs",
"docs",
"factions",
"fonts",
"mods",
"questpacks",
"quests",
"sound",
"soundfonts",
"spellicons",
"tables",
"text",
"textures",
"worlddata",
"aa",
]
def dataLooksValid(
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
for entry in filetree:
if not entry.isDir():
continue
if entry.name().casefold() in self.validDirNames:
return mobase.ModDataChecker.VALID
return mobase.ModDataChecker.INVALID
class DaggerfallUnityGame(BasicGame):
def init(self, organizer: mobase.IOrganizer) -> bool:
super().init(organizer)
self._register_feature(DaggerfallUnityModDataChecker())
return True
Name = "Daggerfall Unity Support Plugin"
Author = "HomerSimpleton"
Version = "1.0.0"
GameName = "Daggerfall Unity"
GameShortName = "daggerfallunity"
GameBinary = "DaggerfallUnity.exe"
GameLauncher = "DaggerfallUnity.exe"
GameDataPath = "%GAME_PATH%/DaggerfallUnity_Data/StreamingAssets"
GameSupportURL = (
r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
"Game:-Daggerfall-Unity"
)
import mobase
from ..basic_game import BasicGame
class DaggerfallUnityModDataChecker(mobase.ModDataChecker):
def __init__(self):
super().__init__()
self.validDirNames = [
"biogs",
"docs",
"factions",
"fonts",
"mods",
"questpacks",
"quests",
"sound",
"soundfonts",
"spellicons",
"tables",
"text",
"textures",
"worlddata",
"aa",
]
def dataLooksValid(
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
for entry in filetree:
if not entry.isDir():
continue
if entry.name().casefold() in self.validDirNames:
return mobase.ModDataChecker.VALID
return mobase.ModDataChecker.INVALID
class DaggerfallUnityGame(BasicGame):
def init(self, organizer: mobase.IOrganizer) -> bool:
super().init(organizer)
self._register_feature(DaggerfallUnityModDataChecker())
return True
Name = "Daggerfall Unity Support Plugin"
Author = "HomerSimpleton"
Version = "1.0.0"
GameName = "Daggerfall Unity"
GameShortName = "daggerfallunity"
GameBinary = "DaggerfallUnity.exe"
GameLauncher = "DaggerfallUnity.exe"
GameDataPath = "%GAME_PATH%/DaggerfallUnity_Data/StreamingAssets"
GameSupportURL = (
r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
"Game:-Daggerfall-Unity"
)
+31 -31
View File
@@ -1,31 +1,31 @@
import mobase
from ..basic_features import BasicGameSaveGameInfo
from ..basic_game import BasicGame
class DAOriginsGame(BasicGame):
Name = "Dragon Age Origins Support Plugin"
Author = "Patchier"
Version = "1.1.1"
GameName = "Dragon Age: Origins"
GameShortName = "dragonage"
GameBinary = r"bin_ship\DAOrigins.exe"
GameDataPath = r"%DOCUMENTS%\BioWare\Dragon Age\packages\core\override"
GameSavesDirectory = r"%DOCUMENTS%\BioWare\Dragon Age\Characters"
GameSaveExtension = "das"
GameSteamId = [17450, 47810]
GameGogId = 1949616134
GameEaDesktopId = [70377, 70843]
GameSupportURL = (
r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
"Game:-Dragon-Age:-Origins"
)
def init(self, organizer: mobase.IOrganizer):
super().init(organizer)
self._register_feature(
BasicGameSaveGameInfo(lambda s: s.parent.joinpath("screen.dds"))
)
return True
import mobase
from ..basic_features import BasicGameSaveGameInfo
from ..basic_game import BasicGame
class DAOriginsGame(BasicGame):
Name = "Dragon Age Origins Support Plugin"
Author = "Patchier"
Version = "1.1.1"
GameName = "Dragon Age: Origins"
GameShortName = "dragonage"
GameBinary = r"bin_ship\DAOrigins.exe"
GameDataPath = r"%DOCUMENTS%\BioWare\Dragon Age\packages\core\override"
GameSavesDirectory = r"%DOCUMENTS%\BioWare\Dragon Age\Characters"
GameSaveExtension = "das"
GameSteamId = [17450, 47810]
GameGogId = 1949616134
GameEaDesktopId = [70377, 70843]
GameSupportURL = (
r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
"Game:-Dragon-Age:-Origins"
)
def init(self, organizer: mobase.IOrganizer):
super().init(organizer)
self._register_feature(
BasicGameSaveGameInfo(lambda s: s.parent.joinpath("screen.dds"))
)
return True
+5 -4
View File
@@ -1,9 +1,10 @@
import json
from pathlib import Path
import mobase
from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths
import mobase
from ..basic_game import BasicGame, BasicGameSaveGame
from ..steam_utils import find_steam_path
@@ -166,7 +167,7 @@ class DarkestDungeonGame(BasicGame):
GameNexusId = 804
GameSteamId = 262060
GameGogId = 1719198803
GameBinary = "_windowsnosteam//darkest.exe"
GameBinary = "_windowsnosteam/win64/darkest.exe"
GameDataPath = ""
GameSupportURL = (
r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
@@ -180,9 +181,9 @@ class DarkestDungeonGame(BasicGame):
def executables(self):
if self.is_steam():
path = QFileInfo(self.gameDirectory(), "_windows/darkest.exe")
path = QFileInfo(self.gameDirectory(), "_windows/win64/darkest.exe")
else:
path = QFileInfo(self.gameDirectory(), "_windowsnosteam/darkest.exe")
path = QFileInfo(self.gameDirectory(), "_windowsnosteam/win64/darkest.exe")
return [
mobase.ExecutableInfo("Darkest Dungeon", path).withWorkingDirectory(
self.gameDirectory()
+2 -1
View File
@@ -1,9 +1,10 @@
import struct
from pathlib import Path
import mobase
from PyQt6.QtGui import QImage
import mobase
from ..basic_features import BasicGameSaveGameInfo
from ..basic_game import BasicGame
+1 -2
View File
@@ -69,8 +69,7 @@ class DivinityOriginalSinEnhancedEditionGame(BasicGame, mobase.IPluginFileMapper
GameDataPath = "Data"
GameSaveExtension = "lsv"
GameDocumentsDirectory = (
"%USERPROFILE%/Documents/Larian Studios/"
"Divinity Original Sin Enhanced Edition"
"%USERPROFILE%/Documents/Larian Studios/Divinity Original Sin Enhanced Edition"
)
GameSavesDirectory = (
"%USERPROFILE%/Documents/Larian Studios/"
+2 -1
View File
@@ -1,6 +1,7 @@
import mobase
from PyQt6.QtCore import QFileInfo
import mobase
from ..basic_game import BasicGame
+2 -1
View File
@@ -1,6 +1,7 @@
import mobase
from PyQt6.QtCore import QFileInfo
import mobase
from ..basic_game import BasicGame

Some files were not shown because too many files have changed in this diff Show More