Skip to main content

wows_data_mgr/
registry.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3use std::path::PathBuf;
4
5use rootcause::prelude::*;
6use serde::Deserialize;
7use serde::Serialize;
8
9#[derive(Debug, Default, Serialize, Deserialize)]
10pub struct LocalRegistry {
11    /// Path to a WoWs installation that always provides the latest builds.
12    /// Checked dynamically — whatever builds exist there are available.
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub latest_path: Option<PathBuf>,
15    #[serde(default)]
16    pub builds: BTreeMap<String, LocalBuildEntry>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct LocalBuildEntry {
21    pub version: String,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub downloaded_at: Option<String>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub registered_at: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub path: Option<PathBuf>,
28}
29
30impl LocalRegistry {
31    pub fn has_build(&self, build: u32) -> bool {
32        self.builds.contains_key(&build.to_string())
33    }
34
35    pub fn get(&self, build: u32) -> Option<&LocalBuildEntry> {
36        self.builds.get(&build.to_string())
37    }
38
39    pub fn set_downloaded(&mut self, build: u32, version: &str) {
40        let now = jiff::Zoned::now();
41        let timestamp = now.strftime("%Y-%m-%dT%H:%M:%S%:z").to_string();
42        self.builds.insert(
43            build.to_string(),
44            LocalBuildEntry {
45                version: version.to_string(),
46                downloaded_at: Some(timestamp),
47                registered_at: None,
48                path: None,
49            },
50        );
51    }
52
53    pub fn set_registered(&mut self, build: u32, version: &str, path: &Path) {
54        let now = jiff::Zoned::now();
55        let timestamp = now.strftime("%Y-%m-%dT%H:%M:%S%:z").to_string();
56        self.builds.insert(
57            build.to_string(),
58            LocalBuildEntry {
59                version: version.to_string(),
60                downloaded_at: None,
61                registered_at: Some(timestamp),
62                path: Some(path.to_path_buf()),
63            },
64        );
65    }
66
67    /// Returns the sorted list of available build numbers.
68    /// Merges explicitly registered/downloaded builds with any builds
69    /// found at `latest_path`.
70    #[allow(dead_code)]
71    pub fn available_builds(&self) -> Vec<u32> {
72        let mut builds: Vec<u32> = self.builds.keys().filter_map(|k| k.parse::<u32>().ok()).collect();
73
74        if let Some(ref latest) = self.latest_path
75            && let Ok(latest_builds) = wowsunpack::game_data::list_available_builds(latest)
76        {
77            for b in latest_builds {
78                if !builds.contains(&b) {
79                    builds.push(b);
80                }
81            }
82        }
83
84        builds.sort();
85        builds
86    }
87
88    /// Returns the game directory for a build.
89    /// Checks in order: explicit registry entry, latest_path, downloaded builds.
90    #[allow(dead_code)]
91    pub fn game_dir_for_build(&self, build: u32, data_dir: &Path) -> Option<PathBuf> {
92        // Check explicit registry entry first
93        if let Some(entry) = self.get(build) {
94            // A registered path that no longer exists (the directory was moved
95            // or renamed) must not shadow a copy that is actually present.
96            if let Some(ref path) = entry.path
97                && path.exists()
98            {
99                return Some(path.clone());
100            }
101            // Downloaded build
102            let dir = data_dir.join("builds").join(build.to_string());
103            if dir.exists() {
104                return Some(dir);
105            }
106        }
107
108        // Check latest_path
109        if let Some(ref latest) = self.latest_path
110            && let Ok(builds) = wowsunpack::game_data::list_available_builds(latest)
111            && builds.contains(&build)
112        {
113            return Some(latest.clone());
114        }
115
116        // Fallback: check if downloaded dir exists even without registry entry
117        let dir = data_dir.join("builds").join(build.to_string());
118        if dir.exists() { Some(dir) } else { None }
119    }
120}
121
122pub fn load_registry(path: &Path) -> LocalRegistry {
123    if !path.exists() {
124        return LocalRegistry::default();
125    }
126    let content = match std::fs::read_to_string(path) {
127        Ok(c) => c,
128        Err(_) => return LocalRegistry::default(),
129    };
130    toml::from_str(&content).unwrap_or_default()
131}
132
133pub fn save_registry(registry: &LocalRegistry, path: &Path) -> Result<(), Report> {
134    if let Some(parent) = path.parent() {
135        std::fs::create_dir_all(parent).attach_with(|| format!("Failed to create directory {}", parent.display()))?;
136    }
137    let content =
138        toml::to_string_pretty(registry).map_err(|e| rootcause::report!("Failed to serialize registry: {e}"))?;
139    std::fs::write(path, content).attach_with(|| format!("Failed to write {}", path.display()))?;
140    Ok(())
141}