From f24f86e8c595abe793264f1575d67097496e7314 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Fri, 15 Sep 2023 13:22:06 -0500 Subject: [PATCH 1/6] Basic save support TODO: Parse the json to display some simple save info --- games/game_bladeandsorcery.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/games/game_bladeandsorcery.py b/games/game_bladeandsorcery.py index cccefd0..2b1213b 100644 --- a/games/game_bladeandsorcery.py +++ b/games/game_bladeandsorcery.py @@ -11,6 +11,9 @@ class BaSGame(BasicGame): GameShortName = "bladeandsorcery" GameBinary = "BladeAndSorcery.exe" GameDataPath = r"BladeAndSorcery_Data\StreamingAssets\Mods" + GameDocumentsDirectory = "%DOCUMENTS%/My Games/BladeAndSorcery" + GameSavesDirectory = "%GAME_DOCUMENTS%/Saves/Default" + GameSaveExtension = "chr" GameSteamId = 629730 GameSupportURL = ( r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" From 1dc70b5a8a81ec513aff2302e4f1e37dc686c850 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Fri, 15 Sep 2023 19:34:22 -0500 Subject: [PATCH 2/6] WIP: Add savegame parsing --- games/game_bladeandsorcery.py | 117 ++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/games/game_bladeandsorcery.py b/games/game_bladeandsorcery.py index 2b1213b..c3ad7ec 100644 --- a/games/game_bladeandsorcery.py +++ b/games/game_bladeandsorcery.py @@ -1,6 +1,118 @@ +from pathlib import Path +import json +import time +from time import mktime + +from PyQt6.QtCore import QDateTime, Qt +from PyQt6.QtGui import QPainter, QPixmap + +import mobase + +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, + BasicGameSaveGameInfoWidget, +) + from ..basic_game import BasicGame +class BaSSaveGame(BasicGameSaveGame): + def __init__(self, filepath): + super().__init__(filepath) + self._filepath = Path(filepath) + save = open(self._filepath.joinpath("SaveGame.inf"), "rb") + save_data = json.load(save) + self.gameMode: str = save_data["gameModeId"] + self.gender: str = "Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female" + self.ethnicity: str = save_data["ethnicGroupId"] + h, m, s = save_data["playTime"].split(":") + self.elapsed: tuple[int, int, float] = (int(h), int(m), float(s)) + self.lastsave: time.struct_time = time.strptime(save_data["lastPlayTime"], "%Y-%m-%dT%H:%M:%S") + self.tutorial: bool = True if save_data["Tutorial"] == "Done" else False + self.swim: bool = save_data["tutorialFlags"]["hasLearnedToSwim"] + self.money: float = save_data["money"] + save.close() + + def getCreationTime(self) -> QDateTime: + return QDateTime.fromSecsSinceEpoch(int(mktime(self.lastsave))) + + def getElapsed(self) -> str: + return f"{self.elapsed[0]} hours, {self.elapsed[1]} minutes, {int(self.elapsed[2])} seconds" + + def getCharacter(self) -> str: + return f"{self.gender} {self.ethnicity}" + + def getGameMode(self) -> str: + return self.gameMode + + def getTutorial(self) -> str: + return "Complete" if self.tutorial else "Incomplete" + + def getCanSwim(self) -> bool: + return self.swim + + def getMoney(self) -> str: + return str(self.money) + + +def getPreview(save: Path) -> QPixmap: + save = BaSSaveGame(save) + lines = [ + [ + ("Name : " + save.getCharacter(), Qt.AlignmentFlag.AlignLeft), + ("- Game Mode : " + save.getGameMode(), Qt.AlignmentFlag.AlignLeft), + ], + [("Saved at : " + save.getCreationTime().toString(), Qt.AlignmentFlag.AlignLeft)], + [("Elapsed time : " + save.getElapsed(), Qt.AlignmentFlag.AlignLeft)], + [("Tutorial : " + save.getTutorial(), Qt.AlignmentFlag.AlignLeft)], + [("Money : " + save.getMoney(), Qt.AlignmentFlag.AlignLeft)], + ] + + pixmap = QPixmap(320, 320) + pixmap.fill() + # rightBuffer = [] + + painter = QPainter() + painter.begin(pixmap) + fm = painter.fontMetrics() + margin = 5 + height = 0 + width = 0 + ln = 0 + for line in lines: + + cHeight = 0 + cWidth = 0 + + for (toPrint, align) in line: + bRect = fm.boundingRect(toPrint) + cHeight = bRect.height() * (ln + 1) + bRect.moveTop(cHeight - bRect.height()) + if align != Qt.AlignmentFlag.AlignLeft: + continue + else: + bRect.moveLeft(cWidth + margin) + cWidth = cWidth + bRect.width() + painter.drawText(bRect, align, toPrint) + + height = max(height, cHeight) + width = max(width, cWidth + (2 * margin)) + ln = ln + 1 + # height = height + lh + + painter.end() + + return pixmap.copy(0, 0, width, height) + + +class BaSSaveGameInfo(BasicGameSaveGameInfo): + def getSaveGameWidget(self, parent=None): + if self._get_preview is not None: + return BasicGameSaveGameInfoWidget(parent, self._get_preview) + return None + + class BaSGame(BasicGame): Name = "Blade & Sorcery Plugin" @@ -19,3 +131,8 @@ class BaSGame(BasicGame): r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" "Game:-Blade-&-Sorcery" ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + BasicGame.init(self, organizer) + self._featureMap[mobase.SaveGameInfo] = BaSSaveGameInfo(get_preview=getPreview) + return True From 76a1e24ffaed6907ea9288db652e7deaa47b2b87 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Sat, 16 Sep 2023 02:55:38 -0500 Subject: [PATCH 3/6] Implement full save display --- games/game_bladeandsorcery.py | 232 +++++++++++++++++++++------------- 1 file changed, 146 insertions(+), 86 deletions(-) diff --git a/games/game_bladeandsorcery.py b/games/game_bladeandsorcery.py index c3ad7ec..2925457 100644 --- a/games/game_bladeandsorcery.py +++ b/games/game_bladeandsorcery.py @@ -1,17 +1,17 @@ from pathlib import Path import json -import time -from time import mktime +import os +from typing import List -from PyQt6.QtCore import QDateTime, Qt -from PyQt6.QtGui import QPainter, QPixmap +from PyQt6.QtCore import QDateTime, QDir, QLocale, Qt +from PyQt6.QtGui import QFont import mobase +from PyQt6.QtWidgets import QSizePolicy, QVBoxLayout, QFormLayout, QLabel, QStyle from ..basic_features.basic_save_game_info import ( BasicGameSaveGame, - BasicGameSaveGameInfo, - BasicGameSaveGameInfoWidget, + BasicGameSaveGameInfo ) from ..basic_game import BasicGame @@ -21,100 +21,153 @@ class BaSSaveGame(BasicGameSaveGame): def __init__(self, filepath): super().__init__(filepath) self._filepath = Path(filepath) - save = open(self._filepath.joinpath("SaveGame.inf"), "rb") + save = open(self._filepath, "rb") save_data = json.load(save) - self.gameMode: str = save_data["gameModeId"] - self.gender: str = "Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female" - self.ethnicity: str = save_data["ethnicGroupId"] + self._gameMode: str = save_data["gameModeId"] + self._gender: str = "Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female" + self._ethnicity: str = save_data["ethnicGroupId"] h, m, s = save_data["playTime"].split(":") - self.elapsed: tuple[int, int, float] = (int(h), int(m), float(s)) - self.lastsave: time.struct_time = time.strptime(save_data["lastPlayTime"], "%Y-%m-%dT%H:%M:%S") - self.tutorial: bool = True if save_data["Tutorial"] == "Done" else False - self.swim: bool = save_data["tutorialFlags"]["hasLearnedToSwim"] - self.money: float = save_data["money"] + self._elapsed: tuple[int, int, float] = (int(h), int(m), float(s)) + self._created: float = os.path.getctime(filepath) + self._modified: float = os.path.getmtime(filepath) save.close() + def getName(self) -> str: + return f"{self.getPlayerSlug()} - {self._gameMode}" + def getCreationTime(self) -> QDateTime: - return QDateTime.fromSecsSinceEpoch(int(mktime(self.lastsave))) + return QDateTime.fromSecsSinceEpoch(int(self._created)) + + def getModifiedTime(self) -> QDateTime: + return QDateTime.fromSecsSinceEpoch(int(self._modified)) + + def getPlayerSlug(self) -> str: + return f"{self._gender} {self._ethnicity}" def getElapsed(self) -> str: - return f"{self.elapsed[0]} hours, {self.elapsed[1]} minutes, {int(self.elapsed[2])} seconds" + return f"{self._elapsed[0]} hours, {self._elapsed[1]} minutes, {int(self._elapsed[2])} seconds" - def getCharacter(self) -> str: - return f"{self.gender} {self.ethnicity}" - def getGameMode(self) -> str: - return self.gameMode - - def getTutorial(self) -> str: - return "Complete" if self.tutorial else "Incomplete" - - def getCanSwim(self) -> bool: - return self.swim - - def getMoney(self) -> str: - return str(self.money) - - -def getPreview(save: Path) -> QPixmap: - save = BaSSaveGame(save) - lines = [ - [ - ("Name : " + save.getCharacter(), Qt.AlignmentFlag.AlignLeft), - ("- Game Mode : " + save.getGameMode(), Qt.AlignmentFlag.AlignLeft), - ], - [("Saved at : " + save.getCreationTime().toString(), Qt.AlignmentFlag.AlignLeft)], - [("Elapsed time : " + save.getElapsed(), Qt.AlignmentFlag.AlignLeft)], - [("Tutorial : " + save.getTutorial(), Qt.AlignmentFlag.AlignLeft)], - [("Money : " + save.getMoney(), Qt.AlignmentFlag.AlignLeft)], - ] - - pixmap = QPixmap(320, 320) - pixmap.fill() - # rightBuffer = [] - - painter = QPainter() - painter.begin(pixmap) - fm = painter.fontMetrics() - margin = 5 - height = 0 - width = 0 - ln = 0 - for line in lines: - - cHeight = 0 - cWidth = 0 - - for (toPrint, align) in line: - bRect = fm.boundingRect(toPrint) - cHeight = bRect.height() * (ln + 1) - bRect.moveTop(cHeight - bRect.height()) - if align != Qt.AlignmentFlag.AlignLeft: - continue - else: - bRect.moveLeft(cWidth + margin) - cWidth = cWidth + bRect.width() - painter.drawText(bRect, align, toPrint) - - height = max(height, cHeight) - width = max(width, cWidth + (2 * margin)) - ln = ln + 1 - # height = height + lh - - painter.end() - - return pixmap.copy(0, 0, width, height) + return self._gameMode + + +class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.resize(400, 125) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(sizePolicy) + self._verticalLayout = QVBoxLayout() + self._verticalLayout.setObjectName("verticalLayout") + self._formLayout = QFormLayout() + self._formLayout.setObjectName("formLayout") + self._formLayout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) + + self._label = QLabel() + self._label.setObjectName("label") + font = QFont() + font.setItalic(True) + self._label.setFont(font) + self._label.setText("Character") + + self._formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self._label) + + self._label_2 = QLabel() + self._label_2.setObjectName("label_2") + self._label_2.setFont(font) + self._label_2.setText("Game Mode") + + self._formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self._label_2) + + self._label_3 = QLabel() + self._label_3.setObjectName("label_4") + self._label_3.setFont(font) + self._label_3.setText("Created At") + + self._formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self._label_3) + + self._label_4 = QLabel() + self._label_4.setObjectName("label_4") + self._label_4.setFont(font) + self._label_4.setText("Last Saved") + + self._formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self._label_4) + + self._label_5 = QLabel() + self._label_5.setObjectName("label_3") + self._label_5.setFont(font) + self._label_5.setText("Session Duration") + + self._formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self._label_5) + + font1 = QFont() + font1.setBold(True) + + self._characterLabel = QLabel() + self._characterLabel.setObjectName("characterLabel") + self._characterLabel.setFont(font1) + self._characterLabel.setText("") + + self._formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self._characterLabel) + + self._gameModeLabel = QLabel() + self._gameModeLabel.setObjectName("gameModeLabel") + self._gameModeLabel.setFont(font1) + self._gameModeLabel.setText("") + + self._formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self._gameModeLabel) + + self._dateLabel = QLabel() + self._dateLabel.setObjectName("dateLabel") + self._dateLabel.setFont(font1) + self._dateLabel.setText("") + + self._formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self._dateLabel) + + self._sessionLabel = QLabel() + self._sessionLabel.setObjectName("sessionLabel") + self._sessionLabel.setFont(font1) + self._sessionLabel.setText("") + + self._formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self._sessionLabel) + + self._elapsedTimeLabel = QLabel() + self._elapsedTimeLabel.setObjectName("elapsedTimeLabel") + self._elapsedTimeLabel.setFont(font1) + self._elapsedTimeLabel.setText("") + + self._formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self._elapsedTimeLabel) + + self._verticalLayout.addLayout(self._formLayout) + + self.setLayout(self._verticalLayout) + self.setWindowFlags(Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget) + self.setWindowOpacity( + self.style().styleHint(QStyle.StyleHint.SH_ToolTipLabel_Opacity) / 255.0 + ) + + def setSave(self, save: BaSSaveGame): + self._characterLabel.setText(save.getPlayerSlug()) + self._gameModeLabel.setText(save.getGameMode()) + t = save.getCreationTime().toLocalTime() + self._dateLabel.setText(QLocale.system().toString(t.date(), QLocale.FormatType.ShortFormat) + + " " + QLocale.system().toString(t.time())) + s = save.getModifiedTime().toLocalTime() + self._sessionLabel.setText(QLocale.system().toString(s.date(), QLocale.FormatType.ShortFormat) + + " " + QLocale.system().toString(s.time())) + self._elapsedTimeLabel.setText(save.getElapsed()) + self.resize(0, 125) class BaSSaveGameInfo(BasicGameSaveGameInfo): def getSaveGameWidget(self, parent=None): - if self._get_preview is not None: - return BasicGameSaveGameInfoWidget(parent, self._get_preview) - return None + return BaSSaveGameInfoWidget(parent) class BaSGame(BasicGame): - Name = "Blade & Sorcery Plugin" Author = "R3z Shark" Version = "0.1.0" @@ -122,7 +175,7 @@ class BaSGame(BasicGame): GameName = "Blade & Sorcery" GameShortName = "bladeandsorcery" GameBinary = "BladeAndSorcery.exe" - GameDataPath = r"BladeAndSorcery_Data\StreamingAssets\Mods" + GameDataPath = r"BladeAndSorcery_Data\\StreamingAssets\\Mods" GameDocumentsDirectory = "%DOCUMENTS%/My Games/BladeAndSorcery" GameSavesDirectory = "%GAME_DOCUMENTS%/Saves/Default" GameSaveExtension = "chr" @@ -134,5 +187,12 @@ class BaSGame(BasicGame): def init(self, organizer: mobase.IOrganizer) -> bool: BasicGame.init(self, organizer) - self._featureMap[mobase.SaveGameInfo] = BaSSaveGameInfo(get_preview=getPreview) + self._featureMap[mobase.SaveGameInfo] = BaSSaveGameInfo() return True + + def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + BaSSaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] From 8011cb9011fcbf703c9a011781c122337d615c68 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Sat, 16 Sep 2023 02:57:02 -0500 Subject: [PATCH 4/6] Bump version --- games/game_bladeandsorcery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/games/game_bladeandsorcery.py b/games/game_bladeandsorcery.py index 2925457..faeefc9 100644 --- a/games/game_bladeandsorcery.py +++ b/games/game_bladeandsorcery.py @@ -169,8 +169,8 @@ def getSaveGameWidget(self, parent=None): class BaSGame(BasicGame): Name = "Blade & Sorcery Plugin" - Author = "R3z Shark" - Version = "0.1.0" + Author = "R3z Shark & Silarn" + Version = "0.5.0" GameName = "Blade & Sorcery" GameShortName = "bladeandsorcery" From a846216e419c9aa289e6c395d0e6e195d59e1a1c Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Tue, 19 Sep 2023 03:16:10 -0500 Subject: [PATCH 5/6] Holt's recommendations --- games/game_bladeandsorcery.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/games/game_bladeandsorcery.py b/games/game_bladeandsorcery.py index faeefc9..ffef7dd 100644 --- a/games/game_bladeandsorcery.py +++ b/games/game_bladeandsorcery.py @@ -21,16 +21,16 @@ class BaSSaveGame(BasicGameSaveGame): def __init__(self, filepath): super().__init__(filepath) self._filepath = Path(filepath) - save = open(self._filepath, "rb") - save_data = json.load(save) + with open(self._filepath, "rb") as save: + save_data = json.load(save) self._gameMode: str = save_data["gameModeId"] - self._gender: str = "Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female" + self._gender = "Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female" self._ethnicity: str = save_data["ethnicGroupId"] h, m, s = save_data["playTime"].split(":") - self._elapsed: tuple[int, int, float] = (int(h), int(m), float(s)) - self._created: float = os.path.getctime(filepath) - self._modified: float = os.path.getmtime(filepath) - save.close() + self._elapsed = (int(h), int(m), float(s)) + f_stat = self._filepath.stat() + self._created = f_stat.st_ctime + self._modified = f_stat.st_mtime def getName(self) -> str: return f"{self.getPlayerSlug()} - {self._gameMode}" From 252ba9f00c1ee13316e7282dc3ec20f172182ba9 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Tue, 19 Sep 2023 03:20:29 -0500 Subject: [PATCH 6/6] Use assert --- games/game_bladeandsorcery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/games/game_bladeandsorcery.py b/games/game_bladeandsorcery.py index ffef7dd..ce6a797 100644 --- a/games/game_bladeandsorcery.py +++ b/games/game_bladeandsorcery.py @@ -149,7 +149,8 @@ def __init__(self, parent=None): self.style().styleHint(QStyle.StyleHint.SH_ToolTipLabel_Opacity) / 255.0 ) - def setSave(self, save: BaSSaveGame): + def setSave(self, save: mobase.ISaveGame): + assert isinstance(save, BaSSaveGame) self._characterLabel.setText(save.getPlayerSlug()) self._gameModeLabel.setText(save.getGameMode()) t = save.getCreationTime().toLocalTime()