From 7ff05bd2264cfe6481c57b1747946e3ad78759b3 Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 22 Feb 2021 17:56:44 +0100 Subject: [PATCH 01/13] Update .gitignore Ignore JetBrains IDE files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2bf0031..9faa574 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__ .tox .vscode +.idea # Mob / Umbrella mob.log From 1b2405fcccbfafe7177408fc6cf01e2d38fcc6fd Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 22 Feb 2021 17:57:35 +0100 Subject: [PATCH 02/13] Fix Darkest Dungeon working directory --- games/game_darkestdungeon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index f0c3e59..1dac04e 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -26,7 +26,7 @@ class DarkestDungeonGame(BasicGame): if not path.exists(): path = QFileInfo(self.gameDirectory(), "_windowsnosteam/darkest.exe") return [ - mobase.ExecutableInfo("Darkest Dungeon", path), + mobase.ExecutableInfo("Darkest Dungeon", path).withWorkingDirectory(self.gameDirectory()), ] def savesDirectory(self): From e0896ad8800dc633ac5a35b9d10503bcd3977727 Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 22 Feb 2021 17:59:14 +0100 Subject: [PATCH 03/13] Remove Darkest Dungeon saves directory --- games/game_darkestdungeon.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index 1dac04e..1660bce 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -28,10 +28,3 @@ class DarkestDungeonGame(BasicGame): return [ mobase.ExecutableInfo("Darkest Dungeon", path).withWorkingDirectory(self.gameDirectory()), ] - - def savesDirectory(self): - return QDir( - "{}/Darkest".format( - QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) - ) - ) From aa8d13949fd9b8ce127c1cd3a54a8cf36344eb4c Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 22 Feb 2021 17:59:33 +0100 Subject: [PATCH 04/13] Update DD version to 0.2.0 --- games/game_darkestdungeon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index 1660bce..a7d9e12 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -10,7 +10,7 @@ from ..basic_game import BasicGame class DarkestDungeonGame(BasicGame): Name = "DarkestDungeon" Author = "erri120" - Version = "0.1.1" + Version = "0.2.0" GameName = "Darkest Dungeon" GameShortName = "darkestdungeon" From 0a54fe54e5ff9ad39d494545252821afaf30e9a9 Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 22 Feb 2021 20:59:33 +0100 Subject: [PATCH 05/13] Add DarkestDungeonSaveGame --- games/game_darkestdungeon.py | 110 ++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index a7d9e12..19d0e97 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -1,10 +1,99 @@ # -*- encoding: utf-8 -*- +from pathlib import Path +from typing import List +import json -from PyQt5.QtCore import QDir, QFileInfo, QStandardPaths +from PyQt5.QtCore import QDir, QFileInfo import mobase -from ..basic_game import BasicGame +from ..basic_game import BasicGame, BasicGameSaveGame + + +class DarkestDungeonSaveGame(BasicGameSaveGame): + def __init__(self, filepath): + super().__init__(filepath) + dataPath = filepath.joinpath('persist.game.json') + self.name = "" + if self.isBinary(dataPath): + self.loadBinarySaveFile(dataPath) + else: + self.loadJSONSaveFile(dataPath) + + @staticmethod + def isBinary(dataPath: Path) -> bool: + with dataPath.open(mode='rb') as fp: + magic = fp.read(4) + # magic number in binary save files + return magic == b'\x01\xb1\x00\x00' + + def loadJSONSaveFile(self, dataPath: Path): + text = dataPath.read_text() + content = json.loads(text) + data = content['data'] + self.name = str(data['estatename']) + + def loadBinarySaveFile(self, dataPath: Path): + # see https://github.com/robojumper/DarkestDungeonSaveEditor/blob/master/docs/dson.md + with dataPath.open(mode='rb') as fp: + # read Header + + # skip to headerLength + fp.seek(8, 0) + headerLength = int.from_bytes(fp.read(4), 'little') + if headerLength != 64: + raise ValueError("Header Length is not 64: "+str(headerLength)) + fp.seek(4, 1) + meta1Size = int.from_bytes(fp.read(4), 'little') + numMeta1Entries = int.from_bytes(fp.read(4), 'little') + meta1Offset = int.from_bytes(fp.read(4), 'little') + fp.seek(16, 1) + numMeta2Entries = int.from_bytes(fp.read(4), 'little') + meta2Offset = int.from_bytes(fp.read(4), 'little') + fp.seek(4, 1) + dataLength = int.from_bytes(fp.read(4), 'little') + dataOffset = int.from_bytes(fp.read(4), 'little') + + # read Meta1 Block + fp.seek(meta1Offset, 0) + meta1DataLength = meta2Offset - meta1Offset + if meta1DataLength % 16 != 0: + raise ValueError("Meta1 has wrong number of bytes: "+str(meta1DataLength)) + + # read Meta2 Block + fp.seek(meta2Offset, 0) + meta2DataLength = dataOffset - meta2Offset + if meta2DataLength % 12 != 0: + raise ValueError("Meta2 has wrong number of bytes: "+str(meta2DataLength)) + meta2List = list() + 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]) + + # read Data + fp.seek(dataOffset, 0) + for x in range(numMeta2Entries): + meta2Entry = meta2List[x] + fp.seek(dataOffset + meta2Entry[1], 0) + nameLength = (meta2Entry[2] & 0b11111111100) >> 2 + # null terminated string + nameBytes = fp.read(nameLength-1) + fp.seek(1, 1) + name = bytes.decode(nameBytes, 'utf-8') + if name != 'estatename': + continue + valueLength = int.from_bytes(fp.read(4), 'little') + valueBytes = fp.read(valueLength-1) + value = bytes.decode(valueBytes, 'utf-8') + self.name = value + break + + def getName(self) -> str: + if self.name == '': + return super().getName() + return self.name class DarkestDungeonGame(BasicGame): @@ -28,3 +117,20 @@ class DarkestDungeonGame(BasicGame): return [ mobase.ExecutableInfo("Darkest Dungeon", path).withWorkingDirectory(self.gameDirectory()), ] + + def savesDirectory(self) -> QDir: + return QDir("C:\\Program Files (x86)\\Steam\\userdata\\149956546\\262060\\remote") + + def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + profiles = list() + 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 + if path.name == "profile_9": + continue + profiles.append(path) + + return [ + DarkestDungeonSaveGame(path) + for path in profiles + ] From d502ef904ab3fd75d2c8a95150b9e9457f4e2b61 Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 22 Feb 2021 21:08:02 +0100 Subject: [PATCH 06/13] Add getSteamPath function --- steam_utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/steam_utils.py b/steam_utils.py index 84ad7b7..657c8cd 100644 --- a/steam_utils.py +++ b/steam_utils.py @@ -101,12 +101,18 @@ def parse_library_info(library_vdf_path): return library_folders -def find_games() -> Dict[str, Path]: +def getSteamPath() -> str: try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Valve\\Steam") as key: value = winreg.QueryValueEx(key, "SteamExe") - steam_path = value[0].replace("/", "\\") + return value[0].replace("/", "\\") except FileNotFoundError: + return "" + + +def find_games() -> Dict[str, Path]: + steam_path = getSteamPath() + if steam_path == "": return {} library_vdf_path = os.path.join( From c2d167ef1b39d79407ebc274ec7151bb3e5111eb Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 22 Feb 2021 21:19:50 +0100 Subject: [PATCH 07/13] Fix getSteamPath not returning Steam Directory --- steam_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/steam_utils.py b/steam_utils.py index 657c8cd..f10a2b6 100644 --- a/steam_utils.py +++ b/steam_utils.py @@ -105,7 +105,7 @@ def getSteamPath() -> str: try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Valve\\Steam") as key: value = winreg.QueryValueEx(key, "SteamExe") - return value[0].replace("/", "\\") + return os.path.dirname(value[0].replace("/", "\\")) except FileNotFoundError: return "" @@ -116,12 +116,12 @@ def find_games() -> Dict[str, Path]: return {} library_vdf_path = os.path.join( - os.path.dirname(steam_path), "steamapps", "libraryfolders.vdf" + steam_path, "steamapps", "libraryfolders.vdf" ) try: library_folders = parse_library_info(library_vdf_path) - library_folders.append(LibraryFolder(os.path.dirname(steam_path))) + library_folders.append(LibraryFolder(steam_path)) except FileNotFoundError: return {} From 0a9dd11a891db2f6c43e53bbf12c5ca7820cc36b Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 22 Feb 2021 21:20:13 +0100 Subject: [PATCH 08/13] Add Darkest Dungeon Save Location Detection --- games/game_darkestdungeon.py | 37 ++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index 19d0e97..2de0768 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -3,11 +3,12 @@ from pathlib import Path from typing import List import json -from PyQt5.QtCore import QDir, QFileInfo +from PyQt5.QtCore import QDir, QFileInfo, QStandardPaths import mobase from ..basic_game import BasicGame, BasicGameSaveGame +from ..steam_utils import getSteamPath class DarkestDungeonSaveGame(BasicGameSaveGame): @@ -111,15 +112,43 @@ class DarkestDungeonGame(BasicGame): GameDataPath = "" def executables(self): - path = QFileInfo(self.gameDirectory(), "_windows/darkest.exe") - if not path.exists(): + if self.isSteam(): + path = QFileInfo(self.gameDirectory(), "_windows/darkest.exe") + else: path = QFileInfo(self.gameDirectory(), "_windowsnosteam/darkest.exe") return [ mobase.ExecutableInfo("Darkest Dungeon", path).withWorkingDirectory(self.gameDirectory()), ] + def isSteam(self) -> bool: + path = QFileInfo(self.gameDirectory(), "_windows/darkest.exe") + return path.exists() + + @staticmethod + def getCloudSaveDirectory(): + steamPath = Path(getSteamPath()) + userData = steamPath.joinpath("userdata") + for child in userData.iterdir(): + name = child.name + try: + userID = int(name) + except ValueError: + userID = -1 + if userID == -1: + continue + cloudSaves = child.joinpath("262060", "remote") + if cloudSaves.exists() and cloudSaves.is_dir(): + return str(cloudSaves) + return None + def savesDirectory(self) -> QDir: - return QDir("C:\\Program Files (x86)\\Steam\\userdata\\149956546\\262060\\remote") + documentsSaves = QDir("{}/Darkest".format(QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation))) + if self.isSteam(): + cloudSaves = self.getCloudSaveDirectory() + if cloudSaves is None: + return documentsSaves + return QDir(cloudSaves) + return documentsSaves def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: profiles = list() From 07855a7164cfa8ac46fc4768c895197e318adb6b Mon Sep 17 00:00:00 2001 From: erri120 Date: Tue, 23 Feb 2021 10:43:12 +0100 Subject: [PATCH 09/13] Fix linting complains --- games/game_darkestdungeon.py | 77 ++++++++++++++++++++---------------- steam_utils.py | 4 +- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index 2de0768..6e624cb 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -14,7 +14,7 @@ from ..steam_utils import getSteamPath class DarkestDungeonSaveGame(BasicGameSaveGame): def __init__(self, filepath): super().__init__(filepath) - dataPath = filepath.joinpath('persist.game.json') + dataPath = filepath.joinpath("persist.game.json") self.name = "" if self.isBinary(dataPath): self.loadBinarySaveFile(dataPath) @@ -23,54 +23,58 @@ class DarkestDungeonSaveGame(BasicGameSaveGame): @staticmethod def isBinary(dataPath: Path) -> bool: - with dataPath.open(mode='rb') as fp: + with dataPath.open(mode="rb") as fp: magic = fp.read(4) # magic number in binary save files - return magic == b'\x01\xb1\x00\x00' + return magic == b"\x01\xb1\x00\x00" def loadJSONSaveFile(self, dataPath: Path): text = dataPath.read_text() content = json.loads(text) - data = content['data'] - self.name = str(data['estatename']) + data = content["data"] + self.name = str(data["estatename"]) def loadBinarySaveFile(self, dataPath: Path): # see https://github.com/robojumper/DarkestDungeonSaveEditor/blob/master/docs/dson.md - with dataPath.open(mode='rb') as fp: + with dataPath.open(mode="rb") as fp: # read Header # skip to headerLength fp.seek(8, 0) - headerLength = int.from_bytes(fp.read(4), 'little') + headerLength = int.from_bytes(fp.read(4), "little") if headerLength != 64: - raise ValueError("Header Length is not 64: "+str(headerLength)) + raise ValueError("Header Length is not 64: " + str(headerLength)) fp.seek(4, 1) - meta1Size = int.from_bytes(fp.read(4), 'little') - numMeta1Entries = int.from_bytes(fp.read(4), 'little') - meta1Offset = int.from_bytes(fp.read(4), 'little') + meta1Size = int.from_bytes(fp.read(4), "little") + numMeta1Entries = int.from_bytes(fp.read(4), "little") + meta1Offset = int.from_bytes(fp.read(4), "little") fp.seek(16, 1) - numMeta2Entries = int.from_bytes(fp.read(4), 'little') - meta2Offset = int.from_bytes(fp.read(4), 'little') + numMeta2Entries = int.from_bytes(fp.read(4), "little") + meta2Offset = int.from_bytes(fp.read(4), "little") fp.seek(4, 1) - dataLength = int.from_bytes(fp.read(4), 'little') - dataOffset = int.from_bytes(fp.read(4), 'little') + dataLength = int.from_bytes(fp.read(4), "little") + dataOffset = int.from_bytes(fp.read(4), "little") # read Meta1 Block fp.seek(meta1Offset, 0) meta1DataLength = meta2Offset - meta1Offset if meta1DataLength % 16 != 0: - raise ValueError("Meta1 has wrong number of bytes: "+str(meta1DataLength)) + raise ValueError( + "Meta1 has wrong number of bytes: " + str(meta1DataLength) + ) # read Meta2 Block fp.seek(meta2Offset, 0) meta2DataLength = dataOffset - meta2Offset if meta2DataLength % 12 != 0: - raise ValueError("Meta2 has wrong number of bytes: "+str(meta2DataLength)) + raise ValueError( + "Meta2 has wrong number of bytes: " + str(meta2DataLength) + ) meta2List = list() 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') + 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]) # read Data @@ -80,19 +84,19 @@ class DarkestDungeonSaveGame(BasicGameSaveGame): fp.seek(dataOffset + meta2Entry[1], 0) nameLength = (meta2Entry[2] & 0b11111111100) >> 2 # null terminated string - nameBytes = fp.read(nameLength-1) + nameBytes = fp.read(nameLength - 1) fp.seek(1, 1) - name = bytes.decode(nameBytes, 'utf-8') - if name != 'estatename': + name = bytes.decode(nameBytes, "utf-8") + if name != "estatename": continue - valueLength = int.from_bytes(fp.read(4), 'little') - valueBytes = fp.read(valueLength-1) - value = bytes.decode(valueBytes, 'utf-8') + valueLength = int.from_bytes(fp.read(4), "little") + valueBytes = fp.read(valueLength - 1) + value = bytes.decode(valueBytes, "utf-8") self.name = value break def getName(self) -> str: - if self.name == '': + if self.name == "": return super().getName() return self.name @@ -117,7 +121,9 @@ class DarkestDungeonGame(BasicGame): else: path = QFileInfo(self.gameDirectory(), "_windowsnosteam/darkest.exe") return [ - mobase.ExecutableInfo("Darkest Dungeon", path).withWorkingDirectory(self.gameDirectory()), + mobase.ExecutableInfo("Darkest Dungeon", path).withWorkingDirectory( + self.gameDirectory() + ), ] def isSteam(self) -> bool: @@ -142,7 +148,11 @@ class DarkestDungeonGame(BasicGame): return None def savesDirectory(self) -> QDir: - documentsSaves = QDir("{}/Darkest".format(QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation))) + documentsSaves = QDir( + "{}/Darkest".format( + QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) + ) + ) if self.isSteam(): cloudSaves = self.getCloudSaveDirectory() if cloudSaves is None: @@ -153,13 +163,10 @@ class DarkestDungeonGame(BasicGame): def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: profiles = list() 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 + # profile_9 is only for the Multiplayer DLC "The Butcher's Circus" + # and contains different files than other profiles if path.name == "profile_9": continue profiles.append(path) - return [ - DarkestDungeonSaveGame(path) - for path in profiles - ] + return [DarkestDungeonSaveGame(path) for path in profiles] diff --git a/steam_utils.py b/steam_utils.py index f10a2b6..0c17d22 100644 --- a/steam_utils.py +++ b/steam_utils.py @@ -115,9 +115,7 @@ def find_games() -> Dict[str, Path]: if steam_path == "": return {} - library_vdf_path = os.path.join( - steam_path, "steamapps", "libraryfolders.vdf" - ) + library_vdf_path = os.path.join(steam_path, "steamapps", "libraryfolders.vdf") try: library_folders = parse_library_info(library_vdf_path) From b12381115dea1d8e476308415009360183b23800 Mon Sep 17 00:00:00 2001 From: erri120 Date: Tue, 23 Feb 2021 10:52:54 +0100 Subject: [PATCH 10/13] Fix flake8 issues --- games/game_darkestdungeon.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index 6e624cb..cb4fb44 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -35,7 +35,7 @@ class DarkestDungeonSaveGame(BasicGameSaveGame): self.name = str(data["estatename"]) def loadBinarySaveFile(self, dataPath: Path): - # see https://github.com/robojumper/DarkestDungeonSaveEditor/blob/master/docs/dson.md + # see https://github.com/robojumper/DarkestDungeonSaveEditor with dataPath.open(mode="rb") as fp: # read Header @@ -45,14 +45,21 @@ class DarkestDungeonSaveGame(BasicGameSaveGame): if headerLength != 64: raise ValueError("Header Length is not 64: " + str(headerLength)) fp.seek(4, 1) - meta1Size = int.from_bytes(fp.read(4), "little") - numMeta1Entries = int.from_bytes(fp.read(4), "little") + + # meta1Size = int.from_bytes(fp.read(4), "little") + fp.seek(4, 1) + # numMeta1Entries = int.from_bytes(fp.read(4), "little") + fp.seek(4, 1) + meta1Offset = int.from_bytes(fp.read(4), "little") fp.seek(16, 1) numMeta2Entries = int.from_bytes(fp.read(4), "little") meta2Offset = int.from_bytes(fp.read(4), "little") fp.seek(4, 1) - dataLength = int.from_bytes(fp.read(4), "little") + + # dataLength = int.from_bytes(fp.read(4), "little") + fp.seek(4, 1) + dataOffset = int.from_bytes(fp.read(4), "little") # read Meta1 Block From b511b49f3352823a12d8b1815ef122a095799c68 Mon Sep 17 00:00:00 2001 From: erri120 Date: Tue, 23 Feb 2021 11:10:03 +0100 Subject: [PATCH 11/13] Fix mypy issues --- games/game_darkestdungeon.py | 2 +- steam_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index cb4fb44..8190d0a 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -15,7 +15,7 @@ class DarkestDungeonSaveGame(BasicGameSaveGame): def __init__(self, filepath): super().__init__(filepath) dataPath = filepath.joinpath("persist.game.json") - self.name = "" + self.name: str = "" if self.isBinary(dataPath): self.loadBinarySaveFile(dataPath) else: diff --git a/steam_utils.py b/steam_utils.py index 0c17d22..328c71d 100644 --- a/steam_utils.py +++ b/steam_utils.py @@ -105,7 +105,7 @@ def getSteamPath() -> str: try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Valve\\Steam") as key: value = winreg.QueryValueEx(key, "SteamExe") - return os.path.dirname(value[0].replace("/", "\\")) + return str(os.path.dirname(value[0].replace("/", "\\"))) except FileNotFoundError: return "" From 32120f765971e8f96a66684b1badfe65851300ec Mon Sep 17 00:00:00 2001 From: erri120 Date: Tue, 23 Feb 2021 11:17:22 +0100 Subject: [PATCH 12/13] Update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8257ca7..a0cd224 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Mod Organizer 2 meta-plugin to make creating game plugins easier and faster. ## Why? -In order to create a MO2 game plugin, one must implements the `IPluginGame` interface. +In order to create a MO2 game plugin, one must implement the `IPluginGame` interface. This interface was initially designed for Bethesda games such as the Elder Scrolls or Fallout series and thus contains a lot of things that are irrelevant for most games. @@ -48,14 +48,14 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic | Game | Author | File | Extras | |------|--------|------|--------| | The Binding of Isaac: Rebirth — [STEAM](https://store.steampowered.com/app/250900/The_Binding_of_Isaac_Rebirth/) |[EzioTheDeadPoet](https://github.com/EzioTheDeadPoet)|[game_thebindingofisaacrebirth.py](games/game_thebindingofisaacrebirth.py)|
  • profile specific ini file
