tiger_pkg/manager/
mod.rs

1pub mod lookup_cache;
2pub mod path_cache;
3
4use std::{
5    fmt::Display,
6    fs,
7    io::Cursor,
8    path::{Path, PathBuf},
9    str::FromStr,
10    sync::Arc,
11};
12
13use ahash::HashMap;
14use anyhow::Context;
15use binrw::{BinRead, BinReaderExt};
16use parking_lot::RwLock;
17use rayon::prelude::*;
18use tracing::{debug_span, info, warn};
19
20use crate::{
21    d2_shared::PackageNamedTagEntry,
22    oodle,
23    package::{Package, PackagePlatform, UEntryHeader},
24    tag::TagHash64,
25    GameVersion, TagHash, Version,
26};
27
28#[derive(Clone, bincode::Decode, bincode::Encode)]
29pub struct HashTableEntryShort {
30    pub hash32: TagHash,
31    pub reference: TagHash,
32}
33
34#[derive(Default, bincode::Decode, bincode::Encode)]
35pub struct TagLookupIndex {
36    pub tag32_entries_by_pkg: HashMap<u16, Vec<UEntryHeader>>,
37    pub tag64_entries: HashMap<u64, HashTableEntryShort>,
38    pub tag32_to_tag64: HashMap<TagHash, TagHash64>,
39
40    pub named_tags: Vec<PackageNamedTagEntry>,
41}
42
43pub struct PackageManager {
44    pub package_dir: PathBuf,
45    pub package_paths: HashMap<u16, PackagePath>,
46    pub version: GameVersion,
47    pub platform: PackagePlatform,
48
49    /// Tag Lookup Index (TLI)
50    pub lookup: TagLookupIndex,
51
52    /// Packages that are currently open for reading
53    pkgs: RwLock<HashMap<u16, Arc<dyn Package>>>,
54}
55
56impl PackageManager {
57    pub fn new<P: AsRef<Path>>(
58        packages_dir: P,
59        version: GameVersion,
60        platform: Option<PackagePlatform>,
61    ) -> anyhow::Result<PackageManager> {
62        // All the latest packages
63        let mut packages: HashMap<u16, String> = Default::default();
64
65        let oo2core_3_path = packages_dir.as_ref().join("../bin/x64/oo2core_3_win64.dll");
66        let oo2core_9_path = packages_dir.as_ref().join("../bin/x64/oo2core_9_win64.dll");
67
68        if oo2core_3_path.exists() {
69            let mut o = oodle::OODLE_3.write();
70            if o.is_err() {
71                if let Ok(oodle) = oodle::Oodle::from_path(oo2core_3_path) {
72                    *o = Ok(oodle);
73                }
74            }
75        }
76
77        if oo2core_9_path.exists() {
78            let mut o = oodle::OODLE_9.write();
79            if o.is_err() {
80                if let Ok(oodle) = oodle::Oodle::from_path(oo2core_9_path) {
81                    *o = Ok(oodle);
82                }
83            }
84        }
85
86        let build_new_cache = match Self::validate_cache(version, platform, packages_dir.as_ref()) {
87            Ok(paths) => {
88                packages = paths;
89                false
90            }
91            Err(e) => {
92                warn!("Caches need to be rebuilt: {e}");
93                true
94            }
95        };
96
97        if build_new_cache {
98            info!("Creating new package cache for {}", version.id());
99            let path = packages_dir.as_ref();
100            // Every package in the given directory, including every patch
101            let mut packages_all = vec![];
102            debug_span!("Discover packages in directory").in_scope(|| -> anyhow::Result<()> {
103                for entry in fs::read_dir(path)? {
104                    let entry = entry?;
105                    let path = entry.path();
106                    if path.is_file() && path.to_string_lossy().to_lowercase().ends_with(".pkg") {
107                        packages_all.push(path.to_string_lossy().to_string());
108                    }
109                }
110
111                Ok(())
112            })?;
113
114            packages_all.sort_by_cached_key(|p| {
115                let p = PackagePath::parse_with_defaults(p);
116                (p.id, p.patch)
117            });
118
119            debug_span!("Filter latest packages").in_scope(|| {
120                for p in packages_all {
121                    let parts: Vec<&str> = p.split('_').collect();
122                    if let Some(Ok(pkg_id)) = parts
123                        .get(parts.len() - 2)
124                        .map(|s| u16::from_str_radix(s, 16))
125                    {
126                        packages.insert(pkg_id, p);
127                    } else {
128                        let _span = debug_span!("Open package to find package ID").entered();
129                        // Take the long route and extract the package ID from the header
130                        if let Ok(pkg) = version.open(&p) {
131                            if pkg.language().english_or_none() {
132                                packages.insert(pkg.pkg_id(), p);
133                            }
134                        }
135                    }
136                }
137            });
138        }
139
140        let package_paths: HashMap<u16, PackagePath> = packages
141            .into_iter()
142            .map(|(id, p)| (id, PackagePath::parse_with_defaults(&p)))
143            .collect();
144
145        let first_path = package_paths.values().next().context("No packages found")?;
146
147        let platform = if let Ok(pkg) = version.open(&first_path.path) {
148            pkg.platform()
149        } else {
150            PackagePlatform::from_str(first_path.platform.as_str())?
151        };
152
153        let mut s = Self {
154            package_dir: packages_dir.as_ref().to_path_buf(),
155            platform,
156            package_paths,
157            version,
158            lookup: Default::default(),
159            pkgs: Default::default(),
160        };
161
162        if build_new_cache {
163            s.build_lookup_tables();
164            s.write_package_cache().ok();
165            s.write_lookup_cache().ok();
166        } else if let Some(lookup_cache) = s.read_lookup_cache() {
167            s.lookup = lookup_cache;
168        } else {
169            info!("No valid index cache found, rebuilding");
170            s.build_lookup_tables();
171            s.write_lookup_cache().ok();
172        }
173
174        Ok(s)
175    }
176}
177
178impl PackageManager {
179    pub fn get_all_by_reference(&self, reference: u32) -> Vec<(TagHash, UEntryHeader)> {
180        self.lookup
181            .tag32_entries_by_pkg
182            .par_iter()
183            .map(|(p, e)| {
184                e.iter()
185                    .enumerate()
186                    .filter(|(_, e)| e.reference == reference)
187                    .map(|(i, e)| (TagHash::new(*p, i as _), e.clone()))
188                    .collect::<Vec<(TagHash, UEntryHeader)>>()
189            })
190            .flatten()
191            .collect()
192    }
193
194    pub fn get_all_by_type(&self, etype: u8, esubtype: Option<u8>) -> Vec<(TagHash, UEntryHeader)> {
195        self.lookup
196            .tag32_entries_by_pkg
197            .par_iter()
198            .map(|(p, e)| {
199                e.iter()
200                    .enumerate()
201                    .filter(|(_, e)| {
202                        e.file_type == etype
203                            && esubtype.map(|t| t == e.file_subtype).unwrap_or(true)
204                    })
205                    .map(|(i, e)| (TagHash::new(*p, i as _), e.clone()))
206                    .collect::<Vec<(TagHash, UEntryHeader)>>()
207            })
208            .flatten()
209            .collect()
210    }
211
212    fn get_or_load_pkg(&self, pkg_id: u16) -> anyhow::Result<Arc<dyn Package>> {
213        let v = self.pkgs.read();
214        if let Some(pkg) = v.get(&pkg_id) {
215            Ok(Arc::clone(pkg))
216        } else {
217            drop(v);
218            let package_path = self
219                .package_paths
220                .get(&pkg_id)
221                .with_context(|| format!("Couldn't get a path for package id {pkg_id:04x}"))?;
222
223            let package = self
224                .version
225                .open(&package_path.path)
226                .with_context(|| format!("Failed to open package '{}'", package_path.filename))?;
227
228            self.pkgs.write().insert(pkg_id, Arc::clone(&package));
229            Ok(package)
230        }
231    }
232
233    pub fn read_tag(&self, tag: impl Into<TagHash>) -> anyhow::Result<Vec<u8>> {
234        let tag = tag.into();
235        self.get_or_load_pkg(tag.pkg_id())?
236            .read_entry(tag.entry_index() as _)
237    }
238
239    pub fn read_tag64(&self, hash: impl Into<TagHash64>) -> anyhow::Result<Vec<u8>> {
240        let hash = hash.into();
241        let tag = self
242            .lookup
243            .tag64_entries
244            .get(&hash.0)
245            .context("Hash not found")?
246            .hash32;
247        self.read_tag(tag)
248    }
249
250    pub fn get_entry(&self, tag: impl Into<TagHash>) -> Option<UEntryHeader> {
251        let tag: TagHash = tag.into();
252
253        self.lookup
254            .tag32_entries_by_pkg
255            .get(&tag.pkg_id())?
256            .get(tag.entry_index() as usize)
257            .cloned()
258    }
259
260    pub fn get_named_tag(&self, name: &str, class_hash: u32) -> Option<TagHash> {
261        self.lookup
262            .named_tags
263            .iter()
264            .find(|n| n.name == name && n.class_hash == class_hash)
265            .map(|n| n.hash)
266    }
267
268    pub fn get_named_tags_by_class(&self, class_hash: u32) -> Vec<(String, TagHash)> {
269        self.lookup
270            .named_tags
271            .iter()
272            .filter(|n| n.class_hash == class_hash)
273            .map(|n| (n.name.clone(), n.hash))
274            .collect()
275    }
276
277    /// Find the name of a tag by its hash, if it has one.
278    pub fn get_tag_name(&self, tag: impl Into<TagHash>) -> Option<String> {
279        let tag: TagHash = tag.into();
280        self.lookup
281            .named_tags
282            .iter()
283            .find(|n| n.hash == tag)
284            .map(|n| n.name.clone())
285    }
286
287    pub fn get_tag64_for_tag32(&self, tag: impl Into<TagHash>) -> Option<TagHash64> {
288        let tag: TagHash = tag.into();
289        self.lookup.tag32_to_tag64.get(&tag).copied()
290    }
291
292    /// Read any BinRead type
293    pub fn read_tag_binrw<'a, T: BinRead>(&self, tag: impl Into<TagHash>) -> anyhow::Result<T>
294    where
295        T::Args<'a>: Default + Clone,
296    {
297        let tag = tag.into();
298        let data = self.read_tag(tag)?;
299        let mut cursor = Cursor::new(&data);
300        Ok(cursor.read_type(self.version.endian())?)
301    }
302
303    /// Read any BinRead type
304    pub fn read_tag64_binrw<'a, T: BinRead>(&self, hash: impl Into<TagHash64>) -> anyhow::Result<T>
305    where
306        T::Args<'a>: Default + Clone,
307    {
308        let data = self.read_tag64(hash)?;
309        let mut cursor = Cursor::new(&data);
310        Ok(cursor.read_type(self.version.endian())?)
311    }
312}
313
314#[derive(Debug, Clone)]
315pub struct PackagePath {
316    /// eg. ps3, w64
317    pub platform: String,
318    /// eg. arch_fallen, dungeon_prophecy, europa
319    pub name: String,
320
321    /// 2-letter language code (en, fr, de, etc.)
322    pub language: Option<String>,
323
324    /// eg. 0059, 043c, unp1, unp2
325    pub id: String,
326    pub patch: u8,
327
328    /// Full path to the package
329    pub path: String,
330    pub filename: String,
331}
332
333impl PackagePath {
334    /// Example path: ps3_arch_fallen_0059_0.pkg
335    pub fn parse(path: &str) -> Option<Self> {
336        let path_filename = Path::new(path).file_name()?.to_string_lossy();
337        let parts: Vec<&str> = path_filename.split('_').collect();
338        if parts.len() < 4 {
339            return None;
340        }
341
342        let platform = parts[0].to_string();
343        let mut name = parts[1..parts.len() - 2].join("_");
344        let mut id = parts[parts.len() - 2].to_string();
345        let mut language = None;
346        if id.len() == 2 {
347            // ID is actually language code
348            language = Some(id.clone());
349            name = parts[1..parts.len() - 3].join("_");
350            id = parts[parts.len() - 3].to_string();
351        }
352
353        let patch = parts[parts.len() - 1].split('.').next()?.parse().ok()?;
354
355        Some(Self {
356            platform,
357            name,
358            language,
359            id,
360            patch,
361            path: path.to_string(),
362            filename: path_filename.to_string(),
363        })
364    }
365
366    pub fn parse_with_defaults(path: &str) -> Self {
367        let path_filename = Path::new(path)
368            .file_name()
369            .map_or(path.to_string(), |p| p.to_string_lossy().to_string());
370        Self::parse(path).unwrap_or_else(|| Self {
371            platform: "unknown".to_string(),
372            name: "unknown".to_string(),
373            id: "unknown".to_string(),
374            language: None,
375            patch: 0,
376            path: path.to_string(),
377            filename: path_filename,
378        })
379    }
380}
381
382impl Display for PackagePath {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        write!(f, "{}", self.filename)
385    }
386}