|
1 | | -from collections.abc import Mapping |
2 | | -from io import BytesIO |
3 | 1 | import json |
4 | | -import mobase |
5 | 2 | import math |
6 | 3 | import os |
7 | 4 | import shutil |
8 | 5 | import struct |
9 | | -import sys |
10 | 6 | import zlib |
| 7 | +import mobase |
11 | 8 |
|
12 | | -from pathlib import Path |
| 9 | +from collections.abc import Mapping, Sequence |
| 10 | +from datetime import datetime |
13 | 11 | from functools import cached_property |
| 12 | +from io import BytesIO |
| 13 | +from typing import Any, Optional |
| 14 | +from pathlib import Path |
14 | 15 |
|
15 | 16 | from ..basic_features import BasicLocalSavegames |
16 | 17 | from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) |
|
19 | 20 | from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo |
20 | 21 |
|
21 | 22 |
|
| 23 | +def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: |
| 24 | + for part in path: |
| 25 | + if type(part) not in (str, int) or type(value) not in (dict, list): |
| 26 | + return default |
| 27 | + value = value[part] |
| 28 | + return value |
| 29 | + |
22 | 30 | class CassetteBeastsModDataChecker(mobase.ModDataChecker): |
23 | 31 | def __init__(self, organizer: mobase.IOrganizer): |
24 | 32 | super().__init__() |
@@ -64,72 +72,113 @@ def __init__(self): |
64 | 72 | class CassetteBeastsSaveGame(BasicGameSaveGame): |
65 | 73 | def __init__(self, filepath: Path): |
66 | 74 | super().__init__(filepath) |
67 | | - self.name: str = "" |
68 | | - self.cheated: str = "" |
69 | | - self.lastsave: int = 0 |
70 | | - self.elapsed: int = 0 |
71 | | - info = bytearray() |
72 | | - data = bytes() |
| 75 | + self.name: str = "(unknown)" |
| 76 | + self.cheated: str = "(unknown)" |
| 77 | + self.lastsave: str = "(unknown)" |
| 78 | + self.elapsed: str = "(unknown)" |
| 79 | + # This doesn't state wether the game would load it, |
| 80 | + # only if the data was properly parsed. |
| 81 | + self.errorMessage: str = "" |
| 82 | + |
| 83 | + save_data = None |
| 84 | + try: |
| 85 | + info = bytearray() |
| 86 | + data = bytes() |
| 87 | + with open(filepath, 'rb') as infile: |
| 88 | + magic_string = infile.read(4) |
73 | 89 |
|
74 | | - with open(filepath, 'rb') as infile: |
75 | | - magic_string = infile.read(4) |
| 90 | + compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) |
76 | 91 |
|
77 | | - compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) |
| 92 | + num_blocks = math.ceil(raw_size / blocksize) |
78 | 93 |
|
79 | | - num_blocks = math.ceil(raw_size / blocksize) |
| 94 | + blocks = [] |
80 | 95 |
|
81 | | - blocks = [] |
| 96 | + for bnum in range(num_blocks): |
| 97 | + block = CassetteBlock() |
| 98 | + block.compressed_size = struct.unpack("I", infile.read(4))[0] |
| 99 | + blocks.append(block) |
82 | 100 |
|
83 | | - for bnum in range(num_blocks): |
84 | | - block = CassetteBlock() |
85 | | - block.compressed_size = struct.unpack("I", infile.read(4))[0] |
86 | | - blocks.append(block) |
| 101 | + for block in blocks: |
| 102 | + block.data = infile.read(block.compressed_size) |
87 | 103 |
|
| 104 | + magic_string = infile.read(4) |
| 105 | + infile.close() |
88 | 106 | for block in blocks: |
89 | | - block.data = infile.read(block.compressed_size) |
90 | | - |
91 | | - magic_string = infile.read(4) |
92 | | - infile.close() |
93 | | - |
94 | | - for block in blocks: |
95 | | - data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) |
96 | | - info = info + data |
97 | | - |
98 | | - save_data = json.load(BytesIO(info)) |
99 | | - self.name = save_data["party"]["player"]["custom"]["name"] |
100 | | - self.cheated = save_data["has_cheated"] |
| 107 | + data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) |
| 108 | + info = info + data |
| 109 | + save_data = json.load(BytesIO(info)) |
| 110 | + except (OSError, struct.error, ValueError) as err: |
| 111 | + s = str(err) |
| 112 | + self.errorMessage = ('{0}: {1}' if s else '{0}').format( |
| 113 | + err.__class__.__name__, s |
| 114 | + ) |
| 115 | + return |
| 116 | + x = json_get_me(save_data, ["party", "player", "custom", "name"], None) |
| 117 | + if type(x) is str: |
| 118 | + self.name = x |
| 119 | + x = json_get_me(save_data, ["saved_datetime"], None) |
| 120 | + if type(x) in (int, float): |
| 121 | + try: |
| 122 | + dt = datetime.fromtimestamp(float(x)) |
| 123 | + except OSError: |
| 124 | + pass |
| 125 | + else: |
| 126 | + self.lastsave = "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( |
| 127 | + dt.year, dt.month, dt.day, |
| 128 | + dt.hour, dt.minute, dt.second |
| 129 | + ) |
| 130 | + x = json_get_me(save_data, ["play_time"], None) |
| 131 | + if type(x) in (int, float): |
| 132 | + a = [ 0, 0, 0, int(x * 10) ] |
| 133 | + a[2:4] = divmod(a[3], 10) |
| 134 | + a[1:3] = divmod(a[2], 60) |
| 135 | + a[0:2] = divmod(a[1], 60) |
| 136 | + self.elapsed = "{0:02d}:{1:02d}:{2:02d}.{3:01d}".format(*a) |
| 137 | + x = json_get_me(save_data, ["has_cheated"], None) |
| 138 | + if type(x) is bool: |
| 139 | + self.cheated = "Yes" if x else "No" |
101 | 140 |
|
102 | 141 | def getName(self) -> str: |
103 | 142 | return self.name |
104 | 143 |
|
105 | 144 | def getCheated(self) -> str: |
106 | 145 | return self.cheated |
107 | 146 |
|
| 147 | + def getLastSaved(self) -> str: |
| 148 | + return self.lastsave |
| 149 | + |
| 150 | + def getPlayTime(self) -> str: |
| 151 | + return self.elapsed |
| 152 | + |
108 | 153 | def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: |
| 154 | + if not save.errorMessage: |
| 155 | + return { |
| 156 | + "Character": save.getName(), |
| 157 | + "Last Saved": save.getLastSaved(), |
| 158 | + "Play Time": save.getPlayTime(), |
| 159 | + "Cheated": save.getCheated() |
| 160 | + } |
109 | 161 | return { |
110 | | - "Character": save.getName(), |
111 | | - "Cheated": save.getCheated() |
| 162 | + "Error loading file:": save.errorMessage |
112 | 163 | } |
113 | 164 |
|
114 | 165 | class CassetteBeastsGame(BasicGame): |
115 | | - appdataenv = os.getenv("APPDATA") |
116 | | - |
117 | 166 | Name = "Cassette Beasts Support Plugin" |
118 | 167 | Author = "modworkshop" |
119 | 168 | Version = "1" |
120 | 169 | GameName = "Cassette Beasts" |
121 | 170 | GameShortName = "cassette-beasts" |
122 | 171 | GameSteamId = 1321440 |
123 | 172 | GameBinary = "CassetteBeasts.exe" |
124 | | - GameDataPath = appdataenv + "/CassetteBeasts/mods" |
125 | | - GameDocumentsDirectory = appdataenv + "/CassetteBeasts" |
| 173 | + GameDataPath = os.getenv("APPDATA") + "/CassetteBeasts/mods" |
| 174 | + GameDocumentsDirectory = os.getenv("APPDATA") + "/CassetteBeasts" |
126 | 175 | GameSaveExtension = "gcpf" |
127 | 176 |
|
128 | 177 | def init(self, organizer: mobase.IOrganizer) -> bool: |
129 | 178 | super().init(organizer) |
130 | 179 | self.dataChecker = CassetteBeastsModDataChecker(organizer) |
131 | 180 | self._register_feature(self.dataChecker) |
132 | | - self._register_feature(BasicLocalSavegames(self)) |
| 181 | + self._register_feature(BasicLocalSavegames(QDir(self.GameDocumentsDirectory))) |
133 | 182 | self._register_feature( |
134 | 183 | BasicGameSaveGameInfo(None, getMetadata) |
135 | 184 | ) |
|
0 commit comments