Files
wicked-quest/src/save_manager.py

320 lines
12 KiB
Python

# -*- 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 Exception:
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,
"cooldown": weapon.cooldown,
"attackDuration": weapon.attackDuration,
"speedBonus": getattr(weapon, "speedBonus", 1.0),
"jumpDurationBonus": getattr(weapon, "jumpDurationBonus", 1.0),
}
)
return serialized
def _deserialize_weapons(self, weapon_data):
"""Deserialize weapons from save data"""
from src.weapon import Weapon
weapons = []
for data in weapon_data:
# Handle backward compatibility for old saves
speedBonus = data.get("speedBonus", 1.0)
jumpDurationBonus = data.get("jumpDurationBonus", 1.0)
cooldown = data.get("cooldown", 500) # Default cooldown for old saves
# For old saves, restore proper bonuses and cooldowns for specific weapons
if data["name"] == "witch_broom" and speedBonus == 1.0:
speedBonus = 1.17
jumpDurationBonus = 1.25
# Restore proper cooldown for nunchucks in old saves
if data["name"] == "nunchucks" and cooldown == 500:
cooldown = 250
weapon = Weapon(
name=data["name"],
damage=data["damage"],
range=data["range"],
attackSound=data["attackSound"],
hitSound=data["hitSound"],
cooldown=cooldown,
attackDuration=data["attackDuration"],
speedBonus=speedBonus,
jumpDurationBonus=jumpDurationBonus,
)
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 Exception:
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 Exception:
return False
def has_saves(self):
"""Check if any save files exist"""
return len(self.get_save_files()) > 0