tiger_pkg/manager/
path_cache.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4    time::SystemTime,
5};
6
7use ahash::HashMap;
8use itertools::Itertools;
9use tracing::info;
10
11use super::PackageManager;
12use crate::{package::PackagePlatform, GameVersion, Version};
13
14impl PackageManager {
15    #[cfg(feature = "ignore_package_cache")]
16    pub(super) fn read_package_cache(silent: bool) -> Option<PathCache> {
17        if !silent {
18            info!("Not loading tag cache: ignore_package_cache is enabled")
19        }
20        None
21    }
22
23    #[cfg(feature = "ignore_package_cache")]
24    pub(super) fn write_package_cache(&self) -> anyhow::Result<()> {
25        Ok(())
26    }
27
28    #[cfg(not(feature = "ignore_package_cache"))]
29    pub(super) fn read_package_cache(silent: bool) -> Option<PathCache> {
30        let cache: Option<PathCache> = serde_json::from_str(
31            &std::fs::read_to_string(exe_relative_path("package_cache.json")).ok()?,
32        )
33        .ok();
34
35        if let Some(ref c) = cache {
36            if c.cache_version != PathCache::VERSION {
37                if !silent {
38                    tracing::warn!("Package cache is outdated, building a new one");
39                }
40                return None;
41            }
42        }
43
44        cache
45    }
46
47    #[cfg(not(feature = "ignore_package_cache"))]
48    pub(super) fn write_package_cache(&self) -> anyhow::Result<()> {
49        let mut cache = Self::read_package_cache(true).unwrap_or_default();
50
51        let timestamp = fs::metadata(&self.package_dir)
52            .ok()
53            .and_then(|m| {
54                Some(
55                    m.modified()
56                        .ok()?
57                        .duration_since(SystemTime::UNIX_EPOCH)
58                        .ok()?
59                        .as_secs(),
60                )
61            })
62            .unwrap_or(0);
63
64        let entry = cache
65            .versions
66            .entry(self.cache_key())
67            .or_insert_with(|| PathCacheEntry {
68                timestamp,
69                version: self.version,
70                platform: self.platform,
71                base_path: self.package_dir.clone(),
72                paths: Default::default(),
73            });
74
75        entry.timestamp = timestamp;
76        entry.base_path = self.package_dir.clone();
77        entry.paths.clear();
78
79        for (id, path) in &self.package_paths {
80            entry.paths.insert(*id, path.path.clone());
81        }
82
83        Ok(std::fs::write(
84            exe_relative_path("package_cache.json"),
85            serde_json::to_string_pretty(&cache)?,
86        )?)
87    }
88
89    pub(super) fn validate_cache(
90        version: GameVersion,
91        platform: Option<PackagePlatform>,
92        packages_dir: &Path,
93    ) -> Result<HashMap<u16, String>, String> {
94        if let Some(cache) = Self::read_package_cache(false) {
95            info!("Loading package cache");
96            if let Some(p) = cache
97                .get_paths(version, platform, Some(packages_dir))
98                .ok()
99                .flatten()
100            {
101                let timestamp = fs::metadata(packages_dir)
102                    .ok()
103                    .and_then(|m| {
104                        Some(
105                            m.modified()
106                                .ok()?
107                                .duration_since(SystemTime::UNIX_EPOCH)
108                                .ok()?
109                                .as_secs(),
110                        )
111                    })
112                    .unwrap_or(0);
113
114                if p.timestamp < timestamp {
115                    Err("Package directory changed".to_string())
116                } else if p.base_path != packages_dir {
117                    Err("Package directory path changed".to_string())
118                } else {
119                    Ok(p.paths.clone())
120                }
121            } else {
122                Err(format!(
123                    "No cache entry found for version {version:?}, platform {platform:?}"
124                ))
125            }
126        } else {
127            Err("Failed to load package cache".to_string())
128        }
129    }
130
131    /// Generates a key unique to the game version + platform combination
132    /// eg. GameVersion::DestinyTheTakenKing and PackagePlatform::PS4 generates cache key "d1_ttk_ps4"
133    pub fn cache_key(&self) -> String {
134        format!("{}_{}", self.version.id(), self.platform)
135    }
136}
137
138#[derive(serde::Serialize, serde::Deserialize)]
139pub(crate) struct PathCache {
140    cache_version: usize,
141    versions: HashMap<String, PathCacheEntry>,
142}
143
144impl Default for PathCache {
145    fn default() -> Self {
146        Self {
147            cache_version: Self::VERSION,
148            versions: HashMap::default(),
149        }
150    }
151}
152
153impl PathCache {
154    pub const VERSION: usize = 5;
155
156    /// Gets path cache entry by version and platform
157    /// If `platform` is None, the first
158    /// This function will return an error if there are multiple entries for the same version when `platform` is None
159    pub fn get_paths(
160        &self,
161        version: GameVersion,
162        platform: Option<PackagePlatform>,
163        base_path: Option<&Path>,
164    ) -> anyhow::Result<Option<&PathCacheEntry>> {
165        if let Some(platform) = platform {
166            return Ok(self.versions.get(&format!("{}_{}", version.id(), platform)));
167        }
168
169        let mut matches = self
170            .versions
171            .iter()
172            .filter(|(_k, v)| {
173                v.version == version && platform.map(|p| v.platform == p).unwrap_or(true)
174            })
175            .map(|(_, v)| v)
176            .collect_vec();
177
178        if matches.len() > 1 {
179            if let Some(base_path) = base_path {
180                matches.retain(|c| c.base_path == base_path)
181            }
182        }
183
184        if matches.len() > 1 {
185            anyhow::bail!(
186                "There is more than one cache entry for version '{}', but no platform was given",
187                version.name()
188            );
189        }
190
191        Ok(matches.first().copied())
192    }
193}
194
195#[derive(serde::Serialize, serde::Deserialize)]
196pub(crate) struct PathCacheEntry {
197    /// Timestamp of the packages directory
198    timestamp: u64,
199    version: GameVersion,
200    platform: PackagePlatform,
201    base_path: PathBuf,
202    paths: HashMap<u16, String>,
203}
204
205pub fn exe_directory() -> PathBuf {
206    std::env::current_exe()
207        .unwrap()
208        .parent()
209        .unwrap()
210        .to_path_buf()
211}
212
213pub fn exe_relative_path(path: &str) -> PathBuf {
214    exe_directory().join(path)
215}