Add unarc mode
This commit is contained in:
parent
bfd8ee75ed
commit
e64ae362ef
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
16
README.md
16
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)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
pub mod arc;
|
||||
pub mod ssq;
|
||||
|
|
143
src/ddr/arc.rs
Normal file
143
src/ddr/arc.rs
Normal 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 })
|
||||
}
|
||||
}
|
47
src/main.rs
47
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<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
|
||||
|
|
Reference in a new issue