This repository has been archived on 2024-01-28. You can view files and clone it, but cannot push or open issues/pull-requests.
brd/src/osu/beatmap.rs

804 lines
20 KiB
Rust
Raw Normal View History

//! The description format of an osu! beatmap.
//!
//! The beatmap file format is described in the [osu! knowledge base].
//!
//! Example (building a minimal beatmap):
//!
//! ```
//! # use brd::osu::beatmap;
//! let awesome_beatmap = beatmap::BeatmapBuilder::default()
//! .general(
//! beatmap::GeneralBuilder::default()
//! .audio_filename("audio.mp3")
//! .build()
//! .unwrap(),
//! )
//! .metadata(
//! beatmap::MetadataBuilder::default()
//! .title("My awesome song")
//! .artist("Awesome artist")
//! .creator("Me")
//! .version("Hard")
//! .source("Awesome songs vol.3")
//! .build()
//! .unwrap(),
//! )
//! .difficulty(
//! beatmap::DifficultyBuilder::default()
//! .hp_drain_rate(4.0)
//! .circle_size(4.0)
//! .overall_difficulty(3.0)
//! .approach_rate(8.0)
//! .slider_multiplier(0.64)
//! .slider_tick_rate(1.0)
//! .build()
//! .unwrap(),
//! )
//! .timing_points(beatmap::TimingPoints(vec![
//! beatmap::TimingPointBuilder::default()
//! .time(0)
//! .beat_length(1000.0 / 3.0)
//! .build()
//! .unwrap(),
//! ]))
//! .hit_objects(beatmap::HitObjects(vec![
//! beatmap::hit_object::HitCircleBuilder::default()
//! .x(256)
//! .y(192)
//! .time(8000)
//! .build()
//! .unwrap()
//! .into(),
//! ]))
//! .build()
//! .unwrap();
//!
//! assert_eq!(
//! format!("{}", awesome_beatmap),
//! r#"osu file format v14
//!
//! [General]
//! AudioFilename: audio.mp3
//! AudioLeadIn: 0
//! PreviewTime: -1
//! Countdown: 1
//! SampleSet: Normal
//! Mode: 0
//!
//! [Editor]
//!
//! [Metadata]
//! Title:My awesome song
//! Artist:Awesome artist
//! Creator:Me
//! Version:Hard
//! Source:Awesome songs vol.3
//! Tags:
//!
//! [Difficulty]
//! HPDrainRate:4
//! CircleSize:4
//! OverallDifficulty:3
//! ApproachRate:8
//! SliderMultiplier:0.64
//! SliderTickRate:1
//!
//! [Events]
//!
//!
//! [TimingPoints]
//! 0,333.33334,4,0,0,100,1,0
//!
//! [Colours]
//!
//!
//! [HitObjects]
//! 256,192,8000,1,0,0:0:0:0:
//! "#
//! );
//! ```
//!
//! [osu! knowledge base]: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)
pub mod hit_object;
pub use hit_object::HitObject;
2020-07-04 23:17:17 +02:00
use std::fmt;
use derive_builder::Builder;
2020-07-04 23:17:17 +02:00
use derive_more::{Deref, DerefMut};
2020-06-22 20:33:12 +02:00
use num_traits::ToPrimitive;
use super::types::*;
2020-06-26 21:10:51 +02:00
use crate::utils;
#[derive(Builder, Clone)]
2020-06-22 20:33:12 +02:00
pub struct General {
#[builder(setter(into))]
2020-06-22 20:33:12 +02:00
pub audio_filename: String,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub audio_lead_in: Time,
#[builder(default = "-1")]
pub preview_time: SignedTime,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub countdown: Countdown,
// SampleSets normal default (BeatmapDefault) does not make sense here
#[builder(default = "SampleSet::Normal")]
2020-06-22 20:33:12 +02:00
pub sample_set: SampleSet,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub mode: Mode,
}
impl fmt::Display for General {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[General]\n\
AudioFilename: {}\n\
AudioLeadIn: {}\n\
PreviewTime: {}\n\
Countdown: {}\n\
SampleSet: {:?}\n\
Mode: {}\n\
",
self.audio_filename,
self.audio_lead_in,
self.preview_time,
ToPrimitive::to_u8(&self.countdown).unwrap(),
self.sample_set,
ToPrimitive::to_u8(&self.mode).unwrap()
)
}
}
#[derive(Clone, Default)]
pub struct Editor;
2020-06-22 20:33:12 +02:00
impl fmt::Display for Editor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "[Editor]")
}
}
#[derive(Builder, Clone)]
#[builder(setter(into))]
2020-06-22 20:33:12 +02:00
pub struct Metadata {
pub title: String,
pub artist: String,
#[builder(default = "\"brd::osu\".to_string()")]
2020-06-22 20:33:12 +02:00
pub creator: String,
pub version: String,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub source: String,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub tags: Vec<String>,
}
impl fmt::Display for Metadata {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[Metadata]\n\
Title:{}\n\
Artist:{}\n\
Creator:{}\n\
Version:{}\n\
Source:{}\n\
Tags:{}\n\
",
self.title,
self.artist,
self.creator,
self.version,
self.source,
self.tags.join(" ")
)
}
}
#[derive(Builder, Clone, Debug)]
#[builder(build_fn(validate = "Self::validate"))]
2020-06-22 20:33:12 +02:00
pub struct Difficulty {
#[builder(setter(into))]
pub hp_drain_rate: RangeSetting,
2020-06-22 20:33:12 +02:00
/// Also is the number of keys in mania
#[builder(setter(into))]
pub circle_size: RangeSetting,
#[builder(setter(into))]
pub overall_difficulty: RangeSetting,
#[builder(setter(into))]
pub approach_rate: RangeSetting,
2020-06-22 20:33:12 +02:00
pub slider_multiplier: f32,
pub slider_tick_rate: f32,
}
impl DifficultyBuilder {
fn validate_option(maybe_value: &Option<RangeSetting>, name: &str) -> Result<(), String> {
if let Some(value) = maybe_value {
if !value.validate() {
return Err(format!(
"{} has to be between {} and {}",
name,
RangeSetting::MIN,
RangeSetting::MAX
));
}
}
Ok(())
}
fn validate(&self) -> Result<(), String> {
Self::validate_option(&self.hp_drain_rate, "hp_drain_rate")?;
Self::validate_option(&self.circle_size, "circle_size")?;
Self::validate_option(&self.overall_difficulty, "overall_difficulty")?;
Self::validate_option(&self.approach_rate, "approach_rate")?;
Ok(())
}
}
2020-06-22 20:33:12 +02:00
impl fmt::Display for Difficulty {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[Difficulty]\n\
HPDrainRate:{}\n\
CircleSize:{}\n\
OverallDifficulty:{}\n\
ApproachRate:{}\n\
SliderMultiplier:{}\n\
SliderTickRate:{}\n\
",
self.hp_drain_rate,
self.circle_size,
self.overall_difficulty,
self.approach_rate,
self.slider_multiplier,
self.slider_tick_rate
)
}
}
#[derive(Clone, Default, Deref, DerefMut)]
2020-06-22 20:33:12 +02:00
pub struct Events(pub Vec<Event>);
impl fmt::Display for Events {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[Events]\n\
{}\n\
",
2020-07-04 23:17:17 +02:00
utils::join_display_values(self.to_vec(), "\n")
2020-06-22 20:33:12 +02:00
)
}
}
#[derive(Clone, Debug, PartialEq)]
2020-06-22 20:33:12 +02:00
pub enum Event {
Background {
filename: String,
x_offset: OsuPixel,
y_offset: OsuPixel,
},
Video {
filename: String,
start_time: Time,
2020-06-22 20:33:12 +02:00
x_offset: OsuPixel,
y_offset: OsuPixel,
},
Break {
start_time: Time,
end_time: Time,
},
}
impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Event::Background {
filename,
x_offset,
y_offset,
} => write!(f, "0,0,{},{},{}", filename, x_offset, y_offset),
Event::Video {
start_time,
filename,
x_offset,
y_offset,
} => write!(
f,
"Video,{},{},{},{}",
start_time, filename, x_offset, y_offset
),
Event::Break {
start_time,
end_time,
} => write!(f, "Break,{},{}", start_time, end_time),
}
}
}
#[derive(Clone, Default, Deref, DerefMut)]
2020-06-22 20:33:12 +02:00
pub struct TimingPoints(pub Vec<TimingPoint>);
impl fmt::Display for TimingPoints {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[TimingPoints]\n\
{}\n\
",
2020-07-04 23:17:17 +02:00
utils::join_display_values(self.to_vec(), "\n")
2020-06-22 20:33:12 +02:00
)
}
}
#[derive(Builder, Clone, Default)]
2020-06-22 20:33:12 +02:00
pub struct TimingPointEffects {
pub kiai_time: bool,
pub omit_first_barline: bool,
}
impl fmt::Display for TimingPointEffects {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
2020-07-08 23:55:04 +02:00
utils::bitarray_to_byte([
2020-06-22 20:33:12 +02:00
self.kiai_time,
false,
false,
self.omit_first_barline,
false,
false,
false,
false
])
)
}
}
#[derive(Builder, Clone)]
2020-06-22 20:33:12 +02:00
pub struct TimingPoint {
pub time: Time,
pub beat_length: f32,
#[builder(default = "4")]
2020-06-22 20:33:12 +02:00
pub meter: u8,
#[builder(default = "SampleSet::BeatmapDefault")]
2020-06-22 20:33:12 +02:00
pub sample_set: SampleSet,
#[builder(default = "0")]
pub sample_index: u32,
#[builder(default = "100")]
2020-06-22 20:33:12 +02:00
pub volume: u8,
#[builder(default = "true")]
2020-06-22 20:33:12 +02:00
pub uninherited: bool,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub effects: TimingPointEffects,
}
impl fmt::Display for TimingPoint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{},{},{},{},{},{},{},{}",
self.time,
self.beat_length,
self.meter,
ToPrimitive::to_u8(&self.sample_set).unwrap(),
self.sample_index,
self.volume,
self.uninherited as u8,
self.effects
)
}
}
#[derive(Clone, Default, Deref, DerefMut)]
2020-06-22 20:33:12 +02:00
pub struct Colours(pub Vec<Colour>);
impl fmt::Display for Colours {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[Colours]\n\
{}\n\
",
2020-07-04 23:17:17 +02:00
utils::join_display_values(self.to_vec(), "\n")
2020-06-22 20:33:12 +02:00
)
}
}
#[derive(Clone, Debug)]
pub enum ColourScope {
Combo(u16),
SliderTrackOverride,
SliderBorder,
}
impl fmt::Display for ColourScope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ColourScope::Combo(i) => write!(f, "Combo{}", i),
_ => write!(f, "{:?}", self),
}
}
}
#[derive(Builder, Clone)]
2020-06-22 20:33:12 +02:00
pub struct Colour {
pub scope: ColourScope,
pub colour: [u8; 3],
}
impl fmt::Display for Colour {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{} : {}",
self.scope,
2020-06-26 21:10:51 +02:00
utils::join_display_values(self.colour.to_vec(), ",")
2020-06-22 20:33:12 +02:00
)
}
}
impl fmt::Display for HitSound {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
2020-07-08 23:55:04 +02:00
utils::bitarray_to_byte([
2020-06-22 20:33:12 +02:00
self.normal,
self.whistle,
self.finish,
self.clap,
false,
false,
false,
false
])
)
}
}
impl fmt::Display for HitSample {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}:{}:{}:{}:{}",
ToPrimitive::to_u8(&self.normal_set).unwrap(),
ToPrimitive::to_u8(&self.addition_set).unwrap(),
self.index,
self.volume,
self.filename
2020-06-22 20:33:12 +02:00
)
}
}
#[derive(Clone, Default, Deref, DerefMut)]
2020-06-22 20:33:12 +02:00
pub struct HitObjects(pub Vec<HitObject>);
impl fmt::Display for HitObjects {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\
[HitObjects]\n\
{}\n\
",
2020-07-04 23:17:17 +02:00
utils::join_display_values(self.to_vec(), "\n")
2020-06-22 20:33:12 +02:00
)
}
}
#[derive(Builder)]
2020-06-22 20:33:12 +02:00
pub struct Beatmap {
#[builder(default = "14")]
2020-06-22 20:33:12 +02:00
pub version: u8,
pub general: General,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub editor: Editor,
pub metadata: Metadata,
pub difficulty: Difficulty,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub events: Events,
pub timing_points: TimingPoints,
#[builder(default)]
2020-06-22 20:33:12 +02:00
pub colours: Colours,
pub hit_objects: HitObjects,
}
impl fmt::Display for Beatmap {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"osu file format v{}\n\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
2020-06-22 20:33:12 +02:00
self.version,
self.general.clone(),
self.editor.clone(),
self.metadata.clone(),
self.difficulty.clone(),
self.events.clone(),
self.timing_points.clone(),
self.colours.clone(),
self.hit_objects.clone()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn general() {
let general = GeneralBuilder::default()
.audio_filename("foo.mp3")
.audio_lead_in(23)
.preview_time(5000)
.countdown(Countdown::Double)
.sample_set(SampleSet::Drum)
.mode(Mode::Mania)
.build()
.unwrap();
assert_eq!(
format!("{}", general),
"[General]\n\
AudioFilename: foo.mp3\n\
AudioLeadIn: 23\n\
PreviewTime: 5000\n\
Countdown: 3\n\
SampleSet: Drum\n\
Mode: 3\n",
)
}
#[test]
fn editor() {
assert_eq!(format!("{}", Editor), "[Editor]\n");
}
#[test]
fn metadata() {
let metadata = MetadataBuilder::default()
.title("Song Title")
.artist("Song Artist")
.creator("mycoolusername42")
.version("Super Hard")
.source("Best Hits Vol. 23")
.tags(vec![
"some".to_string(),
"descriptive".to_string(),
"tags".to_string(),
])
.build()
.unwrap();
assert_eq!(
format!("{}", metadata),
"[Metadata]\n\
Title:Song Title\n\
Artist:Song Artist\n\
Creator:mycoolusername42\n\
Version:Super Hard\n\
Source:Best Hits Vol. 23\n\
Tags:some descriptive tags\n"
);
}
#[test]
fn dificulty_builder_error() {
assert_eq!(
DifficultyBuilder::default()
.hp_drain_rate(25.0)
.circle_size(5.0)
.overall_difficulty(5.0)
.approach_rate(5.0)
.build()
.unwrap_err(),
"hp_drain_rate has to be between 0 and 10"
);
}
#[test]
fn difficulty() {
let difficulty = DifficultyBuilder::default()
.hp_drain_rate(4.0)
.circle_size(5.0)
.overall_difficulty(6.0)
.approach_rate(7.0)
.slider_multiplier(0.64)
.slider_tick_rate(1.0)
.build()
.unwrap();
assert_eq!(
format!("{}", difficulty),
"[Difficulty]\n\
HPDrainRate:4\n\
CircleSize:5\n\
OverallDifficulty:6\n\
ApproachRate:7\n\
SliderMultiplier:0.64\n\
SliderTickRate:1\n"
)
}
#[test]
fn events() {
let mut events = Events(Vec::new());
events.push(Event::Background {
filename: "foo.jpg".to_string(),
x_offset: 42.into(),
y_offset: 23.into(),
});
events.push(Event::Video {
filename: "foo.mp4".to_string(),
start_time: 500,
x_offset: 42.into(),
y_offset: 23.into(),
});
events.push(Event::Break {
start_time: 23000,
end_time: 42000,
});
assert_eq!(
format!("{}", events),
"[Events]\n\
0,0,foo.jpg,42,23\n\
Video,500,foo.mp4,42,23\n\
Break,23000,42000\n"
)
}
#[test]
fn timing_points() {
let mut timing_points = TimingPoints(Vec::new());
timing_points.push(
TimingPointBuilder::default()
.time(0)
.beat_length(1000.0 / 3.0)
.build()
.unwrap(),
);
timing_points.push(
TimingPointBuilder::default()
.time(5000)
.beat_length(500.0)
.meter(8)
.sample_set(SampleSet::Drum)
.sample_index(1)
.volume(50)
.uninherited(false)
.effects(
TimingPointEffectsBuilder::default()
.kiai_time(true)
.omit_first_barline(true)
.build()
.unwrap(),
)
.build()
.unwrap(),
);
assert_eq!(
format!("{}", timing_points),
"[TimingPoints]\n\
0,333.33334,4,0,0,100,1,0\n\
5000,500,8,3,1,50,0,9\n"
);
}
#[test]
fn colours() {
let mut colours = Colours::default();
colours.push(
ColourBuilder::default()
.scope(ColourScope::Combo(42))
.colour([0, 127, 255])
.build()
.unwrap(),
);
colours.push(
ColourBuilder::default()
.scope(ColourScope::SliderTrackOverride)
.colour([127, 255, 0])
.build()
.unwrap(),
);
colours.push(
ColourBuilder::default()
.scope(ColourScope::SliderBorder)
.colour([255, 0, 127])
.build()
.unwrap(),
);
assert_eq!(
format!("{}", colours),
"[Colours]\n\
Combo42 : 0,127,255\n\
SliderTrackOverride : 127,255,0\n\
SliderBorder : 255,0,127\n"
)
}
#[test]
fn hit_sound() {
assert_eq!(format!("{}", HitSound::default()), "0");
assert_eq!(
format!(
"{}",
HitSoundBuilder::default().normal(true).build().unwrap()
),
"1"
);
assert_eq!(
format!(
"{}",
HitSoundBuilder::default().whistle(true).build().unwrap()
),
"2"
);
assert_eq!(
format!(
"{}",
HitSoundBuilder::default().finish(true).build().unwrap()
),
"4"
);
assert_eq!(
format!("{}", HitSoundBuilder::default().clap(true).build().unwrap()),
"8"
);
}
#[test]
fn hit_sample() {
assert_eq!(format!("{}", HitSample::default()), "0:0:0:0:");
assert_eq!(
format!(
"{}",
HitSampleBuilder::default()
.normal_set(SampleSet::Drum)
.addition_set(SampleSet::Normal)
.index(23)
.volume(42)
.filename("foo.mp3")
.build()
.unwrap()
),
"3:1:23:42:foo.mp3"
);
}
#[test]
fn hit_objects() {
let mut hit_objects: HitObjects = Default::default();
hit_objects.push(
hit_object::HitCircleBuilder::default()
.x(200)
.y(400)
.time(5732)
.build()
.unwrap()
.into(),
);
hit_objects.push(
hit_object::HitCircleBuilder::default()
.x(400)
.y(500)
.time(7631)
.build()
.unwrap()
.into(),
);
assert_eq!(
format!("{}", hit_objects),
"[HitObjects]\n\
200,400,5732,1,0,0:0:0:0:\n\
400,500,7631,1,0,0:0:0:0:\n"
);
}
}