added more settings to Difficulty (unused)

This commit is contained in:
MaxOhn
2024-03-04 12:21:16 +01:00
parent f1f9513bf3
commit c5fdf4e610
13 changed files with 242 additions and 101 deletions
+28 -64
View File
@@ -14,33 +14,26 @@ use crate::{
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, ConvertedDifficulty}; /// use rosu_pp::{Beatmap, Difficulty};
/// use rosu_pp::catch::{Catch, CatchDifficultyAttributes}; /// use rosu_pp::catch::{Catch, CatchDifficultyAttributes};
/// ///
/// let converted = Beatmap::from_path("./resources/2118524.osu") /// let converted = Beatmap::from_path("./resources/2118524.osu")
/// .unwrap() /// .unwrap()
/// .unchecked_into_converted(); /// .unchecked_into_converted();
/// ///
/// let difficulty = ConvertedDifficulty::<Catch>::new(); /// let attrs: CatchDifficultyAttributes = Difficulty::new()
/// // Same as `Difficulty::new().with_mode::<Catch>();`
///
/// let attrs: CatchDifficultyAttributes = difficulty
/// .mods(8 + 1024) // HDFL /// .mods(8 + 1024) // HDFL
/// .with_mode::<Catch>() // -> `ConvertedDifficulty`
/// .calculate(&converted); /// .calculate(&converted);
/// ``` /// ```
#[must_use] #[must_use]
pub struct ConvertedDifficulty<M> { pub struct ConvertedDifficulty<'a, M> {
inner: Difficulty, inner: &'a Difficulty,
_mode: PhantomData<M>, _mode: PhantomData<M>,
} }
impl<M> ConvertedDifficulty<M> { impl<'a, M> ConvertedDifficulty<'a, M> {
/// Create a new difficulty calculator for a generic mode. pub(crate) const fn new(difficulty: &'a Difficulty) -> Self {
pub const fn new() -> Self {
Self::from_difficulty(Difficulty::new())
}
pub(crate) const fn from_difficulty(difficulty: Difficulty) -> Self {
Self { Self {
inner: difficulty, inner: difficulty,
_mode: PhantomData, _mode: PhantomData,
@@ -48,102 +41,73 @@ impl<M> ConvertedDifficulty<M> {
} }
/// Return the internal [`Difficulty`]. /// Return the internal [`Difficulty`].
pub const fn into_inner(self) -> Difficulty { pub const fn inner(self) -> &'a Difficulty {
self.inner self.inner
} }
/// Cast from generic mode `M` to `N`. /// Cast from generic mode `M` to `N`.
pub const fn cast<N: IGameMode>(self) -> ConvertedDifficulty<N> { pub const fn cast<N: IGameMode>(self) -> ConvertedDifficulty<'a, N> {
ConvertedDifficulty { ConvertedDifficulty {
inner: self.inner, inner: self.inner,
_mode: PhantomData, _mode: PhantomData,
} }
} }
/// Specify mods through their bit values.
///
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
pub const fn mods(self, mods: u32) -> Self {
Self {
inner: self.inner.mods(mods),
..self
}
}
/// Amount of passed objects for partial plays, e.g. a fail.
pub const fn passed_objects(self, passed_objects: u32) -> Self {
Self {
inner: self.inner.passed_objects(passed_objects),
..self
}
}
/// Adjust the clock rate used in the calculation between 0.01 and 100.0.
///
/// If none is specified, it will take the clock rate based on the mods
/// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
pub fn clock_rate(self, clock_rate: f64) -> Self {
Self {
inner: self.inner.clock_rate(clock_rate),
..self
}
}
} }
impl<M: IGameMode> ConvertedDifficulty<M> { impl<M: IGameMode> ConvertedDifficulty<'_, M> {
/// Perform the difficulty calculation for a [`Converted`] beatmap and /// Perform the difficulty calculation for a [`Converted`] beatmap and
/// process the final skill values. /// process the final skill values.
pub fn calculate(&self, map: &Converted<'_, M>) -> M::DifficultyAttributes { pub fn calculate(self, map: &Converted<'_, M>) -> M::DifficultyAttributes {
M::difficulty(&self.inner, map) M::difficulty(self.inner, map)
} }
/// Perform a difficulty calculation for a [`Converted`] beatmap without /// Perform a difficulty calculation for a [`Converted`] beatmap without
/// processing the final skill values. /// processing the final skill values.
pub fn strains(&self, map: &Converted<'_, M>) -> M::Strains { pub fn strains(self, map: &Converted<'_, M>) -> M::Strains {
M::strains(&self.inner, map) M::strains(self.inner, map)
} }
/// Create a gradual difficulty calculator for a [`Converted`] beatmap. /// Create a gradual difficulty calculator for a [`Converted`] beatmap.
pub fn gradual_difficulty(&self, map: &Converted<'_, M>) -> M::GradualDifficulty { pub fn gradual_difficulty(self, map: &Converted<'_, M>) -> M::GradualDifficulty {
M::gradual_difficulty(&self.inner, map) M::gradual_difficulty(self.inner, map)
} }
/// Create a gradual performance calculator for a [`Converted`] beatmap. /// Create a gradual performance calculator for a [`Converted`] beatmap.
pub fn gradual_performance(&self, map: &Converted<'_, M>) -> M::GradualPerformance { pub fn gradual_performance(self, map: &Converted<'_, M>) -> M::GradualPerformance {
M::gradual_performance(&self.inner, map) M::gradual_performance(self.inner, map)
} }
} }
impl<M: IGameMode> From<Difficulty> for ConvertedDifficulty<M> { impl<'a, M: IGameMode> From<&'a Difficulty> for ConvertedDifficulty<'a, M> {
fn from(difficulty: Difficulty) -> Self { fn from(difficulty: &'a Difficulty) -> Self {
Self::from_difficulty(difficulty) Self::new(difficulty)
} }
} }
impl<M> AsRef<Difficulty> for ConvertedDifficulty<M> { impl<M> AsRef<Difficulty> for ConvertedDifficulty<'_, M> {
fn as_ref(&self) -> &Difficulty { fn as_ref(&self) -> &Difficulty {
&self.inner self.inner
} }
} }
impl<M> Copy for ConvertedDifficulty<M> {} impl<M> Copy for ConvertedDifficulty<'_, M> {}
impl<M> Clone for ConvertedDifficulty<M> { impl<M> Clone for ConvertedDifficulty<'_, M> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
*self *self
} }
} }
impl<M> Debug for ConvertedDifficulty<M> { impl<M> Debug for ConvertedDifficulty<'_, M> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("ConvertedDifficulty") f.debug_struct("ConvertedDifficulty")
.field("inner", &self.inner) .field("inner", self.inner)
.field("mode", &GenericFormatter::<M>::new()) .field("mode", &GenericFormatter::<M>::new())
.finish() .finish()
} }
} }
impl<M> PartialEq for ConvertedDifficulty<M> { impl<M> PartialEq for ConvertedDifficulty<'_, M> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.inner == other.inner self.inner == other.inner
} }
+181 -7
View File
@@ -1,4 +1,7 @@
use std::{borrow::Cow, num::NonZeroU32}; use std::{
borrow::Cow,
num::{NonZeroU32, NonZeroU64},
};
use rosu_map::section::general::GameMode; use rosu_map::section::general::GameMode;
@@ -35,7 +38,7 @@ use crate::{model::mode::IGameMode, util::mods::Mods};
/// .mods(8 + 1024) // HDFL /// .mods(8 + 1024) // HDFL
/// .calculate(&map); /// .calculate(&map);
/// ``` /// ```
#[derive(Copy, Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[must_use] #[must_use]
pub struct Difficulty { pub struct Difficulty {
mods: u32, mods: u32,
@@ -49,6 +52,24 @@ pub struct Difficulty {
/// This allows for an optimization to reduce the struct size by storing its /// This allows for an optimization to reduce the struct size by storing its
/// bits as a [`NonZeroU32`]. /// bits as a [`NonZeroU32`].
clock_rate: Option<NonZeroU32>, clock_rate: Option<NonZeroU32>,
ar: Option<ModsDependent>,
cs: Option<ModsDependent>,
hp: Option<ModsDependent>,
od: Option<ModsDependent>,
/// The slider multiplier will be clamped internally between 0.05 and 50.0.
///
/// Since its minimum value is 0.05, its bits are never zero.
///
/// This allows for an optimization to reduce the struct size by storing its
/// bits as a [`NonZeroU64`].
slider_multiplier: Option<NonZeroU64>,
hardrock_offsets: Option<bool>,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ModsDependent {
pub value: f32,
pub with_mods: bool,
} }
impl Difficulty { impl Difficulty {
@@ -58,12 +79,21 @@ impl Difficulty {
mods: 0, mods: 0,
passed_objects: None, passed_objects: None,
clock_rate: None, clock_rate: None,
ar: None,
cs: None,
hp: None,
od: None,
slider_multiplier: None,
hardrock_offsets: None,
} }
} }
/// Use this [`Difficulty`] as a calculator for a specific [`IGameMode`]. /// Use this [`Difficulty`] as a calculator for a specific [`IGameMode`].
pub const fn with_mode<M: IGameMode>(self) -> ConvertedDifficulty<M> { ///
ConvertedDifficulty::from_difficulty(self) /// Note that [`ConvertedDifficulty`] won't allow to further customize
/// fields so be sure they're all set before converting to it.
pub const fn with_mode<M: IGameMode>(&self) -> ConvertedDifficulty<'_, M> {
ConvertedDifficulty::new(self)
} }
/// Specify mods through their bit values. /// Specify mods through their bit values.
@@ -81,10 +111,14 @@ impl Difficulty {
} }
} }
/// Adjust the clock rate used in the calculation between 0.01 and 100.0. /// Adjust the clock rate used in the calculation.
/// ///
/// If none is specified, it will take the clock rate based on the mods /// If none is specified, it will take the clock rate based on the mods
/// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise. /// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
///
/// | Minimum | Maximum |
/// | :-----: | :-----: |
/// | 0.01 | 100 |
pub fn clock_rate(self, clock_rate: f64) -> Self { pub fn clock_rate(self, clock_rate: f64) -> Self {
let clock_rate = (clock_rate as f32).clamp(0.01, 100.0).to_bits(); let clock_rate = (clock_rate as f32).clamp(0.01, 100.0).to_bits();
@@ -98,9 +132,117 @@ impl Difficulty {
} }
} }
/// Perform the difficulty calculation. /// Override a beatmap's set AR.
/// ///
/// The returned attributes depend on the map's mode. /// Only relevant for osu! and osu!catch.
///
/// `with_mods` determines if the given value should be used before
/// or after accounting for mods, e.g. on `true` the value will be
/// used as is and on `false` it will be modified based on the mods.
///
/// | Minimum | Maximum |
/// | :-----: | :-----: |
/// | -20 | 20 |
pub fn ar(self, ar: f32, with_mods: bool) -> Self {
Self {
ar: Some(ModsDependent {
value: ar.clamp(-20.0, 20.0),
with_mods,
}),
..self
}
}
/// Override a beatmap's set CS.
///
/// Only relevant for osu! and osu!catch.
///
/// `with_mods` determines if the given value should be used before
/// or after accounting for mods, e.g. on `true` the value will be
/// used as is and on `false` it will be modified based on the mods.
///
/// | Minimum | Maximum |
/// | :-----: | :-----: |
/// | -20 | 20 |
pub fn cs(self, cs: f32, with_mods: bool) -> Self {
Self {
cs: Some(ModsDependent {
value: cs.clamp(-20.0, 20.0),
with_mods,
}),
..self
}
}
/// Override a beatmap's set HP.
///
/// `with_mods` determines if the given value should be used before
/// or after accounting for mods, e.g. on `true` the value will be
/// used as is and on `false` it will be modified based on the mods.
///
/// | Minimum | Maximum |
/// | :-----: | :-----: |
/// | -20 | 20 |
pub fn hp(self, hp: f32, with_mods: bool) -> Self {
Self {
hp: Some(ModsDependent {
value: hp.clamp(-20.0, 20.0),
with_mods,
}),
..self
}
}
/// Override a beatmap's set OD.
///
/// `with_mods` determines if the given value should be used before
/// or after accounting for mods, e.g. on `true` the value will be
/// used as is and on `false` it will be modified based on the mods.
///
/// | Minimum | Maximum |
/// | :-----: | :-----: |
/// | -20 | 20 |
pub fn od(self, od: f32, with_mods: bool) -> Self {
Self {
od: Some(ModsDependent {
value: od.clamp(-20.0, 20.0),
with_mods,
}),
..self
}
}
/// Adjust a beatmap's set scroll speed.
///
/// Only relevant for osu!taiko.
///
/// | Minimum | Maximum |
/// | :-----: | :-----: |
/// | 0.05 | 50 |
pub fn slider_multiplier(self, slider_multiplier: f64) -> Self {
let slider_multiplier = slider_multiplier.clamp(0.05, 50.0).to_bits();
// SAFETY: The minimum value is 0.05 so its bits can never be fully
// zero.
let non_zero = unsafe { NonZeroU64::new_unchecked(slider_multiplier) };
Self {
slider_multiplier: Some(non_zero),
..self
}
}
/// Adjust patterns as if the HR mod is enabled.
///
/// Only relevant for osu!catch.
pub const fn hardrock_offsets(self, hardrock_offsets: bool) -> Self {
Self {
hardrock_offsets: Some(hardrock_offsets),
..self
}
}
/// Perform the difficulty calculation.
pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes { pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes {
let map = Cow::Borrowed(map); let map = Cow::Borrowed(map);
@@ -157,4 +299,36 @@ impl Difficulty {
pub(crate) fn get_passed_objects(&self) -> usize { pub(crate) fn get_passed_objects(&self) -> usize {
self.passed_objects.map_or(usize::MAX, |n| n as usize) self.passed_objects.map_or(usize::MAX, |n| n as usize)
} }
pub(crate) const fn get_ar(&self) -> Option<ModsDependent> {
self.ar
}
pub(crate) const fn get_cs(&self) -> Option<ModsDependent> {
self.cs
}
pub(crate) const fn get_hp(&self) -> Option<ModsDependent> {
self.hp
}
pub(crate) const fn get_od(&self) -> Option<ModsDependent> {
self.od
}
pub(crate) fn get_slider_multiplier(&self) -> f64 {
self.slider_multiplier
.map(NonZeroU64::get)
.map_or(1.0, f64::from_bits)
}
pub(crate) const fn get_hardrock_offsets(&self) -> Option<bool> {
self.hardrock_offsets
}
}
impl Default for Difficulty {
fn default() -> Self {
Self::new()
}
} }
+3 -2
View File
@@ -163,7 +163,7 @@ impl ExactSizeIterator for CatchGradualDifficulty {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{any::difficulty::converted::ConvertedDifficulty, Beatmap}; use crate::Beatmap;
use super::*; use super::*;
@@ -207,8 +207,9 @@ mod tests {
assert_eq!(next_gradual, next_gradual_3rd); assert_eq!(next_gradual, next_gradual_3rd);
} }
let expected = ConvertedDifficulty::new() let expected = Difficulty::new()
.passed_objects(i as u32) .passed_objects(i as u32)
.with_mode()
.calculate(&converted); .calculate(&converted);
assert_eq!(next_gradual, expected); assert_eq!(next_gradual, expected);
+2 -2
View File
@@ -542,7 +542,7 @@ mod test {
use proptest::prelude::*; use proptest::prelude::*;
use crate::{any::difficulty::converted::ConvertedDifficulty, Beatmap}; use crate::Beatmap;
use super::*; use super::*;
@@ -559,7 +559,7 @@ mod test {
.unwrap() .unwrap()
.unchecked_into_converted::<Catch>(); .unchecked_into_converted::<Catch>();
let attrs = ConvertedDifficulty::new().calculate(&converted); let attrs = Difficulty::new().with_mode().calculate(&converted);
assert_eq!(N_FRUITS, attrs.n_fruits); assert_eq!(N_FRUITS, attrs.n_fruits);
assert_eq!(N_DROPLETS, attrs.n_droplets); assert_eq!(N_DROPLETS, attrs.n_droplets);
+1 -1
View File
@@ -165,7 +165,7 @@
#[doc(inline)] #[doc(inline)]
pub use self::{ pub use self::{
any::{ConvertedDifficulty, Difficulty, GradualDifficulty, GradualPerformance, Performance}, any::{Difficulty, GradualDifficulty, GradualPerformance, Performance},
model::beatmap::{Beatmap, Converted}, model::beatmap::{Beatmap, Converted},
}; };
+3 -2
View File
@@ -209,7 +209,7 @@ fn increment_combo_raw(is_circle: bool, start_time: f64, end_time: f64, curr_com
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{any::difficulty::converted::ConvertedDifficulty, mania::Mania, Beatmap}; use crate::{mania::Mania, Beatmap};
use super::*; use super::*;
@@ -257,8 +257,9 @@ mod tests {
assert_eq!(next_gradual, next_gradual_3rd); assert_eq!(next_gradual, next_gradual_3rd);
} }
let expected = ConvertedDifficulty::new() let expected = Difficulty::new()
.passed_objects(i as u32) .passed_objects(i as u32)
.with_mode()
.calculate(&converted); .calculate(&converted);
assert_eq!(next_gradual, expected); assert_eq!(next_gradual, expected);
+2 -2
View File
@@ -916,7 +916,7 @@ mod tests {
use proptest::prelude::*; use proptest::prelude::*;
use crate::{any::difficulty::converted::ConvertedDifficulty, Beatmap}; use crate::Beatmap;
use super::*; use super::*;
@@ -931,7 +931,7 @@ mod tests {
.unwrap() .unwrap()
.unchecked_into_converted::<Mania>(); .unchecked_into_converted::<Mania>();
let attrs = ConvertedDifficulty::new().calculate(&converted); let attrs = Difficulty::new().with_mode().calculate(&converted);
assert_eq!(N_OBJECTS, converted.hit_objects.len() as u32); assert_eq!(N_OBJECTS, converted.hit_objects.len() as u32);
+10 -10
View File
@@ -48,6 +48,16 @@ impl<'a, M> Converted<'a, M> {
pub fn into_inner(self) -> Cow<'a, Beatmap> { pub fn into_inner(self) -> Cow<'a, Beatmap> {
self.map self.map
} }
/// Borrow the contained [`Beatmap`] to cheaply create a new owned
/// [`Converted`].
///
/// This is the same as `.clone()` except cheap - but its lifetime might be
/// shorter.
#[must_use]
pub fn as_owned(&'a self) -> Self {
Self::new(Cow::Borrowed(self.map.as_ref()))
}
} }
impl<M: IGameMode> Converted<'_, M> { impl<M: IGameMode> Converted<'_, M> {
@@ -85,16 +95,6 @@ impl<M: IGameMode> Converted<'_, M> {
} }
impl<'a, M: IGameMode> Converted<'a, M> { impl<'a, M: IGameMode> Converted<'a, M> {
/// Borrow the contained [`Beatmap`] to cheaply create a new owned
/// [`Converted`].
///
/// This is the same as `.clone()` except cheap - but its lifetime might be
/// shorter.
#[must_use]
pub fn as_owned(&'a self) -> Self {
Self::new(Cow::Borrowed(self.map.as_ref()))
}
/// Create a performance calculator for the map. /// Create a performance calculator for the map.
pub fn performance(self) -> M::Performance<'a> { pub fn performance(self) -> M::Performance<'a> {
M::performance(self) M::performance(self)
+3 -3
View File
@@ -261,8 +261,7 @@ mod osu_objects {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{osu::Osu, Beatmap};
use crate::{any::difficulty::converted::ConvertedDifficulty, osu::Osu, Beatmap};
use super::*; use super::*;
@@ -310,8 +309,9 @@ mod tests {
assert_eq!(next_gradual, next_gradual_3rd); assert_eq!(next_gradual, next_gradual_3rd);
} }
let expected = ConvertedDifficulty::new() let expected = Difficulty::new()
.passed_objects(i as u32) .passed_objects(i as u32)
.with_mode()
.calculate(&converted); .calculate(&converted);
assert_eq!(next_gradual, expected); assert_eq!(next_gradual, expected);
+2 -2
View File
@@ -823,7 +823,7 @@ mod test {
use proptest::prelude::*; use proptest::prelude::*;
use crate::{any::difficulty::converted::ConvertedDifficulty, Beatmap}; use crate::Beatmap;
use super::*; use super::*;
@@ -838,7 +838,7 @@ mod test {
.unwrap() .unwrap()
.unchecked_into_converted::<Osu>(); .unchecked_into_converted::<Osu>();
let attrs = ConvertedDifficulty::new().calculate(&converted); let attrs = Difficulty::new().with_mode().calculate(&converted);
assert_eq!( assert_eq!(
(attrs.n_circles, attrs.n_sliders, attrs.n_spinners), (attrs.n_circles, attrs.n_sliders, attrs.n_spinners),
+3 -2
View File
@@ -260,7 +260,7 @@ impl ExactSizeIterator for TaikoGradualDifficulty {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{any::difficulty::converted::ConvertedDifficulty, Beatmap}; use crate::Beatmap;
use super::*; use super::*;
@@ -312,8 +312,9 @@ mod tests {
assert_eq!(next_gradual, next_gradual_3rd); assert_eq!(next_gradual, next_gradual_3rd);
} }
let expected = ConvertedDifficulty::new() let expected = Difficulty::new()
.passed_objects(i as u32) .passed_objects(i as u32)
.with_mode()
.calculate(&converted); .calculate(&converted);
assert_eq!(next_gradual, expected); assert_eq!(next_gradual, expected);
+2 -2
View File
@@ -501,7 +501,7 @@ mod test {
use proptest::prelude::*; use proptest::prelude::*;
use crate::{any::difficulty::converted::ConvertedDifficulty, Beatmap}; use crate::Beatmap;
use super::*; use super::*;
@@ -516,7 +516,7 @@ mod test {
.unwrap() .unwrap()
.unchecked_into_converted::<Taiko>(); .unchecked_into_converted::<Taiko>();
let attrs = ConvertedDifficulty::new().calculate(&converted); let attrs = Difficulty::new().with_mode().calculate(&converted);
assert_eq!(MAX_COMBO, attrs.max_combo); assert_eq!(MAX_COMBO, attrs.max_combo);
+2 -2
View File
@@ -5,7 +5,7 @@ use rosu_pp::{
mania::{Mania, ManiaDifficultyAttributes}, mania::{Mania, ManiaDifficultyAttributes},
osu::{Osu, OsuDifficultyAttributes}, osu::{Osu, OsuDifficultyAttributes},
taiko::{Taiko, TaikoDifficultyAttributes}, taiko::{Taiko, TaikoDifficultyAttributes},
Beatmap, ConvertedDifficulty, Beatmap, Difficulty,
}; };
use self::common::*; use self::common::*;
@@ -25,7 +25,7 @@ macro_rules! test_cases {
$( $(
let mods = 0 $( + $mods )*; let mods = 0 $( + $mods )*;
let expected = test_cases!(@$mode { $( $key: $value, )* }); let expected = test_cases!(@$mode { $( $key: $value, )* });
let actual = ConvertedDifficulty::new().mods(mods).calculate(&map); let actual = Difficulty::new().mods(mods).with_mode().calculate(&map);
run(&actual, &expected, mods); run(&actual, &expected, mods);
)* )*
}; };