Files
modorganizer-basic_games/basic_game.py
T

659 lines
22 KiB
Python
Raw Normal View History

from __future__ import annotations
2020-06-10 23:22:53 +02:00
import shutil
2021-11-24 20:55:11 -07:00
import sys
from pathlib import Path
from typing import Callable, Generic, TypeVar
2020-06-10 23:22:53 +02:00
import mobase
from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths
from PyQt6.QtGui import QIcon
2020-06-10 23:22:53 +02:00
from .basic_features.basic_save_game_info import (
BasicGameSaveGame,
BasicGameSaveGameInfo,
)
2020-11-18 19:17:55 +01:00
2020-06-10 23:22:53 +02:00
def replace_variables(value: str, game: BasicGame) -> str:
2021-04-25 15:37:46 -07:00
"""Replace special paths in the given value."""
2020-06-12 22:47:37 +02:00
if value.find("%DOCUMENTS%") != -1:
value = value.replace(
"%DOCUMENTS%",
2022-04-21 22:13:00 +02:00
QStandardPaths.writableLocation(
QStandardPaths.StandardLocation.DocumentsLocation
),
2020-06-12 22:47:37 +02:00
)
if value.find("%USERPROFILE%") != -1:
value = value.replace(
"%USERPROFILE%",
2022-04-21 22:13:00 +02:00
QStandardPaths.writableLocation(
QStandardPaths.StandardLocation.HomeLocation
),
)
2020-06-12 22:47:37 +02:00
if value.find("%GAME_DOCUMENTS%") != -1:
value = value.replace(
"%GAME_DOCUMENTS%", game.documentsDirectory().absolutePath()
)
if value.find("%GAME_PATH%") != -1:
value = value.replace("%GAME_PATH%", game.gameDirectory().absolutePath())
return value
_T = TypeVar("_T")
2020-06-11 21:52:01 +02:00
class BasicGameMapping(Generic[_T]):
2020-06-11 21:52:01 +02:00
# The game:
_game: "BasicGame"
# Name of the attribute for exposure:
2020-06-11 21:52:01 +02:00
_exposed_name: str
# Name of the internal method:
2020-06-11 21:52:01 +02:00
_internal_method_name: str
# Required:
2020-06-11 21:52:01 +02:00
_required: bool
2020-06-11 21:52:01 +02:00
# Callable returning a default value (if not required):
_default: Callable[["BasicGame"], _T]
# Function to apply to the value:
_apply_fn: Callable[[_T | str], _T] | None
def __init__(
self,
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
2020-06-11 21:52:01 +02:00
self._exposed_name = exposed_name
self._internal_method_name = internal_method
self._apply_fn = apply_fn
if hasattr(game, self._exposed_name):
value = getattr(game, self._exposed_name)
2020-06-12 22:47:37 +02:00
2020-06-11 21:52:01 +02:00
if self._apply_fn is not None:
try:
value = self._apply_fn(value)
except: # noqa
raise ValueError(
"Basic game plugin from {} has an invalid {} property.".format(
game._fromName, # pyright: ignore[reportPrivateUsage]
self._exposed_name,
2020-06-11 21:52:01 +02:00
)
)
self._default = lambda game: value # type: ignore
elif default is not None:
self._default = default # type: ignore
2020-06-11 21:52:01 +02:00
elif getattr(game.__class__, self._internal_method_name) is getattr(
BasicGame, self._internal_method_name
):
raise ValueError(
"Basic game plugin from {} is missing {} property.".format(
game._fromName, # pyright: ignore[reportPrivateUsage]
self._exposed_name,
2020-06-11 21:52:01 +02:00
)
)
def get(self) -> _T:
2021-04-25 15:37:46 -07:00
"""Return the value of this mapping."""
2020-06-12 22:47:37 +02:00
value = self._default(self._game) # type: ignore
if isinstance(value, str):
return replace_variables(value, self._game) # type: ignore
elif isinstance(value, QDir):
return QDir(replace_variables(value.path(), self._game)) # type: ignore
2020-09-27 15:41:56 +02:00
# MO2 does not support Path anywhere so we always convert to str:
elif isinstance(value, Path):
return replace_variables(str(value), self._game) # type: ignore
2020-06-12 22:47:37 +02:00
return value
class BasicGameOptionsMapping(BasicGameMapping[list[_T]]):
"""
Represents a game mappings for which multiple options are possible. The game
plugin is responsible to choose the right option depending on the context.
"""
_index: int
def __init__(
self,
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
self._current_default = default
def set_index(self, index: int):
"""
Set the index of the option to use.
Args:
index: Index of the option to use.
"""
self._index = index
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.
Args:
value: The value to set the index to.
"""
try:
self._index = self.get().index(value)
except ValueError:
self._index = -1
def has_value(self) -> bool:
"""
Check if a value was set for this options mapping.
Returns:
True if a value was set, False otherwise.
"""
return self._index != -1
def current(self) -> _T:
values = self._default(self._game) # type: ignore
if not values:
return self._current_default(self._game) # type: ignore
if self._index == -1:
value = values[0]
else:
value = values[self._index]
if isinstance(value, str):
return replace_variables(value, self._game) # type: ignore
elif isinstance(value, QDir):
return QDir(replace_variables(value.path(), self._game)) # type: ignore
return value
class BasicGameMappings:
name: BasicGameMapping[str]
author: BasicGameMapping[str]
version: BasicGameMapping[mobase.VersionInfo]
description: BasicGameMapping[str]
gameName: BasicGameMapping[str]
gameShortName: BasicGameMapping[str]
gameNexusName: BasicGameMapping[str]
validShortNames: BasicGameMapping[list[str]]
nexusGameId: BasicGameMapping[int]
binaryName: BasicGameMapping[str]
launcherName: BasicGameMapping[str]
dataDirectory: BasicGameMapping[str]
2020-06-12 22:47:37 +02:00
documentsDirectory: BasicGameMapping[QDir]
iniFiles: BasicGameMapping[list[str]]
2020-06-12 22:47:37 +02:00
savesDirectory: BasicGameMapping[QDir]
savegameExtension: BasicGameMapping[str]
steamAPPId: BasicGameOptionsMapping[str]
gogAPPId: BasicGameOptionsMapping[str]
2021-11-24 17:23:34 -07:00
originManifestIds: BasicGameOptionsMapping[str]
originWatcherExecutables: BasicGameMapping[list[str]]
epicAPPId: BasicGameOptionsMapping[str]
2022-04-11 21:35:21 +02:00
eaDesktopContentId: BasicGameOptionsMapping[str]
2021-12-26 23:53:38 -07:00
supportURL: BasicGameMapping[str]
2020-06-12 22:47:37 +02:00
@staticmethod
def _default_documents_directory(game: mobase.IPluginGame):
2020-06-12 22:47:37 +02:00
folders = [
"{}/My Games/{}".format(
2022-04-21 22:13:00 +02:00
QStandardPaths.writableLocation(
QStandardPaths.StandardLocation.DocumentsLocation
),
2020-06-12 22:47:37 +02:00
game.gameName(),
),
"{}/{}".format(
2022-04-21 22:13:00 +02:00
QStandardPaths.writableLocation(
QStandardPaths.StandardLocation.DocumentsLocation
),
2020-06-12 22:47:37 +02:00
game.gameName(),
),
]
for folder in folders:
qdir = QDir(folder)
if qdir.exists():
return qdir
return QDir()
# Game mappings:
def __init__(self, game: BasicGame):
2020-06-12 22:47:37 +02:00
self._game = game
self.name = BasicGameMapping(game, "Name", "name")
self.author = BasicGameMapping(game, "Author", "author")
self.version = BasicGameMapping(
game,
"Version",
"version",
apply_fn=lambda s: mobase.VersionInfo(s) if isinstance(s, str) else s,
)
self.description = BasicGameMapping(
game,
"Description",
"description",
lambda g: "Adds basic support for game {}.".format(g.gameName()),
)
self.gameName = BasicGameMapping(game, "GameName", "gameName")
self.gameShortName = BasicGameMapping(game, "GameShortName", "gameShortName")
self.gameNexusName = BasicGameMapping(
2020-10-31 17:46:18 +01:00
game,
"GameNexusName",
"gameNexusName",
default=lambda g: g.gameShortName(),
)
self.validShortNames = BasicGameMapping(
game,
"GameValidShortNames",
"validShortNames",
default=lambda g: [],
apply_fn=lambda value: [c.strip() for c in value.split(",")] # type: ignore
if isinstance(value, str)
else value,
)
self.nexusGameId = BasicGameMapping(
game, "GameNexusId", "nexusGameID", default=lambda g: 0, apply_fn=int
)
self.binaryName = BasicGameMapping(game, "GameBinary", "binaryName")
self.launcherName = BasicGameMapping(
2020-10-31 17:46:18 +01:00
game,
"GameLauncher",
"getLauncherName",
default=lambda g: "",
)
self.dataDirectory = BasicGameMapping(game, "GameDataPath", "dataDirectory")
2020-06-12 22:47:37 +02:00
self.documentsDirectory = BasicGameMapping(
game,
"GameDocumentsDirectory",
"documentsDirectory",
apply_fn=lambda s: QDir(s) if isinstance(s, str) else s,
default=BasicGameMappings._default_documents_directory,
)
self.iniFiles = BasicGameMapping(
game,
"GameIniFiles",
"iniFiles",
lambda g: [],
apply_fn=lambda value: [c.strip() for c in value.split(",")]
if isinstance(value, str)
else value,
)
2020-06-12 22:47:37 +02:00
self.savesDirectory = BasicGameMapping(
game,
"GameSavesDirectory",
"savesDirectory",
apply_fn=lambda s: QDir(s) if isinstance(s, str) else s,
default=lambda g: g.documentsDirectory(),
)
self.savegameExtension = BasicGameMapping(
game, "GameSaveExtension", "savegameExtension", default=lambda g: "save"
)
# Convert Union[int, str, List[Union[int, str]]] to 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 = [str(v)]
return [str(x) for x in v]
self.steamAPPId = BasicGameOptionsMapping(
game, "GameSteamId", "steamAPPId", default=lambda g: "", apply_fn=ids_apply
)
self.gogAPPId = BasicGameOptionsMapping(
game, "GameGogId", "gogAPPId", default=lambda g: "", apply_fn=ids_apply
)
2021-11-24 17:23:34 -07:00
self.originManifestIds = BasicGameOptionsMapping(
2021-11-24 20:55:11 -07:00
game,
"GameOriginManifestIds",
"originManifestIds",
default=lambda g: "",
apply_fn=ids_apply,
)
self.originWatcherExecutables = BasicGameMapping(
game,
"GameOriginWatcherExecutables",
"originWatcherExecutables",
apply_fn=lambda s: [s] if isinstance(s, str) else s,
default=lambda g: [],
2021-11-24 17:23:34 -07:00
)
self.epicAPPId = BasicGameOptionsMapping(
game, "GameEpicId", "epicAPPId", default=lambda g: "", apply_fn=ids_apply
)
self.eaDesktopContentId = BasicGameOptionsMapping(
game,
"GameEaDesktopId",
"eaDesktopContentId",
default=lambda g: "",
apply_fn=ids_apply,
)
2021-12-26 23:53:38 -07:00
self.supportURL = BasicGameMapping(
game, "GameSupportURL", "supportURL", default=lambda g: ""
)
_GameFeature = (
mobase.BSAInvalidation
| mobase.DataArchives
| mobase.GamePlugins
| mobase.LocalSavegames
| mobase.ModDataChecker
| mobase.ModDataContent
| mobase.SaveGameInfo
| mobase.ScriptExtender
| mobase.UnmanagedMods
)
2020-06-10 23:22:53 +02:00
class BasicGame(mobase.IPluginGame):
2020-07-20 21:18:13 +02:00
"""This class implements some methods from mobase.IPluginGame
2020-06-10 23:22:53 +02:00
to make it easier to create game plugins without having to implement
2020-07-20 21:18:13 +02:00
all the methods of mobase.IPluginGame."""
2020-06-10 23:22:53 +02:00
# 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]
@staticmethod
def setup():
2022-04-11 21:35:21 +02:00
from .eadesktop_utils import find_games as find_eadesktop_games
from .epic_utils import find_games as find_epic_games
from .gog_utils import find_games as find_gog_games
2021-11-24 17:23:34 -07:00
from .origin_utils import find_games as find_origin_games
2022-04-21 22:13:00 +02:00
from .steam_utils import find_games as find_steam_games
BasicGame.steam_games = find_steam_games()
BasicGame.gog_games = find_gog_games()
2021-11-24 17:23:34 -07:00
BasicGame.origin_games = find_origin_games()
BasicGame.epic_games = find_epic_games()
2022-04-11 21:35:21 +02:00
BasicGame.eadesktop_games = find_eadesktop_games()
2020-06-10 23:22:53 +02:00
# File containing the plugin:
_fromName: str
# Organizer obtained in init:
_organizer: mobase.IOrganizer
# Path to the game, as set by MO2:
_gamePath: str
# The feature map:
_featureMap: dict[type[_GameFeature], _GameFeature]
2020-06-10 23:22:53 +02:00
def __init__(self):
super(BasicGame, self).__init__()
if not hasattr(self, "_fromName"):
self._fromName = self.__class__.__name__
2020-06-12 22:47:37 +02:00
self._gamePath = ""
self._featureMap = {}
2020-06-10 23:22:53 +02:00
2020-10-16 17:02:47 -05:00
self._mappings: BasicGameMappings = BasicGameMappings(self)
2020-06-10 23:22:53 +02:00
# Specific to BasicGame:
def is_steam(self) -> bool:
return self._mappings.steamAPPId.has_value()
def is_gog(self) -> bool:
return self._mappings.gogAPPId.has_value()
2021-11-24 17:23:34 -07:00
def is_origin(self) -> bool:
return self._mappings.originManifestIds.has_value()
def is_epic(self) -> bool:
return self._mappings.epicAPPId.has_value()
2022-04-11 21:35:21 +02:00
def is_eadesktop(self) -> bool:
return self._mappings.eaDesktopContentId.has_value()
2020-11-06 20:15:21 +01:00
# IPlugin interface:
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def init(self, organizer: mobase.IOrganizer) -> bool:
2020-06-10 23:22:53 +02:00
self._organizer = organizer
self._featureMap[mobase.SaveGameInfo] = BasicGameSaveGameInfo()
2021-11-24 20:55:11 -07:00
if self._mappings.originWatcherExecutables.get():
from .origin_utils import OriginWatcher
self.origin_watcher = OriginWatcher(
self._mappings.originWatcherExecutables.get()
)
if not self._organizer.onAboutToRun(
lambda appName: self.origin_watcher.spawn_origin_watcher()
):
print("Failed to register onAboutToRun callback!", file=sys.stderr)
return False
if not self._organizer.onFinishedRun(
lambda appName, result: self.origin_watcher.stop_origin_watcher()
):
print("Failed to register onFinishedRun callback!", file=sys.stderr)
return False
2020-06-10 23:22:53 +02:00
return True
2020-06-11 21:52:01 +02:00
def name(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.name.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def author(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.author.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def description(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.description.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def version(self) -> mobase.VersionInfo:
2020-10-16 17:02:47 -05:00
return self._mappings.version.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def isActive(self) -> bool:
if not self._organizer.managedGame():
return False
2020-11-10 18:06:02 +01:00
# Note: self is self._organizer.managedGame() does not work:
return self.name() == self._organizer.managedGame().name()
2020-06-10 23:22:53 +02:00
def settings(self) -> list[mobase.PluginSetting]:
2020-06-10 23:22:53 +02:00
return []
2020-11-06 20:15:21 +01:00
# IPluginGame interface:
def detectGame(self):
for steam_id in self._mappings.steamAPPId.get():
if steam_id in BasicGame.steam_games:
self.setGamePath(BasicGame.steam_games[steam_id])
return
for gog_id in self._mappings.gogAPPId.get():
if gog_id in BasicGame.gog_games:
self.setGamePath(BasicGame.gog_games[gog_id])
return
2021-11-24 17:23:34 -07:00
for origin_manifest_id in self._mappings.originManifestIds.get():
if origin_manifest_id in BasicGame.origin_games:
self.setGamePath(BasicGame.origin_games[origin_manifest_id])
return
for epic_id in self._mappings.epicAPPId.get():
if epic_id in BasicGame.epic_games:
self.setGamePath(BasicGame.epic_games[epic_id])
return
2022-04-11 21:35:21 +02:00
for eadesktop_content_id in self._mappings.eaDesktopContentId.get():
if eadesktop_content_id in BasicGame.eadesktop_games:
self.setGamePath(BasicGame.eadesktop_games[eadesktop_content_id])
return
2020-06-11 21:52:01 +02:00
def gameName(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.gameName.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def gameShortName(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.gameShortName.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def gameIcon(self) -> QIcon:
2020-06-10 23:22:53 +02:00
return mobase.getIconForExecutable(
self.gameDirectory().absoluteFilePath(self.binaryName())
)
def validShortNames(self) -> list[str]:
2020-10-16 17:02:47 -05:00
return self._mappings.validShortNames.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def gameNexusName(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.gameNexusName.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def nexusModOrganizerID(self) -> int:
2020-06-10 23:22:53 +02:00
return 0
2020-06-11 21:52:01 +02:00
def nexusGameID(self) -> int:
2020-10-16 17:02:47 -05:00
return self._mappings.nexusGameId.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def steamAPPId(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.steamAPPId.current()
def gogAPPId(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.gogAPPId.current()
2020-06-10 23:22:53 +02:00
def epicAPPId(self) -> str:
return self._mappings.epicAPPId.current()
2022-04-11 21:35:21 +02:00
def eaDesktopContentId(self) -> str:
return self._mappings.eaDesktopContentId.current()
2020-06-11 21:52:01 +02:00
def binaryName(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.binaryName.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def getLauncherName(self) -> str:
2020-10-16 17:02:47 -05:00
return self._mappings.launcherName.get()
2020-06-10 23:22:53 +02:00
2021-12-27 00:16:10 -07:00
def getSupportURL(self) -> str:
return self._mappings.supportURL.get()
def iniFiles(self) -> list[str]:
return self._mappings.iniFiles.get()
def executables(self) -> list[mobase.ExecutableInfo]:
execs: list[mobase.ExecutableInfo] = []
2020-06-11 21:52:01 +02:00
if self.getLauncherName():
execs.append(
mobase.ExecutableInfo(
self.gameName(),
QFileInfo(
self.gameDirectory().absoluteFilePath(self.getLauncherName())
),
)
)
execs.append(
2020-06-10 23:22:53 +02:00
mobase.ExecutableInfo(
self.gameName(),
QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())),
)
2020-06-11 21:52:01 +02:00
)
return execs
2020-06-10 23:22:53 +02:00
def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]:
return []
def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
2020-11-18 19:17:55 +01:00
ext = self._mappings.savegameExtension.get()
return [
BasicGameSaveGame(path)
for path in Path(folder.absolutePath()).glob(f"**/*.{ext}")
]
2020-06-10 23:22:53 +02:00
def initializeProfile(
self, directory: QDir, settings: mobase.ProfileSetting
) -> None:
if settings & mobase.ProfileSetting.CONFIGURATION:
for iniFile in self.iniFiles():
2021-12-24 23:15:00 -06:00
try:
shutil.copyfile(
self.documentsDirectory().absoluteFilePath(iniFile),
directory.absoluteFilePath(QFileInfo(iniFile).fileName()),
2021-12-24 23:15:00 -06:00
)
except FileNotFoundError:
Path(
directory.absoluteFilePath(QFileInfo(iniFile).fileName())
).touch()
2020-06-10 23:22:53 +02:00
def setGameVariant(self, variant: str) -> None:
2020-06-10 23:22:53 +02:00
pass
2020-06-11 21:52:01 +02:00
def gameVersion(self) -> str:
return mobase.getFileVersion(
2020-06-10 23:22:53 +02:00
self.gameDirectory().absoluteFilePath(self.binaryName())
)
def looksValid(self, directory: QDir):
return directory.exists(self.binaryName())
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def isInstalled(self) -> bool:
2020-11-06 20:15:21 +01:00
return bool(self._gamePath)
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def gameDirectory(self) -> QDir:
2020-06-10 23:22:53 +02:00
"""
@return directory (QDir) to the game installation.
"""
return QDir(self._gamePath)
2020-06-11 21:52:01 +02:00
def dataDirectory(self) -> QDir:
return QDir(
2020-10-16 17:02:47 -05:00
self.gameDirectory().absoluteFilePath(self._mappings.dataDirectory.get())
)
2020-06-10 23:22:53 +02:00
def setGamePath(self, path: Path | str) -> None:
self._gamePath = str(path)
path = Path(path)
2022-05-02 16:59:21 +02:00
# Check if we have a matching steam, GOG, Origin or EA Desktop id and set the
# index accordingly:
for steamid, steampath in BasicGame.steam_games.items():
if steampath == path:
2020-10-16 17:02:47 -05:00
self._mappings.steamAPPId.set_value(steamid)
for gogid, gogpath in BasicGame.gog_games.items():
if gogpath == path:
2020-10-16 17:02:47 -05:00
self._mappings.steamAPPId.set_value(gogid)
2021-11-24 17:23:34 -07:00
for originid, originpath in BasicGame.origin_games.items():
if originpath == path:
self._mappings.originManifestIds.set_value(originid)
for epicid, epicpath in BasicGame.epic_games.items():
if epicpath == path:
self._mappings.epicAPPId.set_value(epicid)
2022-04-11 21:35:21 +02:00
for eadesktopid, eadesktoppath in BasicGame.eadesktop_games.items():
if eadesktoppath == path:
self._mappings.eaDesktopContentId.set_value(eadesktopid)
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def documentsDirectory(self) -> QDir:
2020-10-16 17:02:47 -05:00
return self._mappings.documentsDirectory.get()
2020-06-10 23:22:53 +02:00
2020-06-11 21:52:01 +02:00
def savesDirectory(self) -> QDir:
2020-10-16 17:02:47 -05:00
return self._mappings.savesDirectory.get()
2020-06-10 23:22:53 +02:00
def _featureList(self):
return self._featureMap