tiger_pkg/manager/
path_cache.rs

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