destiny_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};
10use clap::ValueEnum;
11
12use crate::{
13    d1_internal_alpha::PackageD1InternalAlpha, d1_legacy::PackageD1Legacy,
14    d1_roi::PackageD1RiseOfIron, d2_beta::PackageD2Beta, d2_beyondlight::PackageD2BeyondLight,
15    d2_shared::PackageNamedTagEntry, PackageD2PreBL, TagHash,
16};
17
18pub const BLOCK_CACHE_SIZE: usize = 128;
19
20pub trait ReadSeek: Read + Seek {}
21impl<R: Read + Seek> ReadSeek for R {}
22
23#[derive(Clone, Debug, bincode::Decode, bincode::Encode)]
24pub struct UEntryHeader {
25    pub reference: u32,
26    pub file_type: u8,
27    pub file_subtype: u8,
28    pub starting_block: u32,
29    pub starting_block_offset: u32,
30    pub file_size: u32,
31}
32
33#[derive(Clone)]
34pub struct UHashTableEntry {
35    pub hash64: u64,
36    pub hash32: TagHash,
37    pub reference: TagHash,
38}
39
40#[derive(BinRead, Debug, Copy, Clone)]
41#[br(repr = u16)]
42pub enum PackageLanguage {
43    None = 0,
44    English = 1,
45    French = 2,
46    Italian = 3,
47    German = 4,
48    Spanish = 5,
49    Japanese = 6,
50    Portuguese = 7,
51    Russian = 8,
52    Polish = 9,
53    SimplifiedChinese = 10,
54    TraditionalChinese = 11,
55    SpanishLatAm = 12,
56    Korean = 13,
57}
58
59impl PackageLanguage {
60    pub fn english_or_none(&self) -> bool {
61        matches!(self, Self::None | Self::English)
62    }
63}
64
65#[derive(
66    serde::Serialize, serde::Deserialize, clap::ValueEnum, PartialEq, PartialOrd, Debug, Clone, Copy,
67)]
68pub enum GameVersion {
69    /// X360 december 2013 internal alpha version of Destiny
70    #[value(name = "d1_devalpha")]
71    DestinyInternalAlpha = 1_0500,
72
73    /// PS4 First Look Alpha
74    #[value(name = "d1_flalpha")]
75    DestinyFirstLookAlpha = 1_0800,
76
77    /// PS3/X360 version of Destiny (The Taken King)
78    #[value(name = "d1_ttk")]
79    DestinyTheTakenKing = 1_2000,
80
81    /// The latest version of Destiny (Rise of Iron)
82    #[value(name = "d1_roi")]
83    DestinyRiseOfIron = 1_2400,
84
85    /// Destiny 2 Beta
86    #[value(name = "d2_beta")]
87    Destiny2Beta = 2_1000,
88
89    #[value(name = "d2_fs")]
90    Destiny2Forsaken = 2_2000,
91
92    /// The last version of Destiny before Beyond Light (Shadowkeep/Season of Arrivals)
93    #[value(name = "d2_sk")]
94    Destiny2Shadowkeep = 2_2600,
95
96    /// Destiny 2 (Beyond Light/Season of the Lost)
97    #[value(name = "d2_bl")]
98    Destiny2BeyondLight = 2_3000,
99
100    /// Destiny 2 (Witch Queen/Season of the Seraph)
101    #[value(name = "d2_wq")]
102    Destiny2WitchQueen = 2_4000,
103
104    /// Destiny 2 (Lightfall)
105    #[value(name = "d2_lf")]
106    Destiny2Lightfall = 2_7000,
107
108    #[value(name = "d2_tfs")]
109    Destiny2TheFinalShape = 2_8000,
110}
111
112impl GameVersion {
113    pub fn open(&self, path: &str) -> anyhow::Result<Arc<dyn Package>> {
114        Ok(match self {
115            GameVersion::DestinyInternalAlpha => Arc::new(PackageD1InternalAlpha::open(path)?),
116            GameVersion::DestinyFirstLookAlpha => Arc::new(PackageD1RiseOfIron::open(path)?),
117            GameVersion::DestinyTheTakenKing => Arc::new(PackageD1Legacy::open(path)?),
118            GameVersion::DestinyRiseOfIron => Arc::new(PackageD1RiseOfIron::open(path)?),
119            GameVersion::Destiny2Beta => Arc::new(PackageD2Beta::open(path)?),
120
121            GameVersion::Destiny2Forsaken | GameVersion::Destiny2Shadowkeep => {
122                Arc::new(PackageD2PreBL::open(path)?)
123            }
124
125            GameVersion::Destiny2BeyondLight
126            | GameVersion::Destiny2WitchQueen
127            | GameVersion::Destiny2Lightfall
128            | GameVersion::Destiny2TheFinalShape => {
129                Arc::new(PackageD2BeyondLight::open(path, *self)?)
130            }
131        })
132    }
133
134    pub fn endian(&self) -> Endian {
135        match self {
136            GameVersion::DestinyInternalAlpha | GameVersion::DestinyTheTakenKing => Endian::Big,
137            _ => Endian::Little,
138        }
139    }
140
141    pub fn is_d1(&self) -> bool {
142        *self <= GameVersion::DestinyRiseOfIron
143    }
144
145    pub fn is_d2(&self) -> bool {
146        *self >= GameVersion::Destiny2Beta
147    }
148
149    pub fn is_prebl(&self) -> bool {
150        GameVersion::Destiny2Beta <= *self && *self <= GameVersion::Destiny2Shadowkeep
151    }
152
153    pub fn id(&self) -> String {
154        self.to_possible_value()
155            .expect("Package version is missing an id/commandline value")
156            .get_name()
157            .to_string()
158    }
159
160    pub fn name(&self) -> &'static str {
161        match self {
162            GameVersion::DestinyInternalAlpha => "Destiny X360 Internal Alpha",
163            GameVersion::DestinyFirstLookAlpha => "Destiny First Look Alpha",
164            GameVersion::DestinyTheTakenKing => "Destiny: The Taken King",
165            GameVersion::DestinyRiseOfIron => "Destiny: Rise of Iron",
166            GameVersion::Destiny2Beta => "Destiny 2: Beta",
167            GameVersion::Destiny2Forsaken => "Destiny 2: Forsaken",
168            GameVersion::Destiny2Shadowkeep => "Destiny 2: Shadowkeep",
169            GameVersion::Destiny2BeyondLight => "Destiny 2: Beyond Light",
170            GameVersion::Destiny2WitchQueen => "Destiny 2: Witch Queen",
171            GameVersion::Destiny2Lightfall => "Destiny 2: Lightfall",
172            GameVersion::Destiny2TheFinalShape => "Destiny 2: The Final Shape",
173        }
174    }
175}
176
177pub trait Package: Send + Sync {
178    fn endianness(&self) -> binrw::Endian;
179
180    fn pkg_id(&self) -> u16;
181    fn patch_id(&self) -> u16;
182
183    /// Every hash64 in this package.
184    /// Does not apply to Destiny 1
185    fn hash64_table(&self) -> Vec<UHashTableEntry>;
186
187    fn named_tags(&self) -> Vec<PackageNamedTagEntry>;
188
189    fn entries(&self) -> &[UEntryHeader];
190
191    fn entry(&self, index: usize) -> Option<UEntryHeader>;
192
193    fn language(&self) -> PackageLanguage;
194
195    fn platform(&self) -> PackagePlatform;
196
197    /// Gets/reads a specific block from the file.
198    /// It's recommended that the implementation caches blocks to prevent re-reads
199    fn get_block(&self, index: usize) -> anyhow::Result<Arc<Vec<u8>>>;
200
201    /// Reads the entire specified entry's data
202    fn read_entry(&self, index: usize) -> anyhow::Result<Vec<u8>> {
203        let _span = tracing::debug_span!("Package::read_entry").entered();
204        let entry = self
205            .entry(index)
206            .ok_or(anyhow!("Entry index is out of range"))?;
207
208        let mut buffer = Vec::with_capacity(entry.file_size as usize);
209        let mut current_offset = 0usize;
210        let mut current_block = entry.starting_block;
211
212        while current_offset < entry.file_size as usize {
213            let remaining_bytes = entry.file_size as usize - current_offset;
214            let block_data = self.get_block(current_block as usize)?;
215
216            if current_block == entry.starting_block {
217                let block_start_offset = entry.starting_block_offset as usize;
218                let block_remaining = block_data.len() - block_start_offset;
219                let copy_size = if block_remaining < remaining_bytes {
220                    block_remaining
221                } else {
222                    remaining_bytes
223                };
224
225                buffer.extend_from_slice(
226                    &block_data[block_start_offset..block_start_offset + copy_size],
227                );
228
229                current_offset += copy_size;
230            } else if remaining_bytes < block_data.len() {
231                // If the block has more bytes than we need, it means we're on the last block
232                buffer.extend_from_slice(&block_data[..remaining_bytes]);
233                current_offset += remaining_bytes;
234            } else {
235                // If the previous 2 conditions failed, it means this whole block belongs to the file
236                buffer.extend_from_slice(&block_data[..]);
237                current_offset += block_data.len();
238            }
239
240            current_block += 1;
241        }
242
243        Ok(buffer)
244    }
245
246    /// Reads the entire specified entry's data
247    /// Tag needs to be in this package
248    fn read_tag(&self, tag: TagHash) -> anyhow::Result<Vec<u8>> {
249        ensure!(tag.pkg_id() == self.pkg_id());
250        self.read_entry(tag.entry_index() as _)
251    }
252
253    // /// Reads the entire specified entry's data
254    // /// Hash needs to be in this package
255    // fn read_hash64(&self, hash: u64) -> anyhow::Result<Vec<u8>> {
256    //     let tag = self.translate_hash64(hash).ok_or_else(|| {
257    //         anyhow::anyhow!(
258    //             "Could not find hash 0x{hash:016x} in this package ({:04x})",
259    //             self.pkg_id()
260    //         )
261    //     })?;
262    //     ensure!(tag.pkg_id() == self.pkg_id());
263    //     self.read_entry(tag.entry_index() as _)
264    // }
265
266    fn get_all_by_reference(&self, reference: u32) -> Vec<(usize, UEntryHeader)> {
267        self.entries()
268            .iter()
269            .enumerate()
270            .filter(|(_, e)| e.reference == reference)
271            .map(|(i, e)| (i, e.clone()))
272            .collect()
273    }
274
275    fn get_all_by_type(&self, etype: u8, esubtype: Option<u8>) -> Vec<(usize, UEntryHeader)> {
276        self.entries()
277            .iter()
278            .enumerate()
279            .filter(|(_, e)| {
280                e.file_type == etype && esubtype.map(|t| t == e.file_subtype).unwrap_or(true)
281            })
282            .map(|(i, e)| (i, e.clone()))
283            .collect()
284    }
285}
286
287/// ! Currently only works for Pre-BL Destiny 2
288pub fn classify_file_prebl(ftype: u8, fsubtype: u8) -> String {
289    match (ftype, fsubtype) {
290        // WWise audio bank
291        (26, 5) => "bnk".to_string(),
292        // WWise audio stream
293        (26, 6) => "wem".to_string(),
294        // Havok file
295        (26, 7) => "hkx".to_string(),
296        // CriWare USM video
297        (27, _) => "usm".to_string(),
298        (32, 1) => "texture.header".to_string(),
299        (32, 2) => "texture_cube.header".to_string(),
300        (32, 4) => "vertex.header".to_string(),
301        (32, 6) => "index.header".to_string(),
302        (40, 4) => "vertex.data".to_string(),
303        (40, 6) => "index.data".to_string(),
304        (48, 1) => "texture.data".to_string(),
305        (48, 2) => "texture_cube.data".to_string(),
306        // DXBC data
307        (41, shader_type) => {
308            let ty = match shader_type {
309                0 => "fragment".to_string(),
310                1 => "vertex".to_string(),
311                6 => "compute".to_string(),
312                u => format!("unk{u}"),
313            };
314
315            format!("cso.{ty}")
316        }
317        (8, _) => "8080".to_string(),
318        _ => "bin".to_string(),
319    }
320}
321
322#[derive(
323    serde::Serialize,
324    serde::Deserialize,
325    clap::ValueEnum,
326    PartialEq,
327    Eq,
328    Debug,
329    Clone,
330    Copy,
331    BinRead,
332)]
333#[br(repr = u16)]
334pub enum PackagePlatform {
335    Tool32,
336    Win32,
337    Win64,
338    X360,
339    PS3,
340    Tool64,
341    Win64v1,
342    PS4,
343    XboxOne,
344    Stadia,
345    PS5,
346    Scarlett,
347}
348
349impl PackagePlatform {
350    pub fn endianness(&self) -> Endian {
351        match self {
352            Self::PS3 | Self::X360 => Endian::Big,
353            Self::XboxOne | Self::PS4 | Self::Win64 => Endian::Little,
354            _ => Endian::Little,
355        }
356    }
357}
358
359impl FromStr for PackagePlatform {
360    type Err = anyhow::Error;
361
362    fn from_str(s: &str) -> Result<Self, Self::Err> {
363        Ok(match s {
364            "ps3" => Self::PS3,
365            "ps4" => Self::PS4,
366            "360" => Self::X360,
367            "w64" => Self::Win64,
368            "xboxone" => Self::XboxOne,
369            s => return Err(anyhow!("Invalid platform '{s}'")),
370        })
371    }
372}
373
374impl Display for PackagePlatform {
375    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
376        match self {
377            PackagePlatform::Tool32 => f.write_str("tool32"),
378            PackagePlatform::Win32 => f.write_str("w32"),
379            PackagePlatform::Win64 => f.write_str("w64"),
380            PackagePlatform::X360 => f.write_str("360"),
381            PackagePlatform::PS3 => f.write_str("ps3"),
382            PackagePlatform::Tool64 => f.write_str("tool64"),
383            PackagePlatform::Win64v1 => f.write_str("w64"),
384            PackagePlatform::PS4 => f.write_str("ps4"),
385            PackagePlatform::XboxOne => f.write_str("xboxone"),
386            PackagePlatform::Stadia => f.write_str("stadia"),
387            PackagePlatform::PS5 => f.write_str("ps5"),
388            PackagePlatform::Scarlett => f.write_str("scarlett"),
389        }
390    }
391}