refactored beatmap types into a new module
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
use crate::Mods;
|
||||
|
||||
/// Summary struct for a [`Beatmap`](crate::Beatmap)'s attributes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BeatmapAttributes {
|
||||
/// The approach rate.
|
||||
pub ar: f64,
|
||||
/// The overall difficulty.
|
||||
pub od: f64,
|
||||
/// The circle size.
|
||||
pub cs: f64,
|
||||
/// The health drain rate
|
||||
pub hp: f64,
|
||||
/// The clock rate with respect to mods.
|
||||
pub clock_rate: f64,
|
||||
}
|
||||
|
||||
impl BeatmapAttributes {
|
||||
const AR0_MS: f64 = 1800.0;
|
||||
const AR5_MS: f64 = 1200.0;
|
||||
const AR10_MS: f64 = 450.0;
|
||||
const AR_MS_STEP_1: f64 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
|
||||
const AR_MS_STEP_2: f64 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self {
|
||||
Self {
|
||||
ar: ar as f64,
|
||||
od: od as f64,
|
||||
cs: cs as f64,
|
||||
hp: hp as f64,
|
||||
clock_rate: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjusts attributes w.r.t. mods.
|
||||
/// AR is further adjusted by its hitwindow.
|
||||
/// OD is __not__ adjusted by its hitwindow.
|
||||
pub fn mods(self, mods: impl Mods) -> Self {
|
||||
if !mods.change_map() {
|
||||
return self;
|
||||
}
|
||||
|
||||
let clock_rate = mods.clock_rate();
|
||||
let multiplier = mods.od_ar_hp_multiplier();
|
||||
|
||||
// AR
|
||||
let mut ar = (self.ar * multiplier) as f64;
|
||||
let mut ar_ms = if ar <= 5.0 {
|
||||
Self::AR0_MS - Self::AR_MS_STEP_1 * ar
|
||||
} else {
|
||||
Self::AR5_MS - Self::AR_MS_STEP_2 * (ar - 5.0)
|
||||
};
|
||||
|
||||
ar_ms = ar_ms.max(Self::AR10_MS).min(Self::AR0_MS);
|
||||
ar_ms /= clock_rate;
|
||||
|
||||
ar = if ar_ms > Self::AR5_MS {
|
||||
(Self::AR0_MS - ar_ms) / Self::AR_MS_STEP_1
|
||||
} else {
|
||||
5.0 + (Self::AR5_MS - ar_ms) / Self::AR_MS_STEP_2
|
||||
};
|
||||
|
||||
// OD
|
||||
let od = (self.od * multiplier).min(10.0);
|
||||
|
||||
// CS
|
||||
let mut cs = self.cs;
|
||||
if mods.hr() {
|
||||
cs *= 1.3;
|
||||
} else if mods.ez() {
|
||||
cs *= 0.5;
|
||||
}
|
||||
cs = cs.min(10.0);
|
||||
|
||||
// HP
|
||||
let hp = (self.hp * multiplier).min(10.0);
|
||||
|
||||
Self {
|
||||
ar,
|
||||
od,
|
||||
cs,
|
||||
hp,
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use std::{cmp::Ordering, iter::Copied, slice::Iter};
|
||||
|
||||
use crate::Beatmap;
|
||||
|
||||
/// New rhythm speed change.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct TimingPoint {
|
||||
/// The beat length for this timing section
|
||||
pub beat_len: f64,
|
||||
/// The start time of this timing section
|
||||
pub time: f64,
|
||||
}
|
||||
|
||||
impl PartialOrd for TimingPoint {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.time.partial_cmp(&other.time)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`TimingPoint`] that depends on a previous one.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct DifficultyPoint {
|
||||
/// The start time for the current speed multiplier
|
||||
pub time: f64,
|
||||
/// The speed multiplier until the next timing point
|
||||
pub speed_multiplier: f64,
|
||||
}
|
||||
|
||||
impl PartialOrd for DifficultyPoint {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.time.partial_cmp(&other.time)
|
||||
}
|
||||
}
|
||||
|
||||
/// Control point for a [`Beatmap`].
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ControlPoint {
|
||||
/// A timing point containing the current beat length.
|
||||
Timing(TimingPoint),
|
||||
/// A difficulty point containing the current speed multiplier.
|
||||
Difficulty(DifficultyPoint),
|
||||
}
|
||||
|
||||
impl ControlPoint {
|
||||
/// Provides the timestamp of the control point.
|
||||
#[inline]
|
||||
pub fn time(&self) -> f64 {
|
||||
match self {
|
||||
Self::Timing(point) => point.time,
|
||||
Self::Difficulty(point) => point.time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator for a [`Beatmap`]'s timing- and difficulty points sorted by timestamp
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ControlPointIter<'p> {
|
||||
timing_points: Copied<Iter<'p, TimingPoint>>,
|
||||
difficulty_points: Copied<Iter<'p, DifficultyPoint>>,
|
||||
|
||||
next_timing: Option<TimingPoint>,
|
||||
next_difficulty: Option<DifficultyPoint>,
|
||||
}
|
||||
|
||||
impl<'p> ControlPointIter<'p> {
|
||||
#[inline]
|
||||
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
||||
let mut timing_points = map.timing_points.iter().copied();
|
||||
let mut difficulty_points = map.difficulty_points.iter().copied();
|
||||
|
||||
Self {
|
||||
next_timing: timing_points.next(),
|
||||
next_difficulty: difficulty_points.next(),
|
||||
|
||||
timing_points,
|
||||
difficulty_points,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'p> Iterator for ControlPointIter<'p> {
|
||||
type Item = ControlPoint;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.next_timing, self.next_difficulty) {
|
||||
(Some(timing), Some(difficulty)) if timing.time <= difficulty.time => {
|
||||
self.next_timing = self.timing_points.next();
|
||||
|
||||
Some(ControlPoint::Timing(timing))
|
||||
}
|
||||
(_, Some(point)) => {
|
||||
self.next_difficulty = self.difficulty_points.next();
|
||||
|
||||
Some(ControlPoint::Difficulty(point))
|
||||
}
|
||||
(Some(point), None) => {
|
||||
self.next_timing = self.timing_points.next();
|
||||
|
||||
Some(ControlPoint::Timing(point))
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
beatmap::{ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn control_point_iter() {
|
||||
let map = Beatmap {
|
||||
timing_points: vec![
|
||||
TimingPoint {
|
||||
time: 1.0,
|
||||
beat_len: 10.0,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 3.0,
|
||||
beat_len: 10.0,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 4.0,
|
||||
beat_len: 10.0,
|
||||
},
|
||||
],
|
||||
difficulty_points: vec![
|
||||
DifficultyPoint {
|
||||
time: 2.0,
|
||||
speed_multiplier: 10.0,
|
||||
},
|
||||
DifficultyPoint {
|
||||
time: 5.0,
|
||||
speed_multiplier: 10.0,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut iter = ControlPointIter::new(&map);
|
||||
|
||||
assert!(matches!(iter.next(), Some(ControlPoint::Timing(_))));
|
||||
assert!(matches!(iter.next(), Some(ControlPoint::Difficulty(_))));
|
||||
assert!(matches!(iter.next(), Some(ControlPoint::Timing(_))));
|
||||
assert!(matches!(iter.next(), Some(ControlPoint::Timing(_))));
|
||||
assert!(matches!(iter.next(), Some(ControlPoint::Difficulty(_))));
|
||||
assert!(matches!(iter.next(), None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use crate::parse::HitObject;
|
||||
|
||||
pub use self::{
|
||||
attributes::BeatmapAttributes,
|
||||
control_points::{ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint},
|
||||
mode::GameMode,
|
||||
};
|
||||
|
||||
mod attributes;
|
||||
mod control_points;
|
||||
mod mode;
|
||||
|
||||
/// The main beatmap struct containing all data relevant
|
||||
/// for difficulty and performance calculation
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Beatmap {
|
||||
/// The game mode.
|
||||
pub mode: GameMode,
|
||||
/// The version of the .osu file.
|
||||
pub version: u8,
|
||||
|
||||
/// The amount of circles.
|
||||
pub n_circles: u32,
|
||||
/// The amount of sliders.
|
||||
pub n_sliders: u32,
|
||||
/// The amount of spinners.
|
||||
pub n_spinners: u32,
|
||||
|
||||
/// The approach rate.
|
||||
pub ar: f32,
|
||||
/// The overall difficulty.
|
||||
pub od: f32,
|
||||
/// The circle size.
|
||||
pub cs: f32,
|
||||
/// The health drain rate.
|
||||
pub hp: f32,
|
||||
/// Base slider velocity in pixels per beat
|
||||
pub slider_mult: f64,
|
||||
/// Amount of slider ticks per beat.
|
||||
pub tick_rate: f64,
|
||||
/// All hitobjects of the beatmap.
|
||||
pub hit_objects: Vec<HitObject>,
|
||||
/// Store the sounds for all objects in their own Vec to minimize the struct size.
|
||||
/// Hitsounds are only used in osu!taiko in which they represent color.
|
||||
pub sounds: Vec<u8>,
|
||||
|
||||
/// Timing points that indicate a new timing section.
|
||||
pub timing_points: Vec<TimingPoint>,
|
||||
|
||||
/// Timing point for the current timing section.
|
||||
pub difficulty_points: Vec<DifficultyPoint>,
|
||||
|
||||
/// The stack leniency that is used to calculate
|
||||
/// the stack offset for stacked positions.
|
||||
pub stack_leniency: f32,
|
||||
}
|
||||
|
||||
impl Beatmap {
|
||||
/// Extract a beatmap's attributes into their own type.
|
||||
#[inline]
|
||||
pub fn attributes(&self) -> BeatmapAttributes {
|
||||
BeatmapAttributes::new(self.ar, self.od, self.cs, self.hp)
|
||||
}
|
||||
|
||||
/// The beats per minute of the map.
|
||||
#[inline]
|
||||
pub fn bpm(&self) -> f64 {
|
||||
match self.timing_points.first() {
|
||||
Some(point) => point.beat_len.recip() * 1000.0 * 60.0,
|
||||
None => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an iterator over the map's timing- and difficulty points sorted by timestamp.
|
||||
#[inline]
|
||||
pub fn control_points(&self) -> ControlPointIter<'_> {
|
||||
ControlPointIter::new(self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/// The mode of a beatmap.
|
||||
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum GameMode {
|
||||
/// osu!standard
|
||||
STD = 0,
|
||||
/// osu!taiko
|
||||
TKO = 1,
|
||||
/// osu!catch
|
||||
CTB = 2,
|
||||
/// osu!mania
|
||||
MNA = 3,
|
||||
}
|
||||
|
||||
impl Default for GameMode {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::STD
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user