use crate::EngineResult;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameSave {
pub version: String,
pub timestamp: u64,
pub level_name: String,
pub player_data: PlayerData,
pub game_state: HashMap<String, SaveValue>,
pub metadata: SaveMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayerData {
pub name: String,
pub level: u32,
pub score: u64,
pub position: (f32, f32),
pub health: f32,
pub inventory: Vec<InventoryItem>,
pub unlocked_achievements: Vec<String>,
pub settings: PlayerSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InventoryItem {
pub id: String,
pub name: String,
pub quantity: u32,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayerSettings {
pub master_volume: f32,
pub music_volume: f32,
pub sfx_volume: f32,
pub graphics_quality: String,
pub controls: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaveMetadata {
pub save_name: String,
pub description: String,
pub playtime_seconds: u64,
pub screenshot_path: Option<String>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SaveValue {
Bool(bool),
Integer(i64),
Float(f64),
String(String),
Array(Vec<SaveValue>),
Object(HashMap<String, SaveValue>),
}
impl SaveValue {
pub fn as_bool(&self) -> Option<bool> {
match self {
SaveValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
SaveValue::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
SaveValue::Float(f) => Some(*f),
_ => None,
}
}
pub fn as_string(&self) -> Option<&String> {
match self {
SaveValue::String(s) => Some(s),
_ => None,
}
}
pub fn as_array(&self) -> Option<&Vec<SaveValue>> {
match self {
SaveValue::Array(arr) => Some(arr),
_ => None,
}
}
pub fn as_object(&self) -> Option<&HashMap<String, SaveValue>> {
match self {
SaveValue::Object(obj) => Some(obj),
_ => None,
}
}
}
impl Default for PlayerData {
fn default() -> Self {
Self {
name: "Player".to_string(),
level: 1,
score: 0,
position: (0.0, 0.0),
health: 100.0,
inventory: Vec::new(),
unlocked_achievements: Vec::new(),
settings: PlayerSettings::default(),
}
}
}
impl Default for PlayerSettings {
fn default() -> Self {
Self {
master_volume: 1.0,
music_volume: 0.8,
sfx_volume: 1.0,
graphics_quality: "medium".to_string(),
controls: HashMap::new(),
}
}
}
impl Default for SaveMetadata {
fn default() -> Self {
Self {
save_name: "Untitled Save".to_string(),
description: String::new(),
playtime_seconds: 0,
screenshot_path: None,
tags: Vec::new(),
}
}
}
pub struct SaveManager {
save_directory: PathBuf,
current_save: Option<GameSave>,
auto_save_enabled: bool,
auto_save_interval: u64,
last_auto_save: u64,
}
impl SaveManager {
pub fn new<P: AsRef<Path>>(save_directory: P) -> EngineResult<Self> {
let save_dir = save_directory.as_ref().to_path_buf();
if !save_dir.exists() {
fs::create_dir_all(&save_dir)?;
}
Ok(Self {
save_directory: save_dir,
current_save: None,
auto_save_enabled: true,
auto_save_interval: 300,
last_auto_save: 0,
})
}
pub fn set_auto_save(&mut self, enabled: bool, interval_seconds: u64) {
self.auto_save_enabled = enabled;
self.auto_save_interval = interval_seconds;
}
pub fn create_new_save(&mut self, save_name: &str) -> GameSave {
let save = GameSave {
version: env!("CARGO_PKG_VERSION").to_string(),
timestamp: self.get_current_timestamp(),
level_name: "Level1".to_string(),
player_data: PlayerData::default(),
game_state: HashMap::new(),
metadata: SaveMetadata {
save_name: save_name.to_string(),
..Default::default()
},
};
self.current_save = Some(save.clone());
save
}
pub fn save_game(&mut self, save_name: &str) -> EngineResult<()> {
let timestamp = self.get_current_timestamp();
let save_path = self.get_save_path(save_name);
if let Some(ref mut save) = self.current_save {
save.timestamp = timestamp;
save.metadata.save_name = save_name.to_string();
let json_data = serde_json::to_string_pretty(save)?;
fs::write(save_path, json_data)?;
self.last_auto_save = timestamp;
}
Ok(())
}
pub fn load_game(&mut self, save_name: &str) -> EngineResult<GameSave> {
let save_path = self.get_save_path(save_name);
if !save_path.exists() {
return Err(format!("Save file not found: {}", save_name).into());
}
let json_data = fs::read_to_string(save_path)?;
let save: GameSave = serde_json::from_str(&json_data)?;
self.current_save = Some(save.clone());
Ok(save)
}
pub fn delete_save(&self, save_name: &str) -> EngineResult<()> {
let save_path = self.get_save_path(save_name);
if save_path.exists() {
fs::remove_file(save_path)?;
}
Ok(())
}
pub fn list_saves(&self) -> EngineResult<Vec<String>> {
let mut saves = Vec::new();
for entry in fs::read_dir(&self.save_directory)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
saves.push(file_name.to_string());
}
}
}
saves.sort();
Ok(saves)
}
pub fn get_save_info(&self, save_name: &str) -> EngineResult<SaveMetadata> {
let save_path = self.get_save_path(save_name);
if !save_path.exists() {
return Err(format!("Save file not found: {}", save_name).into());
}
let json_data = fs::read_to_string(save_path)?;
let save: GameSave = serde_json::from_str(&json_data)?;
Ok(save.metadata)
}
pub fn update_current_save<F>(&mut self, updater: F) -> EngineResult<()>
where
F: FnOnce(&mut GameSave),
{
let timestamp = self.get_current_timestamp();
if let Some(ref mut save) = self.current_save {
updater(save);
save.timestamp = timestamp;
}
Ok(())
}
pub fn get_current_save(&self) -> Option<&GameSave> {
self.current_save.as_ref()
}
pub fn get_current_save_mut(&mut self) -> Option<&mut GameSave> {
self.current_save.as_mut()
}
pub fn should_auto_save(&self) -> bool {
if !self.auto_save_enabled || self.current_save.is_none() {
return false;
}
let current_time = self.get_current_timestamp();
current_time - self.last_auto_save >= self.auto_save_interval
}
pub fn auto_save(&mut self) -> EngineResult<()> {
if let Some(ref save) = self.current_save.clone() {
let auto_save_name = format!("{}_autosave", save.metadata.save_name);
self.save_game(&auto_save_name)?;
}
Ok(())
}
pub fn export_save(&self, save_name: &str, export_path: &Path) -> EngineResult<()> {
let save_path = self.get_save_path(save_name);
fs::copy(save_path, export_path)?;
Ok(())
}
pub fn import_save(&self, import_path: &Path, save_name: &str) -> EngineResult<()> {
let save_path = self.get_save_path(save_name);
fs::copy(import_path, save_path)?;
Ok(())
}
pub fn backup_saves(&self, backup_path: &Path) -> EngineResult<()> {
if !backup_path.exists() {
fs::create_dir_all(backup_path)?;
}
for entry in fs::read_dir(&self.save_directory)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Some(file_name) = path.file_name() {
let backup_file = backup_path.join(file_name);
fs::copy(&path, backup_file)?;
}
}
}
Ok(())
}
fn get_save_path(&self, save_name: &str) -> PathBuf {
self.save_directory.join(format!("{}.json", save_name))
}
fn get_current_timestamp(&self) -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
}
pub trait Saveable {
fn to_save_value(&self) -> SaveValue;
fn from_save_value(value: &SaveValue) -> Option<Self> where Self: Sized;
}
impl Saveable for bool {
fn to_save_value(&self) -> SaveValue {
SaveValue::Bool(*self)
}
fn from_save_value(value: &SaveValue) -> Option<Self> {
value.as_bool()
}
}
impl Saveable for i32 {
fn to_save_value(&self) -> SaveValue {
SaveValue::Integer(*self as i64)
}
fn from_save_value(value: &SaveValue) -> Option<Self> {
value.as_i64().map(|i| i as i32)
}
}
impl Saveable for f32 {
fn to_save_value(&self) -> SaveValue {
SaveValue::Float(*self as f64)
}
fn from_save_value(value: &SaveValue) -> Option<Self> {
value.as_f64().map(|f| f as f32)
}
}
impl Saveable for String {
fn to_save_value(&self) -> SaveValue {
SaveValue::String(self.clone())
}
fn from_save_value(value: &SaveValue) -> Option<Self> {
value.as_string().cloned()
}
}
#[macro_export]
macro_rules! save_value {
($save:expr, $key:expr, $value:expr) => {
$save.game_state.insert($key.to_string(), $value.to_save_value());
};
}
#[macro_export]
macro_rules! load_value {
($save:expr, $key:expr, $type:ty) => {
$save.game_state.get($key)
.and_then(|v| <$type>::from_save_value(v))
};
}