WIP: Implement texture files #191
4 changed files with 141 additions and 90 deletions
|
@ -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 clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||||
use color_eyre::eyre::{self, Context, OptionExt as _, Result};
|
use color_eyre::eyre::{self, Context, OptionExt, Result};
|
||||||
use color_eyre::Help;
|
use color_eyre::Help;
|
||||||
use path_slash::PathBufExt as _;
|
use path_slash::PathBufExt as _;
|
||||||
use sdk::filetype::texture;
|
use sdk::filetype::texture;
|
||||||
use sdk::murmur::IdString64;
|
use sdk::murmur::IdString64;
|
||||||
use sdk::Bundle;
|
use sdk::{Bundle, BundleFile, BundleFileType};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
pub(crate) fn command_definition() -> Command {
|
pub(crate) fn command_definition() -> Command {
|
||||||
|
@ -29,7 +30,9 @@ pub(crate) fn command_definition() -> Command {
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("patch")
|
Arg::new("patch")
|
||||||
.help("Create a patch bundle. Optionally, a patch NUMBER may be specified as \
|
.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_<NUMBER>` suffix is added to \
|
||||||
|
the given bundle name.")
|
||||||
.short('p')
|
.short('p')
|
||||||
.long("patch")
|
.long("patch")
|
||||||
.num_args(0..=1)
|
.num_args(0..=1)
|
||||||
|
@ -38,30 +41,28 @@ pub(crate) fn command_definition() -> Command {
|
||||||
.value_name("NUMBER")
|
.value_name("NUMBER")
|
||||||
.value_parser(value_parser!(u16))
|
.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(
|
.subcommand(
|
||||||
Command::new("replace")
|
Command::new("replace")
|
||||||
.about("Replace an existing file in the bundle")
|
.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(
|
||||||
Arg::new("variant")
|
Arg::new("variant")
|
||||||
.help("Sepcify the variant index to replace.")
|
.help("In combination with '--raw', specify the variant index to replace.")
|
||||||
.long("variant")
|
.long("variant")
|
||||||
.default_value("0")
|
.default_value("0")
|
||||||
.value_parser(value_parser!(u8))
|
.value_parser(value_parser!(u8))
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("new-file")
|
Arg::new("raw")
|
||||||
.help("Path to the file to inject.")
|
.help("Insert the given file as raw binary data.\n\
|
||||||
.required(true)
|
Cannot be used with '--patch'.")
|
||||||
.value_parser(value_parser!(PathBuf)),
|
.long("raw")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("bundle")
|
Arg::new("bundle")
|
||||||
|
@ -73,6 +74,12 @@ pub(crate) fn command_definition() -> Command {
|
||||||
Arg::new("bundle-file")
|
Arg::new("bundle-file")
|
||||||
.help("The name of a file in the bundle whose content should be replaced.")
|
.help("The name of a file in the bundle whose content should be replaced.")
|
||||||
.required(true),
|
.required(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("new-file")
|
||||||
|
.help("Path to the file to inject.")
|
||||||
|
.required(true)
|
||||||
|
.value_parser(value_parser!(PathBuf)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// .subcommand(
|
// .subcommand(
|
||||||
|
@ -93,14 +100,42 @@ pub(crate) fn command_definition() -> Command {
|
||||||
// )
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn compile_file(
|
||||||
|
path: impl AsRef<Path> + std::fmt::Debug,
|
||||||
|
name: impl Into<IdString64> + std::fmt::Debug,
|
||||||
|
file_type: BundleFileType,
|
||||||
|
) -> Result<BundleFile> {
|
||||||
|
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(
|
#[tracing::instrument(
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(
|
fields(
|
||||||
bundle_path = tracing::field::Empty,
|
bundle_path = tracing::field::Empty,
|
||||||
in_file_path = tracing::field::Empty,
|
in_file_path = tracing::field::Empty,
|
||||||
compile = tracing::field::Empty,
|
|
||||||
output_path = 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<()> {
|
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");
|
unreachable!("clap is configured to require a subcommand, and they're all handled above");
|
||||||
};
|
};
|
||||||
|
|
||||||
let compile = matches.get_one::<String>("compile");
|
|
||||||
|
|
||||||
let bundle_path = sub_matches
|
let bundle_path = sub_matches
|
||||||
.get_one::<PathBuf>("bundle")
|
.get_one::<PathBuf>("bundle")
|
||||||
.expect("required parameter not found");
|
.expect("required parameter not found");
|
||||||
|
|
||||||
let in_file_path = sub_matches
|
let in_file_path = sub_matches
|
||||||
.get_one::<PathBuf>("file")
|
.get_one::<PathBuf>("new-file")
|
||||||
.expect("required parameter not found");
|
.expect("required parameter not found");
|
||||||
|
|
||||||
let patch_number = matches
|
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())
|
IdString64::from(path.to_slash_lossy().to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let file_type = if let Some(forced_type) = matches.get_one::<String>("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();
|
let span = tracing::Span::current();
|
||||||
if !span.is_disabled() {
|
if !span.is_disabled() {
|
||||||
span.record("bundle_path", bundle_path.display().to_string());
|
span.record("bundle_path", bundle_path.display().to_string());
|
||||||
span.record("in_file_path", in_file_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("output_path", output_path.display().to_string());
|
||||||
span.record("compile", compile);
|
span.record("raw", sub_matches.get_flag("raw"));
|
||||||
span.record("file_name", target_name.display().to_string());
|
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 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?;
|
let binary = fs::read(bundle_path).await?;
|
||||||
Bundle::from_binary(&ctx, name, binary)
|
Bundle::from_binary(&ctx, bundle_name.clone(), binary)
|
||||||
.wrap_err_with(|| format!("Failed to open bundle '{}'", bundle_path.display()))?
|
.wrap_err_with(|| format!("Failed to open bundle '{}'", bundle_path.display()))?
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if op == "copy" {
|
if op == "copy" {
|
||||||
unimplemented!("Implement copying a file from one bundle to the other.");
|
unimplemented!("Implement copying a file from one bundle to the other.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_data = tokio::fs::read(&in_file_path)
|
let output_bundle = match op {
|
||||||
.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 {
|
|
||||||
"replace" => {
|
"replace" => {
|
||||||
let Some(file) = bundle
|
let Some(file) = bundle
|
||||||
.files_mut()
|
.files_mut()
|
||||||
.find(|file| file.matches_name(&target_name))
|
.find(|file| *file.base_name() == target_name)
|
||||||
else {
|
else {
|
||||||
let err = eyre::eyre!(
|
let err = eyre::eyre!(
|
||||||
"No file with name '{}' in bundle '{}'",
|
"No file with name '{}' in bundle '{}'",
|
||||||
|
@ -228,6 +238,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if sub_matches.get_flag("raw") {
|
||||||
let variant_index = sub_matches
|
let variant_index = sub_matches
|
||||||
.get_one::<u8>("variant")
|
.get_one::<u8>("variant")
|
||||||
.expect("argument with default missing");
|
.expect("argument with default missing");
|
||||||
|
@ -247,15 +258,38 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
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" => {
|
"add" => {
|
||||||
unimplemented!("Implement adding a new file to the bundle.");
|
unimplemented!("Implement adding a new file to the bundle.");
|
||||||
}
|
}
|
||||||
_ => unreachable!("no other operations exist"),
|
_ => unreachable!("no other operations exist"),
|
||||||
}
|
};
|
||||||
|
|
||||||
let data = bundle
|
let data = output_bundle
|
||||||
.to_binary()
|
.to_binary()
|
||||||
.wrap_err("Failed to write changed bundle to output")?;
|
.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
|
.await
|
||||||
.wrap_err_with(|| format!("Failed to write data to '{}'", output_path.display()))?;
|
.wrap_err_with(|| format!("Failed to write data to '{}'", output_path.display()))?;
|
||||||
|
|
||||||
|
tracing::info!("Modified bundle written to '{}'", output_path.display());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ struct BundleFileHeader {
|
||||||
len_data_file_name: usize,
|
len_data_file_name: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct BundleFileVariant {
|
pub struct BundleFileVariant {
|
||||||
property: u32,
|
property: u32,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
|
@ -140,9 +141,12 @@ bitflags! {
|
||||||
#[derive(Default, Clone, Copy, Debug)]
|
#[derive(Default, Clone, Copy, Debug)]
|
||||||
pub struct Properties: u32 {
|
pub struct Properties: u32 {
|
||||||
const DATA = 0b100;
|
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 {
|
pub struct BundleFile {
|
||||||
file_type: BundleFileType,
|
file_type: BundleFileType,
|
||||||
name: IdString64,
|
name: IdString64,
|
||||||
|
@ -164,6 +168,18 @@ impl BundleFile {
|
||||||
self.variants.push(variant)
|
self.variants.push(variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_variants(&mut self, variants: Vec<BundleFileVariant>) {
|
||||||
|
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))]
|
#[tracing::instrument(name = "File::read", skip(ctx, r))]
|
||||||
pub fn from_reader<R>(ctx: &crate::Context, r: &mut R, props: Properties) -> Result<Self>
|
pub fn from_reader<R>(ctx: &crate::Context, r: &mut R, props: Properties) -> Result<Self>
|
||||||
where
|
where
|
||||||
|
|
|
@ -7,14 +7,13 @@ use color_eyre::{Help, Report, SectionExt};
|
||||||
use oodle::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe, CHUNK_SIZE};
|
use oodle::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe, CHUNK_SIZE};
|
||||||
|
|
||||||
use crate::binary::sync::*;
|
use crate::binary::sync::*;
|
||||||
use crate::bundle::file::Properties;
|
|
||||||
use crate::murmur::{HashGroup, IdString64, Murmur64};
|
use crate::murmur::{HashGroup, IdString64, Murmur64};
|
||||||
|
|
||||||
pub(crate) mod database;
|
pub(crate) mod database;
|
||||||
pub(crate) mod file;
|
pub(crate) mod file;
|
||||||
pub(crate) mod filetype;
|
pub(crate) mod filetype;
|
||||||
|
|
||||||
pub use file::{BundleFile, BundleFileVariant};
|
pub use file::{BundleFile, BundleFileVariant, Properties};
|
||||||
pub use filetype::BundleFileType;
|
pub use filetype::BundleFileType;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
|
||||||
|
|
|
@ -10,5 +10,5 @@ pub mod murmur;
|
||||||
pub use binary::{FromBinary, ToBinary};
|
pub use binary::{FromBinary, ToBinary};
|
||||||
pub use bundle::database::BundleDatabase;
|
pub use bundle::database::BundleDatabase;
|
||||||
pub use bundle::decompress;
|
pub use bundle::decompress;
|
||||||
pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant};
|
pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant, Properties};
|
||||||
pub use context::{CmdLine, Context};
|
pub use context::{CmdLine, Context};
|
||||||
|
|
Loading…
Add table
Reference in a new issue