mirror of
https://github.com/ModOrganizer2/modorganizer-basic_games.git
synced 2026-07-27 14:07:29 -07:00
278 lines
9.5 KiB
Python
278 lines
9.5 KiB
Python
from __future__ import annotations
|
|
|
|
import fnmatch
|
|
import os
|
|
from collections.abc import Iterable
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
|
|
from PyQt6.QtCore import QDir, qWarning
|
|
|
|
import mobase
|
|
|
|
from ..basic_features import BasicModDataChecker, GlobPatterns
|
|
from ..basic_features.basic_save_game_info import (
|
|
BasicGameSaveGame,
|
|
BasicGameSaveGameInfo,
|
|
)
|
|
from ..basic_features.utils import is_directory
|
|
from ..basic_game import BasicGame
|
|
|
|
|
|
class SubnauticaModDataChecker(BasicModDataChecker):
|
|
use_qmods: bool = False
|
|
|
|
def __init__(self, patterns: GlobPatterns | None = None, use_qmods: bool = False):
|
|
super().__init__(
|
|
GlobPatterns(
|
|
unfold=["BepInExPack_Subnautica"],
|
|
valid=[
|
|
"BepInEx",
|
|
"doorstop_libs",
|
|
"doorstop_config.ini",
|
|
"run_bepinex.sh",
|
|
"winhttp.dll",
|
|
"QMods",
|
|
".doorstop_version", # Added in Tobey's BepInEx Pack for Subnautica v5.4.23
|
|
"changelog.txt",
|
|
"libdoorstop.dylib",
|
|
],
|
|
delete=[
|
|
"*.txt",
|
|
"*.md",
|
|
"icon.png",
|
|
"license",
|
|
"manifest.json",
|
|
],
|
|
move={
|
|
"plugins": "BepInEx/",
|
|
"patchers": "BepInEx/",
|
|
"CustomCraft2SML": "QMods/" if use_qmods else "BepInEx/plugins/",
|
|
"CustomCraft3": "QMods/" if use_qmods else "BepInEx/plugins/",
|
|
},
|
|
).merge(patterns or GlobPatterns()),
|
|
)
|
|
self.use_qmods = use_qmods
|
|
|
|
def dataLooksValid(
|
|
self, filetree: mobase.IFileTree
|
|
) -> mobase.ModDataChecker.CheckReturn:
|
|
# fix: single root folders get traversed by Simple Installer
|
|
parent = filetree.parent()
|
|
if parent is not None and self.dataLooksValid(parent) is self.FIXABLE:
|
|
return self.FIXABLE
|
|
check_return = super().dataLooksValid(filetree)
|
|
# A single unknown folder with a dll file in is to be moved to BepInEx/plugins/
|
|
if (
|
|
check_return is self.INVALID
|
|
and len(filetree) == 1
|
|
and is_directory(folder := filetree[0])
|
|
and any(fnmatch.fnmatch(entry.name(), "*.dll") for entry in folder)
|
|
):
|
|
return self.FIXABLE
|
|
return check_return
|
|
|
|
def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree:
|
|
filetree = super().fix(filetree)
|
|
if (
|
|
self.dataLooksValid(filetree) is self.FIXABLE
|
|
and len(filetree) == 1
|
|
and is_directory(folder := filetree[0])
|
|
and any(fnmatch.fnmatch(entry.name(), "*.dll") for entry in folder)
|
|
):
|
|
filetree.move(folder, "QMods/" if self.use_qmods else "BepInEx/plugins/")
|
|
return filetree
|
|
|
|
|
|
class SubnauticaGame(BasicGame, mobase.IPluginFileMapper):
|
|
Name = "Subnautica Support Plugin"
|
|
Author = "dekart811, Zash"
|
|
Version = "2.2"
|
|
|
|
GameName = "Subnautica"
|
|
GameShortName = "subnautica"
|
|
GameNexusName = "subnautica"
|
|
GameSteamId = 264710
|
|
GameEpicId = "Jaguar"
|
|
GameBinary = "Subnautica.exe"
|
|
GameDataPath = "_ROOT" # Custom mappings to actual root folders below.
|
|
GameDocumentsDirectory = r"%GAME_PATH%"
|
|
GameSupportURL = (
|
|
r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
|
|
"Game:-Subnautica"
|
|
)
|
|
GameSavesDirectory = r"%GAME_PATH%\SNAppData\SavedGames"
|
|
|
|
_game_extra_save_paths = [
|
|
r"%USERPROFILE%\Appdata\LocalLow\Unknown Worlds"
|
|
r"\Subnautica\Subnautica\SavedGames"
|
|
]
|
|
|
|
_forced_libraries = ["winhttp.dll"]
|
|
|
|
_root_blacklist = {GameDataPath.casefold()}
|
|
|
|
class MapType(Enum):
|
|
FILE = 0
|
|
FOLDER = 1
|
|
|
|
_root_extra_overwrites: dict[str, MapType] = {
|
|
"qmodmanager_log-Subnautica.txt": MapType.FILE,
|
|
"qmodmanager-config.json": MapType.FILE,
|
|
"BepInEx_Shim_Backup": MapType.FOLDER,
|
|
}
|
|
"""Extra files & folders created in game root by mods / BepInEx after game launch,
|
|
but not included in the mod archives.
|
|
"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
mobase.IPluginFileMapper.__init__(self)
|
|
|
|
def init(self, organizer: mobase.IOrganizer) -> bool:
|
|
super().init(organizer)
|
|
self._set_mod_data_checker()
|
|
self._register_feature(
|
|
BasicGameSaveGameInfo(lambda s: Path(s or "", "screenshot.jpg"))
|
|
)
|
|
|
|
organizer.onPluginSettingChanged(self._settings_change_callback)
|
|
return True
|
|
|
|
def _set_mod_data_checker(
|
|
self, extra_patterns: GlobPatterns | None = None, use_qmod: bool | None = None
|
|
):
|
|
self._register_feature(
|
|
SubnauticaModDataChecker(
|
|
patterns=(GlobPatterns() if extra_patterns is None else extra_patterns),
|
|
use_qmods=(
|
|
bool(self._organizer.pluginSetting(self.name(), "use_qmods"))
|
|
if use_qmod is None
|
|
else use_qmod
|
|
),
|
|
)
|
|
)
|
|
|
|
def _settings_change_callback(
|
|
self,
|
|
plugin_name: str,
|
|
setting: str,
|
|
old: mobase.MoVariant,
|
|
new: mobase.MoVariant,
|
|
):
|
|
if plugin_name == self.name() and setting == "use_qmods":
|
|
self._set_mod_data_checker(use_qmod=bool(new))
|
|
|
|
def settings(self) -> list[mobase.PluginSetting]:
|
|
return [
|
|
mobase.PluginSetting(
|
|
"use_qmods",
|
|
(
|
|
"Install */.dll mods in legacy QMods folder,"
|
|
" instead of BepInEx/plugins (default)."
|
|
),
|
|
default_value=False,
|
|
)
|
|
]
|
|
|
|
def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
|
|
return [
|
|
BasicGameSaveGame(folder)
|
|
for save_path in (
|
|
folder.absolutePath(),
|
|
*(os.path.expandvars(p) for p in self._game_extra_save_paths),
|
|
)
|
|
for folder in Path(save_path).glob("slot*")
|
|
]
|
|
|
|
def executables(self) -> list[mobase.ExecutableInfo]:
|
|
binary = self.gameDirectory().absoluteFilePath(self.binaryName())
|
|
return [
|
|
mobase.ExecutableInfo(
|
|
self.gameName(),
|
|
binary,
|
|
).withArgument("-vrmode none"),
|
|
mobase.ExecutableInfo(
|
|
f"{self.gameName()} VR",
|
|
self.gameDirectory().absoluteFilePath(self.binaryName()),
|
|
),
|
|
]
|
|
|
|
def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]:
|
|
return [
|
|
mobase.ExecutableForcedLoadSetting(self.binaryName(), lib).withEnabled(True)
|
|
for lib in self._forced_libraries
|
|
]
|
|
|
|
def mappings(self) -> list[mobase.Mapping]:
|
|
game = self._organizer.managedGame()
|
|
game_path = Path(game.gameDirectory().absolutePath())
|
|
overwrite_path = Path(self._organizer.overwritePath())
|
|
|
|
return [
|
|
*(
|
|
# Extra overwrites
|
|
self._overwrite_mapping(
|
|
overwrite_path / name,
|
|
dest,
|
|
is_dir=(map_type is self.MapType.FOLDER),
|
|
)
|
|
for name, map_type in self._root_extra_overwrites.items()
|
|
if not (dest := game_path / name).exists()
|
|
),
|
|
*self._root_mappings(game_path, overwrite_path),
|
|
]
|
|
|
|
def _root_mappings(
|
|
self, game_path: Path, overwrite_path: Path
|
|
) -> Iterable[mobase.Mapping]:
|
|
for mod_path in self._active_mod_paths():
|
|
mod_name = mod_path.name
|
|
|
|
for child in mod_path.iterdir():
|
|
# Check blacklist
|
|
if child.name.casefold() in self._root_blacklist:
|
|
qWarning(f"Skipping {child.name} ({mod_name})")
|
|
continue
|
|
destination = game_path / child.name
|
|
# Check existing
|
|
if destination.exists():
|
|
qWarning(
|
|
f"Overwriting of existing game files/folders is not supported! "
|
|
f"{destination.as_posix()} ({mod_name})"
|
|
)
|
|
continue
|
|
# Mapping: mod -> root
|
|
yield mobase.Mapping(
|
|
source=str(child),
|
|
destination=str(destination),
|
|
is_directory=child.is_dir(),
|
|
create_target=False,
|
|
)
|
|
if child.is_dir():
|
|
# Mapping: overwrite <-> root
|
|
yield self._overwrite_mapping(
|
|
overwrite_path / child.name, destination, is_dir=True
|
|
)
|
|
|
|
def _active_mod_paths(self) -> Iterable[Path]:
|
|
mods_parent_path = Path(self._organizer.modsPath())
|
|
modlist = self._organizer.modList().allModsByProfilePriority()
|
|
for mod in modlist:
|
|
if self._organizer.modList().state(mod) & mobase.ModState.ACTIVE:
|
|
yield mods_parent_path / mod
|
|
|
|
def _overwrite_mapping(
|
|
self, overwrite_source: Path, destination: Path, is_dir: bool
|
|
) -> mobase.Mapping:
|
|
"""Mapping: overwrite <-> root"""
|
|
if is_dir:
|
|
# Root folders in overwrite need to exits.
|
|
overwrite_source.mkdir(parents=True, exist_ok=True)
|
|
return mobase.Mapping(
|
|
str(overwrite_source),
|
|
str(destination),
|
|
is_dir,
|
|
create_target=True,
|
|
)
|