# -*- coding: utf-8 -*- import os import pickle import glob from datetime import datetime from pathlib import Path class SaveManager: def __init__(self): """Initialize save manager with XDG-compliant save directory""" # Use XDG_CONFIG_HOME or default to ~/.config config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) self.save_dir = Path(config_home) / 'storm-games' / 'wicked-quest' self.save_dir.mkdir(parents=True, exist_ok=True) self.max_saves = 10 def create_save(self, player, current_level, game_start_time, current_game): """Create a save file with current game state""" if not player.can_save(): return False, "Not enough bone dust to save (need 200)" # Validate required parameters if current_game is None: return False, "No game selected to save" if current_level is None: return False, "No current level to save" # Spend the bone dust if not player.spend_save_bone_dust(200): return False, "Failed to spend bone dust" # Create save data save_data = { 'player_state': { 'xPos': player.xPos, 'yPos': player.yPos, 'health': player._health, 'maxHealth': player._maxHealth, 'lives': player._lives, 'coins': player._coins, 'saveBoneDust': player._saveBoneDust, 'jackOLanternCount': player._jack_o_lantern_count, 'shinBoneCount': player.shinBoneCount, 'inventory': player.inventory, 'collectedItems': player.collectedItems, 'weapons': self._serialize_weapons(player.weapons), 'currentWeaponName': player.currentWeapon.name if player.currentWeapon else None, 'stats': self._serialize_stats(player.stats), 'scoreboard': self._serialize_scoreboard(player.scoreboard) }, 'game_state': { 'currentLevel': current_level, 'currentGame': current_game, 'gameStartTime': game_start_time, 'saveTime': datetime.now() }, 'version': '1.0' } # Generate filename with timestamp timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") filename = f"save_{timestamp}.pickle" filepath = self.save_dir / filename try: # Write to temporary file first, then rename for atomic operation temp_filepath = filepath.with_suffix('.tmp') with open(temp_filepath, 'wb') as f: pickle.dump(save_data, f) f.flush() # Ensure data is written to disk os.fsync(f.fileno()) # Force write to disk # Atomic rename (replaces old file if it exists) temp_filepath.rename(filepath) # Clean up old saves if we exceed max_saves self._cleanup_old_saves() return True, f"Game saved to {filename}" except Exception as e: # Clean up temp file if it exists if temp_filepath.exists(): try: temp_filepath.unlink() except: pass return False, f"Failed to save game: {str(e)}" def _serialize_weapons(self, weapons): """Serialize weapons for saving""" serialized = [] for weapon in weapons: serialized.append({ 'name': weapon.name, 'damage': weapon.damage, 'range': weapon.range, 'attackSound': weapon.attackSound, 'hitSound': weapon.hitSound, 'attackDuration': weapon.attackDuration }) return serialized def _deserialize_weapons(self, weapon_data): """Deserialize weapons from save data""" from src.weapon import Weapon weapons = [] for data in weapon_data: weapon = Weapon( name=data['name'], damage=data['damage'], range=data['range'], attackSound=data['attackSound'], hitSound=data['hitSound'], attackDuration=data['attackDuration'] ) weapons.append(weapon) return weapons def _serialize_stats(self, stats): """Serialize stats for saving""" return { 'total': stats.total.copy(), 'level': stats.level.copy() } def _deserialize_stats(self, stats_data): """Deserialize stats from save data""" from src.stat_tracker import StatTracker stats = StatTracker() if 'total' in stats_data: stats.total.update(stats_data['total']) if 'level' in stats_data: stats.level.update(stats_data['level']) return stats def _serialize_scoreboard(self, scoreboard): """Serialize scoreboard for saving""" return { 'currentScore': getattr(scoreboard, 'currentScore', 0), 'highScores': getattr(scoreboard, 'highScores', []) } def _deserialize_scoreboard(self, scoreboard_data): """Deserialize scoreboard from save data""" from libstormgames import Scoreboard scoreboard = Scoreboard() if 'currentScore' in scoreboard_data: scoreboard.currentScore = scoreboard_data['currentScore'] if 'highScores' in scoreboard_data: scoreboard.highScores = scoreboard_data['highScores'] return scoreboard def get_save_files(self): """Get list of save files with metadata""" save_files = [] pattern = str(self.save_dir / "save_*.pickle") for filepath in glob.glob(pattern): try: with open(filepath, 'rb') as f: save_data = pickle.load(f) # Validate save data structure if not self._validate_save_data(save_data): print(f"Invalid save file structure: {filepath}") continue # Extract save info save_time = save_data['game_state']['saveTime'] level = save_data['game_state']['currentLevel'] game_name = save_data['game_state']['currentGame'] # Format display name formatted_time = save_time.strftime("%B %d %I:%M%p") display_name = f"{formatted_time} {game_name} Level {level}" save_files.append({ 'filepath': filepath, 'display_name': display_name, 'save_time': save_time, 'level': level, 'game_name': game_name, 'save_data': save_data }) except (pickle.PickleError, EOFError, OSError) as e: print(f"Corrupted save file {filepath}: {e}") # Try to remove corrupted save file try: os.remove(filepath) print(f"Removed corrupted save file: {filepath}") except: pass continue except Exception as e: print(f"Error reading save file {filepath}: {e}") continue # Sort by save time (newest first) save_files.sort(key=lambda x: x['save_time'], reverse=True) return save_files def load_save(self, filepath): """Load game state from save file""" try: with open(filepath, 'rb') as f: save_data = pickle.load(f) return True, save_data except Exception as e: return False, f"Failed to load save: {str(e)}" def restore_player_state(self, player, save_data): """Restore player state from save data""" player_state = save_data['player_state'] # Restore basic attributes player.xPos = player_state['xPos'] player.yPos = player_state['yPos'] player._health = player_state['health'] player._maxHealth = player_state['maxHealth'] player._lives = player_state['lives'] player._coins = player_state['coins'] player._saveBoneDust = player_state['saveBoneDust'] player._jack_o_lantern_count = player_state['jackOLanternCount'] player.shinBoneCount = player_state['shinBoneCount'] player.inventory = player_state['inventory'] player.collectedItems = player_state['collectedItems'] # Restore weapons player.weapons = self._deserialize_weapons(player_state['weapons']) # Restore current weapon current_weapon_name = player_state.get('currentWeaponName') if current_weapon_name: for weapon in player.weapons: if weapon.name == current_weapon_name: player.currentWeapon = weapon break # Restore stats if 'stats' in player_state: player.stats = self._deserialize_stats(player_state['stats']) else: from src.stat_tracker import StatTracker player.stats = StatTracker() # Restore scoreboard if 'scoreboard' in player_state: player.scoreboard = self._deserialize_scoreboard(player_state['scoreboard']) else: from libstormgames import Scoreboard player.scoreboard = Scoreboard() def _cleanup_old_saves(self): """Remove old save files if we exceed max_saves""" save_files = self.get_save_files() if len(save_files) > self.max_saves: # Remove oldest saves for save_file in save_files[self.max_saves:]: try: os.remove(save_file['filepath']) except Exception as e: print(f"Error removing old save {save_file['filepath']}: {e}") def _validate_save_data(self, save_data): """Validate that save data has required structure""" try: # Check for required top-level keys required_keys = ['player_state', 'game_state', 'version'] if not all(key in save_data for key in required_keys): return False # Check player_state structure player_required = ['xPos', 'yPos', 'health', 'maxHealth', 'lives', 'coins', 'saveBoneDust'] if not all(key in save_data['player_state'] for key in player_required): return False # Check game_state structure game_required = ['currentLevel', 'currentGame', 'gameStartTime', 'saveTime'] if not all(key in save_data['game_state'] for key in game_required): return False return True except: return False def has_saves(self): """Check if any save files exist""" return len(self.get_save_files()) > 0