diff --git a/crates/dtmt/src/cmd/bundle/inject.rs b/crates/dtmt/src/cmd/bundle/inject.rs index 3d04fd6..958615a 100644 --- a/crates/dtmt/src/cmd/bundle/inject.rs +++ b/crates/dtmt/src/cmd/bundle/inject.rs @@ -1,12 +1,13 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::str::FromStr as _; -use clap::{value_parser, Arg, ArgMatches, Command}; -use color_eyre::eyre::{self, Context, OptionExt as _, Result}; +use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; +use color_eyre::eyre::{self, Context, OptionExt, Result}; use color_eyre::Help; use path_slash::PathBufExt as _; use sdk::filetype::texture; use sdk::murmur::IdString64; -use sdk::Bundle; +use sdk::{Bundle, BundleFile, BundleFileType}; use tokio::fs; pub(crate) fn command_definition() -> Command { @@ -29,7 +30,9 @@ pub(crate) fn command_definition() -> Command { .arg( Arg::new("patch") .help("Create a patch bundle. Optionally, a patch NUMBER may be specified as \ - '--patch=123'.\nThe maximum number is 1.") + '--patch=123'.\nThe maximum number is 999, the default is 1.\n\ + If `--output` is not specified, the `.patch_` suffix is added to \ + the given bundle name.") .short('p') .long("patch") .num_args(0..=1) @@ -38,30 +41,28 @@ pub(crate) fn command_definition() -> Command { .value_name("NUMBER") .value_parser(value_parser!(u16)) ) + .arg( + Arg::new("type") + .help("Compile the new file as the given TYPE. If omitted, the file type is \ + is guessed from the file extension.") + .value_name("TYPE") + ) .subcommand( Command::new("replace") .about("Replace an existing file in the bundle") - .arg( - Arg::new("compile") - .help("Compile the file as TYPE before injecting. \ - Metadata will also be overwritten") - .long("compile") - .short('c') - .value_name("TYPE") - .value_parser(["texture"]) - ) .arg( Arg::new("variant") - .help("Sepcify the variant index to replace.") + .help("In combination with '--raw', specify the variant index to replace.") .long("variant") .default_value("0") .value_parser(value_parser!(u8)) ) .arg( - Arg::new("new-file") - .help("Path to the file to inject.") - .required(true) - .value_parser(value_parser!(PathBuf)), + Arg::new("raw") + .help("Insert the given file as raw binary data.\n\ + Cannot be used with '--patch'.") + .long("raw") + .action(ArgAction::SetTrue) ) .arg( Arg::new("bundle") @@ -73,6 +74,12 @@ pub(crate) fn command_definition() -> Command { Arg::new("bundle-file") .help("The name of a file in the bundle whose content should be replaced.") .required(true), + ) + .arg( + Arg::new("new-file") + .help("Path to the file to inject.") + .required(true) + .value_parser(value_parser!(PathBuf)), ), ) // .subcommand( @@ -93,14 +100,42 @@ pub(crate) fn command_definition() -> Command { // ) } +#[tracing::instrument] +async fn compile_file( + path: impl AsRef + std::fmt::Debug, + name: impl Into + std::fmt::Debug, + file_type: BundleFileType, +) -> Result { + let path = path.as_ref(); + + let file_data = fs::read(&path) + .await + .wrap_err_with(|| format!("Failed to read file '{}'", path.display()))?; + let sjson = String::from_utf8(file_data) + .wrap_err_with(|| format!("Invalid UTF8 data in '{}'", path.display()))?; + + let root = path.parent().ok_or_eyre("File path has no parent")?; + + match file_type { + BundleFileType::Texture => texture::compile(name.into(), sjson, root) + .await + .wrap_err_with(|| format!("Failed to compile file as texture: {}", path.display())), + _ => eyre::bail!( + "Compilation for type '{}' is not implemented, yet", + file_type + ), + } +} + #[tracing::instrument( skip_all, fields( bundle_path = tracing::field::Empty, in_file_path = tracing::field::Empty, - compile = tracing::field::Empty, output_path = tracing::field::Empty, - file_name = tracing::field::Empty, + target_name = tracing::field::Empty, + file_type = tracing::field::Empty, + raw = tracing::field::Empty, ) )] pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { @@ -108,14 +143,12 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { unreachable!("clap is configured to require a subcommand, and they're all handled above"); }; - let compile = matches.get_one::("compile"); - let bundle_path = sub_matches .get_one::("bundle") .expect("required parameter not found"); let in_file_path = sub_matches - .get_one::("file") + .get_one::("new-file") .expect("required parameter not found"); let patch_number = matches @@ -149,69 +182,46 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { IdString64::from(path.to_slash_lossy().to_string()) }; + let file_type = if let Some(forced_type) = matches.get_one::("type") { + BundleFileType::from_str(forced_type.as_str()).wrap_err("Unknown file type")? + } else { + in_file_path + .extension() + .and_then(|s| s.to_str()) + .ok_or_eyre("File extension missing") + .and_then(BundleFileType::from_str) + .wrap_err("Unknown file type") + .with_suggestion(|| "Use '--type TYPE' to specify the file type")? + }; + { let span = tracing::Span::current(); if !span.is_disabled() { span.record("bundle_path", bundle_path.display().to_string()); span.record("in_file_path", in_file_path.display().to_string()); span.record("output_path", output_path.display().to_string()); - span.record("compile", compile); - span.record("file_name", target_name.display().to_string()); + span.record("raw", sub_matches.get_flag("raw")); + span.record("target_name", target_name.display().to_string()); + span.record("file_type", format!("{:?}", file_type)); } } + let bundle_name = Bundle::get_name_from_path(&ctx, bundle_path); let mut bundle = { - let name = Bundle::get_name_from_path(&ctx, bundle_path); - if patch_number.is_some() { - unimplemented!("Create patch bundle"); - } else { - let binary = fs::read(bundle_path).await?; - Bundle::from_binary(&ctx, name, binary) - .wrap_err_with(|| format!("Failed to open bundle '{}'", bundle_path.display()))? - } + let binary = fs::read(bundle_path).await?; + Bundle::from_binary(&ctx, bundle_name.clone(), binary) + .wrap_err_with(|| format!("Failed to open bundle '{}'", bundle_path.display()))? }; if op == "copy" { unimplemented!("Implement copying a file from one bundle to the other."); } - let file_data = tokio::fs::read(&in_file_path) - .await - .wrap_err_with(|| format!("Failed to read file '{}'", in_file_path.display()))?; - - let data = if let Some(compile_type) = compile { - match compile_type.as_str() { - "texture" => { - let sjson = String::from_utf8(file_data).wrap_err_with(|| { - format!("Invalid UTF8 data in '{}'", in_file_path.display()) - })?; - - let root = in_file_path - .parent() - .ok_or_eyre("File path has no parent")?; - - let texture = texture::compile(target_name.clone(), sjson, root) - .await - .wrap_err_with(|| { - format!( - "Failed to compile file as texture: {}", - in_file_path.display() - ) - })?; - - texture.to_binary()? - } - _ => unreachable!("clap only allows file types that we implement"), - } - } else { - file_data - }; - - match op { + let output_bundle = match op { "replace" => { let Some(file) = bundle .files_mut() - .find(|file| file.matches_name(&target_name)) + .find(|file| *file.base_name() == target_name) else { let err = eyre::eyre!( "No file with name '{}' in bundle '{}'", @@ -228,34 +238,58 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { }); }; - let variant_index = sub_matches - .get_one::("variant") - .expect("argument with default missing"); + if sub_matches.get_flag("raw") { + let variant_index = sub_matches + .get_one::("variant") + .expect("argument with default missing"); - let Some(variant) = file.variants_mut().nth(*variant_index as usize) else { - let err = eyre::eyre!( - "Variant index '{}' does not exist in '{}'", - variant_index, - target_name.display() - ); + let Some(variant) = file.variants_mut().nth(*variant_index as usize) else { + let err = eyre::eyre!( + "Variant index '{}' does not exist in '{}'", + variant_index, + target_name.display() + ); - return Err(err).with_suggestion(|| { - format!( - "See '{} bundle inject add --help' if you want to add it as a new file", - clap::crate_name!(), - ) - }); - }; + return Err(err).with_suggestion(|| { + format!( + "See '{} bundle inject add --help' if you want to add it as a new file", + clap::crate_name!(), + ) + }); + }; - variant.set_data(data); + let data = tokio::fs::read(&in_file_path).await.wrap_err_with(|| { + format!("Failed to read file '{}'", in_file_path.display()) + })?; + variant.set_data(data); + file.set_modded(true); + bundle + } else { + let mut bundle_file = compile_file(in_file_path, target_name.clone(), file_type) + .await + .wrap_err("Failed to compile")?; + + bundle_file.set_modded(true); + + if patch_number.is_some() { + let mut output_bundle = Bundle::new(bundle_name); + output_bundle.add_file(bundle_file); + output_bundle + } else { + *file = bundle_file; + + dbg!(&file); + bundle + } + } } "add" => { unimplemented!("Implement adding a new file to the bundle."); } _ => unreachable!("no other operations exist"), - } + }; - let data = bundle + let data = output_bundle .to_binary() .wrap_err("Failed to write changed bundle to output")?; @@ -263,5 +297,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { .await .wrap_err_with(|| format!("Failed to write data to '{}'", output_path.display()))?; + tracing::info!("Modified bundle written to '{}'", output_path.display()); + Ok(()) } diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index f2429fa..0d0a99f 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -21,6 +21,7 @@ struct BundleFileHeader { len_data_file_name: usize, } +#[derive(Clone)] pub struct BundleFileVariant { property: u32, data: Vec, @@ -140,9 +141,12 @@ bitflags! { #[derive(Default, Clone, Copy, Debug)] pub struct Properties: u32 { const DATA = 0b100; + // A custom flag used by DTMT to signify a file altered by mods. + const MODDED = 1 << 31; } } +#[derive(Clone, Debug)] pub struct BundleFile { file_type: BundleFileType, name: IdString64, @@ -164,6 +168,18 @@ impl BundleFile { self.variants.push(variant) } + pub fn set_variants(&mut self, variants: Vec) { + self.variants = variants; + } + + pub fn set_props(&mut self, props: Properties) { + self.props = props; + } + + pub fn set_modded(&mut self, is_modded: bool) { + self.props.set(Properties::MODDED, is_modded); + } + #[tracing::instrument(name = "File::read", skip(ctx, r))] pub fn from_reader(ctx: &crate::Context, r: &mut R, props: Properties) -> Result where diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index 0d1e4b5..2d498c8 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -7,14 +7,13 @@ use color_eyre::{Help, Report, SectionExt}; use oodle::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe, CHUNK_SIZE}; use crate::binary::sync::*; -use crate::bundle::file::Properties; use crate::murmur::{HashGroup, IdString64, Murmur64}; pub(crate) mod database; pub(crate) mod file; pub(crate) mod filetype; -pub use file::{BundleFile, BundleFileVariant}; +pub use file::{BundleFile, BundleFileVariant, Properties}; pub use filetype::BundleFileType; #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] diff --git a/lib/sdk/src/lib.rs b/lib/sdk/src/lib.rs index 1174258..5fad6c8 100644 --- a/lib/sdk/src/lib.rs +++ b/lib/sdk/src/lib.rs @@ -10,5 +10,5 @@ pub mod murmur; pub use binary::{FromBinary, ToBinary}; pub use bundle::database::BundleDatabase; pub use bundle::decompress; -pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant}; +pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant, Properties}; pub use context::{CmdLine, Context};