added GradualDifficultyAttributes and GradualPerformanceAttributes

This commit is contained in:
MaxOhn
2021-11-24 19:17:13 +01:00
parent 2736b4c769
commit db1bac08d7
6 changed files with 416 additions and 2 deletions
+1
View File
@@ -14,6 +14,7 @@
- Added methods `TaikoDifficultyAttributes::max_combo` and `OsuDifficultyAttributes::max_combo`
- Added structs `{Mode}GradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the mode's `stars` function over and over.
- Added structs `{Mode}GradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `{Mode}PP` over and over.
- Added `BeatmapExt::gradual_difficulty` and `BeatmapExt::gradual_performance` to gradually calculate the difficulty or performance on maps of any mode.
# v0.3.0
+3 -1
View File
@@ -5,6 +5,8 @@ use super::{FruitsGradualDifficultyAttributes, FruitsPerformanceAttributes};
// TODO: Benchmark if Copy is faster than Clone
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
///
/// This struct is used for [`FruitsGradualPerformanceAttributes`].
#[derive(Copy, Clone, Debug, Default)]
pub struct FruitsScoreState {
/// Maximum combo that the score has had so far.
@@ -39,7 +41,7 @@ impl FruitsScoreState {
/// To process multiple objects at once, use
/// [`process_next_n_objects`](`FruitsGradualPerformanceAttributes::process_next_n_objects`) instead.
///
/// Both methods require an [`FruitsScoreState`] that contains the current
/// Both methods require a [`FruitsScoreState`] that contains the current
/// hitresults as well as the maximum combo so far.
///
/// Note that neither hits nor misses of tiny droplets require
+372
View File
@@ -0,0 +1,372 @@
use crate::{
fruits::{
FruitsGradualDifficultyAttributes, FruitsGradualPerformanceAttributes, FruitsScoreState,
},
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes},
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState},
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState},
Beatmap, DifficultyAttributes, GameMode, Mods, PerformanceAttributes,
};
/// Gradually calculate the difficulty attributes on maps of any mode.
///
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will
/// be processed and the [`DifficultyAttributes`] will be updated and returned.
///
/// If you want to calculate performance attributes, use
/// [`GradualPerformanceAttributes`](crate::GradualPerformanceAttributes) instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, GradualDifficultyAttributes};
///
/// # /*
/// let map: Beatmap = ...
/// # */
/// # let map = Beatmap::default();
///
/// let mods = 64; // DT
/// let mut iter = GradualDifficultyAttributes::new(&map, mods);
///
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
/// let attrs2 = iter.next(); // after the second hit object
///
/// // Remaining hit objects
/// for difficulty in iter {
/// // ...
/// }
/// ```
#[derive(Clone, Debug)]
pub enum GradualDifficultyAttributes<'map> {
#[cfg(feature = "fruits")]
/// Gradual osu!fruits difficulty attributes.
Fruits(FruitsGradualDifficultyAttributes<'map>),
#[cfg(feature = "mania")]
/// Gradual osu!mania difficulty attributes.
Mania(ManiaGradualDifficultyAttributes<'map>),
#[cfg(feature = "osu")]
/// Gradual osu!standard difficulty attributes.
Osu(OsuGradualDifficultyAttributes),
#[cfg(feature = "taiko")]
/// Gradual osu!taiko difficulty attributes.
Taiko(TaikoGradualDifficultyAttributes<'map>),
}
impl<'map> GradualDifficultyAttributes<'map> {
/// Create a new gradual difficulty calculator for maps of any mode.
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
match map.mode {
#[cfg(feature = "osu")]
GameMode::STD => Self::Osu(OsuGradualDifficultyAttributes::new(map, mods)),
#[cfg(feature = "taiko")]
GameMode::TKO => Self::Taiko(TaikoGradualDifficultyAttributes::new(map, mods)),
#[cfg(feature = "fruits")]
GameMode::CTB => Self::Fruits(FruitsGradualDifficultyAttributes::new(map, mods)),
#[cfg(feature = "mania")]
GameMode::MNA => Self::Mania(ManiaGradualDifficultyAttributes::new(map, mods)),
#[allow(unreachable_patterns)]
_ => panic!("feature for mode {:?} is not enabled", map.mode),
}
}
}
impl Iterator for GradualDifficultyAttributes<'_> {
type Item = DifficultyAttributes;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
match self {
#[cfg(feature = "fruits")]
GradualDifficultyAttributes::Fruits(f) => f.next().map(DifficultyAttributes::Fruits),
#[cfg(feature = "mania")]
GradualDifficultyAttributes::Mania(m) => m.next().map(DifficultyAttributes::Mania),
#[cfg(feature = "osu")]
GradualDifficultyAttributes::Osu(o) => o.next().map(DifficultyAttributes::Osu),
#[cfg(feature = "taiko")]
GradualDifficultyAttributes::Taiko(t) => t.next().map(DifficultyAttributes::Taiko),
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
#[cfg(feature = "fruits")]
GradualDifficultyAttributes::Fruits(f) => f.size_hint(),
#[cfg(feature = "mania")]
GradualDifficultyAttributes::Mania(m) => m.size_hint(),
#[cfg(feature = "osu")]
GradualDifficultyAttributes::Osu(o) => o.size_hint(),
#[cfg(feature = "taiko")]
GradualDifficultyAttributes::Taiko(t) => t.size_hint(),
}
}
}
// TODO: Benchmark if Copy is faster than Clone
/// Aggregation for a score's current state i.e. what wa
/// the maximum combo so far, what are the current
/// hitresults and what is the current score.
///
/// This struct is used for [`GradualPerformanceAttributes`].
///
/// Depending on your mode features, some fields might be optimized out.
#[derive(Copy, Clone, Debug, Default)]
pub struct ScoreState {
#[cfg(any(feature = "osu", feature = "fruits", feature = "taiko"))]
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
///
/// Note that for osu!ctb only fruits and droplets are considered for combo.
///
/// Irrelevant for osu!mania.
pub max_combo: usize,
#[cfg(feature = "fruits")]
/// Amount of current katus (tiny droplet misses for osu!ctb).
///
/// Only relevant for osu!ctb.
pub n_katu: usize,
#[cfg(any(feature = "osu", feature = "fruits", feature = "taiko"))]
/// Amount of current 300s (fruits for osu!ctb).
///
/// Irrelevant for osu!mania.
pub n300: usize,
#[cfg(any(feature = "osu", feature = "fruits", feature = "taiko"))]
/// Amount of current 100s (droplets for osu!ctb).
///
/// Irrelevant for osu!mania.
pub n100: usize,
#[cfg(any(feature = "osu", feature = "fruits"))]
/// Amount of current 50s (tiny droplets for osu!ctb).
///
/// Irrelevant for osu!taiko and osu!mania.
pub n50: usize,
#[cfg(any(feature = "osu", feature = "fruits", feature = "taiko"))]
/// Amount of current misses (fruits + droplets for osu!ctb).
///
/// Irrelevant for osu!mania.
pub misses: usize,
#[cfg(feature = "mania")]
/// The current score.
///
/// Only relevant for osu!mania.
pub score: u32,
}
impl ScoreState {
/// Create a new empty score state.
pub fn new() -> Self {
Self::default()
}
}
#[cfg(feature = "fruits")]
impl From<ScoreState> for FruitsScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n_fruits: state.n300,
n_droplets: state.n100,
n_tiny_droplets: state.n50,
n_tiny_droplet_misses: state.n_katu,
misses: state.misses,
}
}
}
#[cfg(feature = "osu")]
impl From<ScoreState> for OsuScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n300: state.n300,
n100: state.n100,
n50: state.n50,
misses: state.misses,
}
}
}
#[cfg(feature = "taiko")]
impl From<ScoreState> for TaikoScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n300: state.n300,
n100: state.n100,
misses: state.misses,
}
}
}
/// Gradually calculate the performance attributes on maps of any mode.
///
/// After each hit object you can call
/// [`process_next_object`](`GradualPerformanceAttributes::process_next_object`)
/// and it will return the resulting current [`PerformanceAttributes`].
/// To process multiple objects at once, use
/// [`process_next_n_objects`](`GradualPerformanceAttributes::process_next_n_objects`) instead.
///
/// Both methods require a [`ScoreState`] that contains the current hitresults
/// as well as the maximum combo so far or just the current score for osu!mania.
/// Since the map could have any mode, all fields of `ScoreState` could be of use
/// and should be updated properly.
///
/// Alternatively, you can match on the map's mode yourself and use the gradual
/// performance attribute struct for the corresponding mode, i.e.
/// [`FruitsGradualPerformanceAttributes`],
/// [`ManiaGradualPerformanceAttributes`],
/// [`OsuGradualPerformanceAttributes`], or
/// [`TaikoGradualPerformanceAttributes`].
///
/// If you only want to calculate difficulty attributes use
/// [`GradualDifficultyAttributes`](crate::GradualDifficultyAttributes) instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, GradualPerformanceAttributes, ScoreState};
///
/// # /*
/// let map: Beatmap = ...
/// # */
/// # let map = Beatmap::default();
///
/// let mods = 64; // DT
/// let mut gradual_perf = GradualPerformanceAttributes::new(&map, mods);
/// let mut state = ScoreState::new(); // empty state, everything is on 0.
///
/// // The first 10 hitresults are 300s and increase the score by 123 each.
/// for _ in 0..10 {
/// state.n300 += 1;
/// state.max_combo += 1;
/// state.score += 123;
///
/// # /*
/// let performance = gradual_perf.process_next_object(state).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state);
/// }
///
/// // Then comes a miss.
/// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset.
/// state.misses += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(state).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state);
///
/// // The next 10 objects will be a mixture of 300s, 100s, and 50s.
/// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 2;
/// state.n100 += 7;
/// state.n50 += 1;
/// state.score += 987;
/// // Don't forget state.n_katu
/// # /*
/// let performance = gradual_perf.process_next_n_objects(state, 10).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_n_objects(state, 10);
///
/// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1;
/// state.max_combo += 1;
/// state.score += 123;
/// # /*
/// let performance = gradual_perf.process_next_object(state).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state);
///
/// // Skip to the end
/// # /*
/// state.max_combo = ...
/// state.n300 = ...
/// ...
/// let final_performance = gradual_perf.process_next_n_objects(state, usize::MAX).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_n_objects(state, usize::MAX);
///
/// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(state).is_none());
/// ```
#[derive(Clone, Debug)]
pub enum GradualPerformanceAttributes<'map> {
#[cfg(feature = "fruits")]
/// Gradual osu!ctb performance attributes.
Fruits(FruitsGradualPerformanceAttributes<'map>),
#[cfg(feature = "mania")]
/// Gradual osu!mania performance attributes.
Mania(ManiaGradualPerformanceAttributes<'map>),
#[cfg(feature = "osu")]
/// Gradual osu!standard performance attributes.
Osu(OsuGradualPerformanceAttributes<'map>),
#[cfg(feature = "taiko")]
/// Gradual osu!taiko performance attributes.
Taiko(TaikoGradualPerformanceAttributes<'map>),
}
impl<'map> GradualPerformanceAttributes<'map> {
/// Create a new gradual performance calculator for maps of any mode.
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
match map.mode {
#[cfg(feature = "osu")]
GameMode::STD => Self::Osu(OsuGradualPerformanceAttributes::new(map, mods)),
#[cfg(feature = "taiko")]
GameMode::TKO => Self::Taiko(TaikoGradualPerformanceAttributes::new(map, mods)),
#[cfg(feature = "fruits")]
GameMode::CTB => Self::Fruits(FruitsGradualPerformanceAttributes::new(map, mods)),
#[cfg(feature = "mania")]
GameMode::MNA => Self::Mania(ManiaGradualPerformanceAttributes::new(map, mods)),
#[allow(unreachable_patterns)]
_ => panic!("feature for mode {:?} is not enabled", map.mode),
}
}
/// Process the next hit object and calculate the
/// performance attributes for the resulting score.
pub fn process_next_object(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
self.process_next_n_objects(state, 1)
}
/// Same as [`process_next_object`](`GradualPerformanceAttributes::process_next_object`)
/// but instead of processing only one object it process `n` many.
///
/// If `n` is 0 it will be considered as 1.
/// If there are still objects to be processed but `n` is larger than the amount
/// of remaining objects, `n` will be considered as the amount of remaining objects.
pub fn process_next_n_objects(
&mut self,
state: ScoreState,
n: usize,
) -> Option<PerformanceAttributes> {
match self {
#[cfg(feature = "fruits")]
GradualPerformanceAttributes::Fruits(f) => f
.process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Fruits),
#[cfg(feature = "mania")]
GradualPerformanceAttributes::Mania(m) => m
.process_next_n_objects(state.score, n)
.map(PerformanceAttributes::Mania),
#[cfg(feature = "osu")]
GradualPerformanceAttributes::Osu(o) => o
.process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Osu),
#[cfg(feature = "taiko")]
GradualPerformanceAttributes::Taiko(t) => t
.process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Taiko),
}
}
}
+35
View File
@@ -120,6 +120,9 @@ pub mod taiko;
/// Beatmap parsing and the contained types.
pub mod parse;
mod gradual;
pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, ScoreState};
mod pp;
pub use pp::{AnyPP, AttributeProvider};
@@ -168,9 +171,21 @@ pub trait BeatmapExt {
///
/// Suitable to plot the difficulty of a map over time.
fn strains(&self, mods: impl Mods) -> Strains;
/// Return an iterator that gives you the `DifficultyAttributes` after each hit object.
///
/// Suitable to efficiently get the map's star rating after multiple different locations.
fn gradual_difficulty(&self, mods: impl Mods) -> GradualDifficultyAttributes<'_>;
/// Return a struct that gives you the `PerformanceAttributes` after every (few) hit object(s).
///
/// Suitable to efficiently get a score's performance after multiple different locations,
/// i.e. live update a score's pp.
fn gradual_performance(&self, mods: u32) -> GradualPerformanceAttributes<'_>;
}
impl BeatmapExt for Beatmap {
#[inline]
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> DifficultyAttributes {
match self.mode {
GameMode::STD => {
@@ -204,6 +219,7 @@ impl BeatmapExt for Beatmap {
}
}
#[inline]
fn max_pp(&self, mods: u32) -> PerformanceAttributes {
match self.mode {
GameMode::STD => {
@@ -242,6 +258,7 @@ impl BeatmapExt for Beatmap {
AnyPP::new(self)
}
#[inline]
fn strains(&self, mods: impl Mods) -> Strains {
match self.mode {
GameMode::STD => {
@@ -274,6 +291,16 @@ impl BeatmapExt for Beatmap {
}
}
}
#[inline]
fn gradual_difficulty(&self, mods: impl Mods) -> GradualDifficultyAttributes<'_> {
GradualDifficultyAttributes::new(self, mods)
}
#[inline]
fn gradual_performance(&self, mods: u32) -> GradualPerformanceAttributes<'_> {
GradualPerformanceAttributes::new(self, mods)
}
}
/// The result of calculating the strains on a map.
@@ -353,6 +380,7 @@ impl DifficultyAttributes {
#[cfg(feature = "fruits")]
impl From<fruits::FruitsDifficultyAttributes> for DifficultyAttributes {
#[inline]
fn from(attributes: fruits::FruitsDifficultyAttributes) -> Self {
Self::Fruits(attributes)
}
@@ -360,6 +388,7 @@ impl From<fruits::FruitsDifficultyAttributes> for DifficultyAttributes {
#[cfg(feature = "mania")]
impl From<mania::ManiaDifficultyAttributes> for DifficultyAttributes {
#[inline]
fn from(attributes: mania::ManiaDifficultyAttributes) -> Self {
Self::Mania(attributes)
}
@@ -367,6 +396,7 @@ impl From<mania::ManiaDifficultyAttributes> for DifficultyAttributes {
#[cfg(feature = "osu")]
impl From<osu::OsuDifficultyAttributes> for DifficultyAttributes {
#[inline]
fn from(attributes: osu::OsuDifficultyAttributes) -> Self {
Self::Osu(attributes)
}
@@ -374,6 +404,7 @@ impl From<osu::OsuDifficultyAttributes> for DifficultyAttributes {
#[cfg(feature = "taiko")]
impl From<taiko::TaikoDifficultyAttributes> for DifficultyAttributes {
#[inline]
fn from(attributes: taiko::TaikoDifficultyAttributes) -> Self {
Self::Taiko(attributes)
}
@@ -491,6 +522,7 @@ impl From<PerformanceAttributes> for DifficultyAttributes {
#[cfg(feature = "fruits")]
impl From<fruits::FruitsPerformanceAttributes> for PerformanceAttributes {
#[inline]
fn from(attributes: fruits::FruitsPerformanceAttributes) -> Self {
Self::Fruits(attributes)
}
@@ -498,6 +530,7 @@ impl From<fruits::FruitsPerformanceAttributes> for PerformanceAttributes {
#[cfg(feature = "mania")]
impl From<mania::ManiaPerformanceAttributes> for PerformanceAttributes {
#[inline]
fn from(attributes: mania::ManiaPerformanceAttributes) -> Self {
Self::Mania(attributes)
}
@@ -505,6 +538,7 @@ impl From<mania::ManiaPerformanceAttributes> for PerformanceAttributes {
#[cfg(feature = "osu")]
impl From<osu::OsuPerformanceAttributes> for PerformanceAttributes {
#[inline]
fn from(attributes: osu::OsuPerformanceAttributes) -> Self {
Self::Osu(attributes)
}
@@ -512,6 +546,7 @@ impl From<osu::OsuPerformanceAttributes> for PerformanceAttributes {
#[cfg(feature = "taiko")]
impl From<taiko::TaikoPerformanceAttributes> for PerformanceAttributes {
#[inline]
fn from(attributes: taiko::TaikoPerformanceAttributes) -> Self {
Self::Taiko(attributes)
}
+2
View File
@@ -5,6 +5,8 @@ use super::{OsuGradualDifficultyAttributes, OsuPerformanceAttributes};
// TODO: Benchmark if Copy is faster than Clone
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
///
/// This struct is used for [`OsuGradualPerformanceAttributes`].
#[derive(Copy, Clone, Debug, Default)]
pub struct OsuScoreState {
/// Maximum combo that the score has had so far.
+3 -1
View File
@@ -5,6 +5,8 @@ use super::{TaikoGradualDifficultyAttributes, TaikoPerformanceAttributes};
// TODO: Benchmark if Copy is faster than Clone
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
///
/// This struct is used for [`TaikoGradualPerformanceAttributes`].
#[derive(Copy, Clone, Debug, Default)]
pub struct TaikoScoreState {
/// Maximum combo that the score has had so far.
@@ -33,7 +35,7 @@ impl TaikoScoreState {
/// To process multiple objects at once, use
/// [`process_next_n_objects`](`TaikoGradualPerformanceAttributes::process_next_n_objects`) instead.
///
/// Both methods require an [`TaikoScoreState`] that contains the current
/// Both methods require a [`TaikoScoreState`] that contains the current
/// hitresults as well as the maximum combo so far.
///
/// If you only want to calculate difficulty attributes use