From e64ae362efafd2ed73e78a5ac6167f191996d51e Mon Sep 17 00:00:00 2001 From: Simon Bruder Date: Sat, 27 Jun 2020 17:15:37 +0200 Subject: [PATCH] Add unarc mode --- Cargo.lock | 6 +++ Cargo.toml | 1 + README.md | 16 +++++- src/ddr.rs | 1 + src/ddr/arc.rs | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 47 +++++++++++++++- 6 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 src/ddr/arc.rs diff --git a/Cargo.lock b/Cargo.lock index 47e9bd5..7b5bb27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,7 @@ dependencies = [ "anyhow", "byteorder", "clap", + "konami-lz77", "log", "num-derive", "num-traits", @@ -186,6 +187,11 @@ dependencies = [ "autocfg", ] +[[package]] +name = "konami-lz77" +version = "0.2.0" +source = "git+https://github.com/sbruder/konami-lz77#78c83a87a64d3c1826e168fb90ea9d671f8cfcff" + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index e7e04fc..a0868b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ anyhow = "1.0.31" byteorder = "1.3.4" clap = "3.0.0-beta.1" log = "0.4.8" +konami-lz77 = { git = "https://github.com/sbruder/konami-lz77" } num-derive = "0.3" num-traits = "0.2" pretty_env_logger = "0.4" diff --git a/README.md b/README.md index fa3c8f5..cf8da44 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # BRD BRD is a tool for working with [*Dance Dance Revolution*][ddr] step charts and -wave banks. It currently supports conversion to [*osu!mania*][osu!mania] -beatmaps and extraction of sounds from wave banks. +wave banks. For currently supported features, see the [Modes](#modes) section. ## Installation @@ -45,6 +44,16 @@ Basic Usage: brd unxwb file.xwb brd unxwb -l file.xwb +### unarc + +This can list and extract files from DDR A ARC archives. It extracts the +contents into the current directory. + +Basic Usage: + + brd unarc file.arc + brd unarc -l file.arc + #### Known Problems * It only supports sounds in [ADPCM][ADPCM] format. If you want to extract @@ -69,6 +78,7 @@ resources: * The [official osu! file format documentation][osu-doc] * [MonoGame][MonoGame]’s [XWB implementation][MonoGame-xwb] * [Luigi Auriemma][aluigi]’s [unxwb][unxwb] (especially the ADPCM header part) + * [mon][mon]’s [ddr\_arc\_extract][ddr_arc_extract] ## License @@ -82,6 +92,8 @@ This project is not affiliated with ppy or Konami. [SaxxonPike]: https://github.com/SaxxonPike [aluigi]: http://aluigi.altervista.org/ [ddr]: https://en.wikipedia.org/wiki/Dance_Dance_Revolution +[ddr_arc_extract]: https://github.com/mon/ddr_arc_extract +[mon]: https://github.com/mon [multimedia.cx-XSB]: https://wiki.multimedia.cx/index.php/XACT#Sound_Banks_.28.xsb.29 [osu!mania]: https://osu.ppy.sh/help/wiki/Game_Modes/osu%21mania [osu-doc]: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format) diff --git a/src/ddr.rs b/src/ddr.rs index 5ec8878..cb63c74 100644 --- a/src/ddr.rs +++ b/src/ddr.rs @@ -1 +1,2 @@ +pub mod arc; pub mod ssq; diff --git a/src/ddr/arc.rs b/src/ddr/arc.rs new file mode 100644 index 0000000..28fc0bc --- /dev/null +++ b/src/ddr/arc.rs @@ -0,0 +1,143 @@ +use std::collections::HashMap; +use std::convert::TryInto; +use std::io; +use std::io::prelude::*; +use std::io::Cursor; +use std::num; +use std::path::PathBuf; + +use byteorder::{ReadBytesExt, LE}; +use konami_lz77::decompress; +use log::{debug, info, trace, warn}; +use thiserror::Error; + +use crate::mini_parser; + +const MAGIC: u32 = 0x19751120; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Invalid magic (expected {expected:#x}, found {found:#x})")] + InvalidMagic { expected: u32, found: u32 }, + #[error("Invalid size after decompresseion (expected {expected}, found {found})")] + DecompressionSize { expected: usize, found: usize }, + #[error(transparent)] + IOError(#[from] io::Error), + #[error(transparent)] + TryFromIntError(#[from] num::TryFromIntError), + #[error(transparent)] + MiniParserError(#[from] mini_parser::MiniParserError), +} + +#[derive(Debug)] +struct CueEntry { + name_offset: usize, + data_offset: usize, + decompressed_size: usize, + compressed_size: usize, +} + +impl CueEntry { + fn parse(data: &[u8]) -> Result { + let mut cursor = Cursor::new(data); + + let name_offset = cursor.read_u32::()?.try_into()?; + let data_offset = cursor.read_u32::()?.try_into()?; + let decompressed_size = cursor.read_u32::()?.try_into()?; + let compressed_size = cursor.read_u32::()?.try_into()?; + + Ok(Self { + name_offset, + data_offset, + decompressed_size, + compressed_size, + }) + } +} + +pub struct ARC { + pub files: HashMap>, +} + +impl ARC { + pub fn parse(data: &[u8]) -> Result { + let mut cursor = Cursor::new(data); + + let magic = cursor.read_u32::()?; + if magic != MAGIC { + return Err(Error::InvalidMagic { + expected: MAGIC, + found: magic, + }); + } + + let version = cursor.read_u32::()?; + debug!("Recognised archive (version {})", version); + if version != 1 { + warn!("Unknown version {}, continuing anyway", version); + } + + let file_count = cursor.read_u32::()?; + debug!("Archive contains {} files", file_count); + + let _compression = cursor.read_u32::()?; + + let mut cue = Vec::new(); + cursor + .take((4 * 4 * file_count).into()) + .read_to_end(&mut cue)?; + let cue: Vec = cue + .chunks(4 * 4) + .map(CueEntry::parse) + .collect::>()?; + + let mut files = HashMap::new(); + + for entry in cue { + let path = PathBuf::from( + String::from_utf8_lossy( + &mini_parser::get_slice_range(data, entry.name_offset..data.len())? + .iter() + .take_while(|byte| **byte != 0) + .cloned() + .collect::>(), + ) + .into_owned(), + ); + + trace!("Found entry with path {}", path.display()); + + let data = mini_parser::get_slice_range( + data, + entry.data_offset..entry.data_offset + entry.compressed_size, + )?; + + let data = if entry.compressed_size != entry.decompressed_size { + trace!("Decompressing file"); + decompress(data) + } else { + trace!("File is not compressed"); + data.to_vec() + }; + + if data.len() != entry.decompressed_size { + return Err(Error::DecompressionSize { + expected: entry.decompressed_size, + found: data.len(), + }); + } + + debug!( + "Processed entry with path {} and length {}", + path.display(), + data.len() + ); + + files.insert(path, data); + } + + info!("Processed {} files", files.len()); + + Ok(Self { files }) + } +} diff --git a/src/main.rs b/src/main.rs index 9a27c5b..3524d7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use clap::Clap; use log::{debug, info, warn}; use brd::converter; -use brd::ddr::ssq::SSQ; +use brd::ddr::{arc::ARC, ssq::SSQ}; use brd::osu; use brd::xact3::xwb::{Sound as XWBSound, WaveBank}; @@ -25,6 +25,12 @@ enum SubCommand { display_order = 1 )] UnXWB(UnXWB), + #[clap( + name = "unarc", + about = "Extracts files from DDR A ARC archives", + display_order = 1 + )] + UnARC(UnARC), #[clap( about = "Converts DDR step charts to osu!mania beatmaps", display_order = 1 @@ -32,6 +38,16 @@ enum SubCommand { DDR2osu(DDR2osu), } +#[derive(Clap)] +struct UnARC { + #[clap(short, long, about = "List available files and exit")] + list_files: bool, + #[clap(short = "f", long, about = "Only extract this file")] + single_file: Option, + #[clap(name = "file")] + file: PathBuf, +} + #[derive(Clap)] struct UnXWB { #[clap(short, long, about = "List available sounds and exit")] @@ -128,6 +144,35 @@ fn main() -> Result<()> { } } } + SubCommand::UnARC(opts) => { + let arc_data = fs::read(&opts.file) + .with_context(|| format!("failed to read ARC file {}", &opts.file.display()))?; + let arc = ARC::parse(&arc_data).context("failed to parse ARC file")?; + + let files = match opts.single_file { + Some(path) => match arc.files.get(&path) { + Some(_) => vec![path], + None => return Err(anyhow!("File “{}” not found in archive", path.display())), + }, + None => arc.files.keys().cloned().collect(), + }; + + for (path, data) in arc.files.iter() { + if files.contains(&path) { + if opts.list_files { + println!("{}", path.display()); + } else { + info!("Writing {}", path.display()); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, data).with_context(|| { + format!("failed to write file to “{}”", path.display()) + })?; + } + } + } + } SubCommand::DDR2osu(opts) => { let sound_name = &opts