From 55a987bff61205c825bae239bcb1ba26500239af Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Fri, 15 Sep 2023 19:35:32 -0500 Subject: [PATCH 1/3] Additional fixes and cleanup for python updates --- .github/workflows/linters.yml | 34 +- __init__.py | 3 +- basic_features/__init__.py | 6 +- basic_features/basic_mod_data_checker.py | 259 ++++------ basic_features/basic_save_game_info.py | 27 +- basic_features/utils.py | 7 + basic_game.py | 145 +++--- games/game_assettocorsa.py | 1 - games/game_blackandwhite2.py | 81 +-- games/game_bladeandsorcery.py | 95 ++-- games/game_control.py | 6 +- games/game_cyberpunk2077.py | 1 - games/game_da2.py | 3 +- games/game_daggerfallunity.py | 4 +- games/game_dao.py | 1 - games/game_darkestdungeon.py | 26 +- games/game_darkmessiahofmightandmagic.py | 14 +- games/game_darksouls.py | 2 - games/game_divinityoriginalsin.py | 2 - games/game_divinityoriginalsinee.py | 31 +- games/game_dragonsdogmadarkarisen.py | 3 - games/game_dungeonsiege2.py | 30 +- games/game_gta-3-de.py | 12 +- games/game_gta-san-andreas-de.py | 12 +- games/game_gta-vice-city-de.py | 12 +- games/game_kerbalspaceprogram.py | 16 +- games/game_kingdomcomedeliverance.py | 9 +- games/game_masterduel.py | 22 +- games/game_mirrorsedge.py | 3 - games/game_monsterhunterworld.py | 1 - games/game_mountandblade2.py | 15 +- games/game_msfs2020.py | 1 - games/game_nierautomata.py | 3 - games/game_nomanssky.py | 6 +- games/game_sims4.py | 3 +- games/game_stalkeranomaly.py | 50 +- games/game_stardewvalley.py | 12 +- games/game_starsector.py | 7 +- games/game_starwars-empire-at-war-foc.py | 9 +- games/game_starwars-empire-at-war.py | 9 +- games/game_subnautica-below-zero.py | 10 +- games/game_subnautica.py | 6 +- games/game_thebindingofisaacrebirth.py | 2 - games/game_trainsimulator.py | 6 +- games/game_valheim.py | 64 ++- games/game_vampirebloodlines.py | 32 +- games/game_witcher1.py | 12 +- games/game_witcher3.py | 8 +- games/game_xplane11.py | 1 - games/game_zeusandposeidon.py | 21 +- games/quarantine/game_masseffectlegendary.py | 3 +- games/stalkeranomaly/XRIO.py | 2 +- games/stalkeranomaly/XRMath.py | 24 +- games/stalkeranomaly/XRNET.py | 2 - games/stalkeranomaly/XRObject.py | 18 +- games/stalkeranomaly/XRSave.py | 17 +- games/stalkeranomaly/__init__.py | 24 +- gog_utils.py | 12 +- poetry.lock | 509 +++++++++++++++++++ pyproject.toml | 70 +++ setup.cfg | 52 -- steam_utils.py | 64 ++- 62 files changed, 1205 insertions(+), 737 deletions(-) create mode 100644 basic_features/utils.py create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 setup.cfg diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 7451134..b78e12c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -5,22 +5,20 @@ on: [push, pull_request] jobs: checks: runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - python-version: [3.8] - steps: - - uses: actions/checkout@v1 - with: - path: basic_games - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - - name: Test with tox - run: tox -e py38-lint + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + - uses: abatilo/actions-poetry@v2 + - name: Install + run: | + poetry install + - name: Lint + run: | + poetry run black . --check --diff + poetry run isort -c . + poetry run mypy . + poetry run ruff . + poetry run pyright . diff --git a/__init__.py b/__init__.py index bc2c3c8..5725b5f 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# pyright: reportUnboundVariable=false import glob import importlib @@ -17,7 +17,6 @@ BasicGame.setup() def createPlugins(): - # List of game class from python: game_plugins: typing.List[BasicGame] = [] diff --git a/basic_features/__init__.py b/basic_features/__init__.py index 326613e..66e9a76 100644 --- a/basic_features/__init__.py +++ b/basic_features/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +from .basic_mod_data_checker import BasicModDataChecker +from .basic_save_game_info import BasicGameSaveGameInfo -from .basic_mod_data_checker import BasicModDataChecker # noqa -from .basic_save_game_info import BasicGameSaveGameInfo # noqa +__all__ = ["BasicModDataChecker", "BasicGameSaveGameInfo"] diff --git a/basic_features/basic_mod_data_checker.py b/basic_features/basic_mod_data_checker.py index d849484..4bceac1 100644 --- a/basic_features/basic_mod_data_checker.py +++ b/basic_features/basic_mod_data_checker.py @@ -1,89 +1,75 @@ -# -*- encoding: utf-8 -*- from __future__ import annotations import fnmatch import re -import sys -from collections.abc import Iterable, Sequence -from typing import ClassVar, MutableMapping, Optional, TypedDict +from dataclasses import dataclass, field +from typing import Iterable import mobase - -def convert_entry_to_tree(entry: mobase.FileTreeEntry) -> Optional[mobase.IFileTree]: - if not entry.isDir(): - return None - if isinstance(entry, mobase.IFileTree): - return entry - if (parent := entry.parent()) is None: - return None - converted_entry = parent.find( - entry.name(), mobase.FileTreeEntry.FileTypes.DIRECTORY - ) - if isinstance(converted_entry, mobase.IFileTree): - return converted_entry - return None +from .utils import is_directory -class RegexPatternDict(dict, MutableMapping[str, re.Pattern]): - """Regex patterns for validation in `BasicModDataChecker`.""" +class OptionalRegexPattern: + _pattern: re.Pattern[str] | None - @classmethod - def from_glob_patterns(cls, glob_patterns: GlobPatternDict) -> RegexPatternDict: - """Returns an instance of `RegexPatternDict`, with the `glob_patterns` - translated to regex. - """ - return cls( - ( - key, - cls.regex_from_glob_list(value) - if isinstance(value, Iterable) - else None, - ) - for key, value in glob_patterns.items() - ) - - def get_match_index(self, key: str, search_str: str) -> Optional[int]: - """Get the index of the matched group if the `self[key]` matches `search_str`. - - Returns: - The 0-based index of the matched group or None for no match. - """ - if pattern := self.get(key): - return self.match_index_of_pattern(search_str, pattern) - return None + def __init__(self, globs: Iterable[str] | None) -> None: + if globs is None: + self._pattern = None + else: + self._pattern = OptionalRegexPattern.regex_from_glob_list(globs) @staticmethod - def match_index_of_pattern(search_str: str, pattern: re.Pattern) -> Optional[int]: - """Get the index of the matched group if `search_str` matches `pattern` - (from this dict). - - Returns: - The 0-based index of the matched group or None for no match. + def regex_from_glob_list(glob_list: Iterable[str]) -> re.Pattern[str]: """ - if (match := pattern.match(search_str)) and match.lastindex: - return match.lastindex - 1 - return None - - @staticmethod - def regex_from_glob_list(glob_list: Iterable[str]) -> re.Pattern: - """Returns a regex pattern form a list of glob patterns. + Returns a regex pattern form a list of glob patterns. Every pattern has a capturing group, so that `match.lastindex - 1` will give the `glob_list` index. """ return re.compile( - f'(?:{"|".join(f"({fnmatch.translate(f)})" for f in glob_list)})', re.I + "|".join(f"({fnmatch.translate(f)})" for f in glob_list), re.I ) + def match(self, value: str) -> bool: + if self._pattern is None: + return False + return bool(self._pattern.match(value)) -class GlobPatternDict(TypedDict, total=False): - """See: `BasicModDataChecker`""" - unfold: Iterable[str] | None - valid: Iterable[str] | None - delete: Iterable[str] | None - move: MutableMapping[str, str] | None +class RegexPatterns: + """ + Regex patterns for validation in `BasicModDataChecker`. + """ + + def __init__(self, globs: GlobPatterns) -> None: + self.unfold = OptionalRegexPattern(globs.unfold) + self.delete = OptionalRegexPattern(globs.delete) + self.valid = OptionalRegexPattern(globs.valid) + + self.move = {key: re.compile(fnmatch.translate(key)) for key in globs.move} + + def move_match(self, value: str) -> str | None: + """ + Retrieve the first move patterns that matches the given value, or None if no + move matches. + """ + for key, pattern in self.move.items(): + if pattern.match(value): + return key + return None + + +@dataclass(frozen=True, unsafe_hash=True) +class GlobPatterns: + """ + See: `BasicModDataChecker` + """ + + unfold: list[str] | None = None + valid: list[str] | None = None + delete: list[str] | None = None + move: dict[str, str] = field(default_factory=dict) class BasicModDataChecker(mobase.ModDataChecker): @@ -94,121 +80,92 @@ class BasicModDataChecker(mobase.ModDataChecker): checked and fixed in definition order of the `file_patterns` dict. Args: - file_patterns (optional): A dict (GlobPatternDict) with the following keys:: + file_patterns (optional): A GlobPatterns object, with the following attributes: + unfold: [ "list of folders to unfold" ], + # (remove and move contents to parent), after being checked and + # fixed recursively. + # Check result: `mobase.ModDataChecker.VALID`. - { - "unfold": [ "list of folders to unfold" ], - # (remove and move contents to parent), after being checked and - # fixed recursively. - # Check result: `mobase.ModDataChecker.VALID`. + valid: [ "list of files and folders in the right path." ], + # Check result: `mobase.ModDataChecker.VALID`. - "valid": [ "list of files and folders in the right path." ], - # Check result: `mobase.ModDataChecker.VALID`. + delete: [ "list of files/folders to delete." ], + # Check result: `mobase.ModDataChecker.FIXABLE`. - "delete": [ "list of files/folders to delete." ], - # Check result: `mobase.ModDataChecker.FIXABLE`. + move: {"Files/folders to move": "target path"} + # If the path ends with `/` or `\\`, the entry will be inserted + # in the corresponding directory instead of replacing it. + # Check result: `mobase.ModDataChecker.FIXABLE`. - "move": {"Files/folders to move": "target path"} - # If the path ends with `/` or `\\`, the entry will be inserted - # in the corresponding directory instead of replacing it. - # Check result: `mobase.ModDataChecker.FIXABLE`. - } + Example: - Example:: - - BasicModDataChecker( - { - "valid": ["valid_folder", "*.ext1"] - "move": {"*.ext2": "path/to/target_folder/"} - } + BasicModDataChecker( + GlobPatterns( + valid=["valid_folder", "*.ext1"] + move={"*.ext2": "path/to/target_folder/"} ) + ) See Also: `mobase.IFileTree.move` for the `"move"` target path specs. """ - default_file_patterns: ClassVar[GlobPatternDict] = {} - """Default for `file_patterns` - for subclasses.""" - - _file_patterns: GlobPatternDict + _file_patterns: GlobPatterns """Private `file_patterns`, updated together with `._regex` and `._move_targets`.""" - _regex: RegexPatternDict + _regex_patterns: RegexPatterns """The regex patterns derived from the file (glob) patterns.""" - _move_targets: Sequence[str] - """Target paths from `file_patterns["move"]`.""" - - def __init__(self, file_patterns: Optional[GlobPatternDict] = None): + def __init__(self, file_patterns: GlobPatterns = GlobPatterns()): super().__init__() - # Init with copy from class var by default (for unique instance var). - self.set_patterns(file_patterns or (self.default_file_patterns.copy())) - def set_patterns(self, file_patterns: GlobPatternDict): - """Sets the file patterns, replacing previous/default values and order.""" self._file_patterns = file_patterns - self.update_patterns() - - def update_patterns(self, file_patterns: Optional[GlobPatternDict] = None): - """Update file patterns. Preserves previous/default definition - (check/fix) order. - """ - if file_patterns: - self._file_patterns.update(file_patterns) - self._regex = RegexPatternDict.from_glob_patterns(self._file_patterns) - if move_map := self._file_patterns.get("move"): - self._move_targets = list(move_map.values()) + self._regex_patterns = RegexPatterns(file_patterns) def dataLooksValid( self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: status = mobase.ModDataChecker.INVALID + + rp = self._regex_patterns for entry in filetree: name = entry.name().casefold() - for key, regex in self._regex.items(): - if not regex or not regex.match(name): - continue - if key == "unfold": - return mobase.ModDataChecker.FIXABLE - elif key == "valid": - if status is not mobase.ModDataChecker.FIXABLE: - status = mobase.ModDataChecker.VALID - elif key in ("move", "delete"): - status = mobase.ModDataChecker.FIXABLE - break + + if rp.unfold.match(name): + if is_directory(entry): + status = self.dataLooksValid(entry) + else: + status = mobase.ModDataChecker.INVALID + break + elif rp.valid.match(name) and status == mobase.ModDataChecker.INVALID: + status = mobase.ModDataChecker.VALID + elif rp.delete.match(name) or rp.move_match(name) is not None: + status = mobase.ModDataChecker.FIXABLE else: - return mobase.ModDataChecker.INVALID + status = mobase.ModDataChecker.INVALID + break return status - def fix(self, filetree: mobase.IFileTree) -> Optional[mobase.IFileTree]: + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + rp = self._regex_patterns for entry in list(filetree): name = entry.name() - # Fix entries in pattern definition order. - for key, regex in self._regex.items(): - if not regex or not regex.match(name): - continue - if key == "valid": - break - elif key == "unfold": - if (folder_tree := convert_entry_to_tree(entry)) is not None: - if folder_tree: # Not empty - # Recursively fix subtree and unfold. - if not (fixed_folder_tree := self.fix(folder_tree)): - return None - filetree.merge(fixed_folder_tree) - folder_tree.detach() - else: - print(f"Cannot unfold {name}!", file=sys.stderr) - return None - elif key == "delete": - entry.detach() - elif key == "move": - if (move_target := self._get_move_target(name)) is not None: - filetree.move(entry, move_target) - break - return filetree - def _get_move_target(self, filename: str) -> Optional[str]: - if (i := self._regex.get_match_index("move", filename)) is None: - return None - return self._move_targets[i] + # unfold first - if this match, entry is a directory (checked in + # dataLooksValid) + if rp.unfold.match(name): + assert is_directory(entry) + filetree.merge(entry) + entry.detach() + + elif rp.valid.match(name): + continue + + elif rp.delete.match(name): + entry.detach() + + elif (move_key := rp.move_match(name)) is not None: + target = self._file_patterns.move[move_key] + filetree.move(entry, target) + + return filetree diff --git a/basic_features/basic_save_game_info.py b/basic_features/basic_save_game_info.py index 13a9b50..32ce2c4 100644 --- a/basic_features/basic_save_game_info.py +++ b/basic_features/basic_save_game_info.py @@ -2,14 +2,13 @@ import sys from pathlib import Path -from typing import Callable +from typing import Callable, Sequence +import mobase from PyQt6.QtCore import QDateTime, Qt from PyQt6.QtGui import QImage, QPixmap from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget -import mobase - class BasicGameSaveGame(mobase.ISaveGame): def __init__(self, filepath: Path): @@ -33,7 +32,13 @@ class BasicGameSaveGame(mobase.ISaveGame): class BasicGameSaveGameInfoWidget(mobase.ISaveGameInfoWidget): - def __init__(self, parent: QWidget, get_preview: Callable[[Path], Path | None]): + def __init__( + self, + parent: QWidget | None, + get_preview: Callable[ + [Path], QPixmap | QImage | Path | str | None + ] = lambda p: None, + ): super().__init__(parent) self._get_preview = get_preview @@ -69,8 +74,6 @@ class BasicGameSaveGameInfoWidget(mobase.ISaveGameInfoWidget): pixmap = QPixmap(str(value)) elif isinstance(value, str): pixmap = QPixmap(value) - elif isinstance(value, QPixmap): - pixmap = value elif isinstance(value, QImage): pixmap = QPixmap.fromImage(value) else: @@ -89,14 +92,20 @@ class BasicGameSaveGameInfoWidget(mobase.ISaveGameInfoWidget): class BasicGameSaveGameInfo(mobase.SaveGameInfo): - def __init__(self, get_preview: Callable[[Path], Path | None] | None = None): + def __init__( + self, + get_preview: Callable[[Path], QPixmap | QImage | Path | str | None] + | None = None, + ): super().__init__() self._get_preview = get_preview - def getMissingAssets(self, save: mobase.ISaveGame): + def getMissingAssets(self, save: mobase.ISaveGame) -> dict[str, Sequence[str]]: return {} - def getSaveGameWidget(self, parent=None): + def getSaveGameWidget( + self, parent: QWidget | None = None + ) -> mobase.ISaveGameInfoWidget | None: if self._get_preview is not None: return BasicGameSaveGameInfoWidget(parent, self._get_preview) return None diff --git a/basic_features/utils.py b/basic_features/utils.py new file mode 100644 index 0000000..b703901 --- /dev/null +++ b/basic_features/utils.py @@ -0,0 +1,7 @@ +from typing import TypeGuard + +import mobase + + +def is_directory(entry: mobase.FileTreeEntry) -> TypeGuard[mobase.IFileTree]: + return entry.isDir() diff --git a/basic_game.py b/basic_game.py index 98e00dd..153cbd9 100644 --- a/basic_game.py +++ b/basic_game.py @@ -1,19 +1,18 @@ -# -*- encoding: utf-8 -*- +from __future__ import annotations import shutil import sys from pathlib import Path -from typing import Callable, Dict, Generic, List, Optional, TypeVar, Union - -from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths -from PyQt6.QtGui import QIcon +from typing import Callable, Generic, TypeVar import mobase +from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths +from PyQt6.QtGui import QIcon from .basic_features.basic_save_game_info import BasicGameSaveGame -def replace_variables(value: str, game: "BasicGame") -> str: +def replace_variables(value: str, game: BasicGame) -> str: """Replace special paths in the given value.""" if value.find("%DOCUMENTS%") != -1: @@ -40,11 +39,10 @@ def replace_variables(value: str, game: "BasicGame") -> str: return value -T = TypeVar("T") +_T = TypeVar("_T") -class BasicGameMapping(Generic[T]): - +class BasicGameMapping(Generic[_T]): # The game: _game: "BasicGame" @@ -58,20 +56,19 @@ class BasicGameMapping(Generic[T]): _required: bool # Callable returning a default value (if not required): - _default: Callable[["BasicGame"], T] + _default: Callable[["BasicGame"], _T] # Function to apply to the value: - _apply_fn: Optional[Callable[[Union[T, str]], T]] + _apply_fn: Callable[[_T | str], _T] | None def __init__( self, - game, - exposed_name, - internal_method, - default: Optional[Callable[["BasicGame"], T]] = None, - apply_fn: Optional[Callable[[Union[T, str]], T]] = None, + game: BasicGame, + exposed_name: str, + internal_method: str, + default: Callable[[BasicGame], _T] | None = None, + apply_fn: Callable[[_T | str], _T] | None = None, ): - self._game = game self._exposed_name = exposed_name self._internal_method_name = internal_method @@ -86,7 +83,8 @@ class BasicGameMapping(Generic[T]): except: # noqa raise ValueError( "Basic game plugin from {} has an invalid {} property.".format( - game._fromName, self._exposed_name + game._fromName, # pyright: ignore[reportPrivateUsage] + self._exposed_name, ) ) self._default = lambda game: value # type: ignore @@ -97,11 +95,12 @@ class BasicGameMapping(Generic[T]): ): raise ValueError( "Basic game plugin from {} is missing {} property.".format( - game._fromName, self._exposed_name + game._fromName, # pyright: ignore[reportPrivateUsage] + self._exposed_name, ) ) - def get(self) -> T: + def get(self) -> _T: """Return the value of this mapping.""" value = self._default(self._game) # type: ignore @@ -117,7 +116,7 @@ class BasicGameMapping(Generic[T]): return value -class BasicGameOptionsMapping(BasicGameMapping[List[T]]): +class BasicGameOptionsMapping(BasicGameMapping[list[_T]]): """ Represents a game mappings for which multiple options are possible. The game @@ -128,11 +127,11 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]): def __init__( self, - game, - exposed_name, - internal_method, - default: Optional[Callable[["BasicGame"], T]] = None, - apply_fn: Optional[Callable[[Union[List[T], str]], List[T]]] = None, + game: BasicGame, + exposed_name: str, + internal_method: str, + default: Callable[[BasicGame], _T] | None = None, + apply_fn: Callable[[list[_T] | str], list[_T]] | None = None, ): super().__init__(game, exposed_name, internal_method, lambda g: [], apply_fn) self._index = -1 @@ -147,7 +146,7 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]): """ self._index = index - def set_value(self, value: T): + def set_value(self, value: _T): """ Set the index corresponding of the given value. If the value is not present, the index is set to -1. @@ -169,7 +168,7 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]): """ return self._index != -1 - def current(self) -> T: + def current(self) -> _T: values = self._default(self._game) # type: ignore if not values: @@ -189,7 +188,6 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]): class BasicGameMappings: - name: BasicGameMapping[str] author: BasicGameMapping[str] version: BasicGameMapping[mobase.VersionInfo] @@ -197,7 +195,7 @@ class BasicGameMappings: gameName: BasicGameMapping[str] gameShortName: BasicGameMapping[str] gameNexusName: BasicGameMapping[str] - validShortNames: BasicGameMapping[List[str]] + validShortNames: BasicGameMapping[list[str]] nexusGameId: BasicGameMapping[int] binaryName: BasicGameMapping[str] launcherName: BasicGameMapping[str] @@ -208,14 +206,13 @@ class BasicGameMappings: steamAPPId: BasicGameOptionsMapping[str] gogAPPId: BasicGameOptionsMapping[str] originManifestIds: BasicGameOptionsMapping[str] - originWatcherExecutables: BasicGameMapping[List[str]] + originWatcherExecutables: BasicGameMapping[list[str]] epicAPPId: BasicGameOptionsMapping[str] eaDesktopContentId: BasicGameOptionsMapping[str] supportURL: BasicGameMapping[str] @staticmethod - def _default_documents_directory(game): - + def _default_documents_directory(game: mobase.IPluginGame): folders = [ "{}/My Games/{}".format( QStandardPaths.writableLocation( @@ -238,7 +235,7 @@ class BasicGameMappings: return QDir() # Game mappings: - def __init__(self, game: "BasicGame"): + def __init__(self, game: BasicGame): self._game = game self.name = BasicGameMapping(game, "Name", "name") @@ -302,9 +299,14 @@ class BasicGameMappings: ) # Convert Union[int, str, List[Union[int, str]]] to List[str]. - def ids_apply(v) -> List[str]: + def ids_apply(v: list[int] | list[str] | int | str) -> list[str]: + """ + Convert various types to a list of string. If the given value is already a + list, returns a new list with all values converted to string, otherwise + returns a list with the value convert to a string as its only element. + """ if isinstance(v, (int, str)): - v = [v] + v = [str(v)] return [str(x) for x in v] self.steamAPPId = BasicGameOptionsMapping( @@ -342,6 +344,19 @@ class BasicGameMappings: ) +_GameFeature = ( + mobase.BSAInvalidation + | mobase.DataArchives + | mobase.GamePlugins + | mobase.LocalSavegames + | mobase.ModDataChecker + | mobase.ModDataContent + | mobase.SaveGameInfo + | mobase.ScriptExtender + | mobase.UnmanagedMods +) + + class BasicGame(mobase.IPluginGame): """This class implements some methods from mobase.IPluginGame @@ -349,11 +364,11 @@ class BasicGame(mobase.IPluginGame): all the methods of mobase.IPluginGame.""" # List of steam, GOG, origin and Epic games: - steam_games: Dict[str, Path] - gog_games: Dict[str, Path] - origin_games: Dict[str, Path] - epic_games: Dict[str, Path] - eadesktop_games: Dict[str, Path] + steam_games: dict[str, Path] + gog_games: dict[str, Path] + origin_games: dict[str, Path] + epic_games: dict[str, Path] + eadesktop_games: dict[str, Path] @staticmethod def setup(): @@ -379,7 +394,7 @@ class BasicGame(mobase.IPluginGame): _gamePath: str # The feature map: - _featureMap: Dict + _featureMap: dict[type[_GameFeature], _GameFeature] def __init__(self): super(BasicGame, self).__init__() @@ -449,7 +464,7 @@ class BasicGame(mobase.IPluginGame): # Note: self is self._organizer.managedGame() does not work: return self.name() == self._organizer.managedGame().name() - def settings(self) -> List[mobase.PluginSetting]: + def settings(self) -> list[mobase.PluginSetting]: return [] # IPluginGame interface: @@ -491,7 +506,7 @@ class BasicGame(mobase.IPluginGame): self.gameDirectory().absoluteFilePath(self.binaryName()) ) - def validShortNames(self) -> List[str]: + def validShortNames(self) -> list[str]: return self._mappings.validShortNames.get() def gameNexusName(self) -> str: @@ -524,8 +539,8 @@ class BasicGame(mobase.IPluginGame): def getSupportURL(self) -> str: return self._mappings.supportURL.get() - def executables(self) -> List[mobase.ExecutableInfo]: - execs = [] + def executables(self) -> list[mobase.ExecutableInfo]: + execs: list[mobase.ExecutableInfo] = [] if self.getLauncherName(): execs.append( mobase.ExecutableInfo( @@ -543,37 +558,41 @@ class BasicGame(mobase.IPluginGame): ) return execs - def executableForcedLoads(self) -> List[mobase.ExecutableForcedLoadSetting]: + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return [] - def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: ext = self._mappings.savegameExtension.get() return [ BasicGameSaveGame(path) for path in Path(folder.absolutePath()).glob(f"**/*.{ext}") ] - def initializeProfile(self, path: QDir, settings: mobase.ProfileSetting): + def initializeProfile( + self, directory: QDir, settings: mobase.ProfileSetting + ) -> None: if settings & mobase.ProfileSetting.CONFIGURATION: for iniFile in self.iniFiles(): try: shutil.copyfile( self.documentsDirectory().absoluteFilePath(iniFile), - path.absoluteFilePath(QFileInfo(iniFile).fileName()), + directory.absoluteFilePath(QFileInfo(iniFile).fileName()), ) except FileNotFoundError: - Path(path.absoluteFilePath(QFileInfo(iniFile).fileName())).touch() + Path( + directory.absoluteFilePath(QFileInfo(iniFile).fileName()) + ).touch() - def primarySources(self): + def primarySources(self) -> list[str]: return [] - def primaryPlugins(self): + def primaryPlugins(self) -> list[str]: return [] - def gameVariants(self): + def gameVariants(self) -> list[str]: return [] - def setGameVariant(self, variantStr): + def setGameVariant(self, variant: str) -> None: pass def gameVersion(self) -> str: @@ -581,23 +600,23 @@ class BasicGame(mobase.IPluginGame): self.gameDirectory().absoluteFilePath(self.binaryName()) ) - def iniFiles(self): + def iniFiles(self) -> list[str]: return [] - def DLCPlugins(self): + def DLCPlugins(self) -> list[str]: return [] - def CCPlugins(self): + def CCPlugins(self) -> list[str]: return [] def loadOrderMechanism(self): - return mobase.LoadOrderMechanism.PluginsTxt + return mobase.LoadOrderMechanism.PLUGINS_TXT def sortMechanism(self): return mobase.SortMechanism.NONE - def looksValid(self, aQDir: QDir): - return aQDir.exists(self.binaryName()) + def looksValid(self, directory: QDir): + return directory.exists(self.binaryName()) def isInstalled(self) -> bool: return bool(self._gamePath) @@ -613,10 +632,10 @@ class BasicGame(mobase.IPluginGame): self.gameDirectory().absoluteFilePath(self._mappings.dataDirectory.get()) ) - def secondaryDataDirectories(self) -> Dict[str, QDir]: + def secondaryDataDirectories(self) -> dict[str, QDir]: return {} - def setGamePath(self, path: Union[Path, str]): + def setGamePath(self, path: Path | str) -> None: self._gamePath = str(path) path = Path(path) diff --git a/games/game_assettocorsa.py b/games/game_assettocorsa.py index 80a476d..da70669 100644 --- a/games/game_assettocorsa.py +++ b/games/game_assettocorsa.py @@ -2,7 +2,6 @@ from ..basic_game import BasicGame class AssettoCorsaGame(BasicGame): - Name = "Assetto Corsa Support Plugin" Author = "Deorder" Version = "0.0.1" diff --git a/games/game_blackandwhite2.py b/games/game_blackandwhite2.py index 4ea88bf..d3953d7 100644 --- a/games/game_blackandwhite2.py +++ b/games/game_blackandwhite2.py @@ -1,16 +1,15 @@ -# -*- encoding: utf-8 -*- import datetime import os import struct import sys import time from pathlib import Path -from typing import List - -from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo, Qt -from PyQt6.QtGui import QPainter, QPixmap +from typing import BinaryIO import mobase +from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo, Qt +from PyQt6.QtGui import QPainter, QPixmap +from PyQt6.QtWidgets import QWidget from ..basic_features.basic_save_game_info import ( BasicGameSaveGame, @@ -99,9 +98,9 @@ class BlackAndWhite2ModDataChecker(mobase.ModDataChecker): _mapFile = ["chl", "bmp", "bwe", "ter", "pat", "xml", "wal", "txt"] _fileIgnore = ["readme", "read me", "meta.ini", "thumbs.db", "backup", ".png"] - def fix(self, tree: mobase.IFileTree): - toMove = [] - for entry in tree: + def fix(self, filetree: mobase.IFileTree): + toMove: list[tuple[mobase.FileTreeEntry, str]] = [] + for entry in filetree: if any([sub in entry.name().casefold() for sub in self._fileIgnore]): continue elif entry.suffix() == "chl": @@ -113,25 +112,24 @@ class BlackAndWhite2ModDataChecker(mobase.ModDataChecker): else: toMove.append((entry, "/Data/landscape/BW2/")) - for (entry, path) in toMove: - tree.move(entry, path, policy=mobase.IFileTree.MERGE) + for entry, path in toMove: + filetree.move(entry, path, policy=mobase.IFileTree.MERGE) - return tree + return filetree def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: # qInfo("Data validation start") - root = tree + root = filetree unpackagedMap = False - for entry in tree: + for entry in filetree: entryName = entry.name().casefold() canIgnore = any([sub in entryName for sub in self._fileIgnore]) if not canIgnore: parent = entry.parent() if parent is not None: - if parent != root: parentName = parent.name().casefold() else: @@ -176,7 +174,7 @@ class BlackAndWhite2SaveGame(BasicGameSaveGame): "empty2": [0x00000108, 0x0000011C], } - def __init__(self, filepath): + def __init__(self, filepath: Path): super().__init__(filepath) self._filepath = Path(filepath) self.name: str = "" @@ -200,11 +198,11 @@ class BlackAndWhite2SaveGame(BasicGameSaveGame): ) - (time.localtime().tm_gmtoff * 1000) info.close() - def readInf(self, inf, key): + def readInf(self, inf: BinaryIO, key: str): inf.seek(self._saveInfLayout[key][0]) return inf.read(self._saveInfLayout[key][1] - self._saveInfLayout[key][0]) - def allFiles(self) -> List[str]: + def allFiles(self) -> list[str]: files = [str(file) for file in self._filepath.glob("./*")] files.append(str(self._filepath)) return files @@ -227,11 +225,11 @@ class BlackAndWhite2SaveGame(BasicGameSaveGame): class BlackAndWhite2LocalSavegames(mobase.LocalSavegames): - def __init__(self, myGameSaveDir): + def __init__(self, my_game_save_dir: QDir): super().__init__() - self._savesDir = myGameSaveDir.absolutePath() + self._savesDir = my_game_save_dir.absolutePath() - def mappings(self, profile_save_dir): + def mappings(self, profile_save_dir: QDir) -> list[mobase.Mapping]: m = mobase.Mapping() m.createTarget = True @@ -241,20 +239,28 @@ class BlackAndWhite2LocalSavegames(mobase.LocalSavegames): return [m] - def prepareProfile(self, profile): + def prepareProfile(self, profile: mobase.IProfile): return profile.localSavesEnabled() -def getPreview(save): - save = BlackAndWhite2SaveGame(save) +def _getPreview(savepath: Path): + save = BlackAndWhite2SaveGame(savepath) lines = [ [ - ("Name : " + save.getName(), Qt.AlignLeft), - ("| Profile : " + save.getSaveGroupIdentifier()[1:], Qt.AlignLeft), + ("Name : " + save.getName(), Qt.AlignmentFlag.AlignLeft), + ( + "| Profile : " + save.getSaveGroupIdentifier()[1:], + Qt.AlignmentFlag.AlignLeft, + ), ], - [("Land number : " + save.getLand(), Qt.AlignLeft)], - [("Saved at : " + save.getCreationTime().toString(), Qt.AlignLeft)], - [("Elapsed time : " + save.getElapsed(), Qt.AlignLeft)], + [("Land number : " + save.getLand(), Qt.AlignmentFlag.AlignLeft)], + [ + ( + "Saved at : " + save.getCreationTime().toString(), + Qt.AlignmentFlag.AlignLeft, + ) + ], + [("Elapsed time : " + save.getElapsed(), Qt.AlignmentFlag.AlignLeft)], ] pixmap = QPixmap(320, 320) @@ -269,15 +275,14 @@ def getPreview(save): width = 0 ln = 0 for line in lines: - cHeight = 0 cWidth = 0 - for (toPrint, align) in line: + for toPrint, align in line: bRect = fm.boundingRect(toPrint) cHeight = bRect.height() * (ln + 1) bRect.moveTop(cHeight - bRect.height()) - if align != Qt.AlignLeft: + if align != Qt.AlignmentFlag.AlignLeft: continue else: bRect.moveLeft(cWidth + margin) @@ -323,7 +328,7 @@ class BlackAndWhite2SaveGameInfoWidget(BasicGameSaveGameInfoWidget): class BlackAndWhite2SaveGameInfo(BasicGameSaveGameInfo): - def getSaveGameWidget(self, parent=None): + def getSaveGameWidget(self, parent: QWidget | None = None): if self._get_preview is not None: return BasicGameSaveGameInfoWidget(parent, self._get_preview) return None @@ -335,7 +340,6 @@ PSTART_MENU = ( class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper): - Name = "Black & White 2 Support Plugin" Author = "Ilyu" Version = "1.0.1" @@ -364,7 +368,7 @@ class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper): self._featureMap[mobase.LocalSavegames] = BlackAndWhite2LocalSavegames( self.savesDirectory() ) - self._featureMap[mobase.SaveGameInfo] = BlackAndWhite2SaveGameInfo(getPreview) + self._featureMap[mobase.SaveGameInfo] = BlackAndWhite2SaveGameInfo(_getPreview) return True def detectGame(self): @@ -378,7 +382,7 @@ class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper): return - def executables(self) -> List[mobase.ExecutableInfo]: + def executables(self) -> list[mobase.ExecutableInfo]: execs = super().executables() """ @@ -401,8 +405,8 @@ class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper): return execs - def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: - profiles = list() + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + profiles: list[Path] = [] for path in Path(folder.absolutePath()).glob("*/Saved Games/*"): if ( path.name == "Autosave" @@ -425,7 +429,6 @@ class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper): class BOTGGame(BlackAndWhite2Game): - Name = "Black & White 2 Battle of the Gods Support Plugin" GameName = "Black & White 2 Battle of the Gods" diff --git a/games/game_bladeandsorcery.py b/games/game_bladeandsorcery.py index ce6a797..60246f2 100644 --- a/games/game_bladeandsorcery.py +++ b/games/game_bladeandsorcery.py @@ -1,30 +1,34 @@ -from pathlib import Path import json -import os -from typing import List - -from PyQt6.QtCore import QDateTime, QDir, QLocale, Qt -from PyQt6.QtGui import QFont +from pathlib import Path import mobase - -from PyQt6.QtWidgets import QSizePolicy, QVBoxLayout, QFormLayout, QLabel, QStyle -from ..basic_features.basic_save_game_info import ( - BasicGameSaveGame, - BasicGameSaveGameInfo +from PyQt6.QtCore import QDateTime, QDir, QLocale, Qt +from PyQt6.QtGui import QFont +from PyQt6.QtWidgets import ( + QFormLayout, + QLabel, + QSizePolicy, + QStyle, + QVBoxLayout, + QWidget, ) +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, +) from ..basic_game import BasicGame class BaSSaveGame(BasicGameSaveGame): - def __init__(self, filepath): + def __init__(self, filepath: Path): super().__init__(filepath) - self._filepath = Path(filepath) with open(self._filepath, "rb") as save: save_data = json.load(save) self._gameMode: str = save_data["gameModeId"] - self._gender = "Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female" + self._gender = ( + "Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female" + ) self._ethnicity: str = save_data["ethnicGroupId"] h, m, s = save_data["playTime"].split(":") self._elapsed = (int(h), int(m), float(s)) @@ -45,17 +49,23 @@ class BaSSaveGame(BasicGameSaveGame): return f"{self._gender} {self._ethnicity}" def getElapsed(self) -> str: - return f"{self._elapsed[0]} hours, {self._elapsed[1]} minutes, {int(self._elapsed[2])} seconds" + return ( + f"{self._elapsed[0]} hours, " + f"{self._elapsed[1]} minutes, " + f"{int(self._elapsed[2])} seconds" + ) def getGameMode(self) -> str: return self._gameMode class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget): - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None): super().__init__(parent) self.resize(400, 125) - sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum) + sizePolicy = QSizePolicy( + QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) @@ -64,7 +74,9 @@ class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget): self._verticalLayout.setObjectName("verticalLayout") self._formLayout = QFormLayout() self._formLayout.setObjectName("formLayout") - self._formLayout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) + self._formLayout.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow + ) self._label = QLabel() self._label.setObjectName("label") @@ -111,14 +123,18 @@ class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget): self._characterLabel.setFont(font1) self._characterLabel.setText("") - self._formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self._characterLabel) + self._formLayout.setWidget( + 0, QFormLayout.ItemRole.FieldRole, self._characterLabel + ) self._gameModeLabel = QLabel() self._gameModeLabel.setObjectName("gameModeLabel") self._gameModeLabel.setFont(font1) self._gameModeLabel.setText("") - self._formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self._gameModeLabel) + self._formLayout.setWidget( + 1, QFormLayout.ItemRole.FieldRole, self._gameModeLabel + ) self._dateLabel = QLabel() self._dateLabel.setObjectName("dateLabel") @@ -132,39 +148,53 @@ class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget): self._sessionLabel.setFont(font1) self._sessionLabel.setText("") - self._formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self._sessionLabel) + self._formLayout.setWidget( + 3, QFormLayout.ItemRole.FieldRole, self._sessionLabel + ) self._elapsedTimeLabel = QLabel() self._elapsedTimeLabel.setObjectName("elapsedTimeLabel") self._elapsedTimeLabel.setFont(font1) self._elapsedTimeLabel.setText("") - self._formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self._elapsedTimeLabel) + self._formLayout.setWidget( + 4, QFormLayout.ItemRole.FieldRole, self._elapsedTimeLabel + ) self._verticalLayout.addLayout(self._formLayout) self.setLayout(self._verticalLayout) - self.setWindowFlags(Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget) - self.setWindowOpacity( - self.style().styleHint(QStyle.StyleHint.SH_ToolTipLabel_Opacity) / 255.0 + self.setWindowFlags( + Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget ) + style = self.style() + if style is not None: + self.setWindowOpacity( + style.styleHint(QStyle.StyleHint.SH_ToolTipLabel_Opacity) / 255.0 + ) def setSave(self, save: mobase.ISaveGame): assert isinstance(save, BaSSaveGame) self._characterLabel.setText(save.getPlayerSlug()) self._gameModeLabel.setText(save.getGameMode()) t = save.getCreationTime().toLocalTime() - self._dateLabel.setText(QLocale.system().toString(t.date(), QLocale.FormatType.ShortFormat) - + " " + QLocale.system().toString(t.time())) + self._dateLabel.setText( + QLocale.system().toString(t.date(), QLocale.FormatType.ShortFormat) + + " " + + QLocale.system().toString(t.time()) + ) s = save.getModifiedTime().toLocalTime() - self._sessionLabel.setText(QLocale.system().toString(s.date(), QLocale.FormatType.ShortFormat) - + " " + QLocale.system().toString(s.time())) + self._sessionLabel.setText( + QLocale.system().toString(s.date(), QLocale.FormatType.ShortFormat) + + " " + + QLocale.system().toString(s.time()) + ) self._elapsedTimeLabel.setText(save.getElapsed()) self.resize(0, 125) class BaSSaveGameInfo(BasicGameSaveGameInfo): - def getSaveGameWidget(self, parent=None): + def getSaveGameWidget(self, parent: QWidget | None = None): return BaSSaveGameInfoWidget(parent) @@ -191,9 +221,8 @@ class BaSGame(BasicGame): self._featureMap[mobase.SaveGameInfo] = BaSSaveGameInfo() return True - def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: ext = self._mappings.savegameExtension.get() return [ - BaSSaveGame(path) - for path in Path(folder.absolutePath()).glob(f"*.{ext}") + BaSSaveGame(path) for path in Path(folder.absolutePath()).glob(f"*.{ext}") ] diff --git a/games/game_control.py b/games/game_control.py index 67f1e11..23c0581 100644 --- a/games/game_control.py +++ b/games/game_control.py @@ -1,16 +1,12 @@ -# -*- encoding: utf-8 -*- - from __future__ import annotations -from PyQt6.QtCore import QFileInfo - import mobase +from PyQt6.QtCore import QFileInfo from ..basic_game import BasicGame class ControlGame(BasicGame): - Name = "Control Support Plugin" Author = "Zash" Version = "1.0.0" diff --git a/games/game_cyberpunk2077.py b/games/game_cyberpunk2077.py index 9f690cd..9b64f21 100644 --- a/games/game_cyberpunk2077.py +++ b/games/game_cyberpunk2077.py @@ -2,7 +2,6 @@ from ..basic_game import BasicGame class Cyberpunk2077Game(BasicGame): - Name = "Cyberpunk 2077 Support Plugin" Author = "6788" Version = "1.0.0" diff --git a/games/game_da2.py b/games/game_da2.py index a8fdc30..045055d 100644 --- a/games/game_da2.py +++ b/games/game_da2.py @@ -5,7 +5,6 @@ from ..basic_game import BasicGame class DA2Game(BasicGame): - Name = "Dragon Age 2 Support Plugin" Author = "Patchier" @@ -26,7 +25,7 @@ class DA2Game(BasicGame): def version(self): # Don't forget to import mobase! - return mobase.VersionInfo(1, 0, 1, mobase.ReleaseType.final) + return mobase.VersionInfo(1, 0, 1, mobase.ReleaseType.FINAL) def init(self, organizer: mobase.IOrganizer): super().init(organizer) diff --git a/games/game_daggerfallunity.py b/games/game_daggerfallunity.py index f76c444..a6983dd 100644 --- a/games/game_daggerfallunity.py +++ b/games/game_daggerfallunity.py @@ -25,9 +25,9 @@ class DaggerfallUnityModDataChecker(mobase.ModDataChecker): ] def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - for entry in tree: + for entry in filetree: if not entry.isDir(): continue if entry.name().casefold() in self.validDirNames: diff --git a/games/game_dao.py b/games/game_dao.py index 0f0c493..458b96a 100644 --- a/games/game_dao.py +++ b/games/game_dao.py @@ -5,7 +5,6 @@ from ..basic_game import BasicGame class DAOriginsGame(BasicGame): - Name = "Dragon Age Origins Support Plugin" Author = "Patchier" Version = "1.1.1" diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index 7102886..2ddc227 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -1,11 +1,8 @@ -# -*- encoding: utf-8 -*- import json from pathlib import Path -from typing import List - -from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths import mobase +from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths from ..basic_game import BasicGame, BasicGameSaveGame from ..steam_utils import find_steam_path @@ -51,9 +48,9 @@ class DarkestDungeonModDataChecker(mobase.ModDataChecker): ] def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - for entry in tree: + for entry in filetree: if not entry.isDir(): continue if entry.name().casefold() in self.validDirNames: @@ -62,7 +59,7 @@ class DarkestDungeonModDataChecker(mobase.ModDataChecker): class DarkestDungeonSaveGame(BasicGameSaveGame): - def __init__(self, filepath): + def __init__(self, filepath: Path): super().__init__(filepath) dataPath = filepath.joinpath("persist.game.json") self.name: str = "" @@ -127,12 +124,12 @@ class DarkestDungeonSaveGame(BasicGameSaveGame): raise ValueError( "Meta2 has wrong number of bytes: " + str(meta2DataLength) ) - meta2List = list() + meta2List: list[tuple[int, int, int]] = [] for x in range(numMeta2Entries): entryHash = int.from_bytes(fp.read(4), "little") offset = int.from_bytes(fp.read(4), "little") fieldInfo = int.from_bytes(fp.read(4), "little") - meta2List.append([entryHash, offset, fieldInfo]) + meta2List.append((entryHash, offset, fieldInfo)) # read Data fp.seek(dataOffset, 0) @@ -193,8 +190,11 @@ class DarkestDungeonGame(BasicGame): ] @staticmethod - def getCloudSaveDirectory(): - steamPath = Path(find_steam_path()) + def getCloudSaveDirectory() -> str | None: + steamPath = find_steam_path() + if steamPath is None: + return None + userData = steamPath.joinpath("userdata") for child in userData.iterdir(): name = child.name @@ -224,8 +224,8 @@ class DarkestDungeonGame(BasicGame): return QDir(cloudSaves) return documentsSaves - def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: - profiles = list() + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + profiles: list[Path] = [] for path in Path(folder.absolutePath()).glob("profile_*"): # profile_9 is only for the Multiplayer DLC "The Butcher's Circus" # and contains different files than other profiles diff --git a/games/game_darkmessiahofmightandmagic.py b/games/game_darkmessiahofmightandmagic.py index 3c8f62f..d1e1a51 100644 --- a/games/game_darkmessiahofmightandmagic.py +++ b/games/game_darkmessiahofmightandmagic.py @@ -1,10 +1,8 @@ -# -*- encoding: utf-8 -*- - import struct - -from PyQt6.QtGui import QImage +from pathlib import Path import mobase +from PyQt6.QtGui import QImage from ..basic_features import BasicGameSaveGameInfo from ..basic_game import BasicGame @@ -31,15 +29,17 @@ class DarkMessiahOfMightAndMagicGame(BasicGame): GameSavesDirectory = "%GAME_PATH%/mm/SAVE" GameSaveExtension = "sav" - def _read_save_tga(self, filename): + def _read_save_tga(self, filepath: Path) -> QImage | None: # Qt TGA reader does not work for TGA, I hope that all files # have the same format: - with open(filename.replace(".sav", ".tga"), "rb") as fp: + with open( + filepath.parent.joinpath(filepath.name.replace(".sav", ".tga")), "rb" + ) as fp: data = fp.read() _, _, w, h, bpp, _ = struct.unpack(" mobase.ModDataChecker.CheckReturn: + folders: list[mobase.IFileTree] = [] + files: list[mobase.FileTreeEntry] = [] - folders: List[mobase.IFileTree] = [] - files: List[mobase.FileTreeEntry] = [] - - for entry in tree: + for entry in filetree: if isinstance(entry, mobase.IFileTree): folders.append(entry) else: @@ -53,9 +52,6 @@ class DivinityOriginalSinEnhancedEditionModDataChecker(mobase.ModDataChecker): return mobase.ModDataChecker.INVALID - def fix(self, tree: mobase.IFileTree) -> Optional[mobase.IFileTree]: - return None - class DivinityOriginalSinEnhancedEditionGame(BasicGame, mobase.IPluginFileMapper): Name = "Divinity: Original Sin (Enhanced Edition) Support Plugin" @@ -101,10 +97,9 @@ class DivinityOriginalSinEnhancedEditionGame(BasicGame, mobase.IPluginFileMapper ] = DivinityOriginalSinEnhancedEditionModDataChecker() return True - def mappings(self) -> List[mobase.Mapping]: - map = [] - modDirs = [self.DOCS_MOD_SPECIAL_NAME] - self._listDirsRecursive(modDirs, prefix=self.DOCS_MOD_SPECIAL_NAME) + def mappings(self) -> list[mobase.Mapping]: + map: list[mobase.Mapping] = [] + modDirs = self._listDirsRecursive(Path(self.DOCS_MOD_SPECIAL_NAME)) for dir_ in modDirs: for file_ in self._organizer.findFiles(path=dir_, filter=lambda x: True): m = mobase.Mapping() @@ -121,9 +116,9 @@ class DivinityOriginalSinEnhancedEditionGame(BasicGame, mobase.IPluginFileMapper def primarySources(self): return self.GameValidShortNames - def _listDirsRecursive(self, dirs_list, prefix=""): - dirs = self._organizer.listDirectories(prefix) + def _listDirsRecursive(self, prefix: Path) -> list[str]: + res = [str(prefix)] + dirs = self._organizer.listDirectories(str(prefix)) for dir_ in dirs: - dir_ = os.path.join(prefix, dir_) - dirs_list.append(dir_) - self._listDirsRecursive(dirs_list, dir_) + res.extend(self._listDirsRecursive(prefix.joinpath(dir_))) + return res diff --git a/games/game_dragonsdogmadarkarisen.py b/games/game_dragonsdogmadarkarisen.py index 77af70e..e26b00e 100644 --- a/games/game_dragonsdogmadarkarisen.py +++ b/games/game_dragonsdogmadarkarisen.py @@ -1,10 +1,7 @@ -# -*- encoding: utf-8 -*- - from ..basic_game import BasicGame class NoMansSkyGame(BasicGame): - Name = "Dragon's Dogma: Dark Arisen Support Plugin" Author = "Luca/EzioTheDeadPoet" Version = "1.0.0" diff --git a/games/game_dungeonsiege2.py b/games/game_dungeonsiege2.py index 3c32ca8..76c365c 100644 --- a/games/game_dungeonsiege2.py +++ b/games/game_dungeonsiege2.py @@ -1,10 +1,5 @@ -# -*- encoding: utf-8 -*- - -from typing import List, Tuple - -from PyQt6.QtCore import QFileInfo - import mobase +from PyQt6.QtCore import QFileInfo from ..basic_game import BasicGame @@ -15,10 +10,9 @@ class DungeonSiegeIIModDataChecker(mobase.ModDataChecker): def get_resources_and_maps( self, tree: mobase.IFileTree - ) -> Tuple[List[mobase.FileTreeEntry], List[mobase.FileTreeEntry]]: - - ress: List[mobase.FileTreeEntry] = [] - maps: List[mobase.FileTreeEntry] = [] + ) -> tuple[list[mobase.FileTreeEntry], list[mobase.FileTreeEntry]]: + ress: list[mobase.FileTreeEntry] = [] + maps: list[mobase.FileTreeEntry] = [] for e in tree: if e.isFile(): @@ -30,31 +24,31 @@ class DungeonSiegeIIModDataChecker(mobase.ModDataChecker): return ress, maps def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: # Check if we have a Resources / Maps folder or .ds2res/.ds2map - ress, maps = self.get_resources_and_maps(tree) + ress, maps = self.get_resources_and_maps(filetree) if not ress and not maps: - if tree.exists("Resources") or tree.exists("Maps"): + if filetree.exists("Resources") or filetree.exists("Maps"): return mobase.ModDataChecker.VALID else: return mobase.ModDataChecker.INVALID return mobase.ModDataChecker.FIXABLE - def fix(self, tree: mobase.IFileTree) -> mobase.IFileTree: - ress, maps = self.get_resources_and_maps(tree) + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + ress, maps = self.get_resources_and_maps(filetree) if ress: - rfolder = tree.addDirectory("Resources") + rfolder = filetree.addDirectory("Resources") for r in ress: rfolder.insert(r, mobase.IFileTree.REPLACE) if maps: - rfolder = tree.addDirectory("Maps") + rfolder = filetree.addDirectory("Maps") for r in maps: rfolder.insert(r, mobase.IFileTree.REPLACE) - return tree + return filetree class DungeonSiegeIIGame(BasicGame): diff --git a/games/game_gta-3-de.py b/games/game_gta-3-de.py index 3b8eef4..09bc6ff 100644 --- a/games/game_gta-3-de.py +++ b/games/game_gta-3-de.py @@ -1,9 +1,8 @@ import os from pathlib import Path -from PyQt6.QtCore import QDir, QFileInfo - import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame @@ -13,9 +12,9 @@ class GTA3DefinitiveEditionModDataChecker(mobase.ModDataChecker): super().__init__() def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - for entry in tree: + for entry in filetree: if Path(entry.name().casefold()).suffix == ".pak": return mobase.ModDataChecker.VALID @@ -23,7 +22,6 @@ class GTA3DefinitiveEditionModDataChecker(mobase.ModDataChecker): class GTA3DefinitiveEditionGame(BasicGame): - Name = "Grand Theft Auto III - Definitive Edition Support Plugin" Author = "dekart811" Version = "1.0" @@ -64,10 +62,10 @@ class GTA3DefinitiveEditionGame(BasicGame): def iniFiles(self): return ["GameUserSettings.ini", "CustomSettings.ini"] - def initializeProfile(self, path: QDir, settings: mobase.ProfileSetting): + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): # Create the mods directory if it doesn't exist modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(path, settings) + super().initializeProfile(directory, settings) diff --git a/games/game_gta-san-andreas-de.py b/games/game_gta-san-andreas-de.py index e06c144..2d58fec 100644 --- a/games/game_gta-san-andreas-de.py +++ b/games/game_gta-san-andreas-de.py @@ -1,9 +1,8 @@ import os from pathlib import Path -from PyQt6.QtCore import QDir, QFileInfo - import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame @@ -13,9 +12,9 @@ class GTASanAndreasDefinitiveEditionModDataChecker(mobase.ModDataChecker): super().__init__() def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - for entry in tree: + for entry in filetree: if Path(entry.name().casefold()).suffix == ".pak": return mobase.ModDataChecker.VALID @@ -23,7 +22,6 @@ class GTASanAndreasDefinitiveEditionModDataChecker(mobase.ModDataChecker): class GTASanAndreasDefinitiveEditionGame(BasicGame): - Name = "Grand Theft Auto: San Andreas - Definitive Edition Support Plugin" Author = "dekart811" Version = "1.0" @@ -66,10 +64,10 @@ class GTASanAndreasDefinitiveEditionGame(BasicGame): def iniFiles(self): return ["GameUserSettings.ini", "CustomSettings.ini"] - def initializeProfile(self, path: QDir, settings: mobase.ProfileSetting): + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): # Create the mods directory if it doesn't exist modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(path, settings) + super().initializeProfile(directory, settings) diff --git a/games/game_gta-vice-city-de.py b/games/game_gta-vice-city-de.py index 44129f3..2a65449 100644 --- a/games/game_gta-vice-city-de.py +++ b/games/game_gta-vice-city-de.py @@ -1,9 +1,8 @@ import os from pathlib import Path -from PyQt6.QtCore import QDir, QFileInfo - import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame @@ -13,9 +12,9 @@ class GTAViceCitysDefinitiveEditionModDataChecker(mobase.ModDataChecker): super().__init__() def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - for entry in tree: + for entry in filetree: if Path(entry.name().casefold()).suffix == ".pak": return mobase.ModDataChecker.VALID @@ -23,7 +22,6 @@ class GTAViceCitysDefinitiveEditionModDataChecker(mobase.ModDataChecker): class GTAViceCityDefinitiveEditionGame(BasicGame): - Name = "Grand Theft Auto: Vice City - Definitive Edition Support Plugin" Author = "dekart811" Version = "1.0" @@ -66,10 +64,10 @@ class GTAViceCityDefinitiveEditionGame(BasicGame): def iniFiles(self): return ["GameUserSettings.ini", "CustomSettings.ini"] - def initializeProfile(self, path: QDir, settings: mobase.ProfileSetting): + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): # Create the mods directory if it doesn't exist modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(path, settings) + super().initializeProfile(directory, settings) diff --git a/games/game_kerbalspaceprogram.py b/games/game_kerbalspaceprogram.py index c2a6a3b..f9f5848 100644 --- a/games/game_kerbalspaceprogram.py +++ b/games/game_kerbalspaceprogram.py @@ -1,7 +1,7 @@ -from os import path from pathlib import Path import mobase +from PyQt6.QtCore import QDir from ..basic_features import BasicGameSaveGameInfo from ..basic_features.basic_save_game_info import BasicGameSaveGame @@ -10,22 +10,20 @@ from ..basic_game import BasicGame class KerbalSpaceProgramSaveGame(BasicGameSaveGame): def allFiles(self): - group = path.parent - banner = group.joinpath("banners").joinpath(f"${self.getName()}.png") - files = [self.filename] + files = [super().getFilepath()] + banner = self._filepath.joinpath("banners").joinpath(f"${self.getName()}.png") if banner.exists(): - files.append(banner) + files.append(banner.as_posix()) return files def getName(self): return self._filepath.stem def getSaveGroupIdentifier(self): - return path.parent.name + return self._filepath.parent.name class KerbalSpaceProgramGame(BasicGame): - Name = "Kerbal Space Program Support Plugin" Author = "LaughingHyena" Version = "1.0.0" @@ -43,7 +41,7 @@ class KerbalSpaceProgramGame(BasicGame): "Game:-Kerbal-Space-Program" ) - def init(self, organizer): + def init(self, organizer: mobase.IOrganizer): super().init(organizer) self._featureMap[mobase.SaveGameInfo] = BasicGameSaveGameInfo( lambda s: str( @@ -52,7 +50,7 @@ class KerbalSpaceProgramGame(BasicGame): ) return True - def listSaves(self, folder): + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: ext = self._mappings.savegameExtension.get() return [ KerbalSpaceProgramSaveGame(path) diff --git a/games/game_kingdomcomedeliverance.py b/games/game_kingdomcomedeliverance.py index 0cfcfd0..503ff5d 100644 --- a/games/game_kingdomcomedeliverance.py +++ b/games/game_kingdomcomedeliverance.py @@ -1,10 +1,7 @@ -# -*- encoding: utf-8 -*- - import os -from PyQt6.QtCore import QDir - import mobase +from PyQt6.QtCore import QDir from ..basic_game import BasicGame @@ -34,7 +31,7 @@ class KingdomComeDeliveranceGame(BasicGame): def iniFiles(self): return ["custom.cfg", "system.cfg", "user.cfg"] - def initializeProfile(self, path: QDir, settings: mobase.ProfileSetting): + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): # Create .cfg files if they don't exist for iniFile in self.iniFiles(): iniPath = self.documentsDirectory().absoluteFilePath(iniFile) @@ -47,4 +44,4 @@ class KingdomComeDeliveranceGame(BasicGame): if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(path, settings) + super().initializeProfile(directory, settings) diff --git a/games/game_masterduel.py b/games/game_masterduel.py index e832bc4..ff9147b 100644 --- a/games/game_masterduel.py +++ b/games/game_masterduel.py @@ -1,17 +1,13 @@ -# -*- encoding: utf-8 -*- - -from os.path import exists, join +from pathlib import Path from typing import List, Optional -from PyQt6.QtCore import QDir, QFileInfo - import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame class MasterDuelGame(BasicGame, mobase.IPluginFileMapper): - Name = "Yu-Gi-Oh! Master Duel Support Plugin" Author = "The Conceptionist & uwx" Version = "1.0.2" @@ -62,7 +58,7 @@ class MasterDuelGame(BasicGame, mobase.IPluginFileMapper): return self._userDataDirCached def mappings(self) -> List[mobase.Mapping]: - modsPath = self._organizer.modsPath() + modsPath = Path(self._organizer.modsPath()) unityMods = self.getUnityDataMods() mappings: List[mobase.Mapping] = [] @@ -71,22 +67,22 @@ class MasterDuelGame(BasicGame, mobase.IPluginFileMapper): m = mobase.Mapping() m.createTarget = False m.isDirectory = True - m.source = join(modsPath, modName, "AssetBundle") + m.source = modsPath.joinpath(modName, "AssetBundle").as_posix() m.destination = self.gameDirectory().filePath( - join("masterduel_Data", "StreamingAssets", "AssetBundle") + Path("masterduel_Data", "StreamingAssets", "AssetBundle").as_posix() ) mappings.append(m) return mappings - def getUnityDataMods(self): - modsPath = self._organizer.modsPath() + def getUnityDataMods(self) -> list[str]: + modsPath = Path(self._organizer.modsPath()) allMods = self._organizer.modList().allModsByProfilePriority() - unityMods = [] + unityMods: list[str] = [] for modName in allMods: if self._organizer.modList().state(modName) & mobase.ModState.ACTIVE != 0: - if exists(join(modsPath, modName, "AssetBundle")): + if modsPath.joinpath(modName, "AssetBundle").exists(): unityMods.append(modName) return unityMods diff --git a/games/game_mirrorsedge.py b/games/game_mirrorsedge.py index 70b8241..db81908 100644 --- a/games/game_mirrorsedge.py +++ b/games/game_mirrorsedge.py @@ -1,10 +1,7 @@ -# -*- encoding: utf-8 -*- - from ..basic_game import BasicGame class MirrorsEdgeGame(BasicGame): - Name = "Mirror's Edge Support Plugin" Author = "Luca/EzioTheDeadPoet" Version = "1.0.0" diff --git a/games/game_monsterhunterworld.py b/games/game_monsterhunterworld.py index 23a9869..290048c 100644 --- a/games/game_monsterhunterworld.py +++ b/games/game_monsterhunterworld.py @@ -2,7 +2,6 @@ from ..basic_game import BasicGame class MonsterHunterWorldGame(BasicGame): - Name = "Monster Hunter: World Support Plugin" Author = "prz" Version = "1.0.0" diff --git a/games/game_mountandblade2.py b/games/game_mountandblade2.py index 485aa58..e710840 100644 --- a/games/game_mountandblade2.py +++ b/games/game_mountandblade2.py @@ -1,17 +1,11 @@ -# -*- encoding: utf-8 -*- - -from typing import List - -from PyQt6.QtCore import QFileInfo - import mobase +from PyQt6.QtCore import QFileInfo from ..basic_game import BasicGame class MountAndBladeIIModDataChecker(mobase.ModDataChecker): - - _valid_folders: List[str] = [ + _valid_folders: list[str] = [ "native", "sandbox", "sandboxcore", @@ -23,10 +17,9 @@ class MountAndBladeIIModDataChecker(mobase.ModDataChecker): super().__init__() def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - - for e in tree: + for e in filetree: if e.isDir(): if e.name().lower() in self._valid_folders: return mobase.ModDataChecker.VALID diff --git a/games/game_msfs2020.py b/games/game_msfs2020.py index b9ecdd1..d0c91ba 100644 --- a/games/game_msfs2020.py +++ b/games/game_msfs2020.py @@ -7,7 +7,6 @@ from ..basic_game import BasicGame class MSFS2020Game(BasicGame): - Name = "Microsoft Flight Simulator 2020 Support Plugin" Author = "Deorder" Version = "0.0.1" diff --git a/games/game_nierautomata.py b/games/game_nierautomata.py index 3bed00f..f106bc4 100644 --- a/games/game_nierautomata.py +++ b/games/game_nierautomata.py @@ -1,10 +1,7 @@ -# -*- encoding: utf-8 -*- - from ..basic_game import BasicGame class NierAutomataGame(BasicGame): - Name = "NieR:Automata Support Plugin" Author = "Luca/EzioTheDeadPoet" Version = "1.0.0" diff --git a/games/game_nomanssky.py b/games/game_nomanssky.py index fdc109d..6653644 100644 --- a/games/game_nomanssky.py +++ b/games/game_nomanssky.py @@ -1,14 +1,10 @@ -# -*- encoding: utf-8 -*- - -from PyQt6.QtCore import QFileInfo - import mobase +from PyQt6.QtCore import QFileInfo from ..basic_game import BasicGame class NoMansSkyGame(BasicGame): - Name = "No Man's Sky Support Plugin" Author = "Luca/EzioTheDeadPoet" Version = "1.0.0" diff --git a/games/game_sims4.py b/games/game_sims4.py index 1c3e5e7..a3c905b 100644 --- a/games/game_sims4.py +++ b/games/game_sims4.py @@ -4,7 +4,6 @@ from ..basic_game import BasicGame class TS4Game(BasicGame): - Name = "The Sims 4 Support Plugin" Author = "R3z Shark" @@ -18,4 +17,4 @@ class TS4Game(BasicGame): def version(self): # Don't forget to import mobase! - return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final) + return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.FINAL) diff --git a/games/game_stalkeranomaly.py b/games/game_stalkeranomaly.py index df05f9e..0b83477 100644 --- a/games/game_stalkeranomaly.py +++ b/games/game_stalkeranomaly.py @@ -1,13 +1,9 @@ -# -*- encoding: utf-8 -*- - from enum import IntEnum from pathlib import Path -from typing import List - -from PyQt6.QtCore import QDir, QFileInfo, Qt -from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget import mobase +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget from ..basic_features.basic_save_game_info import ( BasicGameSaveGame, @@ -18,7 +14,7 @@ from .stalkeranomaly import XRSave class StalkerAnomalyModDataChecker(mobase.ModDataChecker): - _valid_folders: List[str] = [ + _valid_folders: list[str] = [ "appdata", "bin", "db", @@ -33,8 +29,8 @@ class StalkerAnomalyModDataChecker(mobase.ModDataChecker): return False - def findLostData(self, tree: mobase.IFileTree) -> List[mobase.FileTreeEntry]: - lost_db: List[mobase.FileTreeEntry] = [] + def findLostData(self, tree: mobase.IFileTree) -> list[mobase.FileTreeEntry]: + lost_db: list[mobase.FileTreeEntry] = [] for e in tree: if e.isFile(): @@ -44,24 +40,24 @@ class StalkerAnomalyModDataChecker(mobase.ModDataChecker): return lost_db def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - if self.hasValidFolders(tree): + if self.hasValidFolders(filetree): return mobase.ModDataChecker.VALID - if self.findLostData(tree): + if self.findLostData(filetree): return mobase.ModDataChecker.FIXABLE return mobase.ModDataChecker.INVALID - def fix(self, tree: mobase.IFileTree) -> mobase.IFileTree: - lost_db = self.findLostData(tree) + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + lost_db = self.findLostData(filetree) if lost_db: - rfolder = tree.addDirectory("db").addDirectory("mods") + rfolder = filetree.addDirectory("db").addDirectory("mods") for r in lost_db: rfolder.insert(r, mobase.IFileTree.REPLACE) - return tree + return filetree class Content(IntEnum): @@ -75,9 +71,9 @@ class Content(IntEnum): class StalkerAnomalyModDataContent(mobase.ModDataContent): - content: List[int] = [] + content: list[int] = [] - def getAllContents(self) -> List[mobase.ModDataContent.Content]: + def getAllContents(self) -> list[mobase.ModDataContent.Content]: return [ mobase.ModDataContent.Content( Content.INTERFACE, "Interface", ":/MO/gui/content/interface" @@ -125,9 +121,9 @@ class StalkerAnomalyModDataContent(mobase.ModDataContent): return mobase.IFileTree.WalkReturn.CONTINUE - def getContentsFor(self, tree: mobase.IFileTree) -> List[int]: + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: self.content = [] - tree.walk(self.walkContent, "/") + filetree.walk(self.walkContent, "/") return self.content @@ -150,7 +146,7 @@ class StalkerAnomalySaveGame(BasicGameSaveGame): return f"{name}, {xr_save.save_fmt} [{time}]" return "" - def allFiles(self) -> List[str]: + def allFiles(self) -> list[str]: filepath = str(self._filepath) paths = [filepath] scoc = filepath.replace(".scop", ".scoc") @@ -163,7 +159,7 @@ class StalkerAnomalySaveGame(BasicGameSaveGame): class StalkerAnomalySaveGameInfoWidget(mobase.ISaveGameInfoWidget): - def __init__(self, parent: QWidget): + def __init__(self, parent: QWidget | None): super().__init__(parent) layout = QVBoxLayout() self._labelSave = self.newLabel(layout) @@ -211,7 +207,7 @@ class StalkerAnomalySaveGameInfoWidget(mobase.ISaveGameInfoWidget): class StalkerAnomalySaveGameInfo(BasicGameSaveGameInfo): - def getSaveGameWidget(self, parent=None): + def getSaveGameWidget(self, parent: QWidget | None = None): return StalkerAnomalySaveGameInfoWidget(parent) @@ -258,11 +254,11 @@ class StalkerAnomalyGame(BasicGame, mobase.IPluginFileMapper): dbg_path = Path(self._gamePath, "gamedata/configs/cache_dbg.ltx") if not dbg_path.exists(): dbg_path.parent.mkdir(parents=True, exist_ok=True) - with open(dbg_path, "w", encoding="utf-8") as file: # noqa + with open(dbg_path, "w", encoding="utf-8"): pass return True - def executables(self) -> List[mobase.ExecutableInfo]: + def executables(self) -> list[mobase.ExecutableInfo]: info = [ ["Anomaly Launcher", "AnomalyLauncher.exe"], ["Anomaly (DX11-AVX)", "bin/AnomalyDX11AVX.exe"], @@ -279,14 +275,14 @@ class StalkerAnomalyGame(BasicGame, mobase.IPluginFileMapper): mobase.ExecutableInfo(inf[0], QFileInfo(gamedir, inf[1])) for inf in info ] - def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: ext = self._mappings.savegameExtension.get() return [ StalkerAnomalySaveGame(path) for path in Path(folder.absolutePath()).glob(f"*.{ext}") ] - def mappings(self) -> List[mobase.Mapping]: + def mappings(self) -> list[mobase.Mapping]: appdata = self.gameDirectory().filePath("appdata") m = mobase.Mapping() m.createTarget = True diff --git a/games/game_stardewvalley.py b/games/game_stardewvalley.py index dd3d507..70db9cd 100644 --- a/games/game_stardewvalley.py +++ b/games/game_stardewvalley.py @@ -1,8 +1,5 @@ -# -*- encoding: utf-8 -*- - -from PyQt6.QtCore import QFileInfo - import mobase +from PyQt6.QtCore import QFileInfo from ..basic_game import BasicGame @@ -12,11 +9,10 @@ class StardewValleyModDataChecker(mobase.ModDataChecker): super().__init__() def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - - for e in tree: - if e.isDir() and e.exists( # type: ignore + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.exists( "manifest.json", mobase.IFileTree.FILE ): return mobase.ModDataChecker.VALID diff --git a/games/game_starsector.py b/games/game_starsector.py index ac37771..567ce0c 100644 --- a/games/game_starsector.py +++ b/games/game_starsector.py @@ -1,10 +1,7 @@ -# -*- encoding: utf-8 -*- from pathlib import Path -from typing import List - -from PyQt6.QtCore import QDir import mobase +from PyQt6.QtCore import QDir from ..basic_game import BasicGame, BasicGameSaveGame @@ -25,7 +22,7 @@ class Starsector(BasicGame): "Game:-Starsector" ) - def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: return [ BasicGameSaveGame(path) for path in Path(folder.absolutePath()).glob("save_*") diff --git a/games/game_starwars-empire-at-war-foc.py b/games/game_starwars-empire-at-war-foc.py index 93131b5..2fc1fa9 100644 --- a/games/game_starwars-empire-at-war-foc.py +++ b/games/game_starwars-empire-at-war-foc.py @@ -1,10 +1,5 @@ -# -*- encoding: utf-8 -*- - -from typing import List - -from PyQt6.QtCore import QFileInfo - import mobase +from PyQt6.QtCore import QFileInfo from ..basic_game import BasicGame @@ -28,7 +23,7 @@ class StarWarsEmpireAtWarGame(BasicGame): "Game:-Star-Wars:-Empire-At-War" ) - def executables(self) -> List[mobase.ExecutableInfo]: + def executables(self) -> list[mobase.ExecutableInfo]: return [ mobase.ExecutableInfo( "STAR WARS Empire at War: Forces of Corruption", diff --git a/games/game_starwars-empire-at-war.py b/games/game_starwars-empire-at-war.py index 0bab7f4..4fb916a 100644 --- a/games/game_starwars-empire-at-war.py +++ b/games/game_starwars-empire-at-war.py @@ -1,10 +1,5 @@ -# -*- encoding: utf-8 -*- - -from typing import List - -from PyQt6.QtCore import QFileInfo - import mobase +from PyQt6.QtCore import QFileInfo from ..basic_game import BasicGame @@ -28,7 +23,7 @@ class StarWarsEmpireAtWarGame(BasicGame): "Game:-Star-Wars:-Empire-At-War" ) - def executables(self) -> List[mobase.ExecutableInfo]: + def executables(self) -> list[mobase.ExecutableInfo]: return [ mobase.ExecutableInfo( "STAR WARS Empire at War", diff --git a/games/game_subnautica-below-zero.py b/games/game_subnautica-below-zero.py index 5b9e3a7..49c1497 100644 --- a/games/game_subnautica-below-zero.py +++ b/games/game_subnautica-below-zero.py @@ -2,11 +2,11 @@ from __future__ import annotations import mobase +from ..basic_features.basic_mod_data_checker import GlobPatterns from . import game_subnautica # namespace to not load SubnauticaGame here, too! class SubnauticaBelowZeroGame(game_subnautica.SubnauticaGame): - Name = "Subnautica Below Zero Support Plugin" Author = "dekart811, Zash" Version = "2.1" @@ -31,7 +31,9 @@ class SubnauticaBelowZeroGame(game_subnautica.SubnauticaGame): def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) - checker = game_subnautica.SubnauticaModDataChecker() - checker.update_patterns({"unfold": ["BepInExPack_BelowZero"]}) - self._featureMap[mobase.ModDataChecker] = checker + self._featureMap[ + mobase.ModDataChecker + ] = game_subnautica.SubnauticaModDataChecker( + GlobPatterns(unfold=["BepInExPack_BelowZero"]) + ) return True diff --git a/games/game_subnautica.py b/games/game_subnautica.py index 49097af..d72677e 100644 --- a/games/game_subnautica.py +++ b/games/game_subnautica.py @@ -1,13 +1,12 @@ from __future__ import annotations -from enum import Enum import os from collections.abc import Iterable +from enum import Enum from pathlib import Path -from PyQt6.QtCore import QDir, qWarning - import mobase +from PyQt6.QtCore import QDir, qWarning from ..basic_features import BasicModDataChecker from ..basic_features.basic_save_game_info import ( @@ -33,7 +32,6 @@ class SubnauticaModDataChecker(BasicModDataChecker): class SubnauticaGame(BasicGame, mobase.IPluginFileMapper): - Name = "Subnautica Support Plugin" Author = "dekart811, Zash" Version = "2.1.1" diff --git a/games/game_thebindingofisaacrebirth.py b/games/game_thebindingofisaacrebirth.py index 4d184dc..a21f11d 100644 --- a/games/game_thebindingofisaacrebirth.py +++ b/games/game_thebindingofisaacrebirth.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- - from ..basic_game import BasicGame diff --git a/games/game_trainsimulator.py b/games/game_trainsimulator.py index 44c369d..6c6e8df 100644 --- a/games/game_trainsimulator.py +++ b/games/game_trainsimulator.py @@ -1,12 +1,10 @@ -from PyQt6.QtCore import QFileInfo - import mobase +from PyQt6.QtCore import QFileInfo from ..basic_game import BasicGame class RailworksGame(BasicGame): - Name = "Train Simulator 20xx Support Plugin" Author = "Ryan Young" @@ -19,7 +17,7 @@ class RailworksGame(BasicGame): def version(self): # Don't forget to import mobase! - return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final) + return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.FINAL) def executables(self): return [ diff --git a/games/game_valheim.py b/games/game_valheim.py index 9c8839c..a7d133a 100644 --- a/games/game_valheim.py +++ b/games/game_valheim.py @@ -1,20 +1,17 @@ -# -*- encoding: utf-8 -*- - from __future__ import annotations import itertools import re import shutil -from collections.abc import Collection, Container, Iterable, Mapping, Sequence +from collections.abc import Collection, Iterable, Mapping, Sequence from dataclasses import dataclass, field from pathlib import Path -from typing import Optional, TextIO, Union - -from PyQt6.QtCore import QDir +from typing import Any, Optional, TextIO import mobase +from PyQt6.QtCore import QDir -from ..basic_features import BasicModDataChecker +from ..basic_features.basic_mod_data_checker import BasicModDataChecker, GlobPatterns from ..basic_features.basic_save_game_info import BasicGameSaveGame from ..basic_game import BasicGame @@ -29,10 +26,10 @@ def move_file(source: Path, target: Path): @dataclass class PartialMatch: - partial_match_regex: re.Pattern = re.compile(r"[A-Z]?[a-z]+") + partial_match_regex: re.Pattern[str] = re.compile(r"[A-Z]?[a-z]+") """Matches words, for e.g. 'Camel' and 'Case' in 'CamelCase'.""" - exclude: Container = field(default_factory=set) + exclude: set[str] = field(default_factory=set) min_length: int = 3 def partial_match(self, str_with_parts: str, search_string: str) -> Collection[str]: @@ -84,10 +81,10 @@ class DebugTable: """ self.new_table(column_keys) - def __call__(self, **kwargs) -> None: + def __call__(self, **kwargs: Any) -> None: self.add(**kwargs) - def new_table(self, column_keys: Optional[Collection[str]] = None) -> None: + def new_table(self, column_keys: Collection[str] | None = None) -> None: if column_keys: self._column_keys = column_keys self._table: list[dict[str, str]] = [ @@ -97,7 +94,7 @@ class DebugTable: def add( self, - **kwargs, + **kwargs: Any, ) -> None: """Add data to the table. Adds a new line if the last row has already data in for the column key. @@ -128,9 +125,9 @@ class OverwriteSync: overwrite_file_pattern: Iterable[str] = ["BepInEx/config/*"] """File pattern (glob) in overwrite folder.""" partial_match: PartialMatch = PartialMatch(exclude={"valheim", "mod"}) - content_match: Optional[ContentMatch] = ContentMatch( + content_match: ContentMatch = ContentMatch( file_glob_patterns=["*.cfg"], - content_regex=re.compile(r"\A.*plugin (?P.+) v[\d\.]+?$", re.I | re.M), + content_regex=re.compile(r"\A.*plugin (?P.+) v[\d.]+?$", re.I | re.M), match_group="mod", ) @@ -160,8 +157,8 @@ class OverwriteSync: self._debug.print() def _get_active_mods( - self, modlist: Optional[mobase.IModList] = None - ) -> Mapping[str, mobase.IModInterface]: + self, modlist: mobase.IModList | None = None + ) -> dict[str, mobase.IModInterface]: """Get all active mods. Args: modlist (optional): the `mobase.IModList`. Defaults to None (get it from @@ -181,10 +178,10 @@ class OverwriteSync: and (modlist.state(name) & mobase.ModState.ACTIVE) } - def _get_mod_dll_map(self, mod_map): + def _get_mod_dll_map(self, mod_map: Mapping[str, str | mobase.IModInterface]): return {name: self._get_mod_dlls(mod) for name, mod in mod_map.items()} - def _get_mod_dlls(self, mod: Union[str, mobase.IModInterface]) -> Sequence[str]: + def _get_mod_dlls(self, mod: str | mobase.IModInterface) -> Sequence[str]: """Get all BepInEx/plugins/*.dll files of a mod.""" if isinstance(mod, str): mod = self.organizer.modList().getMod(mod) @@ -202,7 +199,7 @@ class OverwriteSync: """Find the mod (name) matching a file in Overwrite (using the mods dll name). Args: - file_name: The name of the file. + file_path: The name of the file. mod_dll_map: Mods names and their dll files `{mod_name: ["ModName.dll"]}`. Returns: @@ -236,8 +233,8 @@ class OverwriteSync: """Find matching mods for the given `search_str`. Args: - search_name: A string to find a mod match for. mod_dll_map: Mods names and - their dll files `{mod_name: ["ModName.dll"]}`. + search_str: A string to find a mod match for. + mod_dll_map: Mods names and their dll files `{mod_name: ["ModName.dll"]}`. Returns: Mods with partial matches, sorted descending by their metric @@ -290,26 +287,25 @@ class ValheimWorldSaveGame(ValheimSaveGame): class ValheimLocalSavegames(mobase.LocalSavegames): - def __init__(self, myGameSaveDir): + def __init__(self, my_game_save_dir: QDir): super().__init__() - self._savesDir = myGameSaveDir.absolutePath() + self._saves_dir = my_game_save_dir.absolutePath() - def mappings(self, profile_save_dir): + def mappings(self, profile_save_dir: QDir): return [ mobase.Mapping( source=profile_save_dir.absolutePath(), - destination=self._savesDir, + destination=self._saves_dir, is_directory=True, create_target=True, ) ] - def prepareProfile(self, profile): + def prepareProfile(self, profile: mobase.IProfile): return profile.localSavesEnabled() class ValheimGame(BasicGame): - Name = "Valheim Support Plugin" Author = "Zash" Version = "1.2.1" @@ -330,11 +326,11 @@ class ValheimGame(BasicGame): def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self._featureMap[mobase.ModDataChecker] = BasicModDataChecker( - { - "unfold": [ + GlobPatterns( + unfold=[ "BepInExPack_Valheim", ], - "valid": [ + valid=[ "meta.ini", # Included in installed mod folder. "BepInEx", "doorstop_libs", @@ -352,7 +348,7 @@ class ValheimGame(BasicGame): # "AdvancedBuilder", ], - "delete": [ + delete=[ "*.txt", "*.md", "README", @@ -362,7 +358,7 @@ class ValheimGame(BasicGame): "*.dll.mdb", "*.pdb", ], - "move": { + move={ "*_VML.dll": "InSlimVML/Mods/", # "plugins": "BepInEx/", @@ -379,7 +375,7 @@ class ValheimGame(BasicGame): # "*.assets": "valheim_Data/", }, - } + ) ) self._featureMap[mobase.LocalSavegames] = ValheimLocalSavegames( self.savesDirectory() @@ -421,7 +417,7 @@ class ValheimGame(BasicGame): self._organizer.onUserInterfaceInitialized(lambda win: self._sync_overwrite()) self._organizer.onFinishedRun(self._game_finished_event_handler) - def _game_finished_event_handler(self, app_path: str, *args) -> None: + def _game_finished_event_handler(self, app_path: str, exit_code: int) -> None: """Sync overwrite folder with mods after game was closed.""" if Path(app_path) == Path( self.gameDirectory().absolutePath(), self.binaryName() diff --git a/games/game_vampirebloodlines.py b/games/game_vampirebloodlines.py index 66fdf0c..0beb50f 100644 --- a/games/game_vampirebloodlines.py +++ b/games/game_vampirebloodlines.py @@ -1,11 +1,8 @@ -# -*- encoding: utf-8 -*- -import os from pathlib import Path from typing import List -from PyQt6.QtCore import QDir - import mobase +from PyQt6.QtCore import QDir from ..basic_game import BasicGame, BasicGameSaveGame @@ -31,9 +28,9 @@ class VampireModDataChecker(mobase.ModDataChecker): ] def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - for entry in tree: + for entry in filetree: if not entry.isDir(): continue if entry.name().casefold() in self.validDirNames: @@ -52,20 +49,20 @@ class VampireSaveGame(BasicGameSaveGame): class VampireLocalSavegames(mobase.LocalSavegames): - def __init__(self, myGameSaveDir): + def __init__(self, my_game_save_dir: QDir): super().__init__() - self._savesDir = myGameSaveDir.absolutePath() + self._saves_dir = my_game_save_dir.absolutePath() - def mappings(self, profile_save_dir): + def mappings(self, profile_save_dir: QDir): m = mobase.Mapping() m.createTarget = True m.isDirectory = True m.source = profile_save_dir.absolutePath() - m.destination = self._savesDir + m.destination = self._saves_dir return [m] - def prepareProfile(self, profile): + def prepareProfile(self, profile: mobase.IProfile): return profile.localSavesEnabled() @@ -94,27 +91,24 @@ class VampireTheMasqueradeBloodlinesGame(BasicGame): def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self._featureMap[mobase.ModDataChecker] = VampireModDataChecker() - self._featureMap[mobase.SaveGameInfo] = VampireSaveGame( - Path(self.savesDirectory().absolutePath()) - ) self._featureMap[mobase.LocalSavegames] = VampireLocalSavegames( self.savesDirectory() ) return True - def initializeProfile(self, path: QDir, settings: mobase.ProfileSetting): + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): # Create .cfg files if they don't exist for iniFile in self.iniFiles(): - iniPath = self.documentsDirectory().absoluteFilePath(iniFile) - if not os.path.exists(iniPath): + iniPath = Path(self.documentsDirectory().absoluteFilePath(iniFile)) + if not iniPath.exists(): with open(iniPath, "w") as _: pass - super().initializeProfile(path, settings) + super().initializeProfile(directory, settings) def version(self): # Don't forget to import mobase! - return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final) + return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.FINAL) def iniFiles(self): return ["autoexec.cfg", "user.cfg"] diff --git a/games/game_witcher1.py b/games/game_witcher1.py index 2d0e050..4fe1a8c 100644 --- a/games/game_witcher1.py +++ b/games/game_witcher1.py @@ -1,26 +1,24 @@ -# -*- encoding: utf-8 -*- from pathlib import Path -from typing import List - -from PyQt6.QtCore import QDir, QFileInfo +from typing import BinaryIO, List import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame, BasicGameSaveGame class Witcher1SaveGame(BasicGameSaveGame): - def __init__(self, filepath): + def __init__(self, filepath: Path): super().__init__(filepath) self.areaName: str = "" self.parseSaveFile(filepath) @staticmethod - def readInt(fp, length=4) -> int: + def readInt(fp: BinaryIO, length: int = 4) -> int: return int.from_bytes(fp.read(length), "little") @staticmethod - def readFixedString(fp, length) -> str: + def readFixedString(fp: BinaryIO, length: int) -> str: b: bytes = fp.read(length) res = b.decode("utf-16") return res.rstrip("\0") diff --git a/games/game_witcher3.py b/games/game_witcher3.py index 12248a4..e5991ef 100644 --- a/games/game_witcher3.py +++ b/games/game_witcher3.py @@ -1,11 +1,8 @@ -# -*- encoding: utf-8 -*- - from pathlib import Path from typing import List -from PyQt6.QtCore import QDir - import mobase +from PyQt6.QtCore import QDir from ..basic_features import BasicGameSaveGameInfo from ..basic_features.basic_save_game_info import BasicGameSaveGame @@ -14,11 +11,10 @@ from ..basic_game import BasicGame class Witcher3SaveGame(BasicGameSaveGame): def allFiles(self): - return [self._filename, self._filename.replace(".sav", ".png")] + return [self._filepath.name, self._filepath.name.replace(".sav", ".png")] class Witcher3Game(BasicGame): - Name = "Witcher 3 Support Plugin" Author = "Holt59" Version = "1.0.0a" diff --git a/games/game_xplane11.py b/games/game_xplane11.py index 23d2ad4..ecbe89a 100644 --- a/games/game_xplane11.py +++ b/games/game_xplane11.py @@ -2,7 +2,6 @@ from ..basic_game import BasicGame class XP11Game(BasicGame): - Name = "X-Plane 11 Support Plugin" Author = "Deorder" Version = "0.0.1" diff --git a/games/game_zeusandposeidon.py b/games/game_zeusandposeidon.py index a0528d6..9a1b090 100644 --- a/games/game_zeusandposeidon.py +++ b/games/game_zeusandposeidon.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- - from typing import List, Optional import mobase @@ -12,13 +10,12 @@ class ZeusAndPoseidonModDataChecker(mobase.ModDataChecker): super().__init__() def dataLooksValid( - self, tree: mobase.IFileTree + self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - folders: List[mobase.IFileTree] = [] files: List[mobase.FileTreeEntry] = [] - for entry in tree: + for entry in filetree: if isinstance(entry, mobase.IFileTree): folders.append(entry) else: @@ -30,25 +27,25 @@ class ZeusAndPoseidonModDataChecker(mobase.ModDataChecker): folder = folders[0] pakfile = folder.name() + ".pak" if folder.exists(pakfile): - if tree.exists(pakfile): + if filetree.exists(pakfile): return mobase.ModDataChecker.VALID else: return mobase.ModDataChecker.FIXABLE return mobase.ModDataChecker.INVALID - def fix(self, tree: mobase.IFileTree) -> Optional[mobase.IFileTree]: - if not isinstance(tree[0], mobase.IFileTree): + def fix(self, filetree: mobase.IFileTree) -> Optional[mobase.IFileTree]: + first_entry = filetree[0] + if not isinstance(first_entry, mobase.IFileTree): return None - entry = tree[0].find(tree[0].name() + ".pak") + entry = first_entry.find(filetree[0].name() + ".pak") if entry is None: return None - tree.copy(entry, "", mobase.IFileTree.InsertPolicy.FAIL_IF_EXISTS) - return tree + filetree.copy(entry, "", mobase.IFileTree.InsertPolicy.FAIL_IF_EXISTS) + return filetree class ZeusAndPoseidonGame(BasicGame): - Name = "Zeus and Poseidon Support Plugin" Author = "Holt59" Version = "1.0.0a" diff --git a/games/quarantine/game_masseffectlegendary.py b/games/quarantine/game_masseffectlegendary.py index 4b9ef71..fadc250 100644 --- a/games/quarantine/game_masseffectlegendary.py +++ b/games/quarantine/game_masseffectlegendary.py @@ -1,8 +1,7 @@ -from ..basic_game import BasicGame +from ...basic_game import BasicGame class MassEffectLegendaryGame(BasicGame): - Name = "Mass Effect Legendary Edition Support Plugin" Author = "LostDragonist" Version = "1.0.0" diff --git a/games/stalkeranomaly/XRIO.py b/games/stalkeranomaly/XRIO.py index 1ff7253..870f28d 100644 --- a/games/stalkeranomaly/XRIO.py +++ b/games/stalkeranomaly/XRIO.py @@ -35,7 +35,7 @@ class XRReader: size = len(self._buffer) if len(self._buffer) <= self._pos: return b"" - (buffer, pos) = self._read(size) + (buffer, _pos) = self._read(size) return buffer def seek(self, pos: int, whence: int = io.SEEK_SET) -> int: diff --git a/games/stalkeranomaly/XRMath.py b/games/stalkeranomaly/XRMath.py index a62f090..355fcaf 100644 --- a/games/stalkeranomaly/XRMath.py +++ b/games/stalkeranomaly/XRMath.py @@ -1,8 +1,5 @@ -# -*- encoding: utf-8 -*- - - class IVec3: - def __init__(self, x, y, z): + def __init__(self, x: float, y: float, z: float): self.x = x self.y = y self.z = z @@ -10,24 +7,15 @@ class IVec3: def __str__(self) -> str: return f"{self.x}, {self.y}, {self.z}" - def set(self, x, y, z): - self.x = x - self.y = y - self.z = z - class IVec4(IVec3): - def __init__(self, x, y, z, w): + def __init__(self, x: float, y: float, z: float, w: float): super().__init__(x, y, z) self.w = w def __str__(self) -> str: return f"{self.x}, {self.y}, {self.z}, f{self.w}" - def set(self, x, y, z, w): - super().set(x, y, z) - self.w = w - class IFlag: def __init__(self, flag: int): @@ -36,14 +24,14 @@ class IFlag: def __str__(self) -> str: return str(self._flag) - def assign(self, mask): + def assign(self, mask: int): self._flag = mask - def has(self, mask) -> bool: + def has(self, mask: int) -> bool: return bool((self._flag & mask) == mask) - def set(self, mask): + def set(self, mask: int) -> None: self._flag |= mask - def remove(self, mask): + def remove(self, mask: int) -> None: self._flag &= ~mask diff --git a/games/stalkeranomaly/XRNET.py b/games/stalkeranomaly/XRNET.py index b006fb2..2df87a9 100644 --- a/games/stalkeranomaly/XRNET.py +++ b/games/stalkeranomaly/XRNET.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- - from .XRIO import XRReader from .XRMath import IVec3, IVec4 diff --git a/games/stalkeranomaly/XRObject.py b/games/stalkeranomaly/XRObject.py index 02eb689..d1eccdb 100644 --- a/games/stalkeranomaly/XRObject.py +++ b/games/stalkeranomaly/XRObject.py @@ -63,7 +63,7 @@ class XRAbstract: if self.flags.has(XRFlag.SPAWN_VERSION): self.version = reader.u16() if self.version == 0: - reader._pos -= 2 + reader._pos -= 2 # pyright: ignore[reportPrivateUsage] return if self.version > 120: self.game_type.set(reader.u16()) @@ -76,7 +76,7 @@ class XRAbstract: else: cl_size = reader.u8() if cl_size > 0: - for x in range(cl_size): + for _ in range(cl_size): data = reader.u8() self.client_data.append(data) if self.version > 79: @@ -95,7 +95,7 @@ class XRVisual: def read_visual(self, reader: XRReader, version: int): self.visual_name = reader.str() - self.flags = reader.u8() + self.flags = IFlag(reader.u8()) class XRBoneData: @@ -104,7 +104,7 @@ class XRBoneData: self.root_bone = 0 self.min = IVec3(0.0, 0.0, 0.0) self.max = IVec3(0.0, 0.0, 0.0) - self.bones = [] + self.bones: list[XRNETState] = [] def load(self, reader: XRReader): self.bones_mask = reader.u64() @@ -112,7 +112,7 @@ class XRBoneData: self.min = reader.fvec3() self.max = reader.fvec3() bones_count = reader.u16() - for x in range(bones_count): + for _ in range(bones_count): bone = XRNETState() bone.read(reader, self.min, self.max) self.bones.append(bone) @@ -187,8 +187,8 @@ class XRCreatureAbstract(XRDynamicObjectVisual): self.squad = 0 self.group = 0 self.health = 1.0 - self.dynamic_out = [] - self.dynamic_in = [] + self.dynamic_out: list[int] = [] + self.dynamic_in: list[int] = [] self.killer_id = -1 self.death_time = 0 @@ -199,11 +199,11 @@ class XRCreatureAbstract(XRDynamicObjectVisual): self.group = reader.u8() self.health = reader.float() * 100 - for x in range(reader.u32()): + for _ in range(reader.u32()): _id = reader.u16() self.dynamic_out.append(_id) - for x in range(reader.u32()): + for _ in range(reader.u32()): _id = reader.u16() self.dynamic_in.append(_id) diff --git a/games/stalkeranomaly/XRSave.py b/games/stalkeranomaly/XRSave.py index 6040ad9..b73a988 100644 --- a/games/stalkeranomaly/XRSave.py +++ b/games/stalkeranomaly/XRSave.py @@ -1,12 +1,10 @@ -# -*- encoding: utf-8 -*- - import io import struct from datetime import datetime from pathlib import Path -from typing import Optional +from typing import BinaryIO, Optional, cast -import lzokay +import lzokay # pyright: ignore[reportMissingTypeStubs] from .XRIO import XRReader, XRStream from .XRObject import XRCreatureActor, XRFlag @@ -101,7 +99,7 @@ class XRSave: else: self.save_fmt = "Unknown" - def readFile(self, file) -> Optional[XRStream]: + def readFile(self, file: BinaryIO) -> Optional[XRStream]: size = self.filepath.stat().st_size if size < 8: return None @@ -110,7 +108,14 @@ class XRSave: if (start == -1) and (version >= 6): file.seek(12) data = file.read(size - 12) - return XRStream(lzokay.decompress(data, source)) + return XRStream( + cast( + bytes, + lzokay.decompress( # pyright: ignore[reportUnknownMemberType] + data, source + ), + ) + ) return None diff --git a/games/stalkeranomaly/__init__.py b/games/stalkeranomaly/__init__.py index ce1313b..a8730a4 100644 --- a/games/stalkeranomaly/__init__.py +++ b/games/stalkeranomaly/__init__.py @@ -1,6 +1,3 @@ -# -*- encoding: utf-8 -*- -# flake8: noqa - from .XRIO import XRReader, XRStream from .XRMath import IFlag, IVec3, IVec4 from .XRNET import XRNETState @@ -18,3 +15,24 @@ from .XRObject import ( XRVisual, ) from .XRSave import XRSave + +__all__ = [ + "IFlag", + "IVec3", + "IVec4", + "XRAbstract", + "XRBoneData", + "XRCreatureAbstract", + "XRCreatureActor", + "XRDynamicObject", + "XRDynamicObjectVisual", + "XRFlag", + "XRNETState", + "XRObject", + "XRReader", + "XRSave", + "XRSkeleton", + "XRStream", + "XRTraderAbstract", + "XRVisual", +] diff --git a/gog_utils.py b/gog_utils.py index 713015c..e5e6d24 100644 --- a/gog_utils.py +++ b/gog_utils.py @@ -1,17 +1,13 @@ -# -*- encoding: utf-8 -*- - # Code adapted from EzioTheDeadPoet / erri120: # https://github.com/ModOrganizer2/modorganizer-basic_games/pull/5 -import winreg # type: ignore +import winreg from pathlib import Path -from typing import Dict -def find_games() -> Dict[str, Path]: - +def find_games() -> dict[str, Path]: # List the game IDs from the registry: - game_ids = [] + game_ids: list[str] = [] try: with winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"Software\Wow6432Node\GOG.com\Games" @@ -25,7 +21,7 @@ def find_games() -> Dict[str, Path]: return {} # For each game, query the path: - games: Dict[str, Path] = {} + games: dict[str, Path] = {} for game_id in game_ids: try: with winreg.OpenKey( diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1667f60 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,509 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "black" +version = "23.9.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, + {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, + {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, + {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, + {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, + {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, + {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, + {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, + {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, + {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, + {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "flake8-black" +version = "0.3.6" +description = "flake8 plugin to call black as a code style validator" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8-black-0.3.6.tar.gz", hash = "sha256:0dfbca3274777792a5bcb2af887a4cad72c72d0e86c94e08e3a3de151bb41c34"}, + {file = "flake8_black-0.3.6-py3-none-any.whl", hash = "sha256:fe8ea2eca98d8a504f22040d9117347f6b367458366952862ac3586e7d4eeaca"}, +] + +[package.dependencies] +black = ">=22.1.0" +flake8 = ">=3" + +[package.extras] +develop = ["build", "twine"] + +[[package]] +name = "flake8-pyproject" +version = "1.2.3" +description = "Flake8 plug-in loading the configuration from pyproject.toml" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, +] + +[package.dependencies] +Flake8 = ">=5" + +[package.extras] +dev = ["pyTest", "pyTest-cov"] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "lzokay" +version = "1.1.5" +description = "Python bindings for LZ👌, a LZO compression/decompression algorithm." +optional = false +python-versions = ">=3.8" +files = [ + {file = "lzokay-1.1.5-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:4faeefdef8132c4db995de8e96a649c890c63a81cee06b3bbcae9d224302fa09"}, + {file = "lzokay-1.1.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ee2c7144dd3916cf0392d3851cebe8f28136e7998b7a50a91f63d7276327d70"}, + {file = "lzokay-1.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:829bfd081c8f03e85994d920ce7cc8d45033d7c467fc2fadc17b5c47667ec045"}, + {file = "lzokay-1.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d6b1d80121cdbc3cb106b3fafc03b139f3de3c134ce58b38fcc2d751fc5b013"}, + {file = "lzokay-1.1.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f07bdd8c3bd9443d3f5ca69a03b9dc35522c5c92fce61db087ded1f348274f"}, + {file = "lzokay-1.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:266db5697a0d01428e4c31633f8dc1e87666ad17bb46b4c48ec7b7b8b70b94bc"}, + {file = "lzokay-1.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:eec7679194643bc1417f8883ef3c9d27e95e00632d4d2a7dc9ef1a1ca92cdd95"}, + {file = "lzokay-1.1.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5426a80664c471c18a47766111629cae5c9435ca8421cd208b00b10de5df5df9"}, + {file = "lzokay-1.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:a4df719d5fef922e0f43ec2408cd70280171ded18ab096c1e88fd1c3ca4dff46"}, + {file = "lzokay-1.1.5-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:88fa389193cde7feb13fd4b57b6b8d626a6159bfe84cd795e664e0a738debb42"}, + {file = "lzokay-1.1.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c83a0f1cc7628237466c298e676a64a1ab4914ccf628b3775f8680477b77481"}, + {file = "lzokay-1.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:f1997c6994239ea24a2c7c7812f9e06bceb6b216c5537b85d8b585dbfd711cb0"}, + {file = "lzokay-1.1.5-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:4389b8b6f3c95aaa33308e2129b026140c850d94440fbea4fdeabcd42477f3b2"}, + {file = "lzokay-1.1.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:867a9fd4a76c830b17366fd2e8bd03fe9b29cf5c13c42bc19562e411cf648dad"}, + {file = "lzokay-1.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:f828864453ddfca036de7470da74245d8dcf369c521291037b1b01ac7c5dbd83"}, + {file = "lzokay-1.1.5.tar.gz", hash = "sha256:3c2e81d178161de58bf233eec87c6ef3dba96fecdd9ba72f8f2c8790adc18f74"}, +] + +[package.extras] +test = ["pytest"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mobase-stubs" +version = "2.5.0.dev14" +description = "PEP561 stub files for the mobase Python API." +optional = false +python-versions = ">=3.11,<4.0" +files = [ + {file = "mobase_stubs-2.5.0.dev14-py3-none-any.whl", hash = "sha256:66475f3ac3c2c9c21385054492920acdea5a105560a5ab2eac8845d24d0a3dce"}, + {file = "mobase_stubs-2.5.0.dev14.tar.gz", hash = "sha256:11bc174ae6fb3d444df7790b902645d9faad11b04410d62ea642c1e540729166"}, +] + +[[package]] +name = "mypy" +version = "1.5.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, + {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, + {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, + {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, + {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, + {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, + {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, + {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, + {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, + {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, + {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, + {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, + {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, + {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, + {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, + {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, + {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, + {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "3.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, + {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"}, + {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"}, + {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"}, + {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"}, + {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"}, + {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"}, + {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"}, + {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"}, + {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"}, + {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"}, + {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"}, + {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"}, + {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"}, + {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"}, + {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"}, + {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"}, + {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"}, + {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"}, + {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"}, + {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"}, + {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"}, + {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"}, + {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"}, + {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"}, + {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"}, + {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"}, + {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] + +[[package]] +name = "pycodestyle" +version = "2.11.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.0-py2.py3-none-any.whl", hash = "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"}, + {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"}, +] + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + +[[package]] +name = "pyqt6" +version = "6.5.2" +description = "Python bindings for the Qt cross platform application toolkit" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "PyQt6-6.5.2-cp37-abi3-macosx_10_14_universal2.whl", hash = "sha256:5bad1437eb0c1ae801103b32ef04ef62ef1cce505b448525f60089ce36329b89"}, + {file = "PyQt6-6.5.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:70468cca4537756c714a57fa1baa5beabb9b38775f52f9611f49870705672c55"}, + {file = "PyQt6-6.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:ff1d12767b578f0f0e87cdb12198e7dcad9a176c40d1d1d799984181b0af93cb"}, + {file = "PyQt6-6.5.2.tar.gz", hash = "sha256:1487ee7350f9ffb66d60ab4176519252c2b371762cbe8f8340fd951f63801280"}, +] + +[package.dependencies] +PyQt6-Qt6 = ">=6.5.0" +PyQt6-sip = ">=13.4,<14" + +[[package]] +name = "pyqt6-qt6" +version = "6.5.2" +description = "The subset of a Qt installation needed by PyQt6." +optional = false +python-versions = "*" +files = [ + {file = "PyQt6_Qt6-6.5.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:4b37f6f93c0980469ccc570998d3e3de243028bae7389fb6330443ab215ce2f6"}, + {file = "PyQt6_Qt6-6.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8dad61b4666d91882d7e1c4d619c71e7429c13e19182f8b3bebf3ecf95107d4c"}, + {file = "PyQt6_Qt6-6.5.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:953f3c0e99e486081a6d438b32fbc240da97457226562eb68cf1b11c516386fd"}, + {file = "PyQt6_Qt6-6.5.2-py3-none-win_amd64.whl", hash = "sha256:5a3c7bb899678bf801136b31cd589ed4d0d54ab32be5fb76c2bdeb161a9662ad"}, +] + +[[package]] +name = "pyqt6-sip" +version = "13.5.2" +description = "The sip module support for PyQt6" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyQt6_sip-13.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c0b4e55c4d0dc44728da90eb1451dfff0d05260b4a3496ff0014494e6c1865a6"}, + {file = "PyQt6_sip-13.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9ad5b8c4c92d62e00ebf254a4c9656668b130f2a1d2792034e0d82b2d6571509"}, + {file = "PyQt6_sip-13.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0fd42da765198b51d7fe12c29721cbe3e14b77ca4f68aa43618149ee7dbeff"}, + {file = "PyQt6_sip-13.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4b5b0c4b557e0293eba008021342459a0f91c69a7a2eb5231276d495d1d2960a"}, + {file = "PyQt6_sip-13.5.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:831f5d606fc5296a80303ab30892c3308954c5766039bf7a96267488bb2524a5"}, + {file = "PyQt6_sip-13.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:91812f0094443b816a74a89954d60bb50060807f54d7c016a4de7bd29454091e"}, + {file = "PyQt6_sip-13.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:94afb031db89159aa330891eba2c937b0378b4b264570998848c7a78eddf7c94"}, + {file = "PyQt6_sip-13.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ace942b78378bff8ae2d6bafccc84465f1ff0cf30720b8321e0bd6c95c36ede6"}, + {file = "PyQt6_sip-13.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:318d4d1c7ef60f69c68227cef78fc439accc9028ec6149200eea3d34c433d373"}, + {file = "PyQt6_sip-13.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87d758ba999baa16459f0a3c7f7ed47a5b45e8991ad691f17345bf3c493a4281"}, + {file = "PyQt6_sip-13.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5b499eff7150d9f31fe835a73cc41f076bba2fcde0f5b0325b1284797f17c0ac"}, + {file = "PyQt6_sip-13.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:1bdb1f7b5b2f6ac31d517a6f3a13c38055640ac0014f67a2e5422d2083ce69ec"}, + {file = "PyQt6_sip-13.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9c9dac067710015895f523f5a2a4d59cbef8624a152b6f9a426e5b848d8c6d29"}, + {file = "PyQt6_sip-13.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dcf602c233ee7600e810927adcb9e518d61bc796a6b2013c17feedd24c4e5413"}, + {file = "PyQt6_sip-13.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:b54b0d8d21c5835af8f6d6d8a323a23106e67b7cd4c31e23c35bb4c321000de8"}, + {file = "PyQt6_sip-13.5.2.tar.gz", hash = "sha256:ebf6264b6feda01ba37d3b60a4bb87493bdb87be70f7b2a5384a7acd4902d88d"}, +] + +[[package]] +name = "pyright" +version = "1.1.327" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.327-py3-none-any.whl", hash = "sha256:3462cda239e9140276238bbdbd0b59d77406f1c2e14d8cb8c20c8e25639c6b3c"}, + {file = "pyright-1.1.327.tar.gz", hash = "sha256:ba74148ad64f22020dbbed6781c4bdb38ecb8a7ca90dc3c87a4f08d1c0e11592"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "ruff" +version = "0.0.291" +description = "An extremely fast Python linter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.291-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b97d0d7c136a85badbc7fd8397fdbb336e9409b01c07027622f28dcd7db366f2"}, + {file = "ruff-0.0.291-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6ab44ea607967171e18aa5c80335237be12f3a1523375fa0cede83c5cf77feb4"}, + {file = "ruff-0.0.291-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04b384f2d36f00d5fb55313d52a7d66236531195ef08157a09c4728090f2ef0"}, + {file = "ruff-0.0.291-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b727c219b43f903875b7503a76c86237a00d1a39579bb3e21ce027eec9534051"}, + {file = "ruff-0.0.291-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87671e33175ae949702774071b35ed4937da06f11851af75cd087e1b5a488ac4"}, + {file = "ruff-0.0.291-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b75f5801547f79b7541d72a211949754c21dc0705c70eddf7f21c88a64de8b97"}, + {file = "ruff-0.0.291-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b09b94efdcd162fe32b472b2dd5bf1c969fcc15b8ff52f478b048f41d4590e09"}, + {file = "ruff-0.0.291-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d5b56bc3a2f83a7a1d7f4447c54d8d3db52021f726fdd55d549ca87bca5d747"}, + {file = "ruff-0.0.291-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f0d88e5f367b2dc8c7d90a8afdcfff9dd7d174e324fd3ed8e0b5cb5dc9b7f6"}, + {file = "ruff-0.0.291-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b3eeee1b1a45a247758ecdc3ab26c307336d157aafc61edb98b825cadb153df3"}, + {file = "ruff-0.0.291-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c06006350c3bb689765d71f810128c9cdf4a1121fd01afc655c87bab4fb4f83"}, + {file = "ruff-0.0.291-py3-none-musllinux_1_2_i686.whl", hash = "sha256:fd17220611047de247b635596e3174f3d7f2becf63bd56301fc758778df9b629"}, + {file = "ruff-0.0.291-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5383ba67ad360caf6060d09012f1fb2ab8bd605ab766d10ca4427a28ab106e0b"}, + {file = "ruff-0.0.291-py3-none-win32.whl", hash = "sha256:1d5f0616ae4cdc7a938b493b6a1a71c8a47d0300c0d65f6e41c281c2f7490ad3"}, + {file = "ruff-0.0.291-py3-none-win_amd64.whl", hash = "sha256:8a69bfbde72db8ca1c43ee3570f59daad155196c3fbe357047cd9b77de65f15b"}, + {file = "ruff-0.0.291-py3-none-win_arm64.whl", hash = "sha256:d867384a4615b7f30b223a849b52104214442b5ba79b473d7edd18da3cde22d6"}, + {file = "ruff-0.0.291.tar.gz", hash = "sha256:c61109661dde9db73469d14a82b42a88c7164f731e6a3b0042e71394c1c7ceed"}, +] + +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "types-psutil" +version = "5.9.5.16" +description = "Typing stubs for psutil" +optional = false +python-versions = "*" +files = [ + {file = "types-psutil-5.9.5.16.tar.gz", hash = "sha256:4e9b219efb625d3d04f6bf106934f87cab49aa41a94b0a3b3089403f47a79228"}, + {file = "types_psutil-5.9.5.16-py3-none-any.whl", hash = "sha256:fec713104d5d143afea7b976cfa691ca1840f5d19e8714a5d02a96ebd061363e"}, +] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "vdf" +version = "3.4" +description = "Library for working with Valve's VDF text format" +optional = false +python-versions = "*" +files = [ + {file = "vdf-3.4-py2.py3-none-any.whl", hash = "sha256:68c1a125cc49e343d535af2dd25074e9cb0908c6607f073947c4a04bbe234534"}, + {file = "vdf-3.4.tar.gz", hash = "sha256:fd5419f41e07a1009e5ffd027c7dcbe43d1f7e8ef453aeaa90d9d04b807de2af"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "08559c779e515410d43f943d96594448d4b4634a141bd0af2cf720eb7d80f619" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a1b9c71 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,70 @@ +# note: this file is for local development, currently the file is not used to build +# the plugin + +[tool.poetry] +name = "basic-games" +version = "0.1.0" +description = "" +authors = [] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +psutil = "5.8.0" +vdf = "3.4" +lzokay = "1.1.5" + +[tool.poetry.group.dev.dependencies] +mobase-stubs = { version = "^2.5.0.dev14", allow-prereleases = true } +pyqt6 = "^6.5.2" +pyright = "^1.1.327" +flake8 = "^6.1.0" +flake8-pyproject = "^1.2.3" +ruff = "^0.0.291" +mypy = "^1.5.1" +isort = "^5.12.0" +black = "^23.9.1" +flake8-black = "^0.3.6" +types-psutil = "^5.9.5.16" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.flake8] +max-line-length = 88 +extend-ignore = ["E203"] + +[tool.black] +exclude = "lib" + +[tool.isort] +profile = "black" +multi_line_output = 3 +skip = "lib" + +[tool.mypy] +warn_return_any = true +warn_unused_configs = true +check_untyped_defs = true +exclude = ["lib", "games/quarantine"] +platform = "win32" + +[[tool.mypy.overrides]] +module = "vdf.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "lzokay.*" +ignore_missing_imports = true + +[tool.ruff] +line-length = 88 +target-version = "py311" + +[tool.pyright] +exclude = ["lib"] +typeCheckingMode = "strict" +pythonPlatform = "Windows" +reportMissingModuleSource = false diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index dca8c57..0000000 --- a/setup.cfg +++ /dev/null @@ -1,52 +0,0 @@ -[flake8] -# Use black line length: -max-line-length = 88 -extend-ignore = - # See https://github.com/PyCQA/pycodestyle/issues/373 - E203, E266 - -[isort] -profile = black -multi_line_output = 3 -known_mobase = mobase -sections=FUTURE,STDLIB,THIRDPARTY,MOBASE,FIRSTPARTY,LOCALFOLDER - -[mypy] -warn_return_any = True -warn_unused_configs = True - -[mypy-psutil.*] -ignore_missing_imports = True - -[mypy-vdf.*] -ignore_missing_imports = True - -[mypy-_lzokay.*] -ignore_missing_imports = True - -[mypy-lzokay.*] -ignore_missing_imports = True - -[tox:tox] -skipsdist = true -envlist = py310-lint - -[testenv:py310-lint] -pip_version = pip>=20 -skip_install = true -deps = - git+https://github.com/TilmanK/PyQt6-stubs.git - mobase-stubs>=2.5.0.dev9 - psutil==5.8.0 - vdf==3.4 - lzokay==1.1.5 - black - flake8 - flake8-black - mypy - isort -commands = - black --check --diff . - isort -c . --skip lib --skip .tox - flake8 . --exclude "lib,.tox,games/quarantine" - mypy . --platform win32 --exclude "lib" --exclude "games/quarantine" diff --git a/steam_utils.py b/steam_utils.py index e854f22..d1d4a06 100644 --- a/steam_utils.py +++ b/steam_utils.py @@ -1,17 +1,15 @@ -# -*- encoding: utf-8 -*- - # Code greatly inspired by https://github.com/LostDragonist/steam-library-setup-tool import sys -import winreg # type: ignore +import winreg from pathlib import Path -from typing import Dict, List, Optional +from typing import TypedDict, cast -import vdf +import vdf # pyright: ignore[reportMissingTypeStubs] class SteamGame: - def __init__(self, appid, installdir): + def __init__(self, appid: str, installdir: str): self.appid = appid self.installdir = installdir @@ -22,15 +20,36 @@ class SteamGame: return "{} ({})".format(self.appid, self.installdir) +class _AppState(TypedDict): + appid: str + installdir: str + + +class _AppManifest(TypedDict): + AppState: _AppState + + +class _LibraryFolder(TypedDict): + path: str + + +class _LibraryFolders(TypedDict, total=False): + libraryfolders: dict[str, _LibraryFolder] + LibraryFolders: dict[str, str] + + class LibraryFolder: def __init__(self, path: Path): self.path = path - self.games = [] + self.games: list[SteamGame] = [] for filepath in path.joinpath("steamapps").glob("appmanifest_*.acf"): try: with open(filepath, "r", encoding="utf-8") as fp: - info = vdf.load(fp) + info = cast( + _AppManifest, + vdf.load(fp), # pyright: ignore[reportUnknownMemberType] + ) app_state = info["AppState"] except KeyError: print( @@ -61,7 +80,7 @@ class LibraryFolder: return "LibraryFolder at {}: {}".format(self.path, self.games) -def parse_library_info(library_vdf_path: Path) -> List[LibraryFolder]: +def parse_library_info(library_vdf_path: Path) -> list[LibraryFolder]: """ Read library folders from the main library file. @@ -74,27 +93,26 @@ def parse_library_info(library_vdf_path: Path) -> List[LibraryFolder]: """ with open(library_vdf_path, "r", encoding="utf-8") as f: - info = vdf.load(f) + info = cast( + _LibraryFolders, + vdf.load(f), # pyright: ignore[reportUnknownMemberType] + ) - library_folders = [] + info_folders: dict[str, str] | dict[str, _LibraryFolder] if "libraryfolders" in info: # new format info_folders = info["libraryfolders"] - def get_path(value): - return value["path"] - elif "LibraryFolders" in info: # old format info_folders = info["LibraryFolders"] - def get_path(value): - return value - else: raise ValueError(f'Unknown file format from "{library_vdf_path}"') + library_folders: list[LibraryFolder] = [] + for key, value in info_folders.items(): # only keys that are integer values contains library folder try: @@ -102,7 +120,11 @@ def parse_library_info(library_vdf_path: Path) -> List[LibraryFolder]: except ValueError: continue - path = get_path(value) + if isinstance(value, str): + path = value + else: + path = value["path"] + try: library_folders.append(LibraryFolder(Path(path))) except Exception as e: @@ -114,7 +136,7 @@ def parse_library_info(library_vdf_path: Path) -> List[LibraryFolder]: return library_folders -def find_steam_path() -> Optional[Path]: +def find_steam_path() -> Path | None: """ Retrieve the Steam path, if available. @@ -129,7 +151,7 @@ def find_steam_path() -> Optional[Path]: return None -def find_games() -> Dict[str, Path]: +def find_games() -> dict[str, Path]: """ Find the list of Steam games installed. @@ -149,7 +171,7 @@ def find_games() -> Dict[str, Path]: except FileNotFoundError: return {} - games: Dict[str, Path] = {} + games: dict[str, Path] = {} for library in library_folders: for game in library.games: games[game.appid] = Path(library.path).joinpath( From 4b20534fa40749422814bda2c8a3b499b49fe35a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 23 Sep 2023 14:21:14 +0200 Subject: [PATCH 2/3] Add contribute section to README.md. --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4759ce2..be29da2 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Download the archive for your MO2 version and extract it directly into your MO2 **Important:** Extract the *folder* in your `plugins` folder, not the individual files. Your `plugins` folder should look like this: -``` +```text dlls/ plugins/ data/ @@ -195,3 +195,23 @@ Game IDs can be found here: - For Legendary (alt. Epic launcher) via command `legendary list-games` or from: `%USERPROFILE%\.config\legendary\installed.json` - For EA Desktop from `\\__Installer\installerdata.xml` + +## Contribute + +We recommend using a dedicated Python environment to write a new basic game plugins. + +1. Install the required version of Python --- Currently Python 3.11 (MO2 2.5). +2. Remove the repository at `${MO2_INSTALL}/plugins/basic_games`. +3. Clone this repository at the location of the old plugin ( + `${MO2_INSTALL}/plugins/basic_games`). +4. Place yourself inside the cloned folder and: + + ```bash + # create a virtual environment (recommended) + py -3.11 -m venv .\venv + .\venv\scripts\Activate.ps1 + + # "install" poetry and the development package + pip install poetry + poetry install + ``` From 5c84ee6e0218b6cbddbd601b07b12c972f8c35d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 23 Sep 2023 14:27:45 +0200 Subject: [PATCH 3/3] Fix CI. --- .github/workflows/linters.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index b78e12c..5b7424e 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -7,6 +7,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + path: "basic_games" - name: Set up Python uses: actions/setup-python@v2 with: @@ -14,9 +16,11 @@ jobs: - uses: abatilo/actions-poetry@v2 - name: Install run: | + cd basic_games poetry install - name: Lint run: | + cd basic_games poetry run black . --check --diff poetry run isort -c . poetry run mypy .