diff --git a/README.md b/README.md
index 6fe4cdf..fea95b2 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,7 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic
| S.T.A.L.K.E.R. Anomaly — [MOD](https://www.stalker-anomaly.com/) | [Qudix](https://github.com/Qudix) | [game_stalkeranomaly.py](games/game_stalkeranomaly.py) |
|
| 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) | |
| 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) | - Empire at War: [game_starwars-empire-at-war.py](games/game_starwars-empire-at-war.py)
- Force of Corruption: [game_starwars-empire-at-war-foc.py](games/game_starwars-empire-at-war-foc.py)
| |
+| Valheim — [STEAM](https://store.steampowered.com/app/892970/Valheim/) | [Zash](https://github.com/ZashIn) | [game_valheim.py](games/game_valheim.py) | - mod data checker
- overwrite config sync
- save game support (no preview)
| 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) | - save game parsing (no preview)
|
| 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) | |
| Zeus and Poseidon — [GOG](https://www.gog.com/game/zeus_poseidon) / [STEAM](https://store.steampowered.com/app/566050/Zeus__Poseidon/) | [Holt59](https://github.com/holt59/) | [game_zeusandpoiseidon.py](games/game_zeusandpoiseidon.py) | |
diff --git a/games/game_gta-san-andreas-de.py b/games/game_gta-san-andreas-de.py
index 9e6e6a5..e368ab5 100644
--- a/games/game_gta-san-andreas-de.py
+++ b/games/game_gta-san-andreas-de.py
@@ -42,7 +42,9 @@ class GTASanAndreasDefinitiveEditionGame(BasicGame):
def init(self, organizer: mobase.IOrganizer) -> bool:
super().init(organizer)
- self._featureMap[mobase.ModDataChecker] = GTASanAndreasDefinitiveEditionModDataChecker()
+ self._featureMap[
+ mobase.ModDataChecker
+ ] = GTASanAndreasDefinitiveEditionModDataChecker()
return True
def executables(self):
diff --git a/games/game_gta-vice-city-de.py b/games/game_gta-vice-city-de.py
index d23e683..33b7590 100644
--- a/games/game_gta-vice-city-de.py
+++ b/games/game_gta-vice-city-de.py
@@ -1,7 +1,7 @@
import os
from pathlib import Path
-from PyQt6.QtCore import QDir,QFileInfo
+from PyQt6.QtCore import QDir, QFileInfo
import mobase
@@ -21,6 +21,7 @@ class GTAViceCitysDefinitiveEditionModDataChecker(mobase.ModDataChecker):
return mobase.ModDataChecker.INVALID
+
class GTAViceCityDefinitiveEditionGame(BasicGame):
Name = "Grand Theft Auto: Vice City - Definitive Edition Support Plugin"
@@ -41,7 +42,9 @@ class GTAViceCityDefinitiveEditionGame(BasicGame):
def init(self, organizer: mobase.IOrganizer) -> bool:
super().init(organizer)
- self._featureMap[mobase.ModDataChecker] = GTAViceCitysDefinitiveEditionModDataChecker()
+ self._featureMap[
+ mobase.ModDataChecker
+ ] = GTAViceCitysDefinitiveEditionModDataChecker()
return True
def executables(self):
diff --git a/games/game_valheim.py b/games/game_valheim.py
new file mode 100644
index 0000000..0d29864
--- /dev/null
+++ b/games/game_valheim.py
@@ -0,0 +1,556 @@
+# -*- encoding: utf-8 -*-
+
+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 pathlib import Path
+from typing import Optional, TextIO, Union
+
+from PyQt6.QtCore import QDir
+
+import mobase
+
+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`."""
+ if not target.parent.exists():
+ target.parent.mkdir(parents=True)
+ shutil.move(str(source.resolve()), str(target.resolve()))
+
+
+@dataclass
+class PartialMatch:
+ partial_match_regex: re.Pattern = re.compile(r"[A-Z]?[a-z]+")
+ """Matches words, for e.g. 'Camel' and 'Case' in 'CamelCase'."""
+
+ exclude: Container = field(default_factory=set)
+ min_length: int = 3
+
+ def partial_match(self, str_with_parts: str, search_string: str) -> Collection[str]:
+ """Returns partial matches of the first string (`str_with_parts`)
+ in the `search_string`.
+
+ See:
+ `partial_match_regex`
+ """
+ parts = self.partial_match_regex.finditer(str_with_parts)
+ search_string_lower = search_string.casefold()
+ return set(
+ p_lower
+ for p in parts
+ if len(p_lower := p[0].casefold()) >= self.min_length
+ and p_lower not in self.exclude
+ and p_lower in search_string_lower
+ )
+
+
+@dataclass
+class ContentMatch:
+ file_glob_patterns: Iterable[str]
+ """File patterns (glob) for content files and `content_regex`."""
+ content_regex: re.Pattern[str]
+ """Regex to get mod name from content. with (?P<`match_group`>) group."""
+ match_group: str
+
+ def match_content(self, file_path: Path) -> str:
+ if (
+ self.content_regex
+ and self.file_glob_patterns
+ and any(file_path.match(p) for p in self.file_glob_patterns)
+ ):
+ with open(file_path) as file:
+ if match := self.content_regex.search(file.read()):
+ return match.group(self.match_group)
+ return ""
+
+
+class DebugTable:
+ """Debug in markdown table."""
+
+ def __init__(self, column_keys: Collection[str]) -> None:
+ """Init the table, adds the header.
+
+ Args:
+ column_keys: The column keys for the table.
+ """
+ self.new_table(column_keys)
+
+ def __call__(self, **kwargs) -> None:
+ self.add(**kwargs)
+
+ def new_table(self, column_keys: Optional[Collection[str]] = None) -> None:
+ if column_keys:
+ self._column_keys = column_keys
+ self._table: list[dict[str, str]] = [
+ {c: c for c in self._column_keys},
+ {c: "-" * len(c) for c in self._column_keys},
+ ]
+
+ def add(
+ self,
+ **kwargs,
+ ) -> None:
+ """Add data to the table. Adds a new line if the last row has already data in
+ for the column key.
+
+ Args:
+ **kwargs: the values per column key for a row.
+ """
+ for k, v in kwargs.items():
+ if not self._table or self._table[-1].get(k, ""):
+ # Append line if element in last list is set.
+ self._table.append(dict.fromkeys(self._column_keys, ""))
+ self._table[-1][k] = str(v)
+
+ def print(self, output_file: Optional[TextIO] = None):
+ if self._table:
+ for line in self._table:
+ print("|", " | ".join(line.values()), "|", file=output_file)
+ output_file and output_file.flush()
+ self._table = []
+
+
+class OverwriteSync:
+ organizer: mobase.IOrganizer
+ game: mobase.IPluginGame
+ search_file_contents: bool = True
+
+ 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(
+ file_glob_patterns=["*.cfg"],
+ content_regex=re.compile(r"\A.*plugin (?P.+) v[\d\.]+?$", re.I | re.M),
+ match_group="mod",
+ )
+
+ _debug = DebugTable("overwrite_file | mod | target_path | matches".split(" | "))
+
+ def __init__(self, organizer: mobase.IOrganizer, game: mobase.IPluginGame) -> None:
+ self.organizer = organizer
+ self.game = game
+
+ def sync(self) -> None:
+ """Sync the Overwrite folder (back) to the mods."""
+ print("Syncing Overwrite with mods")
+ modlist = self.organizer.modList()
+ mod_map = self._get_active_mods(modlist)
+ mod_dll_map = self._get_mod_dll_map(mod_map)
+ overwrite_path = Path(self.organizer.overwritePath())
+ self._debug.new_table()
+ for pattern in self.overwrite_file_pattern:
+ for file_path in overwrite_path.glob(pattern):
+ self._debug(overwrite_file=file_path.name)
+ if mod := self._find_mod_for_overwrite_file(file_path, mod_dll_map):
+ # Move cfg to mod folder
+ mod_path = Path(modlist.getMod(mod).absolutePath())
+ target_path = mod_path / file_path.relative_to(overwrite_path)
+ self._debug(mod=mod, target_path=target_path)
+ move_file(file_path, target_path)
+ self._debug.print()
+
+ def _get_active_mods(
+ self, modlist: Optional[mobase.IModList] = None
+ ) -> Mapping[str, mobase.IModInterface]:
+ """Get all active mods.
+
+ Args: modlist (optional): the `mobase.IModList`. Defaults to None (get it from
+ `self._organizer`).
+
+ Returns: `{mod_name: mobase.IModInterface}`
+ """
+ modlist = modlist or self.organizer.modList()
+
+ return {
+ name: mod
+ for name in modlist.allMods() # allModsByProfilePriority ?
+ if (mod := modlist.getMod(name)).gameName() == self.game.gameShortName()
+ and not mod.isForeign()
+ and not mod.isBackup()
+ and not mod.isSeparator()
+ and (modlist.state(name) & mobase.ModState.ACTIVE)
+ }
+
+ def _get_mod_dll_map(self, mod_map):
+ 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]:
+ """Get all BepInEx/plugins/*.dll files of a mod."""
+ if isinstance(mod, str):
+ mod = self.organizer.modList().getMod(mod)
+ plugins = mod.fileTree().find("BepInEx/plugins/", mobase.IFileTree.DIRECTORY)
+ if isinstance(plugins, mobase.IFileTree):
+ return [name for p in plugins if (name := p.name()).endswith(".dll")]
+ else:
+ return []
+
+ def _find_mod_for_overwrite_file(
+ self,
+ file_path: Path,
+ mod_dll_map: Mapping[str, Collection[str]],
+ ) -> str:
+ """Find the mod (name) matching a file in Overwrite (using the mods dll name).
+
+ Args:
+ file_name: The name of the file.
+ mod_dll_map: Mods names and their dll files `{mod_name: ["ModName.dll"]}`.
+
+ Returns:
+ The name of the mod matching the given file_name best.
+ If there is no clear mod match for the file, an empty string "" is returned.
+ """
+ if file_path.is_dir():
+ return ""
+ file_name = file_path.stem
+ # matching metric: combined length of partial matches per mod.
+ matching_mods = self._get_matching_mods(file_name, mod_dll_map)
+ if len(matching_mods) == 0:
+ if self.search_file_contents and self.content_match:
+ # Get mod name from file content.
+ long_mod_name = self.content_match.match_content(file_path)
+ matching_mods = self._get_matching_mods(long_mod_name, mod_dll_map)
+ if len(matching_mods) == 1:
+ # Only a single mod match found.
+ return matching_mods[0][0]
+ elif len(matching_mods) > 1:
+ # Find mod with longest (combined) partial_match.
+ self._debug.add(matches=matching_mods)
+ # Do not return a mod for multiple "equal" matches.
+ if matching_mods[0][1] > matching_mods[1][1]:
+ return matching_mods[0][0]
+ return ""
+
+ def _get_matching_mods(
+ self, search_str: str, mod_dll_map: Mapping[str, Collection[str]]
+ ) -> Sequence[tuple[str, int, set[str]]]:
+ """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"]}`.
+
+ Returns:
+ Mods with partial matches, sorted descending by their metric
+ (length of combined partial matches):
+ {mod_name: (len_of_combined_partial_matches, {partial_matches, ...}), ...}
+ """
+ return sorted(
+ (
+ (name, sum(len(s) for s in partial_matches), partial_matches)
+ for name, dlls in mod_dll_map.items()
+ if len(
+ partial_matches := set(
+ itertools.chain.from_iterable(
+ self.partial_match.partial_match(search_str, dll)
+ for dll in dlls
+ )
+ )
+ )
+ > 0
+ ),
+ key=lambda x: x[1],
+ reverse=True,
+ )
+
+
+@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.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}"
+
+ def getSaveGroupIdentifier(self) -> str:
+ return self._filepath.parent.name
+
+ def allFiles(self) -> list[str]:
+ files = super().allFiles()
+ files.extend(
+ self._filepath.with_suffix(suffix).as_posix()
+ for suffix in [self._filepath.suffix + ".old"]
+ )
+ return files
+
+
+class ValheimWorldSaveGame(ValheimSaveGame):
+ def allFiles(self) -> list[str]:
+ files = super().allFiles()
+ files.extend(
+ self._filepath.with_suffix(suffix).as_posix()
+ for suffix in [".db", ".db.old"]
+ )
+ return files
+
+
+class ValheimLocalSavegames(mobase.LocalSavegames):
+ def __init__(self, myGameSaveDir):
+ super().__init__()
+ self._savesDir = myGameSaveDir.absolutePath()
+
+ def mappings(self, profile_save_dir):
+ return [
+ mobase.Mapping(
+ source=profile_save_dir.absolutePath(),
+ destination=self._savesDir,
+ is_directory=True,
+ create_target=True,
+ )
+ ]
+
+ def prepareProfile(self, profile):
+ return profile.localSavesEnabled()
+
+
+class ValheimGame(BasicGame):
+
+ Name = "Valheim Support Plugin"
+ Author = "Zash"
+ Version = "1.1.0"
+
+ GameName = "Valheim"
+ GameShortName = "valheim"
+ GameNexusId = 3667
+ GameSteamId = [892970, 896660, 1223920]
+ GameBinary = "valheim.exe"
+ GameDataPath = ""
+ GameSavesDirectory = r"%USERPROFILE%/AppData/LocalLow/IronGate/Valheim"
+
+ forced_libraries = ["winhttp.dll"]
+
+ def init(self, organizer: mobase.IOrganizer) -> bool:
+ super().init(organizer)
+ self._featureMap[mobase.ModDataChecker] = ValheimGameModDataChecker()
+ self._featureMap[mobase.LocalSavegames] = ValheimLocalSavegames(
+ self.savesDirectory()
+ )
+ self._overwrite_sync = OverwriteSync(organizer=self._organizer, game=self)
+ self._register_event_handler()
+ return True
+
+ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]:
+ return [
+ mobase.ExecutableForcedLoadSetting(self.binaryName(), lib).withEnabled(True)
+ for lib in self.forced_libraries
+ ]
+
+ def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
+ save_games = super().listSaves(folder)
+ path = Path(folder.absolutePath())
+ save_games.extend(ValheimSaveGame(f) for f in path.glob("characters/*.fch"))
+ save_games.extend(ValheimWorldSaveGame(f) for f in path.glob("worlds/*.fwl"))
+ return save_games
+
+ def settings(self) -> list[mobase.PluginSetting]:
+ settings = super().settings()
+ settings.extend(
+ [
+ mobase.PluginSetting(
+ "sync_overwrite", "Sync overwrite with mods", True
+ ),
+ mobase.PluginSetting(
+ "search_overwrite_file_content",
+ "Search content of files in overwrite for matching mod",
+ True,
+ ),
+ ]
+ )
+ return settings
+
+ def _register_event_handler(self):
+ 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:
+ """Sync overwrite folder with mods after game was closed."""
+ if Path(app_path) == Path(
+ self.gameDirectory().absolutePath(), self.binaryName()
+ ):
+ self._sync_overwrite()
+
+ def _sync_overwrite(self) -> None:
+ if self._organizer.pluginSetting(self.name(), "sync_overwrite") is not False:
+ self._overwrite_sync.search_file_contents = (
+ self._organizer.pluginSetting(
+ self.name(), "search_overwrite_file_content"
+ )
+ is not False
+ )
+ self._overwrite_sync.sync()
diff --git a/games/game_vampriebloodlines.py b/games/game_vampirebloodlines.py
similarity index 92%
rename from games/game_vampriebloodlines.py
rename to games/game_vampirebloodlines.py
index b77895f..63870be 100644
--- a/games/game_vampriebloodlines.py
+++ b/games/game_vampirebloodlines.py
@@ -1,122 +1,123 @@
-# -*- encoding: utf-8 -*-
-import os
-import mobase
-
-from PyQt6.QtCore import QDir
-
-from typing import List
-from pathlib import Path
-
-from ..basic_game import BasicGame, BasicGameSaveGame
-from ..basic_features import BasicGameSaveGameInfo
-
-
-class VampireModDataChecker(mobase.ModDataChecker):
- def __init__(self):
- super().__init__()
- self.validDirNames = [
- "cfg",
- "cl_dlls",
- "dlg",
- "dlls",
- "maps",
- "materials",
- "media",
- "models",
- "particles",
- "python",
- "resource",
- "scripts",
- "sound",
- "vdata",
- ]
-
- def dataLooksValid(
- self, tree: mobase.IFileTree
- ) -> mobase.ModDataChecker.CheckReturn:
- for entry in tree:
- if not entry.isDir():
- continue
- if entry.name().casefold() in self.validDirNames:
- return mobase.ModDataChecker.VALID
- return mobase.ModDataChecker.INVALID
-
-
-class VampireSaveGame(BasicGameSaveGame):
- _filepath: Path
-
- def __init__(self, filepath: Path):
- super().__init__(filepath)
- self._filepath = filepath
- self.name = None
- self.elapsedTime = None
-
-
-class VampireLocalSavegames(mobase.LocalSavegames):
- def __init__(self, myGameSaveDir):
- super().__init__()
- self._savesDir = myGameSaveDir.absolutePath()
-
- def mappings(self, profile_save_dir):
- m = mobase.Mapping()
- m.createTarget = True
- m.isDirectory = True
- m.source = profile_save_dir.absolutePath()
- m.destination = self._savesDir
-
- return [m]
-
- def prepareProfile(self, profile):
- return profile.localSavesEnabled()
-
-
-class VampireTheMasqueradeBloodlinesGame(BasicGame):
- Name = "Vampire - The Masquerade: Bloodlines Support Plugin"
- Author = "John"
- Version = "1.0.0"
- Description = "Adds support for Vampires: The Masquerade - Bloodlines, with the only caveat that global save games end up in overwrite."
-
- GameName = "Vampire - The Masquerade: Bloodlines"
- GameShortName = "vampirebloodlines"
- GameNexusName = "vampirebloodlines"
- GameNexusId = 437
- GameSteamId = [2600]
- GameGogId = [1207659240]
- GameBinary = "vampire.exe"
- GameDataPath = "vampire"
- GameDocumentsDirectory = "%GAME_PATH%/vampire/cfg"
- GameSavesDirectory = "%GAME_PATH%/vampire/SAVE"
- GameSaveExtension = "sav"
-
- def init(self, organizer: mobase.IOrganizer) -> bool:
- super().init(organizer)
- self._featureMap[mobase.ModDataChecker] = VampireModDataChecker()
- self._featureMap[mobase.SaveGameInfo] = VampireSaveGame(self.savesDirectory())
- self._featureMap[mobase.LocalSavegames] = VampireLocalSavegames(
- self.savesDirectory()
- )
- return True
-
- def initializeProfile(self, path: QDir, settings: int):
- # Create .cfg files if they don't exist
- for iniFile in self.iniFiles():
- iniPath = self.documentsDirectory().absoluteFilePath(iniFile)
- if not os.path.exists(iniPath):
- with open(iniPath, "w") as _:
- pass
-
- super().initializeProfile(path, settings)
-
- def version(self):
- # Don't forget to import mobase!
- return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final)
-
- def iniFiles(self):
- return ["autoexec.cfg", "user.cfg"]
-
- def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]:
- ext = self._mappings.savegameExtension.get()
- return [
- VampireSaveGame(path)
- for path in Path(folder.absolutePath()).glob(f"*.{ext}")
- ]
+# -*- encoding: utf-8 -*-
+import os
+import mobase
+
+from PyQt6.QtCore import QDir
+
+from typing import List
+from pathlib import Path
+
+from ..basic_game import BasicGame, BasicGameSaveGame
+
+
+class VampireModDataChecker(mobase.ModDataChecker):
+ def __init__(self):
+ super().__init__()
+ self.validDirNames = [
+ "cfg",
+ "cl_dlls",
+ "dlg",
+ "dlls",
+ "maps",
+ "materials",
+ "media",
+ "models",
+ "particles",
+ "python",
+ "resource",
+ "scripts",
+ "sound",
+ "vdata",
+ ]
+
+ def dataLooksValid(
+ self, tree: mobase.IFileTree
+ ) -> mobase.ModDataChecker.CheckReturn:
+ for entry in tree:
+ if not entry.isDir():
+ continue
+ if entry.name().casefold() in self.validDirNames:
+ return mobase.ModDataChecker.VALID
+ return mobase.ModDataChecker.INVALID
+
+
+class VampireSaveGame(BasicGameSaveGame):
+ _filepath: Path
+
+ def __init__(self, filepath: Path):
+ super().__init__(filepath)
+ self._filepath = filepath
+ self.name = None
+ self.elapsedTime = None
+
+
+class VampireLocalSavegames(mobase.LocalSavegames):
+ def __init__(self, myGameSaveDir):
+ super().__init__()
+ self._savesDir = myGameSaveDir.absolutePath()
+
+ def mappings(self, profile_save_dir):
+ m = mobase.Mapping()
+ m.createTarget = True
+ m.isDirectory = True
+ m.source = profile_save_dir.absolutePath()
+ m.destination = self._savesDir
+
+ return [m]
+
+ def prepareProfile(self, profile):
+ return profile.localSavesEnabled()
+
+
+class VampireTheMasqueradeBloodlinesGame(BasicGame):
+ Name = "Vampire - The Masquerade: Bloodlines Support Plugin"
+ Author = "John"
+ Version = "1.0.0"
+ Description = "Adds support for Vampires: The Masquerade - Bloodlines"
+
+ GameName = "Vampire - The Masquerade: Bloodlines"
+ GameShortName = "vampirebloodlines"
+ GameNexusName = "vampirebloodlines"
+ GameNexusId = 437
+ GameSteamId = [2600]
+ GameGogId = [1207659240]
+ GameBinary = "vampire.exe"
+ GameDataPath = "vampire"
+ GameDocumentsDirectory = "%GAME_PATH%/vampire/cfg"
+ GameSavesDirectory = "%GAME_PATH%/vampire/SAVE"
+ GameSaveExtension = "sav"
+
+ 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: int):
+ # Create .cfg files if they don't exist
+ for iniFile in self.iniFiles():
+ iniPath = self.documentsDirectory().absoluteFilePath(iniFile)
+ if not os.path.exists(iniPath):
+ with open(iniPath, "w") as _:
+ pass
+
+ super().initializeProfile(path, settings)
+
+ def version(self):
+ # Don't forget to import mobase!
+ return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final)
+
+ def iniFiles(self):
+ return ["autoexec.cfg", "user.cfg"]
+
+ def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]:
+ ext = self._mappings.savegameExtension.get()
+ return [
+ VampireSaveGame(path)
+ for path in Path(folder.absolutePath()).glob(f"*.{ext}")
+ ]