295 lines
11 KiB
Python
295 lines
11 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:
|
|
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
|
|
|