mirror of
https://github.com/ModOrganizer2/modorganizer-basic_games.git
synced 2026-07-27 14:07:29 -07:00
Merge pull request #113 from ModOrganizer2/python311
Updates for Python 3.11 compatibility
This commit is contained in:
@@ -5,22 +5,24 @@ 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
|
||||
with:
|
||||
path: "basic_games"
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.11
|
||||
- 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 .
|
||||
poetry run ruff .
|
||||
poetry run pyright .
|
||||
|
||||
@@ -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 `<EA Games install location>\<game title>\__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
|
||||
```
|
||||
|
||||
+1
-2
@@ -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] = []
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from typing import TypeGuard
|
||||
|
||||
import mobase
|
||||
|
||||
|
||||
def is_directory(entry: mobase.FileTreeEntry) -> TypeGuard[mobase.IFileTree]:
|
||||
return entry.isDir()
|
||||
+82
-63
@@ -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)
|
||||
|
||||
@@ -2,7 +2,6 @@ from ..basic_game import BasicGame
|
||||
|
||||
|
||||
class AssettoCorsaGame(BasicGame):
|
||||
|
||||
Name = "Assetto Corsa Support Plugin"
|
||||
Author = "Deorder"
|
||||
Version = "0.0.1"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}")
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,7 +2,6 @@ from ..basic_game import BasicGame
|
||||
|
||||
|
||||
class Cyberpunk2077Game(BasicGame):
|
||||
|
||||
Name = "Cyberpunk 2077 Support Plugin"
|
||||
Author = "6788"
|
||||
Version = "1.0.0"
|
||||
|
||||
+1
-2
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -5,7 +5,6 @@ from ..basic_game import BasicGame
|
||||
|
||||
|
||||
class DAOriginsGame(BasicGame):
|
||||
|
||||
Name = "Dragon Age Origins Support Plugin"
|
||||
Author = "Patchier"
|
||||
Version = "1.1.1"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("<HHHHBB", data[8:18])
|
||||
if bpp != 24:
|
||||
return None
|
||||
return QImage(data[18:], w, h, QImage.Format_RGB888)
|
||||
return QImage(data[18:], w, h, QImage.Format.Format_RGB888)
|
||||
|
||||
def init(self, organizer: mobase.IOrganizer):
|
||||
super().init(organizer)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from ..basic_game import BasicGame
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import mobase
|
||||
|
||||
from ..basic_features import BasicGameSaveGameInfo
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user