epik/games/
advanced.rs

1// Advanced Game Management for v1.2.0
2// Supports move installations, save game backups, config profiles, mod support
3
4use crate::{Error, Result};
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::PathBuf;
8
9// Game configuration profile
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct GameConfigProfile {
12    pub id: String,
13    pub app_name: String,
14    pub name: String,
15    pub description: Option<String>,
16    pub settings: std::collections::HashMap<String, String>,
17    pub launch_args: String,
18    pub wine_prefix: Option<String>,
19    pub environment_vars: std::collections::HashMap<String, String>,
20    pub created_at: chrono::DateTime<chrono::Utc>,
21    pub modified_at: chrono::DateTime<chrono::Utc>,
22    pub is_active: bool,
23}
24
25impl GameConfigProfile {
26    pub fn new(app_name: String, name: String) -> Self {
27        let now = chrono::Utc::now();
28        Self {
29            id: uuid::Uuid::new_v4().to_string(),
30            app_name,
31            name,
32            description: None,
33            settings: std::collections::HashMap::new(),
34            launch_args: String::new(),
35            wine_prefix: None,
36            environment_vars: std::collections::HashMap::new(),
37            created_at: now,
38            modified_at: now,
39            is_active: false,
40        }
41    }
42
43    pub fn set_setting(&mut self, key: String, value: String) {
44        self.settings.insert(key, value);
45        self.modified_at = chrono::Utc::now();
46    }
47
48    pub fn set_launch_args(&mut self, args: String) {
49        self.launch_args = args;
50        self.modified_at = chrono::Utc::now();
51    }
52
53    pub fn set_env_var(&mut self, key: String, value: String) {
54        self.environment_vars.insert(key, value);
55        self.modified_at = chrono::Utc::now();
56    }
57}
58
59// Backup information
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SaveGameBackup {
62    pub id: String,
63    pub app_name: String,
64    pub backup_name: String,
65    pub backup_path: PathBuf,
66    pub size_bytes: u64,
67    pub created_at: chrono::DateTime<chrono::Utc>,
68    pub is_automatic: bool,
69    pub description: Option<String>,
70}
71
72impl SaveGameBackup {
73    pub fn new(app_name: String, backup_path: PathBuf, is_automatic: bool) -> Result<Self> {
74        let size_bytes = Self::calculate_size(&backup_path)?;
75        let now = chrono::Utc::now();
76
77        Ok(Self {
78            id: uuid::Uuid::new_v4().to_string(),
79            app_name,
80            backup_name: format!("Backup_{}", now.format("%Y%m%d_%H%M%S")),
81            backup_path,
82            size_bytes,
83            created_at: now,
84            is_automatic,
85            description: None,
86        })
87    }
88
89    fn calculate_size(path: &PathBuf) -> Result<u64> {
90        let mut total = 0u64;
91
92        if path.is_file() {
93            total = fs::metadata(path)?.len();
94        } else if path.is_dir() {
95            for entry in fs::read_dir(path)? {
96                let entry = entry?;
97                let metadata = entry.metadata()?;
98                if metadata.is_file() {
99                    total += metadata.len();
100                } else if metadata.is_dir() {
101                    total += Self::calculate_size(&entry.path())?;
102                }
103            }
104        }
105
106        Ok(total)
107    }
108
109    pub fn size_mb(&self) -> f32 {
110        (self.size_bytes as f32) / (1024.0 * 1024.0)
111    }
112
113    pub fn size_gb(&self) -> f32 {
114        (self.size_bytes as f32) / (1024.0 * 1024.0 * 1024.0)
115    }
116}
117
118// Mod information
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct Mod {
121    pub id: String,
122    pub app_name: String,
123    pub name: String,
124    pub version: String,
125    pub mod_path: PathBuf,
126    pub enabled: bool,
127    pub size_bytes: u64,
128    pub installed_at: chrono::DateTime<chrono::Utc>,
129}
130
131impl Mod {
132    pub fn new(app_name: String, name: String, mod_path: PathBuf) -> Result<Self> {
133        let size_bytes = SaveGameBackup::calculate_size(&mod_path)?;
134
135        Ok(Self {
136            id: uuid::Uuid::new_v4().to_string(),
137            app_name,
138            name,
139            version: "1.0".to_string(),
140            mod_path,
141            enabled: true,
142            size_bytes,
143            installed_at: chrono::Utc::now(),
144        })
145    }
146
147    pub fn size_mb(&self) -> f32 {
148        (self.size_bytes as f32) / (1024.0 * 1024.0)
149    }
150}
151
152// Installation move operation status
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154pub enum MoveStatus {
155    Pending,
156    InProgress,
157    Completed,
158    Failed,
159    Cancelled,
160}
161
162// Advanced game manager
163pub struct AdvancedGameManager {
164    profiles: std::collections::HashMap<String, GameConfigProfile>,
165    backups: std::collections::HashMap<String, SaveGameBackup>,
166    mods: std::collections::HashMap<String, Mod>,
167}
168
169impl Default for AdvancedGameManager {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl AdvancedGameManager {
176    pub fn new() -> Self {
177        Self {
178            profiles: std::collections::HashMap::new(),
179            backups: std::collections::HashMap::new(),
180            mods: std::collections::HashMap::new(),
181        }
182    }
183
184    // Profile management
185    pub fn create_profile(&mut self, app_name: String, name: String) -> String {
186        let profile = GameConfigProfile::new(app_name, name);
187        let id = profile.id.clone();
188        self.profiles.insert(id.clone(), profile);
189        id
190    }
191
192    pub fn delete_profile(&mut self, profile_id: &str) -> Result<()> {
193        if self.profiles.remove(profile_id).is_some() {
194            Ok(())
195        } else {
196            Err(Error::Other(format!("Profile not found: {}", profile_id)))
197        }
198    }
199
200    pub fn get_profile(&self, profile_id: &str) -> Option<&GameConfigProfile> {
201        self.profiles.get(profile_id)
202    }
203
204    pub fn get_profile_mut(&mut self, profile_id: &str) -> Option<&mut GameConfigProfile> {
205        self.profiles.get_mut(profile_id)
206    }
207
208    pub fn list_profiles_for_game(&self, app_name: &str) -> Vec<&GameConfigProfile> {
209        self.profiles
210            .values()
211            .filter(|p| p.app_name == app_name)
212            .collect()
213    }
214
215    pub fn set_active_profile(&mut self, app_name: &str, profile_id: &str) -> Result<()> {
216        // Deactivate all profiles for this game
217        for profile in self.profiles.values_mut() {
218            if profile.app_name == app_name {
219                profile.is_active = false;
220            }
221        }
222
223        // Activate selected profile
224        if let Some(profile) = self.profiles.get_mut(profile_id) {
225            if profile.app_name == app_name {
226                profile.is_active = true;
227                Ok(())
228            } else {
229                Err(Error::Other("Profile not found for game".to_string()))
230            }
231        } else {
232            Err(Error::Other(format!("Profile not found: {}", profile_id)))
233        }
234    }
235
236    pub fn get_active_profile(&self, app_name: &str) -> Option<&GameConfigProfile> {
237        self.profiles
238            .values()
239            .find(|p| p.app_name == app_name && p.is_active)
240    }
241
242    // Backup management
243    pub fn create_backup(&mut self, backup: SaveGameBackup) -> String {
244        let id = backup.id.clone();
245        self.backups.insert(id.clone(), backup);
246        id
247    }
248
249    pub fn delete_backup(&mut self, backup_id: &str) -> Result<()> {
250        if let Some(backup) = self.backups.remove(backup_id) {
251            // Delete backup file
252            if backup.backup_path.exists() {
253                if backup.backup_path.is_file() {
254                    fs::remove_file(&backup.backup_path)?;
255                } else if backup.backup_path.is_dir() {
256                    fs::remove_dir_all(&backup.backup_path)?;
257                }
258            }
259            Ok(())
260        } else {
261            Err(Error::Other(format!("Backup not found: {}", backup_id)))
262        }
263    }
264
265    pub fn get_backup(&self, backup_id: &str) -> Option<&SaveGameBackup> {
266        self.backups.get(backup_id)
267    }
268
269    pub fn list_backups_for_game(&self, app_name: &str) -> Vec<&SaveGameBackup> {
270        self.backups
271            .values()
272            .filter(|b| b.app_name == app_name)
273            .collect()
274    }
275
276    pub fn get_total_backup_size(&self, app_name: &str) -> u64 {
277        self.list_backups_for_game(app_name)
278            .iter()
279            .map(|b| b.size_bytes)
280            .sum()
281    }
282
283    // Mod management
284    pub fn install_mod(&mut self, mod_info: Mod) -> Result<String> {
285        let id = mod_info.id.clone();
286        self.mods.insert(id.clone(), mod_info);
287        Ok(id)
288    }
289
290    pub fn uninstall_mod(&mut self, mod_id: &str) -> Result<()> {
291        if let Some(mod_info) = self.mods.remove(mod_id) {
292            // Delete mod directory
293            if mod_info.mod_path.exists() {
294                fs::remove_dir_all(&mod_info.mod_path)?;
295            }
296            Ok(())
297        } else {
298            Err(Error::Other(format!("Mod not found: {}", mod_id)))
299        }
300    }
301
302    pub fn get_mod(&self, mod_id: &str) -> Option<&Mod> {
303        self.mods.get(mod_id)
304    }
305
306    pub fn get_mod_mut(&mut self, mod_id: &str) -> Option<&mut Mod> {
307        self.mods.get_mut(mod_id)
308    }
309
310    pub fn list_mods_for_game(&self, app_name: &str) -> Vec<&Mod> {
311        self.mods
312            .values()
313            .filter(|m| m.app_name == app_name)
314            .collect()
315    }
316
317    pub fn enable_mod(&mut self, mod_id: &str) -> Result<()> {
318        if let Some(mod_info) = self.mods.get_mut(mod_id) {
319            mod_info.enabled = true;
320            Ok(())
321        } else {
322            Err(Error::Other(format!("Mod not found: {}", mod_id)))
323        }
324    }
325
326    pub fn disable_mod(&mut self, mod_id: &str) -> Result<()> {
327        if let Some(mod_info) = self.mods.get_mut(mod_id) {
328            mod_info.enabled = false;
329            Ok(())
330        } else {
331            Err(Error::Other(format!("Mod not found: {}", mod_id)))
332        }
333    }
334
335    pub fn get_enabled_mods(&self, app_name: &str) -> Vec<&Mod> {
336        self.list_mods_for_game(app_name)
337            .into_iter()
338            .filter(|m| m.enabled)
339            .collect()
340    }
341
342    pub fn get_total_mods_size(&self, app_name: &str) -> u64 {
343        self.list_mods_for_game(app_name)
344            .iter()
345            .map(|m| m.size_bytes)
346            .sum()
347    }
348
349    // Installation management
350    pub async fn move_installation(
351        &self,
352        current_path: &PathBuf,
353        new_path: &PathBuf,
354    ) -> Result<()> {
355        // Verify source exists
356        if !current_path.exists() {
357            return Err(Error::Other(format!(
358                "Source path does not exist: {:?}",
359                current_path
360            )));
361        }
362
363        // Verify destination doesn't exist
364        if new_path.exists() {
365            return Err(Error::Other(format!(
366                "Destination path already exists: {:?}",
367                new_path
368            )));
369        }
370
371        // Create destination parent directory
372        if let Some(parent) = new_path.parent() {
373            fs::create_dir_all(parent)?;
374        }
375
376        // Move installation
377        fs::rename(current_path, new_path)?;
378
379        Ok(())
380    }
381
382    // Create backup of saves (thread-safe async copy)
383    pub async fn backup_saves(
384        &mut self,
385        save_path: &PathBuf,
386        backup_dir: &PathBuf,
387        app_name: &str,
388        auto: bool,
389    ) -> Result<String> {
390        // Create backup directory if needed
391        fs::create_dir_all(backup_dir)?;
392
393        let backup_subdir = backup_dir.join(format!(
394            "backup_{}",
395            chrono::Utc::now().format("%Y%m%d_%H%M%S")
396        ));
397        fs::create_dir_all(&backup_subdir)?;
398
399        // Copy save files
400        copy_recursively(save_path, &backup_subdir)?;
401
402        let backup = SaveGameBackup::new(app_name.to_string(), backup_subdir, auto)?;
403        Ok(self.create_backup(backup))
404    }
405
406    // Restore saves from backup
407    pub async fn restore_saves(
408        &self,
409        backup_id: &str,
410        target_path: &PathBuf,
411    ) -> Result<()> {
412        if let Some(backup) = self.backups.get(backup_id) {
413            // Verify target exists
414            if !target_path.exists() {
415                fs::create_dir_all(target_path)?;
416            }
417
418            // Copy backup to target
419            copy_recursively(&backup.backup_path, target_path)?;
420            Ok(())
421        } else {
422            Err(Error::Other(format!("Backup not found: {}", backup_id)))
423        }
424    }
425}
426
427// Helper function for recursive directory copy
428fn copy_recursively(src: &PathBuf, dst: &PathBuf) -> Result<()> {
429    fs::create_dir_all(dst)?;
430
431    for entry in fs::read_dir(src)? {
432        let entry = entry?;
433        let path = entry.path();
434        let file_name = entry.file_name();
435        let dest_path = dst.join(file_name);
436
437        if path.is_dir() {
438            copy_recursively(&path, &dest_path)?;
439        } else {
440            fs::copy(&path, &dest_path)?;
441        }
442    }
443
444    Ok(())
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    #[test]
452    fn test_profile_creation() {
453        let mut manager = AdvancedGameManager::new();
454        let profile_id = manager.create_profile("game1".to_string(), "High Quality".to_string());
455
456        let profile = manager.get_profile(&profile_id);
457        assert!(profile.is_some());
458        assert_eq!(profile.unwrap().name, "High Quality");
459    }
460
461    #[test]
462    fn test_profile_settings() {
463        let mut manager = AdvancedGameManager::new();
464        let profile_id = manager.create_profile("game1".to_string(), "Profile".to_string());
465
466        if let Some(profile) = manager.get_profile_mut(&profile_id) {
467            profile.set_setting("graphics".to_string(), "ultra".to_string());
468            profile.set_launch_args("--fullscreen".to_string());
469        }
470
471        let profile = manager.get_profile(&profile_id).unwrap();
472        assert_eq!(profile.settings.get("graphics").unwrap(), "ultra");
473        assert_eq!(profile.launch_args, "--fullscreen");
474    }
475
476    #[test]
477    fn test_active_profile() {
478        let mut manager = AdvancedGameManager::new();
479        let profile_id1 = manager.create_profile("game1".to_string(), "Profile 1".to_string());
480        let profile_id2 = manager.create_profile("game1".to_string(), "Profile 2".to_string());
481
482        manager.set_active_profile("game1", &profile_id1).unwrap();
483        assert!(manager.get_profile(&profile_id1).unwrap().is_active);
484
485        manager.set_active_profile("game1", &profile_id2).unwrap();
486        assert!(!manager.get_profile(&profile_id1).unwrap().is_active);
487        assert!(manager.get_profile(&profile_id2).unwrap().is_active);
488    }
489
490    #[test]
491    fn test_mod_management() {
492        let mut manager = AdvancedGameManager::new();
493        let mod_path = PathBuf::from("/tmp/test_mod");
494
495        let mod_info = Mod::new(
496            "game1".to_string(),
497            "Test Mod".to_string(),
498            mod_path,
499        ).unwrap();
500
501        let mod_id = manager.install_mod(mod_info).unwrap();
502        assert!(manager.get_mod(&mod_id).is_some());
503    }
504}