restructure Difficulty and ModeDifficulty

This commit is contained in:
MaxOhn
2024-03-03 09:57:44 +01:00
parent 3705338d12
commit e749a791fc
39 changed files with 446 additions and 448 deletions
+133
View File
@@ -0,0 +1,133 @@
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
marker::PhantomData,
};
use crate::{
model::{beatmap::Converted, mode::IGameMode},
util::generic_fmt::GenericFormatter,
Difficulty,
};
/// Difficulty calculator on maps of a given mode.
#[must_use]
pub struct ConvertedDifficulty<M> {
inner: Difficulty,
_mode: PhantomData<M>,
}
impl<M> ConvertedDifficulty<M> {
/// Create a new difficulty calculator for a generic mode.
pub const fn new() -> Self {
Self::from_difficulty(Difficulty::new())
}
pub(crate) const fn from_difficulty(difficulty: Difficulty) -> Self {
Self {
inner: difficulty,
_mode: PhantomData,
}
}
/// Return the internal [`Difficulty`].
pub const fn into_inner(self) -> Difficulty {
self.inner
}
/// Cast from generic mode `M` to `N`.
pub fn cast<N: IGameMode>(self) -> ConvertedDifficulty<N> {
ConvertedDifficulty {
inner: self.inner,
_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> {
/// Perform the difficulty calculation for a [`Converted`] beatmap and
/// process the final skill values.
pub fn calculate(&self, map: &Converted<'_, M>) -> M::DifficultyAttributes {
M::difficulty(&self.inner, map)
}
/// Perform a difficulty calculation for a [`Converted`] beatmap without
/// processing the final skill values.
pub fn strains(&self, map: &Converted<'_, M>) -> M::Strains {
M::strains(&self.inner, map)
}
/// Create a gradual difficulty calculator for a [`Converted`] beatmap.
pub fn gradual_difficulty(&self, map: &Converted<'_, M>) -> M::GradualDifficulty {
M::gradual_difficulty(&self.inner, map)
}
/// Create a gradual performance calculator for a [`Converted`] beatmap.
pub fn gradual_performance(&self, map: &Converted<'_, M>) -> M::GradualPerformance {
M::gradual_performance(&self.inner, map)
}
}
impl<M: IGameMode> From<Difficulty> for ConvertedDifficulty<M> {
fn from(difficulty: Difficulty) -> Self {
Self::from_difficulty(difficulty)
}
}
impl<M> AsRef<Difficulty> for ConvertedDifficulty<M> {
fn as_ref(&self) -> &Difficulty {
&self.inner
}
}
impl<M> Clone for ConvertedDifficulty<M> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
_mode: PhantomData,
}
}
}
impl<M> Debug for ConvertedDifficulty<M> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("ConvertedDifficulty")
.field("inner", &self.inner)
.field("mode", &GenericFormatter::<M>::new())
.finish()
}
}
impl<M> PartialEq for ConvertedDifficulty<M> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
+21 -14
View File
@@ -4,11 +4,12 @@ use rosu_map::section::general::GameMode;
use crate::{
any::DifficultyAttributes,
catch::{CatchBeatmap, CatchGradualDifficulty},
mania::{ManiaBeatmap, ManiaGradualDifficulty},
osu::{OsuBeatmap, OsuGradualDifficulty},
taiko::{TaikoBeatmap, TaikoGradualDifficulty},
Beatmap, ModeDifficulty,
catch::{Catch, CatchBeatmap, CatchGradualDifficulty},
mania::{Mania, ManiaBeatmap, ManiaGradualDifficulty},
model::mode::IGameMode,
osu::{Osu, OsuBeatmap, OsuGradualDifficulty},
taiko::{Taiko, TaikoBeatmap, TaikoGradualDifficulty},
Beatmap, Converted, Difficulty,
};
/// Gradually calculate the difficulty attributes on maps of any mode.
@@ -22,10 +23,10 @@ use crate::{
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, GradualDifficulty, ModeDifficulty};
/// use rosu_pp::{Beatmap, GradualDifficulty, Difficulty};
///
/// let map = Beatmap::from_path("./resources/2785319.osu").unwrap();
/// let difficulty = ModeDifficulty::new().mods(64); // DT
/// let difficulty = Difficulty::new().mods(64); // DT
/// let mut iter = GradualDifficulty::new(&difficulty, &map);
///
/// // the difficulty of the map after the first object
@@ -53,22 +54,28 @@ pub enum GradualDifficulty {
macro_rules! from_converted {
( $fn:ident, $mode:ident, $converted:ident ) => {
#[doc = concat!("Create a [`GradualDifficulty`] for a [`", stringify!($converted), "`]")]
pub fn $fn(difficulty: &ModeDifficulty, converted: &$converted<'_>) -> Self {
Self::$mode(difficulty.gradual_difficulty(converted))
pub fn $fn(difficulty: &Difficulty, converted: &$converted<'_>) -> Self {
Self::$mode($mode::gradual_difficulty(difficulty, converted))
}
};
}
impl GradualDifficulty {
/// Create a [`GradualDifficulty`] for a map of any mode.
pub fn new(difficulty: &ModeDifficulty, map: &Beatmap) -> Self {
pub fn new(difficulty: &Difficulty, map: &Beatmap) -> Self {
let map = Cow::Borrowed(map);
match map.mode {
GameMode::Osu => Self::Osu(difficulty.gradual_difficulty(&OsuBeatmap::new(map))),
GameMode::Taiko => Self::Taiko(difficulty.gradual_difficulty(&TaikoBeatmap::new(map))),
GameMode::Catch => Self::Catch(difficulty.gradual_difficulty(&CatchBeatmap::new(map))),
GameMode::Mania => Self::Mania(difficulty.gradual_difficulty(&ManiaBeatmap::new(map))),
GameMode::Osu => Self::Osu(Osu::gradual_difficulty(difficulty, &Converted::new(map))),
GameMode::Taiko => {
Self::Taiko(Taiko::gradual_difficulty(difficulty, &Converted::new(map)))
}
GameMode::Catch => {
Self::Catch(Catch::gradual_difficulty(difficulty, &Converted::new(map)))
}
GameMode::Mania => {
Self::Mania(Mania::gradual_difficulty(difficulty, &Converted::new(map)))
}
}
}
+70 -113
View File
@@ -3,144 +3,79 @@ use std::borrow::Cow;
use rosu_map::section::general::GameMode;
use crate::{
catch::{Catch, CatchBeatmap},
mania::{Mania, ManiaBeatmap},
catch::Catch,
mania::Mania,
model::beatmap::{Beatmap, Converted},
osu::{Osu, OsuBeatmap},
taiko::{Taiko, TaikoBeatmap},
osu::Osu,
taiko::Taiko,
GradualDifficulty, GradualPerformance,
};
use self::mode::ModeDifficulty;
use self::converted::ConvertedDifficulty;
use super::{attributes::DifficultyAttributes, Strains};
pub mod converted;
pub mod gradual;
pub mod mode;
pub mod object;
pub mod skills;
use crate::{model::mode::IGameMode, util::mods::Mods};
/// Difficulty calculator on maps of any mode.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, Default, PartialEq)]
#[must_use]
pub struct Difficulty<'map> {
map: Cow<'map, Beatmap>,
inner: ModeDifficulty,
pub struct Difficulty {
mods: u32,
passed_objects: Option<u32>,
clock_rate: Option<f64>,
}
impl<'map> Difficulty<'map> {
/// Create a new difficulty calculator for the given beatmap.
pub const fn new(map: &'map Beatmap) -> Self {
Self::new_with_cow(Cow::Borrowed(map))
}
const fn new_with_cow(map: Cow<'map, Beatmap>) -> Self {
impl Difficulty {
/// Create a new difficulty calculator.
pub const fn new() -> Self {
Self {
map,
inner: ModeDifficulty::new(),
mods: 0,
passed_objects: None,
clock_rate: None,
}
}
}
macro_rules! impl_from_mode {
( $mode:ident ) => {
impl<'a> From<Converted<'a, $mode>> for Difficulty<'a> {
fn from(converted: Converted<'a, $mode>) -> Self {
Self::new_with_cow(converted.into_inner())
}
}
impl<'a, 'b: 'a> From<&'b Converted<'a, $mode>> for Difficulty<'a> {
fn from(converted: &'b Converted<'a, $mode>) -> Self {
Self::new(&converted)
}
}
};
}
impl_from_mode!(Osu);
impl_from_mode!(Taiko);
impl_from_mode!(Catch);
impl_from_mode!(Mania);
impl Difficulty<'_> {
/// Attempt to convert the map to the specified mode.
/// Use this [`&Difficulty`] as a calculator for a specific [`IGameMode`].
///
/// If the conversion is incompatible, `None` is returned.
///
/// If the given mode should be ignored in case it is incompatible, use
/// [`mode_or_ignore`] instead.
///
/// [`mode_or_ignore`]: Self::mode_or_ignore
pub fn try_mode(&mut self, mode: GameMode) -> Option<&mut Self> {
let map = match mode {
GameMode::Osu => OsuBeatmap::try_from_ref(self.map.as_ref())?.into_inner(),
GameMode::Taiko => TaikoBeatmap::try_from_ref(self.map.as_ref())?.into_inner(),
GameMode::Catch => CatchBeatmap::try_from_ref(self.map.as_ref())?.into_inner(),
GameMode::Mania => ManiaBeatmap::try_from_ref(self.map.as_ref())?.into_inner(),
/// [`&Difficulty`]: Difficulty
pub const fn with_mode<M: IGameMode>(&self) -> ConvertedDifficulty<M> {
let this = Self {
mods: self.mods,
passed_objects: self.passed_objects,
clock_rate: self.clock_rate,
};
if matches!(map, Cow::Owned(_)) {
let map = map.into_owned();
self.map = Cow::Owned(map);
}
Some(self)
}
/// Attempt to convert the map to the specified mode.
///
/// If the conversion is incompatible, the map won't be modified.
///
/// To see whether the given mode is incompatible, use [`try_mode`]
/// instead.
///
/// [`try_mode`]: Self::try_mode
pub fn mode_or_ignore(&mut self, mode: GameMode) -> &mut Self {
let map = self.map.as_ref();
let map_opt = match mode {
GameMode::Osu => OsuBeatmap::try_from_ref(map).map(Converted::into_inner),
GameMode::Taiko => TaikoBeatmap::try_from_ref(map).map(Converted::into_inner),
GameMode::Catch => CatchBeatmap::try_from_ref(map).map(Converted::into_inner),
GameMode::Mania => ManiaBeatmap::try_from_ref(map).map(Converted::into_inner),
};
match map_opt {
Some(cow @ Cow::Owned(_)) => {
let map = cow.into_owned();
self.map = Cow::Owned(map);
}
Some(Cow::Borrowed(_)) | None => {}
}
self
ConvertedDifficulty::from_difficulty(this)
}
/// Specify mods through their bit values.
///
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
pub fn mods(self, mods: u32) -> Self {
Self {
inner: self.inner.mods(mods),
..self
}
pub const fn mods(self, mods: u32) -> Self {
Self { mods, ..self }
}
/// Amount of passed objects for partial plays, e.g. a fail.
pub fn passed_objects(self, passed_objects: u32) -> Self {
pub const fn passed_objects(self, passed_objects: u32) -> Self {
Self {
inner: self.inner.passed_objects(passed_objects),
passed_objects: Some(passed_objects),
..self
}
}
/// Adjust the clock rate used in the calculation.
/// 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),
clock_rate: Some(clock_rate.clamp(0.01, 100.0)),
..self
}
}
@@ -148,19 +83,19 @@ impl Difficulty<'_> {
/// Perform the difficulty calculation.
///
/// The returned attributes depend on the map's mode.
pub fn calculate(&self) -> DifficultyAttributes {
let map = Cow::Borrowed(self.map.as_ref());
pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes {
let map = Cow::Borrowed(map);
match self.map.mode {
GameMode::Osu => DifficultyAttributes::Osu(self.inner.calculate(&OsuBeatmap::new(map))),
match map.mode {
GameMode::Osu => DifficultyAttributes::Osu(Osu::difficulty(self, &Converted::new(map))),
GameMode::Taiko => {
DifficultyAttributes::Taiko(self.inner.calculate(&TaikoBeatmap::new(map)))
DifficultyAttributes::Taiko(Taiko::difficulty(self, &Converted::new(map)))
}
GameMode::Catch => {
DifficultyAttributes::Catch(self.inner.calculate(&CatchBeatmap::new(map)))
DifficultyAttributes::Catch(Catch::difficulty(self, &Converted::new(map)))
}
GameMode::Mania => {
DifficultyAttributes::Mania(self.inner.calculate(&ManiaBeatmap::new(map)))
DifficultyAttributes::Mania(Mania::difficulty(self, &Converted::new(map)))
}
}
}
@@ -169,14 +104,36 @@ impl Difficulty<'_> {
/// strains, return them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(&self) -> Strains {
let map = Cow::Borrowed(self.map.as_ref());
pub fn strains(&self, map: &Beatmap) -> Strains {
let map = Cow::Borrowed(map);
match self.map.mode {
GameMode::Osu => Strains::Osu(self.inner.strains(&OsuBeatmap::new(map))),
GameMode::Taiko => Strains::Taiko(self.inner.strains(&TaikoBeatmap::new(map))),
GameMode::Catch => Strains::Catch(self.inner.strains(&CatchBeatmap::new(map))),
GameMode::Mania => Strains::Mania(self.inner.strains(&ManiaBeatmap::new(map))),
match map.mode {
GameMode::Osu => Strains::Osu(Osu::strains(self, &Converted::new(map))),
GameMode::Taiko => Strains::Taiko(Taiko::strains(self, &Converted::new(map))),
GameMode::Catch => Strains::Catch(Catch::strains(self, &Converted::new(map))),
GameMode::Mania => Strains::Mania(Mania::strains(self, &Converted::new(map))),
}
}
/// Create a gradual difficulty calculator for a [`Beatmap`].
pub fn gradual_difficulty(&self, map: &Beatmap) -> GradualDifficulty {
GradualDifficulty::new(self, map)
}
/// Create a gradual performance calculator for a [`Beatmap`].
pub fn gradual_performance(&self, map: &Beatmap) -> GradualPerformance {
GradualPerformance::new(self, map)
}
pub(crate) const fn get_mods(&self) -> u32 {
self.mods
}
pub(crate) fn get_clock_rate(&self) -> f64 {
self.clock_rate.unwrap_or_else(|| self.mods.clock_rate())
}
pub(crate) fn get_passed_objects(&self) -> usize {
self.passed_objects.map_or(usize::MAX, |n| n as usize)
}
}
-87
View File
@@ -1,87 +0,0 @@
use crate::{
model::{beatmap::Converted, mode::IGameMode},
util::mods::Mods,
};
/// Difficulty calculator on maps of a given mode.
#[derive(Clone, Debug, Default, PartialEq)]
#[must_use]
pub struct ModeDifficulty {
mods: u32,
passed_objects: Option<u32>,
clock_rate: Option<f64>,
}
impl ModeDifficulty {
/// Create a new difficulty calculator.
pub const fn new() -> Self {
Self {
mods: 0,
passed_objects: None,
clock_rate: None,
}
}
/// 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 { mods, ..self }
}
/// Amount of passed objects for partial plays, e.g. a fail.
pub const fn passed_objects(self, passed_objects: u32) -> Self {
Self {
passed_objects: Some(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 {
clock_rate: Some(clock_rate.clamp(0.01, 100.0)),
..self
}
}
/// Perform the difficulty calculation for a [`Converted`] beatmap and
/// process the final skill values.
pub fn calculate<M: IGameMode>(&self, map: &Converted<'_, M>) -> M::DifficultyAttributes {
M::difficulty(self, map)
}
/// Perform a difficulty calculation for a [`Converted`] beatmap without
/// processing the final skill values.
pub fn strains<M: IGameMode>(&self, map: &Converted<'_, M>) -> M::Strains {
M::strains(self, map)
}
/// Create a gradual difficulty calculator for a [`Converted`] beatmap.
pub fn gradual_difficulty<M: IGameMode>(&self, map: &Converted<'_, M>) -> M::GradualDifficulty {
M::gradual_difficulty(self, map)
}
/// Create a gradual performance calculator for a [`Converted`] beatmap.
pub fn gradual_performance<M: IGameMode>(
&self,
map: &Converted<'_, M>,
) -> M::GradualPerformance {
M::gradual_performance(self, map)
}
pub(crate) const fn get_mods(&self) -> u32 {
self.mods
}
pub(crate) fn get_clock_rate(&self) -> f64 {
self.clock_rate.unwrap_or_else(|| self.mods.clock_rate())
}
pub(crate) fn get_passed_objects(&self) -> usize {
self.passed_objects.map_or(usize::MAX, |n| n as usize)
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ pub use self::{
attributes::{
AttributeProvider, DifficultyAttributes, ModeAttributeProvider, PerformanceAttributes,
},
difficulty::{gradual::GradualDifficulty, mode::ModeDifficulty, Difficulty},
difficulty::{converted::ConvertedDifficulty, gradual::GradualDifficulty, Difficulty},
performance::{gradual::GradualPerformance, HitResultPriority, Performance},
score_state::ScoreState,
strains::Strains,
+21 -14
View File
@@ -4,11 +4,12 @@ use rosu_map::section::general::GameMode;
use crate::{
any::{PerformanceAttributes, ScoreState},
catch::{CatchBeatmap, CatchGradualPerformance},
mania::{ManiaBeatmap, ManiaGradualPerformance},
osu::{OsuBeatmap, OsuGradualPerformance},
taiko::{TaikoBeatmap, TaikoGradualPerformance},
Beatmap, ModeDifficulty,
catch::{Catch, CatchBeatmap, CatchGradualPerformance},
mania::{Mania, ManiaBeatmap, ManiaGradualPerformance},
model::mode::IGameMode,
osu::{Osu, OsuBeatmap, OsuGradualPerformance},
taiko::{Taiko, TaikoBeatmap, TaikoGradualPerformance},
Beatmap, Converted, Difficulty,
};
/// Gradually calculate the performance attributes on maps of any mode.
@@ -31,10 +32,10 @@ use crate::{
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, GradualPerformance, ModeDifficulty, any::ScoreState};
/// use rosu_pp::{Beatmap, GradualPerformance, Difficulty, any::ScoreState};
///
/// let map = Beatmap::from_path("./resources/2785319.osu").unwrap();
/// let difficulty = ModeDifficulty::new().mods(64); // DT
/// let difficulty = Difficulty::new().mods(64); // DT
/// let mut gradual = GradualPerformance::new(&difficulty, &map);
/// let mut state = ScoreState::new(); // empty state, everything is on 0.
///
@@ -102,22 +103,28 @@ pub enum GradualPerformance {
macro_rules! from_converted {
( $fn:ident, $mode:ident, $converted:ident ) => {
#[doc = concat!("Create a [`GradualPerformance`] for a [`", stringify!($converted), "`]")]
pub fn $fn(difficulty: &ModeDifficulty, converted: &$converted<'_>) -> Self {
Self::$mode(difficulty.gradual_performance(converted))
pub fn $fn(difficulty: &Difficulty, converted: &$converted<'_>) -> Self {
Self::$mode($mode::gradual_performance(difficulty, converted))
}
};
}
impl GradualPerformance {
/// Create a [`GradualPerformance`] for a map of any mode.
pub fn new(difficulty: &ModeDifficulty, map: &Beatmap) -> Self {
pub fn new(difficulty: &Difficulty, map: &Beatmap) -> Self {
let map = Cow::Borrowed(map);
match map.mode {
GameMode::Osu => Self::Osu(difficulty.gradual_performance(&OsuBeatmap::new(map))),
GameMode::Taiko => Self::Taiko(difficulty.gradual_performance(&TaikoBeatmap::new(map))),
GameMode::Catch => Self::Catch(difficulty.gradual_performance(&CatchBeatmap::new(map))),
GameMode::Mania => Self::Mania(difficulty.gradual_performance(&ManiaBeatmap::new(map))),
GameMode::Osu => Self::Osu(Osu::gradual_performance(difficulty, &Converted::new(map))),
GameMode::Taiko => {
Self::Taiko(Taiko::gradual_performance(difficulty, &Converted::new(map)))
}
GameMode::Catch => {
Self::Catch(Catch::gradual_performance(difficulty, &Converted::new(map)))
}
GameMode::Mania => {
Self::Mania(Mania::gradual_performance(difficulty, &Converted::new(map)))
}
}
}