Merge pull request #29 from MaxOhn/perf-without-map

feat!: performance calculation with difficulty attributes instead of beatmap
This commit is contained in:
Badewanne3
2023-12-30 10:23:48 +01:00
committed by GitHub
17 changed files with 674 additions and 211 deletions
+6
View File
@@ -274,6 +274,12 @@ impl CatchDifficultyAttributes {
pub fn max_combo(&self) -> usize { pub fn max_combo(&self) -> usize {
self.n_fruits + self.n_droplets self.n_fruits + self.n_droplets
} }
/// Returns a builder for performance calculation.
#[inline]
pub fn pp(self) -> CatchPP<'static> {
CatchPP::from(self)
}
} }
/// The result of a performance calculation on an osu!catch map. /// The result of a performance calculation on an osu!catch map.
+102 -53
View File
@@ -1,5 +1,8 @@
use super::{CatchDifficultyAttributes, CatchPerformanceAttributes, CatchScoreState, CatchStars}; use super::{CatchDifficultyAttributes, CatchPerformanceAttributes, CatchScoreState, CatchStars};
use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes}; use crate::{
util::{MapOrElse, MapRef},
Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes,
};
use std::cmp::Ordering; use std::cmp::Ordering;
/// Performance calculator on osu!catch maps. /// Performance calculator on osu!catch maps.
@@ -34,8 +37,7 @@ use std::cmp::Ordering;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
pub struct CatchPP<'map> { pub struct CatchPP<'map> {
pub(crate) map: &'map Beatmap, pub(crate) map_or_attrs: MapOrElse<MapRef<'map>, CatchDifficultyAttributes>,
pub(crate) attributes: Option<CatchDifficultyAttributes>,
pub(crate) mods: u32, pub(crate) mods: u32,
pub(crate) acc: Option<f64>, pub(crate) acc: Option<f64>,
pub(crate) combo: Option<usize>, pub(crate) combo: Option<usize>,
@@ -54,8 +56,7 @@ impl<'map> CatchPP<'map> {
#[inline] #[inline]
pub fn new(map: &'map Beatmap) -> Self { pub fn new(map: &'map Beatmap) -> Self {
Self { Self {
map, map_or_attrs: MapOrElse::from(map),
attributes: None,
mods: 0, mods: 0,
acc: None, acc: None,
combo: None, combo: None,
@@ -75,8 +76,8 @@ impl<'map> CatchPP<'map> {
/// be sure to put them in here so that they don't have to be recalculated. /// be sure to put them in here so that they don't have to be recalculated.
#[inline] #[inline]
pub fn attributes(mut self, attributes: impl CatchAttributeProvider) -> Self { pub fn attributes(mut self, attributes: impl CatchAttributeProvider) -> Self {
if let Some(attributes) = attributes.attributes() { if let Some(attrs) = attributes.attributes() {
self.attributes = Some(attributes); self.map_or_attrs = MapOrElse::Else(attrs);
} }
self self
@@ -195,9 +196,13 @@ impl<'map> CatchPP<'map> {
/// Create the [`CatchScoreState`] that will be used for performance calculation. /// Create the [`CatchScoreState`] that will be used for performance calculation.
pub fn generate_state(&mut self) -> CatchScoreState { pub fn generate_state(&mut self) -> CatchScoreState {
let attrs = match self.attributes { let attrs = match self.map_or_attrs {
Some(ref attrs) => attrs, MapOrElse::Map(ref map) => {
None => self.attributes.insert(self.generate_attributes()), let attrs = self.generate_attributes(map.as_ref());
self.map_or_attrs.else_or_insert(attrs)
}
MapOrElse::Else(ref attrs) => attrs,
}; };
let n_misses = self let n_misses = self
@@ -332,10 +337,10 @@ impl<'map> CatchPP<'map> {
pub fn calculate(mut self) -> CatchPerformanceAttributes { pub fn calculate(mut self) -> CatchPerformanceAttributes {
let state = self.generate_state(); let state = self.generate_state();
let attrs = self let attrs = match self.map_or_attrs {
.attributes MapOrElse::Map(ref map) => self.generate_attributes(map.as_ref()),
.take() MapOrElse::Else(attrs) => attrs,
.unwrap_or_else(|| self.generate_attributes()); };
let inner = CatchPPInner { let inner = CatchPPInner {
attrs, attrs,
@@ -346,8 +351,8 @@ impl<'map> CatchPP<'map> {
inner.calculate() inner.calculate()
} }
fn generate_attributes(&self) -> CatchDifficultyAttributes { fn generate_attributes(&self, map: &Beatmap) -> CatchDifficultyAttributes {
let mut calculator = CatchStars::new(self.map).mods(self.mods); let mut calculator = CatchStars::new(map).mods(self.mods);
if let Some(passed_objects) = self.passed_objects { if let Some(passed_objects) = self.passed_objects {
calculator = calculator.passed_objects(passed_objects); calculator = calculator.passed_objects(passed_objects);
@@ -359,6 +364,62 @@ impl<'map> CatchPP<'map> {
calculator.calculate() calculator.calculate()
} }
/// Try to create [`CatchPP`] through [`OsuPP`].
///
/// Returns `None` if [`OsuPP`] already replaced its internal [`Beatmap`]
/// with [`OsuDifficultyAttributes`], i.e. if [`OsuPP::attributes`]
/// or [`OsuPP::generate_state`] was called.
///
/// [`OsuDifficultyAttributes`]: crate::osu::OsuDifficultyAttributes
#[inline]
pub fn try_from_osu(osu: OsuPP<'map>) -> Option<Self> {
let OsuPP {
map_or_attrs,
mods,
acc,
combo,
n300,
n100,
n50,
n_misses,
passed_objects,
clock_rate,
hitresult_priority: _,
} = osu;
let MapOrElse::Map(map) = map_or_attrs else {
return None;
};
Some(Self {
map_or_attrs: MapOrElse::Map(map),
mods,
acc,
combo,
n_fruits: n300,
n_droplets: n100,
n_tiny_droplets: n50,
n_tiny_droplet_misses: None,
n_misses,
passed_objects,
clock_rate,
})
}
/// Try to create [`CatchPP`] through a [`CatchAttributeProvider`].
///
/// If you already calculated the attributes for the current map-mod
/// combination, the [`Beatmap`] is no longer necessary to calculate
/// performance attributes so this method can be used instead of
/// [`CatchPP::new`].
///
/// Returns `None` only if the [`CatchAttributeProvider`] did not contain
/// attributes for catch e.g. if it's [`DifficultyAttributes::Taiko`].
#[inline]
pub fn try_from_attributes(attributes: impl CatchAttributeProvider) -> Option<Self> {
attributes.attributes().map(Self::from)
}
} }
struct CatchPPInner { struct CatchPPInner {
@@ -442,41 +503,6 @@ impl CatchPPInner {
} }
} }
impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
#[inline]
fn from(osu: OsuPP<'map>) -> Self {
let OsuPP {
map,
attributes: _,
mods,
acc,
combo,
n300,
n100,
n50,
n_misses,
passed_objects,
clock_rate,
hitresult_priority: _,
} = osu;
Self {
map,
attributes: None,
mods,
acc,
combo,
n_fruits: n300,
n_droplets: n100,
n_tiny_droplets: n50,
n_tiny_droplet_misses: None,
n_misses,
passed_objects,
clock_rate,
}
}
}
fn accuracy( fn accuracy(
n_fruits: usize, n_fruits: usize,
n_droplets: usize, n_droplets: usize,
@@ -490,6 +516,31 @@ fn accuracy(
numerator as f64 / denominator as f64 numerator as f64 / denominator as f64
} }
impl From<CatchDifficultyAttributes> for CatchPP<'_> {
fn from(attrs: CatchDifficultyAttributes) -> Self {
Self {
map_or_attrs: MapOrElse::Else(attrs),
mods: 0,
acc: None,
combo: None,
n_fruits: None,
n_droplets: None,
n_tiny_droplets: None,
n_tiny_droplet_misses: None,
n_misses: None,
passed_objects: None,
clock_rate: None,
}
}
}
impl From<CatchPerformanceAttributes> for CatchPP<'_> {
fn from(attrs: CatchPerformanceAttributes) -> Self {
attrs.difficulty.into()
}
}
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation. /// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
pub trait CatchAttributeProvider { pub trait CatchAttributeProvider {
/// Provide the actual difficulty attributes. /// Provide the actual difficulty attributes.
@@ -513,7 +564,6 @@ impl CatchAttributeProvider for CatchPerformanceAttributes {
impl CatchAttributeProvider for DifficultyAttributes { impl CatchAttributeProvider for DifficultyAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<CatchDifficultyAttributes> { fn attributes(self) -> Option<CatchDifficultyAttributes> {
#[allow(irrefutable_let_patterns)]
if let Self::Catch(attributes) = self { if let Self::Catch(attributes) = self {
Some(attributes) Some(attributes)
} else { } else {
@@ -525,7 +575,6 @@ impl CatchAttributeProvider for DifficultyAttributes {
impl CatchAttributeProvider for PerformanceAttributes { impl CatchAttributeProvider for PerformanceAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<CatchDifficultyAttributes> { fn attributes(self) -> Option<CatchDifficultyAttributes> {
#[allow(irrefutable_let_patterns)]
if let Self::Catch(attributes) = self { if let Self::Catch(attributes) = self {
Some(attributes.difficulty) Some(attributes.difficulty)
} else { } else {
+7 -1
View File
@@ -306,6 +306,12 @@ impl DifficultyAttributes {
Self::Mania(attrs) => attrs.max_combo, Self::Mania(attrs) => attrs.max_combo,
} }
} }
/// Returns a builder for performance calculation.
#[inline]
pub fn pp(self) -> AnyPP<'static> {
AnyPP::from(self)
}
} }
impl From<osu::OsuDifficultyAttributes> for DifficultyAttributes { impl From<osu::OsuDifficultyAttributes> for DifficultyAttributes {
@@ -379,7 +385,7 @@ impl PerformanceAttributes {
Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty.clone()), Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty.clone()),
Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty.clone()), Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty.clone()),
Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty.clone()), Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty.clone()),
Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty), Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty.clone()),
} }
} }
+5
View File
@@ -162,6 +162,7 @@ struct ManiaGradualDifficultyInner {
diff_objects: Box<[ManiaDifficultyObject]>, diff_objects: Box<[ManiaDifficultyObject]>,
curr_combo: usize, curr_combo: usize,
clock_rate: f64, clock_rate: f64,
is_convert: bool,
} }
impl ManiaGradualDifficultyInner { impl ManiaGradualDifficultyInner {
@@ -202,6 +203,7 @@ impl ManiaGradualDifficultyInner {
diff_objects: Box::from([]), diff_objects: Box::from([]),
curr_combo: 0, curr_combo: 0,
clock_rate, clock_rate,
is_convert,
} }
} }
}; };
@@ -226,6 +228,7 @@ impl ManiaGradualDifficultyInner {
diff_objects: diff_objects.into_boxed_slice(), diff_objects: diff_objects.into_boxed_slice(),
curr_combo, curr_combo,
clock_rate, clock_rate,
is_convert,
} }
} }
@@ -249,6 +252,8 @@ impl ManiaGradualDifficultyInner {
stars: self.strain.clone().difficulty_value() * STAR_SCALING_FACTOR, stars: self.strain.clone().difficulty_value() * STAR_SCALING_FACTOR,
hit_window: self.hit_window, hit_window: self.hit_window,
max_combo: self.curr_combo, max_combo: self.curr_combo,
is_convert: self.is_convert,
n_objects: self.diff_objects.len() + 1,
}) })
} }
+39 -2
View File
@@ -131,12 +131,15 @@ impl<'map> ManiaStars<'map> {
.clock_rate(clock_rate) .clock_rate(clock_rate)
.hit_windows(); .hit_windows();
let n_objects = self.map.hit_objects.len();
let ManiaResult { strain, max_combo } = calculate_result(self); let ManiaResult { strain, max_combo } = calculate_result(self);
ManiaDifficultyAttributes { ManiaDifficultyAttributes {
stars: strain.difficulty_value() * STAR_SCALING_FACTOR, stars: strain.difficulty_value() * STAR_SCALING_FACTOR,
hit_window, hit_window,
max_combo, max_combo,
n_objects,
is_convert,
} }
} }
@@ -227,14 +230,18 @@ struct ManiaResult {
} }
/// The result of a difficulty calculation on an osu!mania map. /// The result of a difficulty calculation on an osu!mania map.
#[derive(Copy, Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, Default, PartialEq)]
pub struct ManiaDifficultyAttributes { pub struct ManiaDifficultyAttributes {
/// The final star rating. /// The final star rating.
pub stars: f64, pub stars: f64,
/// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc). /// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc).
pub hit_window: f64, pub hit_window: f64,
/// The amount of hitobjects in the map.
pub n_objects: usize,
/// The maximum achievable combo. /// The maximum achievable combo.
pub max_combo: usize, pub max_combo: usize,
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
pub is_convert: bool,
} }
impl ManiaDifficultyAttributes { impl ManiaDifficultyAttributes {
@@ -243,10 +250,28 @@ impl ManiaDifficultyAttributes {
pub fn max_combo(&self) -> usize { pub fn max_combo(&self) -> usize {
self.max_combo self.max_combo
} }
/// Return the amount of hitobjects.
#[inline]
pub fn n_objects(&self) -> usize {
self.n_objects
}
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
#[inline]
pub fn is_convert(&self) -> bool {
self.is_convert
}
/// Returns a builder for performance calculation.
#[inline]
pub fn pp(self) -> ManiaPP<'static> {
ManiaPP::from(self)
}
} }
/// The result of a performance calculation on an osu!mania map. /// The result of a performance calculation on an osu!mania map.
#[derive(Copy, Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, Default, PartialEq)]
pub struct ManiaPerformanceAttributes { pub struct ManiaPerformanceAttributes {
/// The difficulty attributes that were used for the performance calculation. /// The difficulty attributes that were used for the performance calculation.
pub difficulty: ManiaDifficultyAttributes, pub difficulty: ManiaDifficultyAttributes,
@@ -274,6 +299,18 @@ impl ManiaPerformanceAttributes {
pub fn max_combo(&self) -> usize { pub fn max_combo(&self) -> usize {
self.difficulty.max_combo self.difficulty.max_combo
} }
/// Return the amount of hitobjects.
#[inline]
pub fn n_objects(&self) -> usize {
self.difficulty.n_objects
}
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
#[inline]
pub fn is_convert(&self) -> bool {
self.difficulty.is_convert
}
} }
impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes { impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes {
+125 -58
View File
@@ -2,7 +2,8 @@ use std::borrow::Cow;
use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaScoreState, ManiaStars}; use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaScoreState, ManiaStars};
use crate::{ use crate::{
Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuPP, PerformanceAttributes, util::MapOrElse, Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuPP,
PerformanceAttributes,
}; };
/// Performance calculator on osu!mania maps. /// Performance calculator on osu!mania maps.
@@ -41,9 +42,8 @@ use crate::{
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
pub struct ManiaPP<'map> { pub struct ManiaPP<'map> {
map: Cow<'map, Beatmap>, map_or_attrs: MapOrElse<Cow<'map, Beatmap>, ManiaDifficultyAttributes>,
is_convert: bool, is_convert_overwrite: Option<bool>,
attributes: Option<ManiaDifficultyAttributes>,
mods: u32, mods: u32,
passed_objects: Option<usize>, passed_objects: Option<usize>,
clock_rate: Option<f64>, clock_rate: Option<f64>,
@@ -66,9 +66,8 @@ impl<'map> ManiaPP<'map> {
let map = map.convert_mode(GameMode::Mania); let map = map.convert_mode(GameMode::Mania);
Self { Self {
is_convert: matches!(map, Cow::Owned(_)), map_or_attrs: MapOrElse::Map(map),
map, is_convert_overwrite: None,
attributes: None,
mods: 0, mods: 0,
passed_objects: None, passed_objects: None,
clock_rate: None, clock_rate: None,
@@ -89,7 +88,7 @@ impl<'map> ManiaPP<'map> {
#[inline] #[inline]
pub fn attributes(mut self, attrs: impl ManiaAttributeProvider) -> Self { pub fn attributes(mut self, attrs: impl ManiaAttributeProvider) -> Self {
if let Some(attrs) = attrs.attributes() { if let Some(attrs) = attrs.attributes() {
self.attributes = Some(attrs); self.map_or_attrs = MapOrElse::Else(attrs);
} }
self self
@@ -199,7 +198,7 @@ impl<'map> ManiaPP<'map> {
/// This only needs to be specified if the map was converted manually beforehand. /// This only needs to be specified if the map was converted manually beforehand.
#[inline] #[inline]
pub fn is_convert(mut self, is_convert: bool) -> Self { pub fn is_convert(mut self, is_convert: bool) -> Self {
self.is_convert = is_convert; self.is_convert_overwrite = Some(is_convert);
self self
} }
@@ -227,8 +226,23 @@ impl<'map> ManiaPP<'map> {
} }
/// Create the [`ManiaScoreState`] that will be used for performance calculation. /// Create the [`ManiaScoreState`] that will be used for performance calculation.
pub fn generate_state(&self) -> ManiaScoreState { pub fn generate_state(&mut self) -> ManiaScoreState {
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len()); let n_objects = match self.passed_objects {
Some(passed) => passed,
None => {
let attrs = match self.map_or_attrs {
MapOrElse::Map(ref map) => {
let attrs = self.generate_attributes(map);
self.map_or_attrs.else_or_insert(attrs)
}
MapOrElse::Else(ref attrs) => attrs,
};
attrs.n_objects
}
};
let priority = self.hitresult_priority.unwrap_or_default(); let priority = self.hitresult_priority.unwrap_or_default();
let n_misses = self.n_misses.map_or(0, |n| n.min(n_objects)); let n_misses = self.n_misses.map_or(0, |n| n.min(n_objects));
@@ -700,12 +714,13 @@ impl<'map> ManiaPP<'map> {
} }
/// Calculate all performance related values, including pp and stars. /// Calculate all performance related values, including pp and stars.
pub fn calculate(self) -> ManiaPerformanceAttributes { pub fn calculate(mut self) -> ManiaPerformanceAttributes {
let state = self.generate_state(); let state = self.generate_state();
let attrs = self let attrs = match self.map_or_attrs {
.attributes MapOrElse::Map(ref map) => self.generate_attributes(map),
.unwrap_or_else(|| self.generate_attributes()); MapOrElse::Else(attrs) => attrs,
};
let inner = ManiaPpInner { let inner = ManiaPpInner {
mods: self.mods, mods: self.mods,
@@ -716,10 +731,15 @@ impl<'map> ManiaPP<'map> {
inner.calculate() inner.calculate()
} }
fn generate_attributes(&self) -> ManiaDifficultyAttributes { fn generate_attributes(&self, map: &Beatmap) -> ManiaDifficultyAttributes {
let mut calculator = ManiaStars::new(self.map.as_ref()) let is_convert = self
.mods(self.mods) .is_convert_overwrite
.is_convert(self.is_convert); .unwrap_or(match self.map_or_attrs {
MapOrElse::Map(ref map) => matches!(map, Cow::Owned(_)),
MapOrElse::Else(ref attrs) => attrs.is_convert,
});
let mut calculator = ManiaStars::new(map).mods(self.mods).is_convert(is_convert);
if let Some(passed_objects) = self.passed_objects { if let Some(passed_objects) = self.passed_objects {
calculator = calculator.passed_objects(passed_objects); calculator = calculator.passed_objects(passed_objects);
@@ -731,6 +751,66 @@ impl<'map> ManiaPP<'map> {
calculator.calculate() calculator.calculate()
} }
/// Try to create [`ManiaPP`] through [`OsuPP`].
///
/// Returns `None` if [`OsuPP`] already replaced its internal [`Beatmap`]
/// with [`OsuDifficultyAttributes`], i.e. if [`OsuPP::attributes`]
/// or [`OsuPP::generate_state`] was called.
///
/// [`OsuDifficultyAttributes`]: crate::osu::OsuDifficultyAttributes
#[inline]
pub fn try_from_osu(osu: OsuPP<'map>) -> Option<Self> {
let OsuPP {
map_or_attrs,
mods,
acc,
combo: _,
n300,
n100,
n50,
n_misses,
passed_objects,
clock_rate,
hitresult_priority,
} = osu;
let MapOrElse::Map(map) = map_or_attrs else {
return None;
};
let map = map.into_inner().convert_mode(GameMode::Mania);
Some(Self {
map_or_attrs: MapOrElse::Map(map),
is_convert_overwrite: None,
mods,
passed_objects,
clock_rate,
n320: None,
n300,
n200: None,
n100,
n50,
n_misses,
acc,
hitresult_priority,
})
}
/// Try to create [`ManiaPP`] through a [`ManiaAttributeProvider`].
///
/// If you already calculated the attributes for the current map-mod
/// combination, the [`Beatmap`] is no longer necessary to calculate
/// performance attributes so this method can be used instead of
/// [`ManiaPP::new`].
///
/// Returns `None` only if the [`ManiaAttributeProvider`] did not contain
/// attributes for mania e.g. if it's [`DifficultyAttributes::Taiko`].
#[inline]
pub fn try_from_attributes(attributes: impl ManiaAttributeProvider) -> Option<Self> {
attributes.attributes().map(Self::from)
}
} }
struct ManiaPpInner { struct ManiaPpInner {
@@ -796,45 +876,6 @@ impl ManiaPpInner {
} }
} }
impl<'map> From<OsuPP<'map>> for ManiaPP<'map> {
#[inline]
fn from(osu: OsuPP<'map>) -> Self {
let OsuPP {
map,
attributes: _,
mods,
acc,
combo: _,
n300,
n100,
n50,
n_misses,
passed_objects,
clock_rate,
hitresult_priority,
} = osu;
let map = map.convert_mode(GameMode::Mania);
Self {
is_convert: matches!(map, Cow::Owned(_)),
map,
attributes: None,
mods,
passed_objects,
clock_rate,
n320: None,
n300,
n200: None,
n100,
n50,
n_misses,
acc,
hitresult_priority,
}
}
}
fn custom_accuracy( fn custom_accuracy(
n320: usize, n320: usize,
n300: usize, n300: usize,
@@ -864,6 +905,32 @@ fn accuracy(
numerator as f64 / denominator as f64 numerator as f64 / denominator as f64
} }
impl From<ManiaDifficultyAttributes> for ManiaPP<'_> {
fn from(attrs: ManiaDifficultyAttributes) -> Self {
Self {
map_or_attrs: MapOrElse::Else(attrs),
is_convert_overwrite: None,
mods: 0,
passed_objects: None,
clock_rate: None,
n320: None,
n300: None,
n200: None,
n100: None,
n50: None,
n_misses: None,
acc: None,
hitresult_priority: None,
}
}
}
impl From<ManiaPerformanceAttributes> for ManiaPP<'_> {
fn from(attrs: ManiaPerformanceAttributes) -> Self {
attrs.difficulty.into()
}
}
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation. /// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
pub trait ManiaAttributeProvider { pub trait ManiaAttributeProvider {
/// Provide the actual difficulty attributes. /// Provide the actual difficulty attributes.
+17
View File
@@ -589,6 +589,18 @@ impl OsuDifficultyAttributes {
pub fn max_combo(&self) -> usize { pub fn max_combo(&self) -> usize {
self.max_combo self.max_combo
} }
/// Return the amount of hitobjects.
#[inline]
pub fn n_objects(&self) -> usize {
self.n_circles + self.n_sliders + self.n_spinners
}
/// Returns a builder for performance calculation.
#[inline]
pub fn pp(self) -> OsuPP<'static> {
OsuPP::from(self)
}
} }
/// The result of a performance calculation on an osu!standard map. /// The result of a performance calculation on an osu!standard map.
@@ -628,6 +640,11 @@ impl OsuPerformanceAttributes {
pub fn max_combo(&self) -> usize { pub fn max_combo(&self) -> usize {
self.difficulty.max_combo self.difficulty.max_combo
} }
/// Return the amount of hitobjects.
#[inline]
pub fn n_objects(&self) -> usize {
self.difficulty.n_objects()
}
} }
impl From<OsuPerformanceAttributes> for OsuDifficultyAttributes { impl From<OsuPerformanceAttributes> for OsuDifficultyAttributes {
+72 -25
View File
@@ -2,8 +2,9 @@ use super::{
OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState, PERFORMANCE_BASE_MULTIPLIER, OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState, PERFORMANCE_BASE_MULTIPLIER,
}; };
use crate::{ use crate::{
AnyPP, Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuStars, util::{MapOrElse, MapRef},
PerformanceAttributes, AnyPP, Beatmap, CatchPP, DifficultyAttributes, GameMode, HitResultPriority, ManiaPP, Mods,
OsuStars, PerformanceAttributes, TaikoPP,
}; };
/// Performance calculator on osu!standard maps. /// Performance calculator on osu!standard maps.
@@ -38,8 +39,7 @@ use crate::{
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
pub struct OsuPP<'map> { pub struct OsuPP<'map> {
pub(crate) map: &'map Beatmap, pub(crate) map_or_attrs: MapOrElse<MapRef<'map>, OsuDifficultyAttributes>,
pub(crate) attributes: Option<OsuDifficultyAttributes>,
pub(crate) mods: u32, pub(crate) mods: u32,
pub(crate) acc: Option<f64>, pub(crate) acc: Option<f64>,
pub(crate) combo: Option<usize>, pub(crate) combo: Option<usize>,
@@ -58,8 +58,7 @@ impl<'map> OsuPP<'map> {
#[inline] #[inline]
pub fn new(map: &'map Beatmap) -> Self { pub fn new(map: &'map Beatmap) -> Self {
Self { Self {
map, map_or_attrs: MapOrElse::from(map),
attributes: None,
mods: 0, mods: 0,
acc: None, acc: None,
combo: None, combo: None,
@@ -75,13 +74,17 @@ impl<'map> OsuPP<'map> {
} }
/// Convert the map into another mode. /// Convert the map into another mode.
///
/// Returns `None` if `self` already replaced it's internal [`Beatmap`]
/// with [`OsuDifficultyAttributes`], i.e. if [`OsuPP::attributes`]
/// or [`OsuPP::generate_state`] was called.
#[inline] #[inline]
pub fn mode(self, mode: GameMode) -> AnyPP<'map> { pub fn try_mode(self, mode: GameMode) -> Option<AnyPP<'map>> {
match mode { match mode {
GameMode::Osu => AnyPP::Osu(self), GameMode::Osu => Some(AnyPP::Osu(self)),
GameMode::Taiko => AnyPP::Taiko(self.into()), GameMode::Taiko => TaikoPP::try_from_osu(self).map(AnyPP::Taiko),
GameMode::Catch => AnyPP::Catch(self.into()), GameMode::Catch => CatchPP::try_from_osu(self).map(AnyPP::Catch),
GameMode::Mania => AnyPP::Mania(self.into()), GameMode::Mania => ManiaPP::try_from_osu(self).map(AnyPP::Mania),
} }
} }
@@ -90,8 +93,8 @@ impl<'map> OsuPP<'map> {
/// be sure to put them in here so that they don't have to be recalculated. /// be sure to put them in here so that they don't have to be recalculated.
#[inline] #[inline]
pub fn attributes(mut self, attributes: impl OsuAttributeProvider) -> Self { pub fn attributes(mut self, attributes: impl OsuAttributeProvider) -> Self {
if let Some(attributes) = attributes.attributes() { if let Some(attrs) = attributes.attributes() {
self.attributes = Some(attributes); self.map_or_attrs = MapOrElse::Else(attrs);
} }
self self
@@ -210,12 +213,17 @@ impl<'map> OsuPP<'map> {
/// Create the [`OsuScoreState`] that will be used for performance calculation. /// Create the [`OsuScoreState`] that will be used for performance calculation.
pub fn generate_state(&mut self) -> OsuScoreState { pub fn generate_state(&mut self) -> OsuScoreState {
let max_combo = match self.attributes { let attrs = match self.map_or_attrs {
Some(ref attrs) => attrs.max_combo, MapOrElse::Map(ref map) => {
None => self.attributes.insert(self.generate_attributes()).max_combo, let attrs = self.generate_attributes(map.as_ref());
self.map_or_attrs.else_or_insert(attrs)
}
MapOrElse::Else(ref attrs) => attrs,
}; };
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len()); let max_combo = attrs.max_combo;
let n_objects = self.passed_objects.unwrap_or(attrs.n_objects());
let priority = self.hitresult_priority.unwrap_or_default(); let priority = self.hitresult_priority.unwrap_or_default();
let n_misses = self.n_misses.map_or(0, |n| n.min(n_objects)); let n_misses = self.n_misses.map_or(0, |n| n.min(n_objects));
@@ -386,10 +394,10 @@ impl<'map> OsuPP<'map> {
pub fn calculate(mut self) -> OsuPerformanceAttributes { pub fn calculate(mut self) -> OsuPerformanceAttributes {
let state = self.generate_state(); let state = self.generate_state();
let attrs = self let attrs = match self.map_or_attrs {
.attributes MapOrElse::Map(ref map) => self.generate_attributes(map.as_ref()),
.take() MapOrElse::Else(attrs) => attrs,
.unwrap_or_else(|| self.generate_attributes()); };
let effective_miss_count = calculate_effective_misses(&attrs, &state); let effective_miss_count = calculate_effective_misses(&attrs, &state);
@@ -404,8 +412,8 @@ impl<'map> OsuPP<'map> {
inner.calculate() inner.calculate()
} }
fn generate_attributes(&self) -> OsuDifficultyAttributes { fn generate_attributes(&self, map: &Beatmap) -> OsuDifficultyAttributes {
let mut calculator = OsuStars::new(self.map).mods(self.mods); let mut calculator = OsuStars::new(map).mods(self.mods);
if let Some(passed_objects) = self.passed_objects { if let Some(passed_objects) = self.passed_objects {
calculator = calculator.passed_objects(passed_objects); calculator = calculator.passed_objects(passed_objects);
@@ -417,6 +425,20 @@ impl<'map> OsuPP<'map> {
calculator.calculate() calculator.calculate()
} }
/// Try to create [`OsuPP`] through a [`OsuAttributeProvider`].
///
/// If you already calculated the attributes for the current map-mod
/// combination, the [`Beatmap`] is no longer necessary to calculate
/// performance attributes so this method can be used instead of
/// [`OsuPP::new`].
///
/// Returns `None` only if the [`OsuAttributeProvider`] did not contain
/// attributes for osu e.g. if it's [`DifficultyAttributes::Taiko`].
#[inline]
pub fn try_from_attributes(attributes: impl OsuAttributeProvider) -> Option<Self> {
attributes.attributes().map(Self::from)
}
} }
struct OsuPpInner { struct OsuPpInner {
@@ -747,6 +769,33 @@ fn accuracy(n300: usize, n100: usize, n50: usize, n_misses: usize) -> f64 {
numerator as f64 / denominator as f64 numerator as f64 / denominator as f64
} }
impl From<OsuDifficultyAttributes> for OsuPP<'_> {
#[inline]
fn from(attrs: OsuDifficultyAttributes) -> Self {
Self {
map_or_attrs: MapOrElse::Else(attrs),
mods: 0,
acc: None,
combo: None,
n300: None,
n100: None,
n50: None,
n_misses: None,
passed_objects: None,
clock_rate: None,
hitresult_priority: None,
}
}
}
impl From<OsuPerformanceAttributes> for OsuPP<'_> {
#[inline]
fn from(attrs: OsuPerformanceAttributes) -> Self {
attrs.difficulty.into()
}
}
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation. /// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
pub trait OsuAttributeProvider { pub trait OsuAttributeProvider {
/// Provide the actual difficulty attributes. /// Provide the actual difficulty attributes.
@@ -770,7 +819,6 @@ impl OsuAttributeProvider for OsuPerformanceAttributes {
impl OsuAttributeProvider for DifficultyAttributes { impl OsuAttributeProvider for DifficultyAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<OsuDifficultyAttributes> { fn attributes(self) -> Option<OsuDifficultyAttributes> {
#[allow(irrefutable_let_patterns)]
if let Self::Osu(attributes) = self { if let Self::Osu(attributes) = self {
Some(attributes) Some(attributes)
} else { } else {
@@ -782,7 +830,6 @@ impl OsuAttributeProvider for DifficultyAttributes {
impl OsuAttributeProvider for PerformanceAttributes { impl OsuAttributeProvider for PerformanceAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<OsuDifficultyAttributes> { fn attributes(self) -> Option<OsuDifficultyAttributes> {
#[allow(irrefutable_let_patterns)]
if let Self::Osu(attributes) = self { if let Self::Osu(attributes) = self {
Some(attributes.difficulty) Some(attributes.difficulty)
} else { } else {
+33 -8
View File
@@ -60,6 +60,16 @@ impl<'map> AnyPP<'map> {
} }
} }
/// Create a new performance calculator through previously calculated
/// attributes.
///
/// Note that the map, mods, and passed object count should be the same
/// as when the attributes were calculated.
#[inline]
pub fn from_attributes(attributes: impl AttributeProvider) -> Self {
Self::from(attributes)
}
/// Consume the performance calculator and calculate /// Consume the performance calculator and calculate
/// performance attributes for the given parameters. /// performance attributes for the given parameters.
#[inline] #[inline]
@@ -86,16 +96,15 @@ impl<'map> AnyPP<'map> {
} }
/// If the map is an osu!standard map, convert it to another mode. /// If the map is an osu!standard map, convert it to another mode.
///
/// Returns `None` if `self` already replaced it's internal [`Beatmap`]
/// with difficulty attributes, i.e. if [`AnyPP::attributes`]
/// or [`AnyPP::generate_state`] was called.
#[inline] #[inline]
pub fn mode(self, mode: GameMode) -> Self { pub fn try_mode(self, mode: GameMode) -> Option<Self> {
match self { match self {
AnyPP::Osu(o) => match mode { AnyPP::Osu(o) => o.try_mode(mode),
GameMode::Osu => AnyPP::Osu(o), other => Some(other),
GameMode::Taiko => AnyPP::Taiko(o.into()),
GameMode::Catch => AnyPP::Catch(o.into()),
GameMode::Mania => AnyPP::Mania(o.into()),
},
other => other,
} }
} }
@@ -287,6 +296,22 @@ impl<'map> AnyPP<'map> {
} }
} }
impl<A: AttributeProvider> From<A> for AnyPP<'_> {
#[inline]
fn from(attrs: A) -> Self {
fn inner(attrs: DifficultyAttributes) -> AnyPP<'static> {
match attrs {
DifficultyAttributes::Osu(attrs) => AnyPP::Osu(attrs.pp()),
DifficultyAttributes::Taiko(attrs) => AnyPP::Taiko(attrs.pp()),
DifficultyAttributes::Catch(attrs) => AnyPP::Catch(attrs.pp()),
DifficultyAttributes::Mania(attrs) => AnyPP::Mania(attrs.pp()),
}
}
inner(attrs.attributes())
}
}
/// While generating remaining hitresults, decide how they should be distributed. /// While generating remaining hitresults, decide how they should be distributed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum HitResultPriority { pub enum HitResultPriority {
-1
View File
@@ -268,7 +268,6 @@ impl Iterator for TaikoGradualDifficulty {
FirstTwoCombos::Both => self.attrs.max_combo = 2, FirstTwoCombos::Both => self.attrs.max_combo = 2,
} }
} }
_ => unreachable!(),
} }
for _ in 0..take { for _ in 0..take {
+21
View File
@@ -170,6 +170,7 @@ impl<'map> TaikoStars<'map> {
hit_window, hit_window,
stars: star_rating, stars: star_rating,
max_combo, max_combo,
is_convert,
} }
} }
@@ -309,6 +310,8 @@ pub struct TaikoDifficultyAttributes {
pub stars: f64, pub stars: f64,
/// The maximum combo. /// The maximum combo.
pub max_combo: usize, pub max_combo: usize,
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
pub is_convert: bool,
} }
impl TaikoDifficultyAttributes { impl TaikoDifficultyAttributes {
@@ -317,6 +320,18 @@ impl TaikoDifficultyAttributes {
pub fn max_combo(&self) -> usize { pub fn max_combo(&self) -> usize {
self.max_combo self.max_combo
} }
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
#[inline]
pub fn is_convert(&self) -> bool {
self.is_convert
}
/// Returns a builder for performance calculation.
#[inline]
pub fn pp(self) -> TaikoPP<'static> {
TaikoPP::from(self)
}
} }
/// The result of a performance calculation on an osu!taiko map. /// The result of a performance calculation on an osu!taiko map.
@@ -352,6 +367,12 @@ impl TaikoPerformanceAttributes {
pub fn max_combo(&self) -> usize { pub fn max_combo(&self) -> usize {
self.difficulty.max_combo self.difficulty.max_combo
} }
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
#[inline]
pub fn is_convert(&self) -> bool {
self.difficulty.is_convert
}
} }
impl From<TaikoPerformanceAttributes> for TaikoDifficultyAttributes { impl From<TaikoPerformanceAttributes> for TaikoDifficultyAttributes {
+113 -60
View File
@@ -2,7 +2,8 @@ use std::borrow::Cow;
use super::{TaikoDifficultyAttributes, TaikoPerformanceAttributes, TaikoScoreState, TaikoStars}; use super::{TaikoDifficultyAttributes, TaikoPerformanceAttributes, TaikoScoreState, TaikoStars};
use crate::{ use crate::{
Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuPP, PerformanceAttributes, util::MapOrElse, Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuPP,
PerformanceAttributes,
}; };
/// Performance calculator on osu!taiko maps. /// Performance calculator on osu!taiko maps.
@@ -37,9 +38,8 @@ use crate::{
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
pub struct TaikoPP<'map> { pub struct TaikoPP<'map> {
pub(crate) map: Cow<'map, Beatmap>, pub(crate) map_or_attrs: MapOrElse<Cow<'map, Beatmap>, TaikoDifficultyAttributes>,
is_convert: bool, is_convert_overwrite: Option<bool>,
attributes: Option<TaikoDifficultyAttributes>,
mods: u32, mods: u32,
combo: Option<usize>, combo: Option<usize>,
acc: Option<f64>, acc: Option<f64>,
@@ -59,9 +59,8 @@ impl<'map> TaikoPP<'map> {
let map = map.convert_mode(GameMode::Taiko); let map = map.convert_mode(GameMode::Taiko);
Self { Self {
is_convert: matches!(map, Cow::Owned(_)), map_or_attrs: MapOrElse::Map(map),
map, is_convert_overwrite: None,
attributes: None,
mods: 0, mods: 0,
combo: None, combo: None,
acc: None, acc: None,
@@ -80,7 +79,7 @@ impl<'map> TaikoPP<'map> {
#[inline] #[inline]
pub fn attributes(mut self, attrs: impl TaikoAttributeProvider) -> Self { pub fn attributes(mut self, attrs: impl TaikoAttributeProvider) -> Self {
if let Some(attrs) = attrs.attributes() { if let Some(attrs) = attrs.attributes() {
self.attributes = Some(attrs); self.map_or_attrs = MapOrElse::Else(attrs);
} }
self self
@@ -133,7 +132,7 @@ impl<'map> TaikoPP<'map> {
/// Specify the amount of misses of the play. /// Specify the amount of misses of the play.
#[inline] #[inline]
pub fn n_misses(mut self, n_misses: usize) -> Self { pub fn n_misses(mut self, n_misses: usize) -> Self {
self.n_misses = Some(n_misses.min(self.map.n_circles as usize)); self.n_misses = Some(n_misses);
self self
} }
@@ -174,7 +173,7 @@ impl<'map> TaikoPP<'map> {
/// This only needs to be specified if the map was converted manually beforehand. /// This only needs to be specified if the map was converted manually beforehand.
#[inline] #[inline]
pub fn is_convert(mut self, is_convert: bool) -> Self { pub fn is_convert(mut self, is_convert: bool) -> Self {
self.is_convert = is_convert; self.is_convert_overwrite = Some(is_convert);
self self
} }
@@ -199,11 +198,17 @@ impl<'map> TaikoPP<'map> {
/// Create the [`TaikoScoreState`] that will be used for performance calculation. /// Create the [`TaikoScoreState`] that will be used for performance calculation.
pub fn generate_state(&mut self) -> TaikoScoreState { pub fn generate_state(&mut self) -> TaikoScoreState {
let max_combo = match self.attributes { let attrs = match self.map_or_attrs {
Some(ref attrs) => attrs.max_combo, MapOrElse::Map(ref map) => {
None => self.attributes.insert(self.generate_attributes()).max_combo, let attrs = self.generate_attributes(map);
self.map_or_attrs.else_or_insert(attrs)
}
MapOrElse::Else(ref attrs) => attrs,
}; };
let max_combo = attrs.max_combo();
let total_result_count = if let Some(passed_objects) = self.passed_objects { let total_result_count = if let Some(passed_objects) = self.passed_objects {
max_combo.min(passed_objects) max_combo.min(passed_objects)
} else { } else {
@@ -286,10 +291,10 @@ impl<'map> TaikoPP<'map> {
pub fn calculate(mut self) -> TaikoPerformanceAttributes { pub fn calculate(mut self) -> TaikoPerformanceAttributes {
let state = self.generate_state(); let state = self.generate_state();
let attrs = self let attrs = match self.map_or_attrs {
.attributes MapOrElse::Map(ref map) => self.generate_attributes(map),
.take() MapOrElse::Else(attrs) => attrs,
.unwrap_or_else(|| self.generate_attributes()); };
let inner = TaikoPpInner { let inner = TaikoPpInner {
mods: self.mods, mods: self.mods,
@@ -300,10 +305,15 @@ impl<'map> TaikoPP<'map> {
inner.calculate() inner.calculate()
} }
fn generate_attributes(&self) -> TaikoDifficultyAttributes { fn generate_attributes(&self, map: &Beatmap) -> TaikoDifficultyAttributes {
let mut calculator = TaikoStars::new(self.map.as_ref()) let is_convert = self
.mods(self.mods) .is_convert_overwrite
.is_convert(self.is_convert); .unwrap_or(match self.map_or_attrs {
MapOrElse::Map(ref map) => matches!(map, Cow::Owned(_)),
MapOrElse::Else(ref attrs) => attrs.is_convert,
});
let mut calculator = TaikoStars::new(map).mods(self.mods).is_convert(is_convert);
if let Some(passed_objects) = self.passed_objects { if let Some(passed_objects) = self.passed_objects {
calculator = calculator.passed_objects(passed_objects); calculator = calculator.passed_objects(passed_objects);
@@ -315,6 +325,64 @@ impl<'map> TaikoPP<'map> {
calculator.calculate() calculator.calculate()
} }
/// Try to create [`TaikoPP`] through [`OsuPP`].
///
/// Returns `None` if [`OsuPP`] already replaced its internal [`Beatmap`]
/// with [`OsuDifficultyAttributes`], i.e. if [`OsuPP::attributes`]
/// or [`OsuPP::generate_state`] was called.
///
/// [`OsuDifficultyAttributes`]: crate::osu::OsuDifficultyAttributes
#[inline]
pub fn try_from_osu(osu: OsuPP<'map>) -> Option<Self> {
let OsuPP {
map_or_attrs,
mods,
acc,
combo,
n300,
n100,
n50: _,
n_misses,
passed_objects,
clock_rate,
hitresult_priority,
} = osu;
let MapOrElse::Map(map) = map_or_attrs else {
return None;
};
let map = map.into_inner().convert_mode(GameMode::Taiko);
Some(Self {
map_or_attrs: MapOrElse::Map(map),
is_convert_overwrite: None,
mods,
combo,
acc,
passed_objects,
clock_rate,
hitresult_priority,
n300,
n100,
n_misses,
})
}
/// Try to create [`TaikoPP`] through a [`TaikoAttributeProvider`].
///
/// If you already calculated the attributes for the current map-mod
/// combination, the [`Beatmap`] is no longer necessary to calculate
/// performance attributes so this method can be used instead of
/// [`TaikoPP::new`].
///
/// Returns `None` only if the [`TaikoAttributeProvider`] did not contain
/// attributes for taiko e.g. if it's [`DifficultyAttributes::Mania`].
#[inline]
pub fn try_from_attributes(attributes: impl TaikoAttributeProvider) -> Option<Self> {
attributes.attributes().map(Self::from)
}
} }
struct TaikoPpInner { struct TaikoPpInner {
@@ -434,43 +502,6 @@ impl TaikoPpInner {
} }
} }
impl<'map> From<OsuPP<'map>> for TaikoPP<'map> {
#[inline]
fn from(osu: OsuPP<'map>) -> Self {
let OsuPP {
map,
attributes: _,
mods,
acc,
combo,
n300,
n100,
n50: _,
n_misses,
passed_objects,
clock_rate,
hitresult_priority,
} = osu;
let map = map.convert_mode(GameMode::Taiko);
Self {
is_convert: matches!(map, Cow::Owned(_)),
map,
attributes: None,
mods,
combo,
acc,
passed_objects,
clock_rate,
hitresult_priority,
n300,
n100,
n_misses,
}
}
}
fn accuracy(n300: usize, n100: usize, n_misses: usize) -> f64 { fn accuracy(n300: usize, n100: usize, n_misses: usize) -> f64 {
if n300 + n100 + n_misses == 0 { if n300 + n100 + n_misses == 0 {
return 0.0; return 0.0;
@@ -482,6 +513,30 @@ fn accuracy(n300: usize, n100: usize, n_misses: usize) -> f64 {
numerator as f64 / denominator as f64 numerator as f64 / denominator as f64
} }
impl From<TaikoDifficultyAttributes> for TaikoPP<'_> {
fn from(attrs: TaikoDifficultyAttributes) -> Self {
Self {
map_or_attrs: MapOrElse::Else(attrs),
is_convert_overwrite: None,
mods: 0,
combo: None,
acc: None,
n_misses: None,
passed_objects: None,
clock_rate: None,
n300: None,
n100: None,
hitresult_priority: None,
}
}
}
impl From<TaikoPerformanceAttributes> for TaikoPP<'_> {
fn from(attrs: TaikoPerformanceAttributes) -> Self {
attrs.difficulty.into()
}
}
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation. /// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
pub trait TaikoAttributeProvider { pub trait TaikoAttributeProvider {
/// Provide the actual difficulty attributes. /// Provide the actual difficulty attributes.
@@ -505,7 +560,6 @@ impl TaikoAttributeProvider for TaikoPerformanceAttributes {
impl TaikoAttributeProvider for DifficultyAttributes { impl TaikoAttributeProvider for DifficultyAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<TaikoDifficultyAttributes> { fn attributes(self) -> Option<TaikoDifficultyAttributes> {
#[allow(irrefutable_let_patterns)]
if let Self::Taiko(attributes) = self { if let Self::Taiko(attributes) = self {
Some(attributes) Some(attributes)
} else { } else {
@@ -517,7 +571,6 @@ impl TaikoAttributeProvider for DifficultyAttributes {
impl TaikoAttributeProvider for PerformanceAttributes { impl TaikoAttributeProvider for PerformanceAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<TaikoDifficultyAttributes> { fn attributes(self) -> Option<TaikoDifficultyAttributes> {
#[allow(irrefutable_let_patterns)]
if let Self::Taiko(attributes) = self { if let Self::Taiko(attributes) = self {
Some(attributes.difficulty) Some(attributes.difficulty)
} else { } else {
+50
View File
@@ -0,0 +1,50 @@
use crate::Beatmap;
#[derive(Clone, Debug)]
pub(crate) enum MapOrElse<M, E> {
Map(M),
Else(E),
}
impl<M, E> MapOrElse<M, E> {
/// Return a mutable reference to `Else`.
///
/// If `self` is of variant `Map`, store `other` in `self`, and return a
/// mutable reference to it.
pub(crate) fn else_or_insert(&mut self, other: E) -> &mut E {
match self {
MapOrElse::Map(_) => {
*self = Self::Else(other);
let Self::Else(ref mut other) = self else {
unreachable!()
};
other
}
MapOrElse::Else(ref mut other) => other,
}
}
}
impl<'map, E> From<&'map Beatmap> for MapOrElse<MapRef<'map>, E> {
fn from(map: &'map Beatmap) -> Self {
Self::Map(MapRef(map))
}
}
/// References don't implement [`Deref`] so we implement a wrapper type.
#[derive(Copy, Clone, Debug)]
pub(crate) struct MapRef<'map>(&'map Beatmap);
impl<'map> MapRef<'map> {
pub(crate) fn into_inner(self) -> &'map Beatmap {
self.0
}
}
impl AsRef<Beatmap> for MapRef<'_> {
fn as_ref(&self) -> &Beatmap {
self.0
}
}
+7 -2
View File
@@ -2,9 +2,14 @@ mod byte_hasher;
mod compact_vec; mod compact_vec;
mod float_ext; mod float_ext;
mod limited_queue; mod limited_queue;
mod map_or_else;
mod tandem_sort; mod tandem_sort;
pub(crate) use self::{ pub(crate) use self::{
byte_hasher::ByteHasher, compact_vec::CompactVec, float_ext::FloatExt, byte_hasher::ByteHasher,
limited_queue::LimitedQueue, tandem_sort::TandemSorter, compact_vec::CompactVec,
float_ext::FloatExt,
limited_queue::LimitedQueue,
map_or_else::{MapOrElse, MapRef},
tandem_sort::TandemSorter,
}; };
+3
View File
@@ -52,6 +52,7 @@ impl_mode! {
hit_window: 35.0, hit_window: 35.0,
stars: 2.9778030386845606, stars: 2.9778030386845606,
max_combo: 289, max_combo: 289,
is_convert: false,
}; };
Catch: 2118524, CatchDifficultyAttributes { Catch: 2118524, CatchDifficultyAttributes {
stars: 3.2502669316166624, stars: 3.2502669316166624,
@@ -64,5 +65,7 @@ impl_mode! {
stars: 3.441830819988125, stars: 3.441830819988125,
hit_window: 40.0, hit_window: 40.0,
max_combo: 956, max_combo: 956,
n_objects: 594,
is_convert: false,
}; };
} }
+1 -1
View File
@@ -71,7 +71,7 @@ fn gradual_complete_next() {
let next_gradual_owned = gradual_owned.next(state.clone()).unwrap(); let next_gradual_owned = gradual_owned.next(state.clone()).unwrap();
let regular_calc = ManiaPP::new(map) let mut regular_calc = ManiaPP::new(map)
.mods(mods) .mods(mods)
.passed_objects(i) .passed_objects(i)
.state(state.clone()); .state(state.clone());
+73
View File
@@ -0,0 +1,73 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{CatchPP, ManiaPP, OsuPP, TaikoPP};
mod common;
#[test]
fn osu() {
let map = test_map!(Osu);
let mods = 8 + 64;
let misses = 2;
let regular = OsuPP::new(map).mods(mods).n_misses(misses).calculate();
let via_diff = OsuPP::from(regular.difficulty.clone())
.mods(mods)
.n_misses(misses)
.calculate();
assert_eq!(regular, via_diff);
}
#[test]
fn taiko() {
let map = test_map!(Taiko);
let mods = 8 + 64;
let misses = 2;
let regular = TaikoPP::new(map).mods(mods).n_misses(misses).calculate();
let via_diff = TaikoPP::from(regular.difficulty.clone())
.mods(mods)
.n_misses(misses)
.calculate();
assert_eq!(regular, via_diff);
}
#[test]
fn catch() {
let map = test_map!(Catch);
let mods = 8 + 64;
let misses = 2;
let regular = CatchPP::new(map).mods(mods).misses(misses).calculate();
let via_diff = CatchPP::from(regular.difficulty.clone())
.mods(mods)
.misses(misses)
.calculate();
assert_eq!(regular, via_diff);
}
#[test]
fn mania() {
let map = test_map!(Mania);
let mods = 8 + 64;
let misses = 2;
let regular = ManiaPP::new(map).mods(mods).n_misses(misses).calculate();
let via_diff = ManiaPP::from(regular.difficulty.clone())
.mods(mods)
.n_misses(misses)
.calculate();
assert_eq!(regular, via_diff);
}