Add unarc mode

This commit is contained in:
Simon Bruder 2020-06-27 17:15:37 +02:00
parent bfd8ee75ed
commit e64ae362ef
No known key found for this signature in database
GPG key ID: 6F03E0000CC5B62F
6 changed files with 211 additions and 3 deletions

6
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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)

View file

@ -1 +1,2 @@
pub mod arc;
pub mod ssq;

143
src/ddr/arc.rs Normal file
View file

@ -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<Self, Error> {
let mut cursor = Cursor::new(data);
let name_offset = cursor.read_u32::<LE>()?.try_into()?;
let data_offset = cursor.read_u32::<LE>()?.try_into()?;
let decompressed_size = cursor.read_u32::<LE>()?.try_into()?;
let compressed_size = cursor.read_u32::<LE>()?.try_into()?;
Ok(Self {
name_offset,
data_offset,
decompressed_size,
compressed_size,
})
}
}
pub struct ARC {
pub files: HashMap<PathBuf, Vec<u8>>,
}
impl ARC {
pub fn parse(data: &[u8]) -> Result<Self, Error> {
let mut cursor = Cursor::new(data);
let magic = cursor.read_u32::<LE>()?;
if magic != MAGIC {
return Err(Error::InvalidMagic {
expected: MAGIC,
found: magic,
});
}
let version = cursor.read_u32::<LE>()?;
debug!("Recognised archive (version {})", version);
if version != 1 {
warn!("Unknown version {}, continuing anyway", version);
}
let file_count = cursor.read_u32::<LE>()?;
debug!("Archive contains {} files", file_count);
let _compression = cursor.read_u32::<LE>()?;
let mut cue = Vec::new();
cursor
.take((4 * 4 * file_count).into())
.read_to_end(&mut cue)?;
let cue: Vec<CueEntry> = cue
.chunks(4 * 4)
.map(CueEntry::parse)
.collect::<Result<_, _>>()?;
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::<Vec<u8>>(),
)
.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 })
}
}

View file

@ -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<PathBuf>,
#[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