use std::collections::HashMap;
use std::fs::{File, create_dir_all};
use std::io::{Read, Write, BufReader, BufWriter};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH, Duration};
use serde::{Serialize, Deserialize, Serializer, Deserializer};
#[ derive( Debug, Clone, Serialize, Deserialize ) ]
pub struct SaveVersion
{
pub major: u32,
pub minor: u32,
pub patch: u32,
pub timestamp: u64,
}
impl SaveVersion
{
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs(),
}
}
pub fn current() -> Self {
Self::new(1, 0, 0)
}
pub fn is_compatible_with(&self, other: &SaveVersion) -> bool {
self.major == other.major && self.minor >= other.minor
}
}
impl Default for SaveVersion {
fn default() -> Self {
Self::current()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaveMetadata {
pub version: SaveVersion,
pub description: String,
pub created_at: u64,
pub size_bytes: u64,
pub compressed: bool,
pub tags: Vec<String>,
pub custom: HashMap<String, String>,
}
impl SaveMetadata {
pub fn new(description: String) -> Self {
Self {
version: SaveVersion::current(),
description,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs(),
size_bytes: 0,
compressed: false,
tags: Vec::new(),
custom: HashMap::new(),
}
}
pub fn with_tag(mut self, tag: String) -> Self {
self.tags.push(tag);
self
}
pub fn with_custom(mut self, key: String, value: String) -> Self {
self.custom.insert(key, value);
self
}
pub fn with_compression(mut self, compressed: bool) -> Self {
self.compressed = compressed;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableGameState {
pub metadata: SaveMetadata,
pub world_data: Vec<u8>,
pub config: GameConfig,
pub progress: PlayerProgress,
pub custom_data: HashMap<String, Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameConfig {
pub difficulty: u32,
pub graphics: GraphicsConfig,
pub audio: AudioConfig,
pub controls: ControlConfig,
pub gameplay: GameplayConfig,
}
impl Default for GameConfig {
fn default() -> Self {
Self {
difficulty: 1,
graphics: GraphicsConfig::default(),
audio: AudioConfig::default(),
controls: ControlConfig::default(),
gameplay: GameplayConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphicsConfig {
pub resolution_width: u32,
pub resolution_height: u32,
pub fullscreen: bool,
pub vsync: bool,
pub quality_level: u32,
pub fov: f32,
}
impl Default for GraphicsConfig {
fn default() -> Self {
Self {
resolution_width: 1920,
resolution_height: 1080,
fullscreen: false,
vsync: true,
quality_level: 2,
fov: 90.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioConfig {
pub master_volume: f32,
pub music_volume: f32,
pub sfx_volume: f32,
pub enabled: bool,
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
master_volume: 1.0,
music_volume: 0.8,
sfx_volume: 0.9,
enabled: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControlConfig {
pub key_bindings: HashMap<String, String>,
pub mouse_sensitivity: f32,
pub invert_mouse_y: bool,
}
impl Default for ControlConfig {
fn default() -> Self {
let mut key_bindings = HashMap::new();
key_bindings.insert("move_up".to_string(), "W".to_string());
key_bindings.insert("move_down".to_string(), "S".to_string());
key_bindings.insert("move_left".to_string(), "A".to_string());
key_bindings.insert("move_right".to_string(), "D".to_string());
key_bindings.insert("action".to_string(), "Space".to_string());
key_bindings.insert("menu".to_string(), "Escape".to_string());
Self {
key_bindings,
mouse_sensitivity: 1.0,
invert_mouse_y: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameplayConfig {
pub auto_save: bool,
pub auto_save_interval: u32,
pub show_tutorials: bool,
pub animation_speed: f32,
pub ui_scale: f32,
}
impl Default for GameplayConfig {
fn default() -> Self {
Self {
auto_save: true,
auto_save_interval: 300, show_tutorials: true,
animation_speed: 1.0,
ui_scale: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayerProgress {
pub level: u32,
pub experience: u64,
pub playtime_seconds: u64,
pub levels_completed: Vec<String>,
pub achievements: Vec<Achievement>,
pub statistics: GameStatistics,
}
impl Default for PlayerProgress {
fn default() -> Self {
Self {
level: 1,
experience: 0,
playtime_seconds: 0,
levels_completed: Vec::new(),
achievements: Vec::new(),
statistics: GameStatistics::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Achievement {
pub id: String,
pub name: String,
pub description: String,
pub unlocked_at: u64,
pub points: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GameStatistics {
pub entities_defeated: u64,
pub distance_moved: f64,
pub items_collected: u64,
pub spells_cast: u64,
pub deaths: u32,
pub levels_completed_count: u32,
}
#[derive(Debug, Clone, Copy)]
pub enum SerializationFormat {
Json,
Binary,
Ron,
}
pub struct GameStateSerializer {
format: SerializationFormat,
compress: bool,
version: SaveVersion,
}
impl GameStateSerializer {
pub fn new() -> Self {
Self {
format: SerializationFormat::Json,
compress: false,
version: SaveVersion::current(),
}
}
pub fn with_format(mut self, format: SerializationFormat) -> Self {
self.format = format;
self
}
pub fn with_compression(mut self, compress: bool) -> Self {
self.compress = compress;
self
}
pub fn with_version(mut self, version: SaveVersion) -> Self {
self.version = version;
self
}
pub fn serialize_game_state(&self, state: &SerializableGameState) -> Result<Vec<u8>, SerializationError> {
let data = match self.format {
SerializationFormat::Json => serde_json::to_vec(state)?,
SerializationFormat::Binary => bincode::serialize(state)?,
SerializationFormat::Ron => ron::ser::to_string(state)?.into_bytes(),
};
if self.compress {
Ok(self.compress_data(data))
} else {
Ok(data)
}
}
pub fn deserialize_game_state(&self, data: &[u8]) -> Result<SerializableGameState, SerializationError> {
let data = if self.compress {
self.decompress_data(data)?
} else {
data.to_vec()
};
let state = match self.format {
SerializationFormat::Json => serde_json::from_slice(&data)?,
SerializationFormat::Binary => bincode::deserialize(&data)?,
SerializationFormat::Ron => {
let text = String::from_utf8(data)?;
ron::from_str(&text).map_err(|e| {
let ron::error::SpannedError { code, .. } = e;
SerializationError::Ron(ron::Error::from(code))
})?
}
};
Ok(state)
}
pub fn create_basic_game_state(description: String) -> SerializableGameState {
SerializableGameState {
metadata: SaveMetadata::new(description),
world_data: vec![0u8; 1024], config: GameConfig::default(),
progress: PlayerProgress::default(),
custom_data: HashMap::new(),
}
}
fn compress_data(&self, data: Vec<u8>) -> Vec<u8> {
let mut compressed = vec![0xC0, 0x4D, 0x50]; compressed.extend_from_slice(&(data.len() as u32).to_le_bytes());
compressed.extend(data);
compressed
}
fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, SerializationError> {
if data.len() < 7 || data[0..3] != [0xC0, 0x4D, 0x50] {
return Err(SerializationError::InvalidCompressionFormat);
}
let original_size = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
if data.len() != original_size + 7 {
return Err(SerializationError::CorruptedData);
}
Ok(data[7..].to_vec())
}
}
impl Default for GameStateSerializer {
fn default() -> Self {
Self::new()
}
}
pub struct SaveManager {
saves_directory: PathBuf,
serializer: GameStateSerializer,
}
impl SaveManager {
pub fn new<P: AsRef<Path>>(saves_directory: P) -> Self {
Self {
saves_directory: saves_directory.as_ref().to_path_buf(),
serializer: GameStateSerializer::new(),
}
}
pub fn with_serializer(mut self, serializer: GameStateSerializer) -> Self {
self.serializer = serializer;
self
}
pub fn save_game_state(&self, save_name: &str, state: &SerializableGameState) -> Result<(), SerializationError> {
create_dir_all(&self.saves_directory)?;
let save_path = self.saves_directory.join(format!("{}.save", save_name));
let metadata_path = self.saves_directory.join(format!("{}.meta", save_name));
let serialized_data = self.serializer.serialize_game_state(state)?;
let mut metadata = state.metadata.clone();
metadata.size_bytes = serialized_data.len() as u64;
let mut save_file = BufWriter::new(File::create(save_path)?);
save_file.write_all(&serialized_data)?;
save_file.flush()?;
let metadata_json = serde_json::to_string_pretty(&metadata)?;
let mut metadata_file = BufWriter::new(File::create(metadata_path)?);
metadata_file.write_all(metadata_json.as_bytes())?;
metadata_file.flush()?;
Ok(())
}
pub fn load_game_state(&self, save_name: &str) -> Result<SerializableGameState, SerializationError> {
let save_path = self.saves_directory.join(format!("{}.save", save_name));
if !save_path.exists() {
return Err(SerializationError::SaveNotFound(save_name.to_string()));
}
let mut save_file = BufReader::new(File::open(save_path)?);
let mut data = Vec::new();
save_file.read_to_end(&mut data)?;
self.serializer.deserialize_game_state(&data)
}
pub fn load_save_metadata(&self, save_name: &str) -> Result<SaveMetadata, SerializationError> {
let metadata_path = self.saves_directory.join(format!("{}.meta", save_name));
if !metadata_path.exists() {
return Err(SerializationError::MetadataNotFound(save_name.to_string()));
}
let mut metadata_file = BufReader::new(File::open(metadata_path)?);
let mut metadata_json = String::new();
metadata_file.read_to_string(&mut metadata_json)?;
Ok(serde_json::from_str(&metadata_json)?)
}
pub fn list_saves(&self) -> Result<Vec<String>, SerializationError> {
if !self.saves_directory.exists() {
return Ok(Vec::new());
}
let mut saves = Vec::new();
for entry in std::fs::read_dir(&self.saves_directory)? {
let entry = entry?;
let path = entry.path();
if let Some(extension) = path.extension() {
if extension == "save" {
if let Some(stem) = path.file_stem() {
if let Some(name) = stem.to_str() {
saves.push(name.to_string());
}
}
}
}
}
saves.sort();
Ok(saves)
}
pub fn delete_save(&self, save_name: &str) -> Result<(), SerializationError> {
let save_path = self.saves_directory.join(format!("{}.save", save_name));
let metadata_path = self.saves_directory.join(format!("{}.meta", save_name));
if save_path.exists() {
std::fs::remove_file(save_path)?;
}
if metadata_path.exists() {
std::fs::remove_file(metadata_path)?;
}
Ok(())
}
pub fn get_saves_info(&self) -> Result<Vec<(String, SaveMetadata)>, SerializationError> {
let save_names = self.list_saves()?;
let mut saves_info = Vec::new();
for name in save_names {
match self.load_save_metadata(&name) {
Ok(metadata) => saves_info.push((name, metadata)),
Err(_) => {
continue;
}
}
}
saves_info.sort_by(|a, b| b.1.created_at.cmp(&a.1.created_at));
Ok(saves_info)
}
}
pub struct ConfigManager {
config_path: PathBuf,
}
impl ConfigManager {
pub fn new<P: AsRef<Path>>(config_path: P) -> Self {
Self {
config_path: config_path.as_ref().to_path_buf(),
}
}
pub fn save_config(&self, config: &GameConfig) -> Result<(), SerializationError> {
if let Some(parent) = self.config_path.parent() {
create_dir_all(parent)?;
}
let config_json = serde_json::to_string_pretty(config)?;
let mut file = BufWriter::new(File::create(&self.config_path)?);
file.write_all(config_json.as_bytes())?;
file.flush()?;
Ok(())
}
pub fn load_config(&self) -> Result<GameConfig, SerializationError> {
if !self.config_path.exists() {
return Ok(GameConfig::default());
}
let mut file = BufReader::new(File::open(&self.config_path)?);
let mut config_json = String::new();
file.read_to_string(&mut config_json)?;
Ok(serde_json::from_str(&config_json)?)
}
pub fn reset_config(&self) -> Result<(), SerializationError> {
self.save_config(&GameConfig::default())
}
}
#[derive(Debug)]
pub enum SerializationError {
Io(std::io::Error),
Json(serde_json::Error),
Binary(bincode::Error),
Ron(ron::Error),
Utf8(std::string::FromUtf8Error),
SaveNotFound(String),
MetadataNotFound(String),
InvalidCompressionFormat,
CorruptedData,
IncompatibleVersion { found: SaveVersion, expected: SaveVersion },
}
impl std::fmt::Display for SerializationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SerializationError::Io(e) => write!(f, "IO error: {}", e),
SerializationError::Json(e) => write!(f, "JSON error: {}", e),
SerializationError::Binary(e) => write!(f, "Binary serialization error: {}", e),
SerializationError::Ron(e) => write!(f, "RON error: {}", e),
SerializationError::Utf8(e) => write!(f, "UTF-8 error: {}", e),
SerializationError::SaveNotFound(name) => write!(f, "Save '{}' not found", name),
SerializationError::MetadataNotFound(name) => write!(f, "Metadata for save '{}' not found", name),
SerializationError::InvalidCompressionFormat => write!(f, "Invalid compression format"),
SerializationError::CorruptedData => write!(f, "Save data is corrupted"),
SerializationError::IncompatibleVersion { found, expected } => {
write!(f, "Incompatible save version: found {}.{}.{}, expected {}.{}.{}",
found.major, found.minor, found.patch,
expected.major, expected.minor, expected.patch)
}
}
}
}
impl std::error::Error for SerializationError {}
impl From<std::io::Error> for SerializationError {
fn from(error: std::io::Error) -> Self {
SerializationError::Io(error)
}
}
impl From<serde_json::Error> for SerializationError {
fn from(error: serde_json::Error) -> Self {
SerializationError::Json(error)
}
}
impl From<bincode::Error> for SerializationError {
fn from(error: bincode::Error) -> Self {
SerializationError::Binary(error)
}
}
impl From<ron::Error> for SerializationError {
fn from(error: ron::Error) -> Self {
SerializationError::Ron(error)
}
}
impl From<std::string::FromUtf8Error> for SerializationError {
fn from(error: std::string::FromUtf8Error) -> Self {
SerializationError::Utf8(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_save_version_compatibility() {
let v1 = SaveVersion::new(1, 0, 0);
let v2 = SaveVersion::new(1, 1, 0);
let v3 = SaveVersion::new(2, 0, 0);
assert!(v2.is_compatible_with(&v1)); assert!(!v1.is_compatible_with(&v2)); assert!(!v3.is_compatible_with(&v1)); }
#[test]
fn test_save_metadata_creation() {
let metadata = SaveMetadata::new("Test Save".to_string())
.with_tag("level1".to_string())
.with_custom("difficulty".to_string(), "hard".to_string())
.with_compression(true);
assert_eq!(metadata.description, "Test Save");
assert!(metadata.tags.contains(&"level1".to_string()));
assert_eq!(metadata.custom.get("difficulty"), Some(&"hard".to_string()));
assert!(metadata.compressed);
}
#[test]
fn test_game_config_serialization() {
let config = GameConfig::default();
let json = serde_json::to_string(&config).unwrap();
let deserialized: GameConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.difficulty, deserialized.difficulty);
assert_eq!(config.graphics.resolution_width, deserialized.graphics.resolution_width);
}
#[test]
fn test_game_state_serializer() {
let serializer = GameStateSerializer::new()
.with_format(SerializationFormat::Json)
.with_compression(false);
let game_state = GameStateSerializer::create_basic_game_state("Test Game".to_string());
let serialized = serializer.serialize_game_state(&game_state).unwrap();
let deserialized = serializer.deserialize_game_state(&serialized).unwrap();
assert_eq!(game_state.metadata.description, deserialized.metadata.description);
assert_eq!(game_state.world_data.len(), deserialized.world_data.len());
}
#[test]
fn test_compression() {
let serializer = GameStateSerializer::new()
.with_compression(true);
let game_state = GameStateSerializer::create_basic_game_state("Compression Test".to_string());
let compressed = serializer.serialize_game_state(&game_state).unwrap();
let decompressed = serializer.deserialize_game_state(&compressed).unwrap();
assert_eq!(game_state.metadata.description, decompressed.metadata.description);
}
#[test]
fn test_save_manager() {
let temp_dir = TempDir::new().unwrap();
let save_manager = SaveManager::new(temp_dir.path());
let game_state = GameStateSerializer::create_basic_game_state("Test Save".to_string());
save_manager.save_game_state("test_save", &game_state).unwrap();
let loaded_state = save_manager.load_game_state("test_save").unwrap();
assert_eq!(game_state.metadata.description, loaded_state.metadata.description);
let saves = save_manager.list_saves().unwrap();
assert!(saves.contains(&"test_save".to_string()));
let metadata = save_manager.load_save_metadata("test_save").unwrap();
assert_eq!(metadata.description, "Test Save");
let saves_info = save_manager.get_saves_info().unwrap();
assert_eq!(saves_info.len(), 1);
assert_eq!(saves_info[0].0, "test_save");
save_manager.delete_save("test_save").unwrap();
let saves_after_delete = save_manager.list_saves().unwrap();
assert!(!saves_after_delete.contains(&"test_save".to_string()));
}
#[test]
fn test_config_manager() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.json");
let config_manager = ConfigManager::new(&config_path);
let config = GameConfig::default();
config_manager.save_config(&config).unwrap();
let loaded_config = config_manager.load_config().unwrap();
assert_eq!(config.difficulty, loaded_config.difficulty);
config_manager.reset_config().unwrap();
let reset_config = config_manager.load_config().unwrap();
assert_eq!(reset_config.difficulty, 1);
}
#[test]
fn test_player_progress_serialization() {
let mut progress = PlayerProgress {
level: 5,
experience: 1500,
..Default::default()
};
progress.achievements.push(Achievement {
id: "first_kill".to_string(),
name: "First Kill".to_string(),
description: "Defeat your first enemy".to_string(),
unlocked_at: 1234567890,
points: 10,
});
let json = serde_json::to_string(&progress).unwrap();
let deserialized: PlayerProgress = serde_json::from_str(&json).unwrap();
assert_eq!(progress.level, deserialized.level);
assert_eq!(progress.achievements.len(), deserialized.achievements.len());
assert_eq!(progress.achievements[0].id, deserialized.achievements[0].id);
}
#[test]
fn test_error_handling() {
let temp_dir = TempDir::new().unwrap();
let save_manager = SaveManager::new(temp_dir.path());
let result = save_manager.load_game_state("nonexistent");
assert!(matches!(result, Err(SerializationError::SaveNotFound(_))));
let result = save_manager.load_save_metadata("nonexistent");
assert!(matches!(result, Err(SerializationError::MetadataNotFound(_))));
}
}