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