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 #[value(name = "d1_devalpha")]
71 DestinyInternalAlpha = 1_0500,
72
73 #[value(name = "d1_flalpha")]
75 DestinyFirstLookAlpha = 1_0800,
76
77 #[value(name = "d1_ttk")]
79 DestinyTheTakenKing = 1_2000,
80
81 #[value(name = "d1_roi")]
83 DestinyRiseOfIron = 1_2400,
84
85 #[value(name = "d2_beta")]
87 Destiny2Beta = 2_1000,
88
89 #[value(name = "d2_fs")]
90 Destiny2Forsaken = 2_2000,
91
92 #[value(name = "d2_sk")]
94 Destiny2Shadowkeep = 2_2600,
95
96 #[value(name = "d2_bl")]
98 Destiny2BeyondLight = 2_3000,
99
100 #[value(name = "d2_wq")]
102 Destiny2WitchQueen = 2_4000,
103
104 #[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 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 fn get_block(&self, index: usize) -> anyhow::Result<Arc<Vec<u8>>>;
200
201 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 buffer.extend_from_slice(&block_data[..remaining_bytes]);
233 current_offset += remaining_bytes;
234 } else {
235 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 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 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
287pub fn classify_file_prebl(ftype: u8, fsubtype: u8) -> String {
289 match (ftype, fsubtype) {
290 (26, 5) => "bnk".to_string(),
292 (26, 6) => "wem".to_string(),
294 (26, 7) => "hkx".to_string(),
296 (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 (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}