Skip to main content

modde_games/cyberpunk/
saves.rs

1//! Save detection for Cyberpunk 2077.
2
3use std::path::Path;
4
5use anyhow::Result;
6use smallvec::SmallVec;
7
8use crate::save_patterns::{CaptureSummary, PatternSaveTracker, PrefixSaveRule};
9use crate::traits::{DetectedSave, SaveTracker};
10
11/// [`SaveTracker`] for Cyberpunk 2077 save directories.
12pub struct CyberpunkSaveTracker;
13
14pub static CYBERPUNK_SAVE_TRACKER: CyberpunkSaveTracker = CyberpunkSaveTracker;
15
16/// Prefixes used by Cyberpunk 2077 save directories.
17const SAVE_PREFIXES: &[PrefixSaveRule] = &[
18    PrefixSaveRule {
19        prefix: "ManualSave-",
20        category: "manual",
21    },
22    PrefixSaveRule {
23        prefix: "AutoSave-",
24        category: "auto",
25    },
26    PrefixSaveRule {
27        prefix: "QuickSave-",
28        category: "quick",
29    },
30    PrefixSaveRule {
31        prefix: "PointOfNoReturn-",
32        category: "point-of-no-return",
33    },
34];
35
36const CYBERPUNK_PATTERN_TRACKER: PatternSaveTracker = PatternSaveTracker {
37    prefix_rules: SAVE_PREFIXES,
38    file_extensions: &[],
39    default_category: "manual",
40    recursive: false,
41    exclude_patterns: &["user.gls"],
42    label_extractor: extract_label,
43    summary: CaptureSummary::ByCategory,
44};
45
46impl SaveTracker for CyberpunkSaveTracker {
47    fn save_patterns(&self) -> SmallVec<[String; 2]> {
48        CYBERPUNK_PATTERN_TRACKER.save_patterns()
49    }
50
51    fn detect_saves(&self, save_dir: &Path) -> Result<Vec<DetectedSave>> {
52        CYBERPUNK_PATTERN_TRACKER.detect_saves(save_dir)
53    }
54
55    fn exclude_patterns(&self) -> SmallVec<[String; 2]> {
56        CYBERPUNK_PATTERN_TRACKER.exclude_patterns()
57    }
58
59    fn describe_capture(&self, saves: &[DetectedSave]) -> String {
60        CYBERPUNK_PATTERN_TRACKER.describe_capture(saves)
61    }
62}
63
64/// Try to extract a human-readable label from save metadata.
65///
66/// Cyberpunk saves with `NamedSaves` may have a metadata.9.json containing
67/// a custom name. Falls back to the directory name.
68fn extract_label(save_path: &Path, dir_name: &str) -> Option<String> {
69    // Try NamedSaves metadata (metadata.9.json)
70    let meta_path = save_path.join("metadata.9.json");
71    if meta_path.exists()
72        && let Ok(content) = std::fs::read_to_string(&meta_path)
73        && let Ok(json) = serde_json::from_str::<serde_json::Value>(&content)
74        && let Some(name) = json
75            .get("customName")
76            .or_else(|| json.get("name"))
77            .and_then(|v| v.as_str())
78        && !name.is_empty()
79    {
80        return Some(name.to_string());
81    }
82
83    // Fall back to directory name (strip prefix and numeric suffix for readability)
84    Some(dir_name.to_string())
85}