1+ from collections .abc import Mapping , Sequence
2+ from datetime import datetime
3+ from functools import cached_property
4+ from io import BytesIO
5+ from typing import Any , Optional
6+ from pathlib import Path
7+ import json
8+ import math
19import os
210import shutil
3- import mobase
11+ import struct
12+ import zlib
413
5- from pathlib import Path
6- from functools import cached_property
14+ import mobase
15+ from PyQt6 . QtCore import QDateTime , QDir , QFile , QFileInfo
716
17+ from ..basic_features import BasicLocalSavegames
18+ from ..basic_features .basic_save_game_info import (BasicGameSaveGame ,BasicGameSaveGameInfo )
819from ..basic_game import BasicGame
920
10- from PyQt6 .QtCore import QDir , QFileInfo
1121
22+ def json_get_me (value : Any , path : Sequence [str | int ], / , default : Any ) -> Any :
23+ for part in path :
24+ if type (part ) not in (str , int ) or type (value ) not in (dict , list ):
25+ return default
26+ value = value [part ]
27+ return value
1228
1329class CassetteBeastsModDataChecker (mobase .ModDataChecker ):
1430 def __init__ (self , organizer : mobase .IOrganizer ):
@@ -47,26 +63,125 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree:
4763 return None
4864 return filetree
4965
66+ class CassetteBlock :
67+ def __init__ (self ):
68+ compressed_size : str = "(unknown)"
69+ data : str = "(unknown)"
70+
71+ class CassetteBeastsSaveGame (BasicGameSaveGame ):
72+ def __init__ (self , filepath : Path ):
73+ super ().__init__ (filepath )
74+ self .name : str = "(unknown)"
75+ self .cheated : str = "(unknown)"
76+ self .lastsave : str = "(unknown)"
77+ self .elapsed : str = "(unknown)"
78+ # This doesn't state wether the game would load it,
79+ # only if the data was properly parsed.
80+ self .errorMessage : str = ""
81+
82+ save_data = None
83+ try :
84+ info = bytearray ()
85+ data = bytes ()
86+ with open (filepath , 'rb' ) as infile :
87+ infile .read (4 )
88+
89+ compression_mode , blocksize , raw_size = struct .unpack ("III" , infile .read (12 ))
90+
91+ num_blocks = math .ceil (raw_size / blocksize )
92+
93+ blocks = []
94+
95+ for _bnum in range (num_blocks ):
96+ block = CassetteBlock ()
97+ block .compressed_size = struct .unpack ("I" , infile .read (4 ))[0 ]
98+ blocks .append (block )
99+
100+ for block in blocks :
101+ block .data = infile .read (block .compressed_size )
102+
103+ infile .read (4 )
104+ infile .close ()
105+ for block in blocks :
106+ data = zlib .decompress (block .data , wbits = 40 , bufsize = blocksize )
107+ info = info + data
108+ save_data = json .load (BytesIO (info ))
109+ except (OSError , struct .error , ValueError ) as err :
110+ s = str (err )
111+ self .errorMessage = ('{0}: {1}' if s else '{0}' ).format (
112+ err .__class__ .__name__ , s
113+ )
114+ return
115+ x = json_get_me (save_data , ["party" , "player" , "custom" , "name" ], None )
116+ if type (x ) is str :
117+ self .name = x
118+ x = json_get_me (save_data , ["saved_datetime" ], None )
119+ if type (x ) in (int , float ):
120+ try :
121+ dt = datetime .fromtimestamp (float (x ))
122+ except OSError :
123+ pass
124+ else :
125+ self .lastsave = "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}" .format (
126+ dt .year , dt .month , dt .day ,
127+ dt .hour , dt .minute , dt .second
128+ )
129+ x = json_get_me (save_data , ["play_time" ], None )
130+ if type (x ) in (int , float ):
131+ a = [ 0 , 0 , 0 , int (x * 10 ) ]
132+ a [2 :4 ] = divmod (a [3 ], 10 )
133+ a [1 :3 ] = divmod (a [2 ], 60 )
134+ a [0 :2 ] = divmod (a [1 ], 60 )
135+ self .elapsed = "{0:02d}:{1:02d}:{2:02d}.{3:01d}" .format (* a )
136+ x = json_get_me (save_data , ["has_cheated" ], None )
137+ if type (x ) is bool :
138+ self .cheated = "Yes" if x else "No"
139+
140+ def getName (self ) -> str :
141+ return self .name
142+
143+ def getCheated (self ) -> str :
144+ return self .cheated
145+
146+ def getLastSaved (self ) -> str :
147+ return self .lastsave
148+
149+ def getPlayTime (self ) -> str :
150+ return self .elapsed
151+
152+ def getMetadata (p : Path , save : mobase .ISaveGame ) -> Mapping [str , str ]:
153+ if not save .errorMessage :
154+ return {
155+ "Character" : save .getName (),
156+ "Last Saved" : save .getLastSaved (),
157+ "Play Time" : save .getPlayTime (),
158+ "Cheated" : save .getCheated ()
159+ }
160+ return {
161+ "Error loading file:" : save .errorMessage
162+ }
50163
51164class CassetteBeastsGame (BasicGame ):
52- appdataenv = os .getenv ("APPDATA" )
53-
54165 Name = "Cassette Beasts Support Plugin"
55166 Author = "modworkshop"
56167 Version = "1"
57168 GameName = "Cassette Beasts"
58169 GameShortName = "cassette-beasts"
59170 GameSteamId = 1321440
60171 GameBinary = "CassetteBeasts.exe"
61- GameDataPath = appdataenv + " /CassetteBeasts/mods"
62- GameDocumentsDirectory = appdataenv + " /CassetteBeasts"
63- GameSavesDirectory = ' %GAME_DOCUMENTS%'
172+ GameDataPath = "%USERPROFILE%/AppData/Roaming /CassetteBeasts/mods"
173+ GameDocumentsDirectory = "%USERPROFILE%/AppData/Roaming /CassetteBeasts"
174+ GameSavesDirectory = " %GAME_DOCUMENTS%"
64175 GameSaveExtension = "gcpf"
65176
66177 def init (self , organizer : mobase .IOrganizer ) -> bool :
67178 super ().init (organizer )
68179 self .dataChecker = CassetteBeastsModDataChecker (organizer )
69180 self ._register_feature (self .dataChecker )
181+ self ._register_feature (BasicLocalSavegames (self ))
182+ self ._register_feature (
183+ BasicGameSaveGameInfo (None , getMetadata )
184+ )
70185 return True
71186
72187 def executables (self ):
@@ -81,6 +196,13 @@ def executables(self):
81196 ),
82197 ]
83198
199+ def listSaves (self , folder : QDir ) -> list [mobase .ISaveGame ]:
200+ ext = self ._mappings .savegameExtension .get ()
201+ return [
202+ CassetteBeastsSaveGame (path )
203+ for path in Path (folder .absolutePath ()).glob (f"*.{ ext } " )
204+ ]
205+
84206 @cached_property
85207 def _base_dlls (self ) -> set [str ]:
86208 base_dir = Path (self .gameDirectory ().absolutePath ())
0 commit comments