feat: Implement bundle content listing
This commit is contained in:
parent
da188155e3
commit
109eb8ffa2
6 changed files with 694 additions and 29 deletions
|
@ -2,7 +2,10 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::{self, Result};
|
||||||
|
use color_eyre::{Help, SectionExt};
|
||||||
|
use dtmt::Bundle;
|
||||||
|
use futures::future::try_join_all;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
pub(crate) fn command_definition() -> Command {
|
pub(crate) fn command_definition() -> Command {
|
||||||
|
@ -14,16 +17,6 @@ pub(crate) fn command_definition() -> Command {
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help("Print machine-readable JSON"),
|
.help("Print machine-readable JSON"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new("oodle")
|
|
||||||
.long("oodle")
|
|
||||||
.default_value("oodle-cli")
|
|
||||||
.help(
|
|
||||||
"Name of or path to the Oodle decompression helper. \
|
|
||||||
The helper is a small executable that wraps the Oodle library \
|
|
||||||
with a CLI.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("bundle")
|
Arg::new("bundle")
|
||||||
.required(true)
|
.required(true)
|
||||||
|
@ -37,6 +30,44 @@ pub(crate) fn command_definition() -> Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub(crate) async fn run(_ctx: Arc<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> {
|
pub(crate) async fn run(ctx: Arc<RwLock<dtmt::Context>>, matches: &ArgMatches) -> Result<()> {
|
||||||
unimplemented!()
|
let bundles = matches
|
||||||
|
.get_many::<PathBuf>("bundle")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let bundles = try_join_all(bundles.into_iter().map(|p| async {
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let path_display = p.display().to_string();
|
||||||
|
async move { Bundle::open(ctx, &p).await }
|
||||||
|
.await
|
||||||
|
.with_section(|| path_display.header("Bundle Path:"))
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if matches.get_flag("json") {
|
||||||
|
unimplemented!("JSON output is not implemented yet");
|
||||||
|
} else {
|
||||||
|
for b in bundles.iter() {
|
||||||
|
println!("Bundle: {}", b.name());
|
||||||
|
|
||||||
|
for f in b.files().iter() {
|
||||||
|
if f.variants().len() != 1 {
|
||||||
|
return Err(eyre::eyre!("Expected exactly one version for this file."))
|
||||||
|
.with_section(|| f.variants().len().to_string().header("Bundle:"))
|
||||||
|
.with_section(|| b.name().clone().header("Bundle:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = &f.variants()[0];
|
||||||
|
println!(
|
||||||
|
"\t{}.{}: {} bytes",
|
||||||
|
f.name(),
|
||||||
|
f.file_type().ext_name(),
|
||||||
|
v.size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
61
src/binary.rs
Normal file
61
src/binary.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use color_eyre::eyre::WrapErr;
|
||||||
|
use color_eyre::{Help, Result, SectionExt};
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};
|
||||||
|
|
||||||
|
macro_rules! make_read {
|
||||||
|
($func:ident, $op:ident, $type:ty) => {
|
||||||
|
pub(crate) async fn $func<R>(mut r: R) -> Result<$type>
|
||||||
|
where
|
||||||
|
R: AsyncRead + AsyncSeek + std::marker::Unpin,
|
||||||
|
{
|
||||||
|
let res = r
|
||||||
|
.$op()
|
||||||
|
.await
|
||||||
|
.wrap_err(concat!("failed to read ", stringify!($type)));
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
let pos = r.stream_position().await;
|
||||||
|
if pos.is_ok() {
|
||||||
|
res.with_section(|| {
|
||||||
|
format!("{pos:#X} ({pos})", pos = pos.unwrap()).header("Position: ")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! make_skip {
|
||||||
|
($func:ident, $read:ident, $op:ident, $type:ty) => {
|
||||||
|
pub(crate) async fn $func<R>(mut r: R, cmp: $type) -> Result<()>
|
||||||
|
where
|
||||||
|
R: AsyncRead + AsyncSeek + std::marker::Unpin,
|
||||||
|
{
|
||||||
|
let val = $read(&mut r).await?;
|
||||||
|
|
||||||
|
if val != cmp {
|
||||||
|
let pos = r.stream_position().await.unwrap_or(u64::MAX);
|
||||||
|
tracing::debug!(
|
||||||
|
pos,
|
||||||
|
expected = cmp,
|
||||||
|
actual = val,
|
||||||
|
"Unexpected value for skipped {}",
|
||||||
|
stringify!($type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
make_read!(read_u8, read_u8, u8);
|
||||||
|
make_read!(read_u32, read_u32_le, u32);
|
||||||
|
make_read!(read_u64, read_u64_le, u64);
|
||||||
|
|
||||||
|
make_skip!(skip_u8, read_u8, read_u8, u8);
|
||||||
|
make_skip!(skip_u32, read_u32, read_u32_le, u32);
|
425
src/bundle/file.rs
Normal file
425
src/bundle/file.rs
Normal file
|
@ -0,0 +1,425 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use color_eyre::{Help, Result, SectionExt};
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::binary::*;
|
||||||
|
use crate::context::lookup_hash;
|
||||||
|
use crate::murmur::{HashGroup, Murmur64};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum BundleFileType {
|
||||||
|
Animation,
|
||||||
|
AnimationCurves,
|
||||||
|
Apb,
|
||||||
|
BakedLighting,
|
||||||
|
Bik,
|
||||||
|
BlendSet,
|
||||||
|
Bones,
|
||||||
|
Chroma,
|
||||||
|
CommonPackage,
|
||||||
|
Config,
|
||||||
|
Crypto,
|
||||||
|
Data,
|
||||||
|
Entity,
|
||||||
|
Flow,
|
||||||
|
Font,
|
||||||
|
Ies,
|
||||||
|
Ini,
|
||||||
|
Input,
|
||||||
|
Ivf,
|
||||||
|
Keys,
|
||||||
|
Level,
|
||||||
|
Lua,
|
||||||
|
Material,
|
||||||
|
Mod,
|
||||||
|
MouseCursor,
|
||||||
|
NavData,
|
||||||
|
NetworkConfig,
|
||||||
|
OddleNet,
|
||||||
|
Package,
|
||||||
|
Particles,
|
||||||
|
PhysicsProperties,
|
||||||
|
RenderConfig,
|
||||||
|
RtPipeline,
|
||||||
|
Scene,
|
||||||
|
Shader,
|
||||||
|
ShaderLibrary,
|
||||||
|
ShaderLibraryGroup,
|
||||||
|
ShadingEnvionmentMapping,
|
||||||
|
ShadingEnvironment,
|
||||||
|
Slug,
|
||||||
|
SlugAlbum,
|
||||||
|
SoundEnvironment,
|
||||||
|
SpuJob,
|
||||||
|
StateMachine,
|
||||||
|
StaticPVS,
|
||||||
|
Strings,
|
||||||
|
SurfaceProperties,
|
||||||
|
Texture,
|
||||||
|
TimpaniBank,
|
||||||
|
TimpaniMaster,
|
||||||
|
Tome,
|
||||||
|
Ugg,
|
||||||
|
Unit,
|
||||||
|
Upb,
|
||||||
|
VectorField,
|
||||||
|
Wav,
|
||||||
|
WwiseBank,
|
||||||
|
WwiseDep,
|
||||||
|
WwiseEvent,
|
||||||
|
WwiseMetadata,
|
||||||
|
WwiseStream,
|
||||||
|
Xml,
|
||||||
|
|
||||||
|
Unknown(Murmur64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BundleFileType {
|
||||||
|
pub fn ext_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
BundleFileType::AnimationCurves => String::from("animation_curves"),
|
||||||
|
BundleFileType::Animation => String::from("animation"),
|
||||||
|
BundleFileType::Apb => String::from("apb"),
|
||||||
|
BundleFileType::BakedLighting => String::from("baked_lighting"),
|
||||||
|
BundleFileType::Bik => String::from("bik"),
|
||||||
|
BundleFileType::BlendSet => String::from("blend_set"),
|
||||||
|
BundleFileType::Bones => String::from("bones"),
|
||||||
|
BundleFileType::Chroma => String::from("chroma"),
|
||||||
|
BundleFileType::CommonPackage => String::from("common_package"),
|
||||||
|
BundleFileType::Config => String::from("config"),
|
||||||
|
BundleFileType::Crypto => String::from("crypto"),
|
||||||
|
BundleFileType::Data => String::from("data"),
|
||||||
|
BundleFileType::Entity => String::from("entity"),
|
||||||
|
BundleFileType::Flow => String::from("flow"),
|
||||||
|
BundleFileType::Font => String::from("font"),
|
||||||
|
BundleFileType::Ies => String::from("ies"),
|
||||||
|
BundleFileType::Ini => String::from("ini"),
|
||||||
|
BundleFileType::Input => String::from("input"),
|
||||||
|
BundleFileType::Ivf => String::from("ivf"),
|
||||||
|
BundleFileType::Keys => String::from("keys"),
|
||||||
|
BundleFileType::Level => String::from("level"),
|
||||||
|
BundleFileType::Lua => String::from("lua"),
|
||||||
|
BundleFileType::Material => String::from("material"),
|
||||||
|
BundleFileType::Mod => String::from("mod"),
|
||||||
|
BundleFileType::MouseCursor => String::from("mouse_cursor"),
|
||||||
|
BundleFileType::NavData => String::from("nav_data"),
|
||||||
|
BundleFileType::NetworkConfig => String::from("network_config"),
|
||||||
|
BundleFileType::OddleNet => String::from("oodle_net"),
|
||||||
|
BundleFileType::Package => String::from("package"),
|
||||||
|
BundleFileType::Particles => String::from("particles"),
|
||||||
|
BundleFileType::PhysicsProperties => String::from("physics_properties"),
|
||||||
|
BundleFileType::RenderConfig => String::from("render_config"),
|
||||||
|
BundleFileType::RtPipeline => String::from("rt_pipeline"),
|
||||||
|
BundleFileType::Scene => String::from("scene"),
|
||||||
|
BundleFileType::ShaderLibraryGroup => String::from("shader_library_group"),
|
||||||
|
BundleFileType::ShaderLibrary => String::from("shader_library"),
|
||||||
|
BundleFileType::Shader => String::from("shader"),
|
||||||
|
BundleFileType::ShadingEnvionmentMapping => String::from("shading_environment_mapping"),
|
||||||
|
BundleFileType::ShadingEnvironment => String::from("shading_environment"),
|
||||||
|
BundleFileType::SlugAlbum => String::from("slug_album"),
|
||||||
|
BundleFileType::Slug => String::from("slug"),
|
||||||
|
BundleFileType::SoundEnvironment => String::from("sound_environment"),
|
||||||
|
BundleFileType::SpuJob => String::from("spu_job"),
|
||||||
|
BundleFileType::StateMachine => String::from("state_machine"),
|
||||||
|
BundleFileType::StaticPVS => String::from("static_pvs"),
|
||||||
|
BundleFileType::Strings => String::from("strings"),
|
||||||
|
BundleFileType::SurfaceProperties => String::from("surface_properties"),
|
||||||
|
BundleFileType::Texture => String::from("texture"),
|
||||||
|
BundleFileType::TimpaniBank => String::from("timpani_bank"),
|
||||||
|
BundleFileType::TimpaniMaster => String::from("timpani_master"),
|
||||||
|
BundleFileType::Tome => String::from("tome"),
|
||||||
|
BundleFileType::Ugg => String::from("ugg"),
|
||||||
|
BundleFileType::Unit => String::from("unit"),
|
||||||
|
BundleFileType::Upb => String::from("upb"),
|
||||||
|
BundleFileType::VectorField => String::from("vector_field"),
|
||||||
|
BundleFileType::Wav => String::from("wav"),
|
||||||
|
BundleFileType::WwiseBank => String::from("wwise_bank"),
|
||||||
|
BundleFileType::WwiseDep => String::from("wwise_dep"),
|
||||||
|
BundleFileType::WwiseEvent => String::from("wwise_event"),
|
||||||
|
BundleFileType::WwiseMetadata => String::from("wwise_metadata"),
|
||||||
|
BundleFileType::WwiseStream => String::from("wwise_stream"),
|
||||||
|
BundleFileType::Xml => String::from("xml"),
|
||||||
|
|
||||||
|
BundleFileType::Unknown(s) => format!("{:016X}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decompiled_ext_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
BundleFileType::Texture => String::from("dds"),
|
||||||
|
BundleFileType::WwiseBank => String::from("bnk"),
|
||||||
|
BundleFileType::WwiseStream => String::from("ogg"),
|
||||||
|
_ => self.ext_name(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for BundleFileType {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self::from(Murmur64::from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Murmur64> for BundleFileType {
|
||||||
|
fn from(hash: Murmur64) -> BundleFileType {
|
||||||
|
match hash.deref() {
|
||||||
|
0x931e336d7646cc26 => BundleFileType::Animation,
|
||||||
|
0xdcfb9e18fff13984 => BundleFileType::AnimationCurves,
|
||||||
|
0x3eed05ba83af5090 => BundleFileType::Apb,
|
||||||
|
0x7ffdb779b04e4ed1 => BundleFileType::BakedLighting,
|
||||||
|
0xaa5965f03029fa18 => BundleFileType::Bik,
|
||||||
|
0xe301e8af94e3b5a3 => BundleFileType::BlendSet,
|
||||||
|
0x18dead01056b72e9 => BundleFileType::Bones,
|
||||||
|
0xb7893adf7567506a => BundleFileType::Chroma,
|
||||||
|
0xfe9754bd19814a47 => BundleFileType::CommonPackage,
|
||||||
|
0x82645835e6b73232 => BundleFileType::Config,
|
||||||
|
0x69108ded1e3e634b => BundleFileType::Crypto,
|
||||||
|
0x8fd0d44d20650b68 => BundleFileType::Data,
|
||||||
|
0x9831ca893b0d087d => BundleFileType::Entity,
|
||||||
|
0x92d3ee038eeb610d => BundleFileType::Flow,
|
||||||
|
0x9efe0a916aae7880 => BundleFileType::Font,
|
||||||
|
0x8f7d5a2c0f967655 => BundleFileType::Ies,
|
||||||
|
0xd526a27da14f1dc5 => BundleFileType::Ini,
|
||||||
|
0x2bbcabe5074ade9e => BundleFileType::Input,
|
||||||
|
0xfa4a8e091a91201e => BundleFileType::Ivf,
|
||||||
|
0xa62f9297dc969e85 => BundleFileType::Keys,
|
||||||
|
0x2a690fd348fe9ac5 => BundleFileType::Level,
|
||||||
|
0xa14e8dfa2cd117e2 => BundleFileType::Lua,
|
||||||
|
0xeac0b497876adedf => BundleFileType::Material,
|
||||||
|
0x3fcdd69156a46417 => BundleFileType::Mod,
|
||||||
|
0xb277b11fe4a61d37 => BundleFileType::MouseCursor,
|
||||||
|
0x169de9566953d264 => BundleFileType::NavData,
|
||||||
|
0x3b1fa9e8f6bac374 => BundleFileType::NetworkConfig,
|
||||||
|
0xb0f2c12eb107f4d8 => BundleFileType::OddleNet,
|
||||||
|
0xad9c6d9ed1e5e77a => BundleFileType::Package,
|
||||||
|
0xa8193123526fad64 => BundleFileType::Particles,
|
||||||
|
0xbf21403a3ab0bbb1 => BundleFileType::PhysicsProperties,
|
||||||
|
0x27862fe24795319c => BundleFileType::RenderConfig,
|
||||||
|
0x9ca183c2d0e76dee => BundleFileType::RtPipeline,
|
||||||
|
0x9d0a795bfe818d19 => BundleFileType::Scene,
|
||||||
|
0xcce8d5b5f5ae333f => BundleFileType::Shader,
|
||||||
|
0xe5ee32a477239a93 => BundleFileType::ShaderLibrary,
|
||||||
|
0x9e5c3cc74575aeb5 => BundleFileType::ShaderLibraryGroup,
|
||||||
|
0x250e0a11ac8e26f8 => BundleFileType::ShadingEnvionmentMapping,
|
||||||
|
0xfe73c7dcff8a7ca5 => BundleFileType::ShadingEnvironment,
|
||||||
|
0xa27b4d04a9ba6f9e => BundleFileType::Slug,
|
||||||
|
0xe9fc9ea7042e5ec0 => BundleFileType::SlugAlbum,
|
||||||
|
0xd8b27864a97ffdd7 => BundleFileType::SoundEnvironment,
|
||||||
|
0xf97af9983c05b950 => BundleFileType::SpuJob,
|
||||||
|
0xa486d4045106165c => BundleFileType::StateMachine,
|
||||||
|
0xe3f0baa17d620321 => BundleFileType::StaticPVS,
|
||||||
|
0x0d972bab10b40fd3 => BundleFileType::Strings,
|
||||||
|
0xad2d3fa30d9ab394 => BundleFileType::SurfaceProperties,
|
||||||
|
0xcd4238c6a0c69e32 => BundleFileType::Texture,
|
||||||
|
0x99736be1fff739a4 => BundleFileType::TimpaniBank,
|
||||||
|
0x00a3e6c59a2b9c6c => BundleFileType::TimpaniMaster,
|
||||||
|
0x19c792357c99f49b => BundleFileType::Tome,
|
||||||
|
0x712d6e3dd1024c9c => BundleFileType::Ugg,
|
||||||
|
0xe0a48d0be9a7453f => BundleFileType::Unit,
|
||||||
|
0xa99510c6e86dd3c2 => BundleFileType::Upb,
|
||||||
|
0xf7505933166d6755 => BundleFileType::VectorField,
|
||||||
|
0x786f65c00a816b19 => BundleFileType::Wav,
|
||||||
|
0x535a7bd3e650d799 => BundleFileType::WwiseBank,
|
||||||
|
0xaf32095c82f2b070 => BundleFileType::WwiseDep,
|
||||||
|
0xaabdd317b58dfc8a => BundleFileType::WwiseEvent,
|
||||||
|
0xd50a8b7e1c82b110 => BundleFileType::WwiseMetadata,
|
||||||
|
0x504b55235d21440e => BundleFileType::WwiseStream,
|
||||||
|
0x76015845a6003765 => BundleFileType::Xml,
|
||||||
|
|
||||||
|
_ => BundleFileType::Unknown(hash),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BundleFileType> for Murmur64 {
|
||||||
|
fn from(t: BundleFileType) -> Murmur64 {
|
||||||
|
match t {
|
||||||
|
BundleFileType::Animation => Murmur64::from(0x931e336d7646cc26),
|
||||||
|
BundleFileType::AnimationCurves => Murmur64::from(0xdcfb9e18fff13984),
|
||||||
|
BundleFileType::Apb => Murmur64::from(0x3eed05ba83af5090),
|
||||||
|
BundleFileType::BakedLighting => Murmur64::from(0x7ffdb779b04e4ed1),
|
||||||
|
BundleFileType::Bik => Murmur64::from(0xaa5965f03029fa18),
|
||||||
|
BundleFileType::BlendSet => Murmur64::from(0xe301e8af94e3b5a3),
|
||||||
|
BundleFileType::Bones => Murmur64::from(0x18dead01056b72e9),
|
||||||
|
BundleFileType::Chroma => Murmur64::from(0xb7893adf7567506a),
|
||||||
|
BundleFileType::CommonPackage => Murmur64::from(0xfe9754bd19814a47),
|
||||||
|
BundleFileType::Config => Murmur64::from(0x82645835e6b73232),
|
||||||
|
BundleFileType::Crypto => Murmur64::from(0x69108ded1e3e634b),
|
||||||
|
BundleFileType::Data => Murmur64::from(0x8fd0d44d20650b68),
|
||||||
|
BundleFileType::Entity => Murmur64::from(0x9831ca893b0d087d),
|
||||||
|
BundleFileType::Flow => Murmur64::from(0x92d3ee038eeb610d),
|
||||||
|
BundleFileType::Font => Murmur64::from(0x9efe0a916aae7880),
|
||||||
|
BundleFileType::Ies => Murmur64::from(0x8f7d5a2c0f967655),
|
||||||
|
BundleFileType::Ini => Murmur64::from(0xd526a27da14f1dc5),
|
||||||
|
BundleFileType::Input => Murmur64::from(0x2bbcabe5074ade9e),
|
||||||
|
BundleFileType::Ivf => Murmur64::from(0xfa4a8e091a91201e),
|
||||||
|
BundleFileType::Keys => Murmur64::from(0xa62f9297dc969e85),
|
||||||
|
BundleFileType::Level => Murmur64::from(0x2a690fd348fe9ac5),
|
||||||
|
BundleFileType::Lua => Murmur64::from(0xa14e8dfa2cd117e2),
|
||||||
|
BundleFileType::Material => Murmur64::from(0xeac0b497876adedf),
|
||||||
|
BundleFileType::Mod => Murmur64::from(0x3fcdd69156a46417),
|
||||||
|
BundleFileType::MouseCursor => Murmur64::from(0xb277b11fe4a61d37),
|
||||||
|
BundleFileType::NavData => Murmur64::from(0x169de9566953d264),
|
||||||
|
BundleFileType::NetworkConfig => Murmur64::from(0x3b1fa9e8f6bac374),
|
||||||
|
BundleFileType::OddleNet => Murmur64::from(0xb0f2c12eb107f4d8),
|
||||||
|
BundleFileType::Package => Murmur64::from(0xad9c6d9ed1e5e77a),
|
||||||
|
BundleFileType::Particles => Murmur64::from(0xa8193123526fad64),
|
||||||
|
BundleFileType::PhysicsProperties => Murmur64::from(0xbf21403a3ab0bbb1),
|
||||||
|
BundleFileType::RenderConfig => Murmur64::from(0x27862fe24795319c),
|
||||||
|
BundleFileType::RtPipeline => Murmur64::from(0x9ca183c2d0e76dee),
|
||||||
|
BundleFileType::Scene => Murmur64::from(0x9d0a795bfe818d19),
|
||||||
|
BundleFileType::Shader => Murmur64::from(0xcce8d5b5f5ae333f),
|
||||||
|
BundleFileType::ShaderLibrary => Murmur64::from(0xe5ee32a477239a93),
|
||||||
|
BundleFileType::ShaderLibraryGroup => Murmur64::from(0x9e5c3cc74575aeb5),
|
||||||
|
BundleFileType::ShadingEnvionmentMapping => Murmur64::from(0x250e0a11ac8e26f8),
|
||||||
|
BundleFileType::ShadingEnvironment => Murmur64::from(0xfe73c7dcff8a7ca5),
|
||||||
|
BundleFileType::Slug => Murmur64::from(0xa27b4d04a9ba6f9e),
|
||||||
|
BundleFileType::SlugAlbum => Murmur64::from(0xe9fc9ea7042e5ec0),
|
||||||
|
BundleFileType::SoundEnvironment => Murmur64::from(0xd8b27864a97ffdd7),
|
||||||
|
BundleFileType::SpuJob => Murmur64::from(0xf97af9983c05b950),
|
||||||
|
BundleFileType::StateMachine => Murmur64::from(0xa486d4045106165c),
|
||||||
|
BundleFileType::StaticPVS => Murmur64::from(0xe3f0baa17d620321),
|
||||||
|
BundleFileType::Strings => Murmur64::from(0x0d972bab10b40fd3),
|
||||||
|
BundleFileType::SurfaceProperties => Murmur64::from(0xad2d3fa30d9ab394),
|
||||||
|
BundleFileType::Texture => Murmur64::from(0xcd4238c6a0c69e32),
|
||||||
|
BundleFileType::TimpaniBank => Murmur64::from(0x99736be1fff739a4),
|
||||||
|
BundleFileType::TimpaniMaster => Murmur64::from(0x00a3e6c59a2b9c6c),
|
||||||
|
BundleFileType::Tome => Murmur64::from(0x19c792357c99f49b),
|
||||||
|
BundleFileType::Ugg => Murmur64::from(0x712d6e3dd1024c9c),
|
||||||
|
BundleFileType::Unit => Murmur64::from(0xe0a48d0be9a7453f),
|
||||||
|
BundleFileType::Upb => Murmur64::from(0xa99510c6e86dd3c2),
|
||||||
|
BundleFileType::VectorField => Murmur64::from(0xf7505933166d6755),
|
||||||
|
BundleFileType::Wav => Murmur64::from(0x786f65c00a816b19),
|
||||||
|
BundleFileType::WwiseBank => Murmur64::from(0x535a7bd3e650d799),
|
||||||
|
BundleFileType::WwiseDep => Murmur64::from(0xaf32095c82f2b070),
|
||||||
|
BundleFileType::WwiseEvent => Murmur64::from(0xaabdd317b58dfc8a),
|
||||||
|
BundleFileType::WwiseMetadata => Murmur64::from(0xd50a8b7e1c82b110),
|
||||||
|
BundleFileType::WwiseStream => Murmur64::from(0x504b55235d21440e),
|
||||||
|
BundleFileType::Xml => Murmur64::from(0x76015845a6003765),
|
||||||
|
|
||||||
|
BundleFileType::Unknown(hash) => hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BundleFileHeader {
|
||||||
|
size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BundleFileHeader {
|
||||||
|
#[tracing::instrument(name = "FileHeader::read", skip_all)]
|
||||||
|
async fn read<R>(mut r: R) -> Result<Self>
|
||||||
|
where
|
||||||
|
R: AsyncRead + AsyncSeek + std::marker::Unpin,
|
||||||
|
{
|
||||||
|
// NOTE: One of these must be the version number, or any kind of
|
||||||
|
// identifier between the different file entries.
|
||||||
|
// Back in VT2 days, these different 'files' were used to separate
|
||||||
|
// versions, e.g. different languages for the same `.strings` file.
|
||||||
|
skip_u32(&mut r, 0).await?;
|
||||||
|
skip_u32(&mut r, 0).await?;
|
||||||
|
skip_u32(&mut r, 0).await?;
|
||||||
|
|
||||||
|
let size_1 = read_u32(&mut r).await? as usize;
|
||||||
|
|
||||||
|
skip_u8(&mut r, 1).await?;
|
||||||
|
|
||||||
|
let size_2 = read_u32(&mut r).await? as usize;
|
||||||
|
|
||||||
|
tracing::debug!(size_1, size_2);
|
||||||
|
|
||||||
|
// NOTE: Very weird. There must be something affecting this.
|
||||||
|
let size = if size_2 == 30 {
|
||||||
|
size_1 + size_2
|
||||||
|
} else {
|
||||||
|
size_1
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { size })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BundleFileVariant {
|
||||||
|
header: BundleFileHeader,
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BundleFileVariant {
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
self.header.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data(&self) -> &Vec<u8> {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BundleFile {
|
||||||
|
file_type: BundleFileType,
|
||||||
|
hash: Murmur64,
|
||||||
|
name: String,
|
||||||
|
variants: Vec<BundleFileVariant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BundleFile {
|
||||||
|
#[tracing::instrument(name = "File::read", skip_all)]
|
||||||
|
pub async fn read<R>(ctx: Arc<RwLock<crate::Context>>, mut r: R) -> Result<Self>
|
||||||
|
where
|
||||||
|
R: AsyncRead + AsyncSeek + std::marker::Unpin,
|
||||||
|
{
|
||||||
|
let file_type = BundleFileType::from(read_u64(&mut r).await?);
|
||||||
|
let hash = Murmur64::from(read_u64(&mut r).await?);
|
||||||
|
let name = lookup_hash(ctx, hash, HashGroup::Filename).await;
|
||||||
|
|
||||||
|
let header_count = read_u8(&mut r)
|
||||||
|
.await
|
||||||
|
.with_section(|| format!("{}.{}", name, file_type.ext_name()).header("File:"))?;
|
||||||
|
let header_count = header_count as usize;
|
||||||
|
|
||||||
|
let mut headers = Vec::with_capacity(header_count);
|
||||||
|
|
||||||
|
for _ in 0..header_count {
|
||||||
|
let header = BundleFileHeader::read(&mut r)
|
||||||
|
.await
|
||||||
|
.with_section(|| format!("{}.{}", name, file_type.ext_name()).header("File:"))?;
|
||||||
|
headers.push(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut variants = Vec::with_capacity(header_count);
|
||||||
|
|
||||||
|
for header in headers.into_iter() {
|
||||||
|
let mut data = vec![0; header.size];
|
||||||
|
r.read_exact(&mut data).await?;
|
||||||
|
|
||||||
|
let variant = BundleFileVariant { header, data };
|
||||||
|
|
||||||
|
variants.push(variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
variants,
|
||||||
|
file_type,
|
||||||
|
hash,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &String {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(&self) -> Murmur64 {
|
||||||
|
self.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_type(&self) -> BundleFileType {
|
||||||
|
self.file_type
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn variants(&self) -> &Vec<BundleFileVariant> {
|
||||||
|
&self.variants
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,23 @@
|
||||||
use std::io::SeekFrom;
|
use std::io::{Cursor, SeekFrom};
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use color_eyre::eyre::{self, Context, Result};
|
use color_eyre::eyre::{self, Context, Result};
|
||||||
use color_eyre::{Help, SectionExt};
|
use color_eyre::{Help, SectionExt};
|
||||||
|
use tokio::fs;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
use tracing::Instrument;
|
||||||
|
|
||||||
|
use crate::binary::*;
|
||||||
|
use crate::context::lookup_hash;
|
||||||
|
use crate::murmur::{HashGroup, Murmur64};
|
||||||
use crate::oodle;
|
use crate::oodle;
|
||||||
|
|
||||||
|
mod file;
|
||||||
|
|
||||||
|
use file::BundleFile;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum BundleFormat {
|
enum BundleFormat {
|
||||||
Darktide,
|
Darktide,
|
||||||
|
@ -24,21 +34,157 @@ impl TryFrom<u32> for BundleFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_u32<R>(mut r: R) -> Result<u32>
|
struct EntryHeader {
|
||||||
|
_name_hash: u64,
|
||||||
|
_extension_hash: u64,
|
||||||
|
_flags: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntryHeader {
|
||||||
|
#[tracing::instrument(name = "FileMeta::read", skip_all)]
|
||||||
|
async fn read<R>(mut r: R) -> Result<Self>
|
||||||
where
|
where
|
||||||
R: AsyncRead + AsyncSeek + std::marker::Unpin,
|
R: AsyncRead + AsyncSeek + std::marker::Unpin,
|
||||||
{
|
{
|
||||||
let res = r.read_u32_le().await.wrap_err("failed to read u32");
|
let extension_hash = r.read_u64().await?;
|
||||||
|
let name_hash = r.read_u64().await?;
|
||||||
|
let flags = read_u32(r).await?;
|
||||||
|
|
||||||
if res.is_err() {
|
// NOTE: Known values so far:
|
||||||
let pos = r.stream_position().await;
|
// - 0x0: seems to be the default
|
||||||
if pos.is_ok() {
|
// - 0x4: seems to be used for files that point to something in `data/`
|
||||||
res.with_section(|| pos.unwrap().to_string().header("Position: "))
|
// seems to correspond to a change in value in the header's 'unknown_3'
|
||||||
} else {
|
if flags != 0x0 {
|
||||||
res
|
tracing::debug!(
|
||||||
|
flags,
|
||||||
|
"Unexpected meta flags for file {:08X}.{:08X}",
|
||||||
|
name_hash,
|
||||||
|
extension_hash
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
_name_hash: name_hash,
|
||||||
|
_extension_hash: extension_hash,
|
||||||
|
_flags: flags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Bundle {
|
||||||
|
_format: BundleFormat,
|
||||||
|
_headers: Vec<EntryHeader>,
|
||||||
|
files: Vec<BundleFile>,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bundle {
|
||||||
|
#[tracing::instrument(name = "Bundle::open", skip(ctx))]
|
||||||
|
pub async fn open<P>(ctx: Arc<RwLock<crate::Context>>, path: P) -> Result<Self>
|
||||||
|
where
|
||||||
|
P: AsRef<Path> + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let path = path.as_ref();
|
||||||
|
let bundle_name = if let Some(name) = path.file_name() {
|
||||||
|
let hash = Murmur64::try_from(name.to_string_lossy().as_ref())?;
|
||||||
|
lookup_hash(ctx.clone(), hash, HashGroup::Filename).await
|
||||||
} else {
|
} else {
|
||||||
res
|
return Err(eyre::eyre!("Invalid path to bundle file"))
|
||||||
|
.with_section(|| path.display().to_string().header("Path:"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut r = fs::File::open(path)
|
||||||
|
.await
|
||||||
|
.wrap_err("Failed to open bundle file")
|
||||||
|
.with_section(|| path.display().to_string().header("Path"))?;
|
||||||
|
|
||||||
|
let format = read_u32(&mut r)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to read from file")
|
||||||
|
.and_then(BundleFormat::try_from)?;
|
||||||
|
|
||||||
|
if format != BundleFormat::Darktide {
|
||||||
|
return Err(eyre::eyre!("Unknown bundle format: {:?}", format));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip unknown 4 bytes
|
||||||
|
r.seek(SeekFrom::Current(4)).await?;
|
||||||
|
|
||||||
|
let num_entries = read_u32(&mut r).await? as usize;
|
||||||
|
|
||||||
|
// Skip unknown 256 bytes. I believe this data is somewhat related to packaging and the
|
||||||
|
// `.package` files
|
||||||
|
r.seek(SeekFrom::Current(256)).await?;
|
||||||
|
|
||||||
|
let mut meta = Vec::with_capacity(num_entries);
|
||||||
|
for _ in 0..num_entries {
|
||||||
|
meta.push(EntryHeader::read(&mut r).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_chunks = read_u32(&mut r).await? as usize;
|
||||||
|
tracing::debug!(num_chunks);
|
||||||
|
let mut chunk_sizes = Vec::with_capacity(num_chunks);
|
||||||
|
for _ in 0..num_chunks {
|
||||||
|
chunk_sizes.push(read_u32(&mut r).await? as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
let unpacked_size = {
|
||||||
|
let size_1 = read_u32(&mut r).await? as usize;
|
||||||
|
|
||||||
|
// Skip unknown 4 bytes
|
||||||
|
r.seek(SeekFrom::Current(4)).await?;
|
||||||
|
|
||||||
|
// NOTE: Unknown why this sometimes needs a second value.
|
||||||
|
// Also unknown if there is a different part in the data that actually
|
||||||
|
// determines whether this second value exists.
|
||||||
|
if size_1 == 0x0 {
|
||||||
|
let size_2 = read_u32(&mut r).await? as usize;
|
||||||
|
// Skip unknown 4 bytes
|
||||||
|
r.seek(SeekFrom::Current(4)).await?;
|
||||||
|
size_2
|
||||||
|
} else {
|
||||||
|
size_1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut decompressed = Vec::new();
|
||||||
|
oodle::decompress(ctx.clone(), r, &mut decompressed, num_chunks).await?;
|
||||||
|
|
||||||
|
if decompressed.len() < unpacked_size {
|
||||||
|
return Err(eyre::eyre!(
|
||||||
|
"Decompressed data does not match the expected size"
|
||||||
|
))
|
||||||
|
.with_section(|| decompressed.len().to_string().header("Actual:"))
|
||||||
|
.with_section(|| unpacked_size.to_string().header("Expected:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate to the actual data size
|
||||||
|
decompressed.resize(unpacked_size, 0);
|
||||||
|
|
||||||
|
let mut r = Cursor::new(decompressed);
|
||||||
|
let mut files = Vec::with_capacity(num_entries);
|
||||||
|
for i in 0..num_entries {
|
||||||
|
let span = tracing::trace_span!("", file_index = i);
|
||||||
|
let file = BundleFile::read(ctx.clone(), &mut r)
|
||||||
|
.instrument(span)
|
||||||
|
.await?;
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name: bundle_name,
|
||||||
|
_format: format,
|
||||||
|
_headers: meta,
|
||||||
|
files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &String {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn files(&self) -> &Vec<BundleFile> {
|
||||||
|
&self.files
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
mod binary;
|
||||||
mod bundle;
|
mod bundle;
|
||||||
mod context;
|
mod context;
|
||||||
pub mod murmur;
|
pub mod murmur;
|
||||||
mod oodle;
|
mod oodle;
|
||||||
|
|
||||||
pub use bundle::decompress;
|
pub use bundle::decompress;
|
||||||
|
pub use bundle::Bundle;
|
||||||
pub use context::lookup_hash;
|
pub use context::lookup_hash;
|
||||||
pub use context::lookup_hash_short;
|
pub use context::lookup_hash_short;
|
||||||
pub use context::Context;
|
pub use context::Context;
|
||||||
|
|
|
@ -78,9 +78,9 @@ where
|
||||||
if !res.status.success() {
|
if !res.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&res.stderr);
|
let stderr = String::from_utf8_lossy(&res.stderr);
|
||||||
let stdout = String::from_utf8_lossy(&res.stdout);
|
let stdout = String::from_utf8_lossy(&res.stdout);
|
||||||
return Err(eyre::eyre!("failed to run Oodle decompression helper")
|
return Err(eyre::eyre!("failed to run Oodle decompression helper"))
|
||||||
.with_section(move || stdout.to_string().header("Logs:"))
|
.with_section(move || stdout.to_string().header("Logs:"))
|
||||||
.with_section(move || stderr.to_string().header("Stderr:")));
|
.with_section(move || stderr.to_string().header("Stderr:"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Add table
Reference in a new issue