tiger_pkg/
package.rs

1use std::{
2    fmt::{Display, Formatter},
3    io::{Read, Seek},
4    str::FromStr,
5    sync::Arc,
6};
7
8use anyhow::{anyhow, ensure};
9use binrw::{BinRead, Endian};
10
11use crate::{d2_shared::PackageNamedTagEntry, TagHash};
12
13pub const BLOCK_CACHE_SIZE: usize = 128;
14
15pub trait ReadSeek: Read + Seek {}
16impl<R: Read + Seek> ReadSeek for R {}
17
18#[derive(Clone, Debug, bincode::Decode, bincode::Encode)]
19pub struct UEntryHeader {
20    pub reference: u32,
21    pub file_type: u8,
22    pub file_subtype: u8,
23    pub starting_block: u32,
24    pub starting_block_offset: u32,
25    pub file_size: u32,
26}
27
28#[derive(Clone)]
29pub struct UHashTableEntry {
30    pub hash64: u64,
31    pub hash32: TagHash,
32    pub reference: TagHash,
33}
34
35#[derive(BinRead, Debug, Copy, Clone)]
36#[br(repr = u16)]
37pub enum PackageLanguage {
38    None = 0,
39    English = 1,
40    French = 2,
41    Italian = 3,
42    German = 4,
43    Spanish = 5,
44    Japanese = 6,
45    Portuguese = 7,
46    Russian = 8,
47    Polish = 9,
48    SimplifiedChinese = 10,
49    TraditionalChinese = 11,
50    SpanishLatAm = 12,
51    Korean = 13,
52}
53
54impl PackageLanguage {
55    pub fn english_or_none(&self) -> bool {
56        matches!(self, Self::None | Self::English)
57    }
58}
59
60pub trait Package: Send + Sync {
61    fn endianness(&self) -> binrw::Endian;
62
63    fn pkg_id(&self) -> u16;
64    fn patch_id(&self) -> u16;
65
66    /// Every hash64 in this package.
67    /// Does not apply to Destiny 1
68    fn hash64_table(&self) -> Vec<UHashTableEntry>;
69
70    fn named_tags(&self) -> Vec<PackageNamedTagEntry>;
71
72    fn entries(&self) -> &[UEntryHeader];
73
74    fn entry(&self, index: usize) -> Option<UEntryHeader>;
75
76    fn language(&self) -> PackageLanguage;
77
78    fn platform(&self) -> PackagePlatform;
79
80    /// Gets/reads a specific block from the file.
81    /// It's recommended that the implementation caches blocks to prevent re-reads
82    fn get_block(&self, index: usize) -> anyhow::Result<Arc<Vec<u8>>>;
83
84    /// Reads the entire specified entry's data
85    fn read_entry(&self, index: usize) -> anyhow::Result<Vec<u8>> {
86        let _span = tracing::debug_span!("Package::read_entry").entered();
87        let entry = self
88            .entry(index)
89            .ok_or(anyhow!("Entry index is out of range"))?;
90
91        let mut buffer = Vec::with_capacity(entry.file_size as usize);
92        let mut current_offset = 0usize;
93        let mut current_block = entry.starting_block;
94
95        while current_offset < entry.file_size as usize {
96            let remaining_bytes = entry.file_size as usize - current_offset;
97            let block_data = self.get_block(current_block as usize)?;
98
99            if current_block == entry.starting_block {
100                let block_start_offset = entry.starting_block_offset as usize;
101                let block_remaining = block_data.len() - block_start_offset;
102                let copy_size = if block_remaining < remaining_bytes {
103                    block_remaining
104                } else {
105                    remaining_bytes
106                };
107
108                buffer.extend_from_slice(
109                    &block_data[block_start_offset..block_start_offset + copy_size],
110                );
111
112                current_offset += copy_size;
113            } else if remaining_bytes < block_data.len() {
114                // If the block has more bytes than we need, it means we're on the last block
115                buffer.extend_from_slice(&block_data[..remaining_bytes]);
116                current_offset += remaining_bytes;
117            } else {
118                // If the previous 2 conditions failed, it means this whole block belongs to the file
119                buffer.extend_from_slice(&block_data[..]);
120                current_offset += block_data.len();
121            }
122
123            current_block += 1;
124        }
125
126        Ok(buffer)
127    }
128
129    /// Reads the entire specified entry's data
130    /// Tag needs to be in this package
131    fn read_tag(&self, tag: TagHash) -> anyhow::Result<Vec<u8>> {
132        ensure!(tag.pkg_id() == self.pkg_id());
133        self.read_entry(tag.entry_index() as _)
134    }
135
136    // /// Reads the entire specified entry's data
137    // /// Hash needs to be in this package
138    // fn read_hash64(&self, hash: u64) -> anyhow::Result<Vec<u8>> {
139    //     let tag = self.translate_hash64(hash).ok_or_else(|| {
140    //         anyhow::anyhow!(
141    //             "Could not find hash 0x{hash:016x} in this package ({:04x})",
142    //             self.pkg_id()
143    //         )
144    //     })?;
145    //     ensure!(tag.pkg_id() == self.pkg_id());
146    //     self.read_entry(tag.entry_index() as _)
147    // }
148
149    fn get_all_by_reference(&self, reference: u32) -> Vec<(usize, UEntryHeader)> {
150        self.entries()
151            .iter()
152            .enumerate()
153            .filter(|(_, e)| e.reference == reference)
154            .map(|(i, e)| (i, e.clone()))
155            .collect()
156    }
157
158    fn get_all_by_type(&self, etype: u8, esubtype: Option<u8>) -> Vec<(usize, UEntryHeader)> {
159        self.entries()
160            .iter()
161            .enumerate()
162            .filter(|(_, e)| {
163                e.file_type == etype && esubtype.map(|t| t == e.file_subtype).unwrap_or(true)
164            })
165            .map(|(i, e)| (i, e.clone()))
166            .collect()
167    }
168}
169
170/// ! Currently only works for Pre-BL Destiny 2
171pub fn classify_file_prebl(ftype: u8, fsubtype: u8) -> String {
172    match (ftype, fsubtype) {
173        // WWise audio bank
174        (26, 5) => "bnk".to_string(),
175        // WWise audio stream
176        (26, 6) => "wem".to_string(),
177        // Havok file
178        (26, 7) => "hkx".to_string(),
179        // CriWare USM video
180        (27, _) => "usm".to_string(),
181        (32, 1) => "texture.header".to_string(),
182        (32, 2) => "texture_cube.header".to_string(),
183        (32, 4) => "vertex.header".to_string(),
184        (32, 6) => "index.header".to_string(),
185        (40, 4) => "vertex.data".to_string(),
186        (40, 6) => "index.data".to_string(),
187        (48, 1) => "texture.data".to_string(),
188        (48, 2) => "texture_cube.data".to_string(),
189        // DXBC data
190        (41, shader_type) => {
191            let ty = match shader_type {
192                0 => "fragment".to_string(),
193                1 => "vertex".to_string(),
194                6 => "compute".to_string(),
195                u => format!("unk{u}"),
196            };
197
198            format!("cso.{ty}")
199        }
200        (8, _) => "8080".to_string(),
201        _ => "bin".to_string(),
202    }
203}
204
205#[derive(
206    serde::Serialize,
207    serde::Deserialize,
208    clap::ValueEnum,
209    PartialEq,
210    Eq,
211    Debug,
212    Clone,
213    Copy,
214    BinRead,
215)]
216#[br(repr = u16)]
217pub enum PackagePlatform {
218    Tool32,
219    Win32,
220    Win64,
221    X360,
222    PS3,
223    Tool64,
224    Win64v1,
225    PS4,
226    XboxOne,
227    Stadia,
228    PS5,
229    Scarlett,
230}
231
232impl PackagePlatform {
233    pub fn endianness(&self) -> Endian {
234        match self {
235            Self::PS3 | Self::X360 => Endian::Big,
236            Self::XboxOne | Self::PS4 | Self::Win64 => Endian::Little,
237            _ => Endian::Little,
238        }
239    }
240}
241
242impl FromStr for PackagePlatform {
243    type Err = anyhow::Error;
244
245    fn from_str(s: &str) -> Result<Self, Self::Err> {
246        Ok(match s {
247            "ps3" => Self::PS3,
248            "ps4" => Self::PS4,
249            "360" => Self::X360,
250            "w64" => Self::Win64,
251            "xboxone" => Self::XboxOne,
252            s => return Err(anyhow!("Invalid platform '{s}'")),
253        })
254    }
255}
256
257impl Display for PackagePlatform {
258    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
259        match self {
260            PackagePlatform::Tool32 => f.write_str("tool32"),
261            PackagePlatform::Win32 => f.write_str("w32"),
262            PackagePlatform::Win64 => f.write_str("w64"),
263            PackagePlatform::X360 => f.write_str("360"),
264            PackagePlatform::PS3 => f.write_str("ps3"),
265            PackagePlatform::Tool64 => f.write_str("tool64"),
266            PackagePlatform::Win64v1 => f.write_str("w64"),
267            PackagePlatform::PS4 => f.write_str("ps4"),
268            PackagePlatform::XboxOne => f.write_str("xboxone"),
269            PackagePlatform::Stadia => f.write_str("stadia"),
270            PackagePlatform::PS5 => f.write_str("ps5"),
271            PackagePlatform::Scarlett => f.write_str("scarlett"),
272        }
273    }
274}