diff --git a/Cargo.lock b/Cargo.lock index 7b5bb27..6d9029a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,8 +56,11 @@ dependencies = [ "num-derive", "num-traits", "pretty_env_logger", + "quick-xml", "quickcheck", "quickcheck_macros", + "serde", + "tabwriter", "thiserror", "zip", ] @@ -317,6 +320,16 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc440ee4802a86e357165021e3e255a9143724da31db1e2ea540214c96a0f82" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quickcheck" version = "0.9.2" @@ -408,6 +421,26 @@ version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +[[package]] +name = "serde" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strsim" version = "0.10.0" @@ -436,6 +469,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tabwriter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36205cfc997faadcc4b0b87aaef3fbedafe20d38d4959a7ca6ff803564051111" +dependencies = [ + "unicode-width", +] + [[package]] name = "termcolor" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index a0868b7..a911344 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,14 @@ edition = "2018" 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" } +log = "0.4.8" num-derive = "0.3" num-traits = "0.2" pretty_env_logger = "0.4" +quick-xml = { version = "0.18", features = [ "serialize" ] } +serde = { version = "1.0", features = [ "derive" ] } +tabwriter = "1.2.1" thiserror = "1.0.20" zip = { version = "0.5.5", default-features = false, features = ["deflate"] } diff --git a/README.md b/README.md index cf8da44..681ffe7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BRD -BRD is a tool for working with [*Dance Dance Revolution*][ddr] step charts and -wave banks. For currently supported features, see the [Modes](#modes) section. +BRD is a tool for working with [*Dance Dance Revolution*][ddr] related data. +For currently supported features, see the [Modes](#modes) section. ## Installation @@ -54,6 +54,11 @@ Basic Usage: brd unarc file.arc brd unarc -l file.arc +### musicdb + +This lists all entries from `musicdb.xml` or `startup.arc` files (only DDR A is +supported). + #### Known Problems * It only supports sounds in [ADPCM][ADPCM] format. If you want to extract diff --git a/src/ddr.rs b/src/ddr.rs index cb63c74..4a3e6ce 100644 --- a/src/ddr.rs +++ b/src/ddr.rs @@ -1,2 +1,3 @@ pub mod arc; +pub mod musicdb; pub mod ssq; diff --git a/src/ddr/musicdb.rs b/src/ddr/musicdb.rs new file mode 100644 index 0000000..4253d7c --- /dev/null +++ b/src/ddr/musicdb.rs @@ -0,0 +1,95 @@ +use std::ops::Deref; +use std::path::PathBuf; +use std::str::FromStr; + +use quick_xml::de::{from_str, DeError}; +use serde::de; +use serde::Deserialize; +use thiserror::Error; + +use crate::ddr::arc; + +#[derive(Debug, Error)] +pub enum Error { + #[error("“data/gamedata/musicdb.xml” not found in archive")] + NotInArchive, + #[error(transparent)] + DeError(#[from] DeError), + #[error(transparent)] + ArcError(#[from] arc::Error), + #[error(transparent)] + FromUtf8Error(#[from] std::string::FromUtf8Error), +} + +/// Type that implements [`serde::de::Deserialize`] for space separated lists in xml tag bodies. +/// +/// [`serde::de::Deserialize`]: ../../../serde/de/trait.Deserialize.html +#[derive(Debug)] +pub struct XMLList(Vec); + +impl<'de, T> serde::de::Deserialize<'de> for XMLList +where + T: FromStr, + T::Err: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(Self( + s.split(' ') + .map(|x| x.parse().map_err(de::Error::custom)) + .collect::, _>>()?, + )) + } +} + +impl Deref for XMLList { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// This currently only includes fields present in every entry. +#[derive(Debug, Deserialize)] +pub struct Entry { + pub mcode: u32, + pub basename: String, + pub title: String, + pub artist: String, + pub bpmmax: u16, + pub series: u8, + #[serde(rename = "diffLv")] + pub diff_lv: XMLList, +} + +/// Holds entries from `musicdb.xml` and can be deserialized from it with [`parse`] +/// +/// [`parse`]: fn.parse.html +#[derive(Debug, Deserialize)] +pub struct MusicDB { + pub music: Vec, +} + +impl MusicDB { + /// Parses `musicdb.xml` found in the `startup.arc` archive of DDR A. Currently does not work + /// for older versions. + pub fn parse(data: &str) -> Result { + from_str(data) + } + + /// Convenience function that reads `musicdb.xml` from `startup.arc` and then parses it. + pub fn parse_from_startup_arc(data: &[u8]) -> Result { + let arc = arc::ARC::parse(&data)?; + + let musicdb_data = arc + .files + .get(&PathBuf::from("data/gamedata/musicdb.xml")) + .ok_or(Error::NotInArchive)?; + + Self::parse(&String::from_utf8(musicdb_data.to_vec())?).map_err(|err| err.into()) + } +} diff --git a/src/lib.rs b/src/lib.rs index ff042b7..7b7568c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,5 +8,5 @@ pub mod converter; pub mod ddr; mod mini_parser; pub mod osu; -mod utils; +pub mod utils; pub mod xact3; diff --git a/src/main.rs b/src/main.rs index 3524d7e..b0e8b82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,17 @@ use std::fs; +use std::io; +use std::io::Write; use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; use clap::Clap; use log::{debug, info, warn}; +use tabwriter::TabWriter; use brd::converter; -use brd::ddr::{arc::ARC, ssq::SSQ}; +use brd::ddr::{arc::ARC, musicdb, ssq::SSQ}; use brd::osu; +use brd::utils; use brd::xact3::xwb::{Sound as XWBSound, WaveBank}; #[derive(Clap)] @@ -31,6 +35,12 @@ enum SubCommand { display_order = 1 )] UnARC(UnARC), + #[clap( + name = "musicdb", + about = "Shows entries from musicdb (supports musicdb.xml and startup.arc from DDR A)", + display_order = 1 + )] + MusicDB(MusicDB), #[clap( about = "Converts DDR step charts to osu!mania beatmaps", display_order = 1 @@ -58,6 +68,12 @@ struct UnXWB { file: PathBuf, } +#[derive(Clap)] +struct MusicDB { + #[clap(name = "file")] + file: PathBuf, +} + #[derive(Clap)] struct DDR2osu { #[clap( @@ -173,6 +189,64 @@ fn main() -> Result<()> { } } } + SubCommand::MusicDB(opts) => { + let extension = opts + .file + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + let musicdb = match extension { + "arc" => { + let arc_data = fs::read(&opts.file).with_context(|| { + format!("failed to read musicdb ARC file {}", &opts.file.display()) + })?; + + musicdb::MusicDB::parse_from_startup_arc(&arc_data) + .context("failed to parse musicdb from ARC file")? + } + _ => { + if extension != "xml" { + warn!("Did not find known extension (arc, xml), trying to parse as XML"); + } + + let musicdb_data = fs::read_to_string(&opts.file).with_context(|| { + format!("failed to read musicdb XML file {}", &opts.file.display()) + })?; + + musicdb::MusicDB::parse(&musicdb_data).context("failed to parse musicdb XML")? + } + }; + + let mut tw = TabWriter::new(io::stdout()); + + writeln!( + tw, + "Code\tBasename\tName\tArtist\tBPM\tSeries\tDifficulties (Single)\t(Double)" + )?; + + for song in musicdb.music { + // Filter 0s + let diff_lv: (Vec<&u8>, Vec<&u8>) = ( + song.diff_lv[..5].iter().filter(|x| **x != 0).collect(), + song.diff_lv[5..].iter().filter(|x| **x != 0).collect(), + ); + + writeln!( + tw, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + song.mcode, + song.basename, + song.title, + song.artist, + song.bpmmax, + song.series, + utils::join_display_values(diff_lv.0, ", "), + utils::join_display_values(diff_lv.1, ", ") + )?; + } + + tw.flush()?; + } SubCommand::DDR2osu(opts) => { let sound_name = &opts