| -| Darkest Dungeon — [GOG](https://www.gog.com/game/darkest_dungeon) / [STEAM](https://store.steampowered.com/app/262060/Darkest_Dungeon/) | [erri120](https://github.com/erri120) | [game_darkestdungeon.py](games/game_darkestdungeon.py) | | -| Dark Messiah of Might & Magic — [STEAM](https://store.steampowered.com/app/2100/Dark_Messiah_of_Might__Magic/) | [Holt59](https://github.com/holt59/) | [game_darkmessiah[...].py](games/game_darkmessiahofmightandmagic.py) |
  • save game preview
| +| Darkest Dungeon — [GOG](https://www.gog.com/game/darkest_dungeon) / [STEAM](https://store.steampowered.com/app/262060/Darkest_Dungeon/) | [erri120](https://github.com/erri120) | [game_darkestdungeon.py](games/game_darkestdungeon.py) |
  • save slot parsing
| +| Dark Messiah of Might & Magic — [STEAM](https://store.steampowered.com/app/2100/Dark_Messiah_of_Might__Magic/) | [Holt59](https://github.com/holt59/) | [game_darkmessiahofmightandmagic.py](games/game_darkmessiahofmightandmagic.py) |
  • save game preview
| | Dark Souls — [STEAM](https://store.steampowered.com/app/211420/DARK_SOULS_Prepare_To_Die_Edition/) | [Holt59](https://github.com/holt59/) | [game_darksouls.py](games/game_darkestdungeon.py) | | | Dungeon Siege II — [GOG](https://www.gog.com/game/dungeon_siege_collection) / [STEAM](https://store.steampowered.com/app/39200/Dungeon_Siege_II/) | [Holt59](https://github.com/holt59/) | [game_dungeonsiege2.py](games/game_dungeonsiege2.py) |
  • mod data checker
| | Kingdom Come: Deliverance — [GOG](https://www.gog.com/game/kingdom_come_deliverance) / [STEAM](https://store.steampowered.com/app/379430/Kingdom_Come_Deliverance/)| [Silencer711](https://github.com/Silencer711) | [game_kingdomcomedeliverance.py](games/game_kingdomcomedeliverance.py) |
  • profile specific cfg files
| | Mirror's Edge — [GOG](https://www.gog.com/game/mirrors_edge) / [STEAM](https://store.steampowered.com/app/17410/Mirrors_Edge)|[EzioTheDeadPoet](https://eziothedeadpoet.github.io/AboutMe/)|[game_mirrorsedge.py](games/game_mirrorsedge.py)| | | Mount & Blade II: Bannerlord — [GOG](https://www.gog.com/game/mount_blade_ii_bannerlord) / [STEAM](https://store.steampowered.com/app/261550/Mount__Blade_II_Bannerlord/) | [Holt59](https://github.com/holt59/) | [game_mountandblade2.py](games/game_mountandblade2.py) |
  • mod data checker
| -|No Man's Sky - [GOG](https://www.gog.com/game/no_mans_sky) / [Steam](https://store.steampowered.com/app/275850/No_Mans_Sky/)|[EzioTheDeadPoet](https://eziothedeadpoet.github.io/AboutMe/)|[game_nomanssky.py](games/game_nomanssky.py)| | +| No Man's Sky - [GOG](https://www.gog.com/game/no_mans_sky) / [Steam](https://store.steampowered.com/app/275850/No_Mans_Sky/)|[EzioTheDeadPoet](https://eziothedeadpoet.github.io/AboutMe/)|[game_nomanssky.py](games/game_nomanssky.py)| | | 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) |
  • mod data checker
| | 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) |
  • mod data checker
| | 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) |
  • save game preview
| From b6a2177fdc908d0bac0deaedf33257d7c24ea5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Tue, 23 Feb 2021 17:08:46 +0100 Subject: [PATCH 13/13] Move is_steam() to BasicGame and clean names. --- basic_game.py | 16 ++++++++++++++++ games/game_darkestdungeon.py | 10 +++------- steam_utils.py | 23 ++++++++++++++++++----- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/basic_game.py b/basic_game.py index 258c9d5..23c2fda 100644 --- a/basic_game.py +++ b/basic_game.py @@ -157,6 +157,15 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]): 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 @@ -335,6 +344,13 @@ class BasicGame(mobase.IPluginGame): self._mappings: BasicGameMappings = BasicGameMappings(self) + # 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() + # IPlugin interface: def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_darkestdungeon.py b/games/game_darkestdungeon.py index 8190d0a..24693f9 100644 --- a/games/game_darkestdungeon.py +++ b/games/game_darkestdungeon.py @@ -8,7 +8,7 @@ from PyQt5.QtCore import QDir, QFileInfo, QStandardPaths import mobase from ..basic_game import BasicGame, BasicGameSaveGame -from ..steam_utils import getSteamPath +from ..steam_utils import find_steam_path class DarkestDungeonSaveGame(BasicGameSaveGame): @@ -133,13 +133,9 @@ class DarkestDungeonGame(BasicGame): ), ] - def isSteam(self) -> bool: - path = QFileInfo(self.gameDirectory(), "_windows/darkest.exe") - return path.exists() - @staticmethod def getCloudSaveDirectory(): - steamPath = Path(getSteamPath()) + steamPath = Path(find_steam_path()) userData = steamPath.joinpath("userdata") for child in userData.iterdir(): name = child.name @@ -160,7 +156,7 @@ class DarkestDungeonGame(BasicGame): QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) ) ) - if self.isSteam(): + if self.is_steam(): cloudSaves = self.getCloudSaveDirectory() if cloudSaves is None: return documentsSaves diff --git a/steam_utils.py b/steam_utils.py index 328c71d..e4fddd5 100644 --- a/steam_utils.py +++ b/steam_utils.py @@ -7,7 +7,7 @@ import sys import winreg # type: ignore from pathlib import Path -from typing import Dict +from typing import Dict, Optional class SteamGame: @@ -101,18 +101,31 @@ def parse_library_info(library_vdf_path): return library_folders -def getSteamPath() -> str: +def find_steam_path() -> Optional[str]: + """ + Retrieve the Steam path, if available. + + Returns: + The Steam path, or None if Steam is not installed. + """ try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Valve\\Steam") as key: value = winreg.QueryValueEx(key, "SteamExe") return str(os.path.dirname(value[0].replace("/", "\\"))) except FileNotFoundError: - return "" + return None def find_games() -> Dict[str, Path]: - steam_path = getSteamPath() - if steam_path == "": + """ + Find the list of Steam games installed. + + Returns: + A mapping from Steam game ID to install locations for available + Steam games. + """ + steam_path = find_steam_path() + if not steam_path: return {} library_vdf_path = os.path.join(steam_path, "steamapps", "libraryfolders.vdf")