use std::{
fmt::{Display, Formatter},
io::{Read, Seek},
str::FromStr,
sync::Arc,
};
use anyhow::{anyhow, ensure};
use binrw::{BinRead, Endian};
use clap::ValueEnum;
use crate::{
d1_internal_alpha::PackageD1InternalAlpha, d1_legacy::PackageD1Legacy,
d1_roi::PackageD1RiseOfIron, d2_beta::PackageD2Beta, d2_beyondlight::PackageD2BeyondLight,
d2_shared::PackageNamedTagEntry, PackageD2PreBL, TagHash,
};
pub const BLOCK_CACHE_SIZE: usize = 128;
pub trait ReadSeek: Read + Seek {}
impl<R: Read + Seek> ReadSeek for R {}
#[derive(Clone, Debug, bincode::Decode, bincode::Encode)]
pub struct UEntryHeader {
pub reference: u32,
pub file_type: u8,
pub file_subtype: u8,
pub starting_block: u32,
pub starting_block_offset: u32,
pub file_size: u32,
}
#[derive(Clone)]
pub struct UHashTableEntry {
pub hash64: u64,
pub hash32: TagHash,
pub reference: TagHash,
}
#[derive(BinRead, Debug, Copy, Clone)]
#[br(repr = u16)]
pub enum PackageLanguage {
None = 0,
English = 1,
French = 2,
Italian = 3,
German = 4,
Spanish = 5,
Japanese = 6,
Portuguese = 7,
Russian = 8,
Polish = 9,
SimplifiedChinese = 10,
TraditionalChinese = 11,
SpanishLatAm = 12,
Korean = 13,
}
impl PackageLanguage {
pub fn english_or_none(&self) -> bool {
matches!(self, Self::None | Self::English)
}
}
#[derive(
serde::Serialize, serde::Deserialize, clap::ValueEnum, PartialEq, PartialOrd, Debug, Clone, Copy,
)]
pub enum GameVersion {
#[value(name = "d1_devalpha")]
DestinyInternalAlpha = 1_0500,
#[value(name = "d1_ttk")]
DestinyTheTakenKing = 1_2000,
#[value(name = "d1_roi")]
DestinyRiseOfIron = 1_2400,
#[value(name = "d2_beta")]
Destiny2Beta = 2_1000,
#[value(name = "d2_fs")]
Destiny2Forsaken = 2_2000,
#[value(name = "d2_sk")]
Destiny2Shadowkeep = 2_2600,
#[value(name = "d2_bl")]
Destiny2BeyondLight,
#[value(name = "d2_wq")]
Destiny2WitchQueen = 4000,
#[value(name = "d2_lf")]
Destiny2Lightfall = 7000,
#[value(name = "d2_tfs")]
Destiny2TheFinalShape = 8000,
}
impl GameVersion {
pub fn open(&self, path: &str) -> anyhow::Result<Arc<dyn Package>> {
Ok(match self {
GameVersion::DestinyInternalAlpha => Arc::new(PackageD1InternalAlpha::open(path)?),
GameVersion::DestinyTheTakenKing => Arc::new(PackageD1Legacy::open(path)?),
GameVersion::DestinyRiseOfIron => Arc::new(PackageD1RiseOfIron::open(path)?),
GameVersion::Destiny2Beta => Arc::new(PackageD2Beta::open(path)?),
GameVersion::Destiny2Forsaken | GameVersion::Destiny2Shadowkeep => {
Arc::new(PackageD2PreBL::open(path)?)
}
GameVersion::Destiny2BeyondLight
| GameVersion::Destiny2WitchQueen
| GameVersion::Destiny2Lightfall
| GameVersion::Destiny2TheFinalShape => {
Arc::new(PackageD2BeyondLight::open(path, *self)?)
}
})
}
pub fn endian(&self) -> Endian {
match self {
GameVersion::DestinyInternalAlpha | GameVersion::DestinyTheTakenKing => Endian::Big,
_ => Endian::Little,
}
}
pub fn is_d1(&self) -> bool {
matches!(
self,
GameVersion::DestinyInternalAlpha
| GameVersion::DestinyTheTakenKing
| GameVersion::DestinyRiseOfIron
)
}
pub fn is_d2(&self) -> bool {
matches!(
self,
GameVersion::Destiny2Beta
| GameVersion::Destiny2Shadowkeep
| GameVersion::Destiny2BeyondLight
| GameVersion::Destiny2WitchQueen
| GameVersion::Destiny2Lightfall
)
}
pub fn id(&self) -> String {
self.to_possible_value()
.expect("Package version is missing an id/commandline value")
.get_name()
.to_string()
}
pub fn name(&self) -> &'static str {
match self {
GameVersion::DestinyInternalAlpha => "Destiny X360 Internal Alpha",
GameVersion::DestinyTheTakenKing => "Destiny: The Taken King",
GameVersion::DestinyRiseOfIron => "Destiny: Rise of Iron",
GameVersion::Destiny2Beta => "Destiny 2: Beta",
GameVersion::Destiny2Forsaken => "Destiny 2: Forsaken",
GameVersion::Destiny2Shadowkeep => "Destiny 2: Shadowkeep",
GameVersion::Destiny2BeyondLight => "Destiny 2: Beyond Light",
GameVersion::Destiny2WitchQueen => "Destiny 2: Witch Queen",
GameVersion::Destiny2Lightfall => "Destiny 2: Lightfall",
GameVersion::Destiny2TheFinalShape => "Destiny 2: The Final Shape",
}
}
}
pub trait Package: Send + Sync {
fn endianness(&self) -> binrw::Endian;
fn pkg_id(&self) -> u16;
fn patch_id(&self) -> u16;
fn hash64_table(&self) -> Vec<UHashTableEntry>;
fn named_tags(&self) -> Vec<PackageNamedTagEntry>;
fn entries(&self) -> &[UEntryHeader];
fn entry(&self, index: usize) -> Option<UEntryHeader>;
fn language(&self) -> PackageLanguage;
fn platform(&self) -> PackagePlatform;
fn get_block(&self, index: usize) -> anyhow::Result<Arc<Vec<u8>>>;
fn read_entry(&self, index: usize) -> anyhow::Result<Vec<u8>> {
let _span = tracing::debug_span!("Package::read_entry").entered();
let entry = self
.entry(index)
.ok_or(anyhow!("Entry index is out of range"))?;
let mut buffer = Vec::with_capacity(entry.file_size as usize);
let mut current_offset = 0usize;
let mut current_block = entry.starting_block;
while current_offset < entry.file_size as usize {
let remaining_bytes = entry.file_size as usize - current_offset;
let block_data = self.get_block(current_block as usize)?;
if current_block == entry.starting_block {
let block_start_offset = entry.starting_block_offset as usize;
let block_remaining = block_data.len() - block_start_offset;
let copy_size = if block_remaining < remaining_bytes {
block_remaining
} else {
remaining_bytes
};
buffer.extend_from_slice(
&block_data[block_start_offset..block_start_offset + copy_size],
);
current_offset += copy_size;
} else if remaining_bytes < block_data.len() {
buffer.extend_from_slice(&block_data[..remaining_bytes]);
current_offset += remaining_bytes;
} else {
buffer.extend_from_slice(&block_data[..]);
current_offset += block_data.len();
}
current_block += 1;
}
Ok(buffer)
}
fn read_tag(&self, tag: TagHash) -> anyhow::Result<Vec<u8>> {
ensure!(tag.pkg_id() == self.pkg_id());
self.read_entry(tag.entry_index() as _)
}
fn get_all_by_reference(&self, reference: u32) -> Vec<(usize, UEntryHeader)> {
self.entries()
.iter()
.enumerate()
.filter(|(_, e)| e.reference == reference)
.map(|(i, e)| (i, e.clone()))
.collect()
}
fn get_all_by_type(&self, etype: u8, esubtype: Option<u8>) -> Vec<(usize, UEntryHeader)> {
self.entries()
.iter()
.enumerate()
.filter(|(_, e)| {
e.file_type == etype && esubtype.map(|t| t == e.file_subtype).unwrap_or(true)
})
.map(|(i, e)| (i, e.clone()))
.collect()
}
}
pub fn classify_file_prebl(ftype: u8, fsubtype: u8) -> String {
match (ftype, fsubtype) {
(26, 5) => "bnk".to_string(),
(26, 6) => "wem".to_string(),
(26, 7) => "hkx".to_string(),
(27, _) => "usm".to_string(),
(32, 1) => "texture.header".to_string(),
(32, 2) => "texture_cube.header".to_string(),
(32, 4) => "vertex.header".to_string(),
(32, 6) => "index.header".to_string(),
(40, 4) => "vertex.data".to_string(),
(40, 6) => "index.data".to_string(),
(48, 1) => "texture.data".to_string(),
(48, 2) => "texture_cube.data".to_string(),
(41, shader_type) => {
let ty = match shader_type {
0 => "fragment".to_string(),
1 => "vertex".to_string(),
6 => "compute".to_string(),
u => format!("unk{u}"),
};
format!("cso.{ty}")
}
(8, _) => "8080".to_string(),
_ => "bin".to_string(),
}
}
#[derive(
serde::Serialize,
serde::Deserialize,
clap::ValueEnum,
PartialEq,
Eq,
Debug,
Clone,
Copy,
BinRead,
)]
#[br(repr = u16)]
pub enum PackagePlatform {
Tool32,
Win32,
Win64,
X360,
PS3,
Tool64,
Win64v1,
PS4,
XboxOne,
Stadia,
PS5,
Scarlett,
}
impl PackagePlatform {
pub fn endianness(&self) -> Endian {
match self {
Self::PS3 | Self::X360 => Endian::Big,
Self::XboxOne | Self::PS4 | Self::Win64 => Endian::Little,
_ => Endian::Little,
}
}
}
impl FromStr for PackagePlatform {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"ps3" => Self::PS3,
"ps4" => Self::PS4,
"360" => Self::X360,
"w64" => Self::Win64,
"xboxone" => Self::XboxOne,
s => return Err(anyhow!("Invalid platform '{s}'")),
})
}
}
impl Display for PackagePlatform {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PackagePlatform::Tool32 => f.write_str("tool32"),
PackagePlatform::Win32 => f.write_str("w32"),
PackagePlatform::Win64 => f.write_str("w64"),
PackagePlatform::X360 => f.write_str("360"),
PackagePlatform::PS3 => f.write_str("ps3"),
PackagePlatform::Tool64 => f.write_str("tool64"),
PackagePlatform::Win64v1 => f.write_str("w64"),
PackagePlatform::PS4 => f.write_str("ps4"),
PackagePlatform::XboxOne => f.write_str("xboxone"),
PackagePlatform::Stadia => f.write_str("stadia"),
PackagePlatform::PS5 => f.write_str("ps5"),
PackagePlatform::Scarlett => f.write_str("scarlett"),
}
}
}