1use crate::{Error, Result};
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::PathBuf;
8
9#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154pub enum MoveStatus {
155 Pending,
156 InProgress,
157 Completed,
158 Failed,
159 Cancelled,
160}
161
162pub 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 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 for profile in self.profiles.values_mut() {
218 if profile.app_name == app_name {
219 profile.is_active = false;
220 }
221 }
222
223 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 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 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 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 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 pub async fn move_installation(
351 &self,
352 current_path: &PathBuf,
353 new_path: &PathBuf,
354 ) -> Result<()> {
355 if !current_path.exists() {
357 return Err(Error::Other(format!(
358 "Source path does not exist: {:?}",
359 current_path
360 )));
361 }
362
363 if new_path.exists() {
365 return Err(Error::Other(format!(
366 "Destination path already exists: {:?}",
367 new_path
368 )));
369 }
370
371 if let Some(parent) = new_path.parent() {
373 fs::create_dir_all(parent)?;
374 }
375
376 fs::rename(current_path, new_path)?;
378
379 Ok(())
380 }
381
382 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 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_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 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 if !target_path.exists() {
415 fs::create_dir_all(target_path)?;
416 }
417
418 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
427fn 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}