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",
|
"anyhow",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"clap",
|
"clap",
|
||||||
|
"konami-lz77",
|
||||||
"log",
|
"log",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
@ -186,6 +187,11 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "konami-lz77"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "git+https://github.com/sbruder/konami-lz77#78c83a87a64d3c1826e168fb90ea9d671f8cfcff"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
|
@ -9,6 +9,7 @@ anyhow = "1.0.31"
|
||||||
byteorder = "1.3.4"
|
byteorder = "1.3.4"
|
||||||
clap = "3.0.0-beta.1"
|
clap = "3.0.0-beta.1"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
|
konami-lz77 = { git = "https://github.com/sbruder/konami-lz77" }
|
||||||
num-derive = "0.3"
|
num-derive = "0.3"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
|
|
16
README.md
16
README.md
|
@ -1,8 +1,7 @@
|
||||||
# BRD
|
# BRD
|
||||||
|
|
||||||
BRD is a tool for working with [*Dance Dance Revolution*][ddr] step charts and
|
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]
|
wave banks. For currently supported features, see the [Modes](#modes) section.
|
||||||
beatmaps and extraction of sounds from wave banks.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -45,6 +44,16 @@ Basic Usage:
|
||||||
brd unxwb file.xwb
|
brd unxwb file.xwb
|
||||||
brd unxwb -l 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
|
#### Known Problems
|
||||||
|
|
||||||
* It only supports sounds in [ADPCM][ADPCM] format. If you want to extract
|
* 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]
|
* The [official osu! file format documentation][osu-doc]
|
||||||
* [MonoGame][MonoGame]’s [XWB implementation][MonoGame-xwb]
|
* [MonoGame][MonoGame]’s [XWB implementation][MonoGame-xwb]
|
||||||
* [Luigi Auriemma][aluigi]’s [unxwb][unxwb] (especially the ADPCM header part)
|
* [Luigi Auriemma][aluigi]’s [unxwb][unxwb] (especially the ADPCM header part)
|
||||||
|
* [mon][mon]’s [ddr\_arc\_extract][ddr_arc_extract]
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -82,6 +92,8 @@ This project is not affiliated with ppy or Konami.
|
||||||
[SaxxonPike]: https://github.com/SaxxonPike
|
[SaxxonPike]: https://github.com/SaxxonPike
|
||||||
[aluigi]: http://aluigi.altervista.org/
|
[aluigi]: http://aluigi.altervista.org/
|
||||||
[ddr]: https://en.wikipedia.org/wiki/Dance_Dance_Revolution
|
[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
|
[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!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)
|
[osu-doc]: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
pub mod arc;
|
||||||
pub mod ssq;
|
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 log::{debug, info, warn};
|
||||||
|
|
||||||
use brd::converter;
|
use brd::converter;
|
||||||
use brd::ddr::ssq::SSQ;
|
use brd::ddr::{arc::ARC, ssq::SSQ};
|
||||||
use brd::osu;
|
use brd::osu;
|
||||||
use brd::xact3::xwb::{Sound as XWBSound, WaveBank};
|
use brd::xact3::xwb::{Sound as XWBSound, WaveBank};
|
||||||
|
|
||||||
|
@ -25,6 +25,12 @@ enum SubCommand {
|
||||||
display_order = 1
|
display_order = 1
|
||||||
)]
|
)]
|
||||||
UnXWB(UnXWB),
|
UnXWB(UnXWB),
|
||||||
|
#[clap(
|
||||||
|
name = "unarc",
|
||||||
|
about = "Extracts files from DDR A ARC archives",
|
||||||
|
display_order = 1
|
||||||
|
)]
|
||||||
|
UnARC(UnARC),
|
||||||
#[clap(
|
#[clap(
|
||||||
about = "Converts DDR step charts to osu!mania beatmaps",
|
about = "Converts DDR step charts to osu!mania beatmaps",
|
||||||
display_order = 1
|
display_order = 1
|
||||||
|
@ -32,6 +38,16 @@ enum SubCommand {
|
||||||
DDR2osu(DDR2osu),
|
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)]
|
#[derive(Clap)]
|
||||||
struct UnXWB {
|
struct UnXWB {
|
||||||
#[clap(short, long, about = "List available sounds and exit")]
|
#[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) => {
|
SubCommand::DDR2osu(opts) => {
|
||||||
let sound_name =
|
let sound_name =
|
||||||
&opts
|
&opts
|
||||||
|
|
Reference in a new issue