2020-07-25 13:19:44 +02:00
|
|
|
|
//! 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;
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
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;
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
use super::types::*;
|
2020-06-26 21:10:51 +02:00
|
|
|
|
use crate::utils;
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[derive(Builder, Clone)]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub struct General {
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(setter(into))]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub audio_filename: String,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default)]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub audio_lead_in: Time,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default = "-1")]
|
|
|
|
|
pub preview_time: SignedTime,
|
|
|
|
|
#[builder(default)]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub countdown: Countdown,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
// SampleSet’s normal default (BeatmapDefault) does not make sense here
|
|
|
|
|
#[builder(default = "SampleSet::Normal")]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub sample_set: SampleSet,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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]")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[derive(Builder, Clone)]
|
|
|
|
|
#[builder(setter(into))]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub struct Metadata {
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub artist: String,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default = "\"brd::osu\".to_string()")]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub creator: String,
|
|
|
|
|
pub version: String,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default)]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub source: String,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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(" ")
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[derive(Builder, Clone, Debug)]
|
|
|
|
|
#[builder(build_fn(validate = "Self::validate"))]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub struct Difficulty {
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(setter(into))]
|
|
|
|
|
pub hp_drain_rate: RangeSetting,
|
2020-06-22 20:33:12 +02:00
|
|
|
|
/// Also is the number of keys in mania
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +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,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
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),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +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
|
|
|
|
|
])
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[derive(Builder, Clone)]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub struct TimingPoint {
|
|
|
|
|
pub time: Time,
|
|
|
|
|
pub beat_length: f32,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default = "4")]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub meter: u8,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default = "SampleSet::BeatmapDefault")]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub sample_set: SampleSet,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default = "0")]
|
|
|
|
|
pub sample_index: u32,
|
|
|
|
|
#[builder(default = "100")]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub volume: u8,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default = "true")]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub uninherited: bool,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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,
|
|
|
|
|
"{}:{}:{}:{}:{}",
|
2020-07-25 13:19:44 +02:00
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +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
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[derive(Builder)]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub struct Beatmap {
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default = "14")]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub version: u8,
|
|
|
|
|
pub general: General,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default)]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub editor: Editor,
|
|
|
|
|
pub metadata: Metadata,
|
|
|
|
|
pub difficulty: Difficulty,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[builder(default)]
|
2020-06-22 20:33:12 +02:00
|
|
|
|
pub events: Events,
|
|
|
|
|
pub timing_points: TimingPoints,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
#[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,
|
2020-07-25 13:19:44 +02:00
|
|
|
|
"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()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-07-25 13:19:44 +02:00
|
|
|
|
|
|
|
|
|
#[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"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|