mirror of
https://github.com/ModOrganizer2/modorganizer-basic_games.git
synced 2026-07-27 14:07:29 -07:00
Merge branch 'ZashIn-feat/BasicModDataChecker' into qt6
# Conflicts: # README.md
This commit is contained in:
@@ -64,7 +64,7 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic
|
||||
| Stardew Valley — [GOG](https://www.gog.com/game/stardew_valley) / [STEAM](https://store.steampowered.com/app/413150/Stardew_Valley/) | [Syer10](https://github.com/Syer10), [Holt59](https://github.com/holt59/) | [game_stardewvalley.py](games/game_stardewvalley.py) | <ul><li>mod data checker</li></ul> |
|
||||
| STAR WARS™ Empire at War: Gold Pack - [GOG](https://www.gog.com/game/star_wars_empire_at_war_gold_pack) / [STEAM](https://store.steampowered.com/app/32470/) | [erri120](https://github.com/erri120) | <ul><li>Empire at War: [game_starwars-empire-at-war.py](games/game_starwars-empire-at-war.py)</li><li>Force of Corruption: [game_starwars-empire-at-war-foc.py](games/game_starwars-empire-at-war-foc.py)</li></ul> | |
|
||||
| Subnautica — [STEAM](https://store.steampowered.com/app/264710/) / [Epic](https://store.epicgames.com/p/subnautica) | dekart811 | [game_subnautica.py](games/game_subnautica.py) | |
|
||||
| Valheim — [STEAM](https://store.steampowered.com/app/892970/Valheim/) | [Zash](https://github.com/ZashIn) | [game_valheim.py](games/game_valheim.py) | <ul><li>mod data checker</li><li>overwrite config sync</li><li>save game support (no preview)</li></ul>
|
||||
| Valheim — [STEAM](https://store.steampowered.com/app/892970/Valheim/) | [Zash](https://github.com/ZashIn) | [game_valheim.py](games/game_valheim.py) | <ul><li>mod data checker</li><li>overwrite config sync</li><li>save game support (no preview)</li></ul> |
|
||||
| The Witcher: Enhanced Edition - [GOG](https://www.gog.com/game/the_witcher) / [STEAM](https://store.steampowered.com/app/20900/The_Witcher_Enhanced_Edition_Directors_Cut/) | [erri120](https://github.com/erri120) | [game_witcher1.py](games/game_witcher1.py) | <ul><li>save game parsing (no preview)</li></ul> |
|
||||
| The Witcher 3: Wild Hunt — [GOG](https://www.gog.com/game/the_witcher_3_wild_hunt) / [STEAM](https://store.steampowered.com/app/292030/The_Witcher_3_Wild_Hunt/) | [Holt59](https://github.com/holt59/) | [game_witcher3.py](games/game_witcher3.py) | <ul><li>save game preview</li></ul> |
|
||||
| Yu-Gi-Oh! Master Duel — [STEAM](https://store.steampowered.com/app/1449850/) | [The Conceptionist](https://github.com/the-conceptionist) & [uwx](https://github.com/uwx) | [game_masterduel.py](games/game_masterduel.py) | |
|
||||
@@ -178,6 +178,10 @@ The meta-plugin provides some useful extra feature:
|
||||
2. **Basic save game preview:** If you use the Python version, and if you can easily obtain a picture (file)
|
||||
for any saves, you can provide basic save-game preview by using the `BasicGameSaveGameInfo`.
|
||||
See [games/game_witcher3.py](games/game_witcher3.py) for more details.
|
||||
3. **Basic mod data checker** (Python):
|
||||
Check and fix different mod archive layouts for an automatic installation with the proper
|
||||
file structure, using simple (glob) patterns via `BasicModDataChecker`.
|
||||
See [games/game_valheim.py](games/game_valheim.py) for an example.
|
||||
|
||||
Game IDs can be found here:
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from .basic_save_game_info import BasicGameSaveGameInfo # noqa
|
||||
from .basic_mod_data_checker import BasicModDataChecker # noqa
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
# -*- 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
class RegexPatternDict(dict, MutableMapping[str, re.Pattern]):
|
||||
"""Regex patterns for validation in `BasicModDataChecker`."""
|
||||
|
||||
@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
|
||||
|
||||
@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.
|
||||
"""
|
||||
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.
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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 BasicModDataChecker(mobase.ModDataChecker):
|
||||
"""Game feature that is used to check and fix the content of a data tree
|
||||
via simple file definitions.
|
||||
|
||||
The file definitions support glob pattern (without subfolders) and are
|
||||
checked and fixed in definition order of the `file_patterns` dict.
|
||||
|
||||
Args:
|
||||
file_patterns (optional): A dict (GlobPatternDict) with the following keys::
|
||||
|
||||
{
|
||||
"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`.
|
||||
|
||||
"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`.
|
||||
}
|
||||
|
||||
Example::
|
||||
|
||||
BasicModDataChecker(
|
||||
{
|
||||
"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
|
||||
"""Private `file_patterns`, updated together with `._regex` and `._move_targets`."""
|
||||
|
||||
_regex: RegexPatternDict
|
||||
"""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):
|
||||
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())
|
||||
|
||||
def dataLooksValid(
|
||||
self, filetree: mobase.IFileTree
|
||||
) -> mobase.ModDataChecker.CheckReturn:
|
||||
status = mobase.ModDataChecker.INVALID
|
||||
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
|
||||
else:
|
||||
return mobase.ModDataChecker.INVALID
|
||||
return status
|
||||
|
||||
def fix(self, filetree: mobase.IFileTree) -> Optional[mobase.IFileTree]:
|
||||
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]
|
||||
+57
-181
@@ -2,12 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import itertools
|
||||
import re
|
||||
import shutil
|
||||
from collections.abc import Collection, Container, Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass, field, fields
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional, TextIO, Union
|
||||
|
||||
@@ -15,25 +14,11 @@ from PyQt6.QtCore import QDir
|
||||
|
||||
import mobase
|
||||
|
||||
from ..basic_features import BasicModDataChecker
|
||||
from ..basic_features.basic_save_game_info import BasicGameSaveGame
|
||||
from ..basic_game import BasicGame
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def move_file(source: Path, target: Path):
|
||||
"""Move `source` to `target`. Creates missing (parent) directories and
|
||||
overwrites existing `target`."""
|
||||
@@ -130,7 +115,8 @@ class DebugTable:
|
||||
if self._table:
|
||||
for line in self._table:
|
||||
print("|", " | ".join(line.values()), "|", file=output_file)
|
||||
output_file and output_file.flush()
|
||||
if output_file:
|
||||
output_file.flush()
|
||||
self._table = []
|
||||
|
||||
|
||||
@@ -277,165 +263,6 @@ class OverwriteSync:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegexFilesDefinition:
|
||||
"""Regex pattern for the file lists in `FilesDefinition` - for globbing support.
|
||||
Fields should match `RegexFilesDefinition`.
|
||||
"""
|
||||
|
||||
set_as_root: Optional[re.Pattern]
|
||||
valid: Optional[re.Pattern]
|
||||
delete: Optional[re.Pattern]
|
||||
move: Optional[re.Pattern]
|
||||
|
||||
@classmethod
|
||||
def from_filesmap(cls, filesdef: FilesDefinition) -> RegexFilesDefinition:
|
||||
"""Returns an instance of `RegexFilesDefinition`,
|
||||
with the file list fields from `FilesDefinition` as regex patterns.
|
||||
"""
|
||||
return cls(
|
||||
**{
|
||||
f.name: (
|
||||
cls.file_list_regex(value)
|
||||
if (value := getattr(filesdef, f.name))
|
||||
else None
|
||||
)
|
||||
for f in fields(cls)
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def file_list_regex(file_list: Iterable[str]) -> re.Pattern:
|
||||
"""Returns a regex pattern for a file list with glob patterns.
|
||||
|
||||
Every pattern has a capturing group,
|
||||
so that match.lastindex - 1 will give the file_list index.
|
||||
"""
|
||||
return re.compile(
|
||||
f'(?:{"|".join(f"({fnmatch.translate(f)})" for f in file_list)})', re.I
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilesDefinition:
|
||||
"""File (pattern) definitions for the `mobase.ModDataChecker`.
|
||||
Fields should match `RegexFilesDefinition`.
|
||||
"""
|
||||
|
||||
set_as_root: Optional[set[str]]
|
||||
"""If a folder from this set is found, it will be set as new root dir (unfolded)."""
|
||||
valid: Optional[set[str]]
|
||||
"""Files and folders in the right path."""
|
||||
delete: Optional[set[str]]
|
||||
"""Files/folders to delete."""
|
||||
move: Optional[dict[str, str]]
|
||||
"""Files/folders to move, like `{"*.ext": "path/to/folder/"}`.
|
||||
If the path ends with / or \\, the entry will be inserted
|
||||
in the corresponding directory instead of replacing it.
|
||||
"""
|
||||
|
||||
regex: RegexFilesDefinition = field(init=False)
|
||||
_move_targets: Sequence[str] = field(init=False, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.regex = RegexFilesDefinition.from_filesmap(self)
|
||||
if self.move:
|
||||
self._move_targets = list(self.move.values())
|
||||
|
||||
def get_move_target(self, index: int) -> str:
|
||||
return self._move_targets[index]
|
||||
|
||||
|
||||
class ValheimGameModDataChecker(mobase.ModDataChecker):
|
||||
files_map = FilesDefinition(
|
||||
set_as_root={
|
||||
"BepInExPack_Valheim",
|
||||
},
|
||||
valid={
|
||||
"meta.ini", # Included in installed mod folder.
|
||||
"BepInEx",
|
||||
"doorstop_libs",
|
||||
"unstripped_corlib",
|
||||
"doorstop_config.ini",
|
||||
"start_game_bepinex.sh",
|
||||
"start_server_bepinex.sh",
|
||||
"winhttp.dll",
|
||||
#
|
||||
"InSlimVML",
|
||||
"valheim_Data",
|
||||
"inslimvml.ini",
|
||||
#
|
||||
"unstripped_managed",
|
||||
#
|
||||
"AdvancedBuilder",
|
||||
},
|
||||
delete={
|
||||
"*.txt",
|
||||
"*.md",
|
||||
"icon.png",
|
||||
"license",
|
||||
"manifest.json",
|
||||
},
|
||||
move={
|
||||
"*_VML.dll": "InSlimVML/Mods/",
|
||||
#
|
||||
"plugins": "BepInEx/",
|
||||
"*.dll": "BepInEx/plugins/",
|
||||
"config": "BepInEx/",
|
||||
"*.cfg": "BepInEx/config/",
|
||||
#
|
||||
"CustomTextures": "BepInEx/plugins/",
|
||||
"*.png": "BepInEx/plugins/CustomTextures/",
|
||||
#
|
||||
"Builds": "AdvancedBuilder/",
|
||||
"*.vbuild": "AdvancedBuilder/Builds/",
|
||||
#
|
||||
"*.assets": "valheim_Data/",
|
||||
},
|
||||
)
|
||||
|
||||
def dataLooksValid(
|
||||
self, filetree: mobase.IFileTree
|
||||
) -> mobase.ModDataChecker.CheckReturn:
|
||||
status = mobase.ModDataChecker.INVALID
|
||||
for entry in filetree:
|
||||
name = entry.name().casefold()
|
||||
regex = self.files_map.regex
|
||||
if regex.set_as_root and regex.set_as_root.match(name):
|
||||
return mobase.ModDataChecker.FIXABLE
|
||||
elif regex.valid and regex.valid.match(name):
|
||||
if status is not mobase.ModDataChecker.FIXABLE:
|
||||
status = mobase.ModDataChecker.VALID
|
||||
elif (regex.move and regex.move.match(name)) or (
|
||||
regex.delete and regex.delete.match(name)
|
||||
):
|
||||
status = mobase.ModDataChecker.FIXABLE
|
||||
else:
|
||||
return mobase.ModDataChecker.INVALID
|
||||
return status
|
||||
|
||||
def fix(self, filetree: mobase.IFileTree) -> Optional[mobase.IFileTree]:
|
||||
for entry in list(filetree):
|
||||
name = entry.name().casefold()
|
||||
regex = self.files_map.regex
|
||||
if regex.set_as_root and regex.set_as_root.match(name):
|
||||
new_root = convert_entry_to_tree(entry)
|
||||
return self.fix(new_root) if new_root else None
|
||||
elif regex.valid and regex.valid.match(name):
|
||||
continue
|
||||
elif regex.delete and regex.delete.match(name):
|
||||
entry.detach()
|
||||
elif regex.move and (match := regex.move.match(name)):
|
||||
if match.lastindex is None:
|
||||
return None
|
||||
else:
|
||||
# Get index of matched group
|
||||
map_index = match.lastindex - 1
|
||||
# Get the move target corresponding to the matched group
|
||||
filetree.move(entry, self.files_map.get_move_target(map_index))
|
||||
return filetree
|
||||
|
||||
|
||||
class ValheimSaveGame(BasicGameSaveGame):
|
||||
def getName(self) -> str:
|
||||
return f"[{self.getSaveGroupIdentifier().rstrip('s')}] {self._filepath.stem}"
|
||||
@@ -485,7 +312,7 @@ class ValheimGame(BasicGame):
|
||||
|
||||
Name = "Valheim Support Plugin"
|
||||
Author = "Zash"
|
||||
Version = "1.1.1"
|
||||
Version = "1.2"
|
||||
|
||||
GameName = "Valheim"
|
||||
GameShortName = "valheim"
|
||||
@@ -495,11 +322,58 @@ class ValheimGame(BasicGame):
|
||||
GameDataPath = ""
|
||||
GameSavesDirectory = r"%USERPROFILE%/AppData/LocalLow/IronGate/Valheim"
|
||||
|
||||
forced_libraries = ["winhttp.dll"]
|
||||
_forced_libraries = ["winhttp.dll"]
|
||||
|
||||
def init(self, organizer: mobase.IOrganizer) -> bool:
|
||||
super().init(organizer)
|
||||
self._featureMap[mobase.ModDataChecker] = ValheimGameModDataChecker()
|
||||
self._featureMap[mobase.ModDataChecker] = BasicModDataChecker(
|
||||
{
|
||||
"unfold": [
|
||||
"BepInExPack_Valheim",
|
||||
],
|
||||
"valid": [
|
||||
"meta.ini", # Included in installed mod folder.
|
||||
"BepInEx",
|
||||
"doorstop_libs",
|
||||
"unstripped_corlib",
|
||||
"doorstop_config.ini",
|
||||
"start_game_bepinex.sh",
|
||||
"start_server_bepinex.sh",
|
||||
"winhttp.dll",
|
||||
#
|
||||
"InSlimVML",
|
||||
"valheim_Data",
|
||||
"inslimvml.ini",
|
||||
#
|
||||
"unstripped_managed",
|
||||
#
|
||||
"AdvancedBuilder",
|
||||
],
|
||||
"delete": [
|
||||
"*.txt",
|
||||
"*.md",
|
||||
"icon.png",
|
||||
"license",
|
||||
"manifest.json",
|
||||
],
|
||||
"move": {
|
||||
"*_VML.dll": "InSlimVML/Mods/",
|
||||
#
|
||||
"plugins": "BepInEx/",
|
||||
"*.dll": "BepInEx/plugins/",
|
||||
"config": "BepInEx/",
|
||||
"*.cfg": "BepInEx/config/",
|
||||
#
|
||||
"CustomTextures": "BepInEx/plugins/",
|
||||
"*.png": "BepInEx/plugins/CustomTextures/",
|
||||
#
|
||||
"Builds": "AdvancedBuilder/",
|
||||
"*.vbuild": "AdvancedBuilder/Builds/",
|
||||
#
|
||||
"*.assets": "valheim_Data/",
|
||||
},
|
||||
}
|
||||
)
|
||||
self._featureMap[mobase.LocalSavegames] = ValheimLocalSavegames(
|
||||
self.savesDirectory()
|
||||
)
|
||||
@@ -510,7 +384,7 @@ class ValheimGame(BasicGame):
|
||||
def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]:
|
||||
return [
|
||||
mobase.ExecutableForcedLoadSetting(self.binaryName(), lib).withEnabled(True)
|
||||
for lib in self.forced_libraries
|
||||
for lib in self._forced_libraries
|
||||
]
|
||||
|
||||
def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
|
||||
@@ -548,6 +422,8 @@ class ValheimGame(BasicGame):
|
||||
self._sync_overwrite()
|
||||
|
||||
def _sync_overwrite(self) -> None:
|
||||
if self._organizer.managedGame() is not self:
|
||||
return
|
||||
if self._organizer.pluginSetting(self.name(), "sync_overwrite") is not False:
|
||||
self._overwrite_sync.search_file_contents = (
|
||||
self._organizer.pluginSetting(
|
||||
|
||||
Reference in New Issue
Block a user