improved osu hitresult generation

This commit is contained in:
MaxOhn
2022-10-15 11:36:41 +02:00
parent fa91703a2a
commit d83217f5d0
8 changed files with 303 additions and 294 deletions
+30 -22
View File
@@ -42,7 +42,7 @@ pub struct CatchPP<'map> {
pub(crate) n_droplets: Option<usize>, pub(crate) n_droplets: Option<usize>,
pub(crate) n_tiny_droplets: Option<usize>, pub(crate) n_tiny_droplets: Option<usize>,
pub(crate) n_tiny_droplet_misses: Option<usize>, pub(crate) n_tiny_droplet_misses: Option<usize>,
pub(crate) n_misses: usize, pub(crate) n_misses: Option<usize>,
passed_objects: Option<usize>, passed_objects: Option<usize>,
clock_rate: Option<f64>, clock_rate: Option<f64>,
} }
@@ -61,7 +61,7 @@ impl<'map> CatchPP<'map> {
n_droplets: None, n_droplets: None,
n_tiny_droplets: None, n_tiny_droplets: None,
n_tiny_droplet_misses: None, n_tiny_droplet_misses: None,
n_misses: 0, n_misses: None,
passed_objects: None, passed_objects: None,
clock_rate: None, clock_rate: None,
} }
@@ -132,7 +132,7 @@ impl<'map> CatchPP<'map> {
/// Specify the amount of fruit / droplet misses of the play. /// Specify the amount of fruit / droplet misses of the play.
#[inline] #[inline]
pub fn misses(mut self, n_misses: usize) -> Self { pub fn misses(mut self, n_misses: usize) -> Self {
self.n_misses = n_misses; self.n_misses = Some(n_misses);
self self
} }
@@ -176,7 +176,7 @@ impl<'map> CatchPP<'map> {
self.n_droplets = Some(n_droplets); self.n_droplets = Some(n_droplets);
self.n_tiny_droplets = Some(n_tiny_droplets); self.n_tiny_droplets = Some(n_tiny_droplets);
self.n_tiny_droplet_misses = Some(n_tiny_droplet_misses); self.n_tiny_droplet_misses = Some(n_tiny_droplet_misses);
self.n_misses = n_misses; self.n_misses = Some(n_misses);
self self
} }
@@ -201,15 +201,17 @@ impl<'map> CatchPP<'map> {
let attributes = self.attributes.as_ref().unwrap(); let attributes = self.attributes.as_ref().unwrap();
let n_droplets = self let n_droplets = self.n_droplets.unwrap_or_else(|| {
.n_droplets attributes
.unwrap_or_else(|| attributes.n_droplets.saturating_sub(self.n_misses)); .n_droplets
.saturating_sub(self.n_misses.unwrap_or(0))
});
let max_combo = attributes.max_combo(); let max_combo = attributes.max_combo();
let n_fruits = self.n_fruits.unwrap_or_else(|| { let n_fruits = self.n_fruits.unwrap_or_else(|| {
max_combo max_combo
.saturating_sub(self.n_misses) .saturating_sub(self.n_misses.unwrap_or(0))
.saturating_sub(n_droplets) .saturating_sub(n_droplets)
}); });
@@ -237,16 +239,20 @@ impl<'map> CatchPP<'map> {
let correct_combo_hits = self let correct_combo_hits = self
.n_fruits .n_fruits
.and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses)) .and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses.unwrap_or(0)))
.filter(|h| *h == max_combo); .filter(|h| *h == max_combo);
let correct_fruits = self let correct_fruits = self.n_fruits.filter(|f| {
.n_fruits *f >= attributes
.filter(|f| *f >= attributes.n_fruits.saturating_sub(self.n_misses)); .n_fruits
.saturating_sub(self.n_misses.unwrap_or(0))
});
let correct_droplets = self let correct_droplets = self.n_droplets.filter(|d| {
.n_droplets *d >= attributes
.filter(|d| *d >= attributes.n_droplets.saturating_sub(self.n_misses)); .n_droplets
.saturating_sub(self.n_misses.unwrap_or(0))
});
let correct_tinies = self let correct_tinies = self
.n_tiny_droplets .n_tiny_droplets
@@ -267,7 +273,7 @@ impl<'map> CatchPP<'map> {
let missing = max_combo let missing = max_combo
.saturating_sub(n_fruits) .saturating_sub(n_fruits)
.saturating_sub(n_droplets) .saturating_sub(n_droplets)
.saturating_sub(self.n_misses); .saturating_sub(self.n_misses.unwrap_or(0));
let missing_fruits = let missing_fruits =
missing.saturating_sub(attributes.n_droplets.saturating_sub(n_droplets)); missing.saturating_sub(attributes.n_droplets.saturating_sub(n_droplets));
@@ -287,7 +293,7 @@ impl<'map> CatchPP<'map> {
n_droplets, n_droplets,
n_tiny_droplets, n_tiny_droplets,
n_tiny_droplet_misses, n_tiny_droplet_misses,
n_misses: self.n_misses, n_misses: self.n_misses.unwrap_or(0),
}; };
} }
@@ -299,7 +305,7 @@ impl<'map> CatchPP<'map> {
n_droplets: self.n_droplets.unwrap_or(0), n_droplets: self.n_droplets.unwrap_or(0),
n_tiny_droplets: self.n_tiny_droplets.unwrap_or(0), n_tiny_droplets: self.n_tiny_droplets.unwrap_or(0),
n_tiny_droplet_misses: self.n_tiny_droplet_misses.unwrap_or(0), n_tiny_droplet_misses: self.n_tiny_droplet_misses.unwrap_or(0),
n_misses: self.n_misses, n_misses: self.n_misses.unwrap_or(0),
} }
} }
@@ -542,8 +548,9 @@ mod test {
let numerator = calculator.n_fruits.unwrap_or(0) let numerator = calculator.n_fruits.unwrap_or(0)
+ calculator.n_droplets.unwrap_or(0) + calculator.n_droplets.unwrap_or(0)
+ calculator.n_tiny_droplets.unwrap_or(0); + calculator.n_tiny_droplets.unwrap_or(0);
let denominator = let denominator = numerator
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses; + calculator.n_tiny_droplet_misses.unwrap_or(0)
+ calculator.n_misses.unwrap_or(0);
let acc = 100.0 * numerator as f64 / denominator as f64; let acc = 100.0 * numerator as f64 / denominator as f64;
assert!( assert!(
@@ -582,8 +589,9 @@ mod test {
let numerator = calculator.n_fruits.unwrap_or(0) let numerator = calculator.n_fruits.unwrap_or(0)
+ calculator.n_droplets.unwrap_or(0) + calculator.n_droplets.unwrap_or(0)
+ calculator.n_tiny_droplets.unwrap_or(0); + calculator.n_tiny_droplets.unwrap_or(0);
let denominator = let denominator = numerator
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses; + calculator.n_tiny_droplet_misses.unwrap_or(0)
+ calculator.n_misses.unwrap_or(0);
let acc = 100.0 * numerator as f64 / denominator as f64; let acc = 100.0 * numerator as f64 / denominator as f64;
assert!( assert!(
+1 -1
View File
@@ -200,7 +200,7 @@ mod gradual;
pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, ScoreState}; pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, ScoreState};
mod pp; mod pp;
pub use pp::{AnyPP, AttributeProvider}; pub use pp::{AnyPP, AttributeProvider, HitResultPriority};
mod stars; mod stars;
pub use stars::AnyStars; pub use stars::AnyStars;
+20 -5
View File
@@ -20,16 +20,31 @@ pub struct ManiaScoreState {
} }
impl ManiaScoreState { impl ManiaScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up. /// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize { pub fn total_hits(&self) -> usize {
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
} }
}
impl ManiaScoreState { /// Calculate the accuracy between `0.0` and `1.0` for this state.
/// Create a new empty score state. #[inline]
pub fn new() -> Self { pub fn accuracy(&self) -> f64 {
Self::default() let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * (self.n320 + self.n300) + 4 * self.n200 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
} }
} }
+15 -28
View File
@@ -1,7 +1,9 @@
use std::borrow::Cow; use std::borrow::Cow;
use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaScoreState, ManiaStars}; use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaScoreState, ManiaStars};
use crate::{Beatmap, DifficultyAttributes, GameMode, Mods, OsuPP, PerformanceAttributes}; use crate::{
Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuPP, PerformanceAttributes,
};
// TODO: update // TODO: update
/// Performance calculator on osu!mania maps. /// Performance calculator on osu!mania maps.
@@ -48,7 +50,7 @@ pub struct ManiaPP<'map> {
pub(crate) n_misses: Option<usize>, pub(crate) n_misses: Option<usize>,
acc: Option<f64>, acc: Option<f64>,
hitresult_priority: Option<ManiaHitResultPriority>, hitresult_priority: Option<HitResultPriority>,
} }
impl<'map> ManiaPP<'map> { impl<'map> ManiaPP<'map> {
@@ -120,7 +122,7 @@ impl<'map> ManiaPP<'map> {
self self
} }
/// Specify the accuracy of a play. /// Specify the accuracy of a play between `0` and `100`.
/// This will be used to generate matching hitresults. /// This will be used to generate matching hitresults.
#[inline] #[inline]
pub fn accuracy(mut self, acc: f64) -> Self { pub fn accuracy(mut self, acc: f64) -> Self {
@@ -131,9 +133,9 @@ impl<'map> ManiaPP<'map> {
/// Specify how hitresults should be generated. /// Specify how hitresults should be generated.
/// ///
/// Defauls to [`ManiaHitResultPriority::BestCase`]. /// Defauls to [`HitResultPriority::BestCase`].
#[inline] #[inline]
pub fn hitresult_priority(mut self, priority: ManiaHitResultPriority) -> Self { pub fn hitresult_priority(mut self, priority: HitResultPriority) -> Self {
self.hitresult_priority = Some(priority); self.hitresult_priority = Some(priority);
self self
@@ -237,7 +239,7 @@ impl<'map> ManiaPP<'map> {
} }
fn generate_hitresults(&self) -> ManiaScoreState { fn generate_hitresults(&self) -> ManiaScoreState {
let n_objects = self.map.hit_objects.len(); let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
let priority = self.hitresult_priority.unwrap_or_default(); let priority = self.hitresult_priority.unwrap_or_default();
let mut state = ManiaScoreState { let mut state = ManiaScoreState {
@@ -250,6 +252,7 @@ impl<'map> ManiaPP<'map> {
}; };
if let Some(acc) = self.acc { if let Some(acc) = self.acc {
// TODO: test
let target_total = (acc * (n_objects * 6) as f64).round() as usize; let target_total = (acc * (n_objects * 6) as f64).round() as usize;
let mut delta = target_total.saturating_sub(n_objects.saturating_sub(state.n_misses)); let mut delta = target_total.saturating_sub(n_objects.saturating_sub(state.n_misses));
@@ -274,7 +277,7 @@ impl<'map> ManiaPP<'map> {
state.n50 += n_objects.saturating_sub(state.total_hits() - state.n50); state.n50 += n_objects.saturating_sub(state.total_hits() - state.n50);
if let ManiaHitResultPriority::BestCase = priority { if let HitResultPriority::BestCase = priority {
// Shift n50 to n200 // Shift n50 to n200
if self.n320.or(self.n300).or(self.n200).or(self.n50).is_none() { if self.n320.or(self.n300).or(self.n200).or(self.n50).is_none() {
let n = (state.n320 + state.n300).min(state.n50 / 2); let n = (state.n320 + state.n300).min(state.n50 / 2);
@@ -309,7 +312,7 @@ impl<'map> ManiaPP<'map> {
let remaining = n_objects.saturating_sub(state.total_hits()); let remaining = n_objects.saturating_sub(state.total_hits());
match priority { match priority {
ManiaHitResultPriority::BestCase => { HitResultPriority::BestCase => {
if self.n320.is_none() { if self.n320.is_none() {
state.n320 = remaining; state.n320 = remaining;
} else if self.n300.is_none() { } else if self.n300.is_none() {
@@ -324,7 +327,7 @@ impl<'map> ManiaPP<'map> {
state.n320 = remaining; state.n320 = remaining;
} }
} }
ManiaHitResultPriority::WorstCase => { HitResultPriority::WorstCase => {
if self.n50.is_none() { if self.n50.is_none() {
state.n50 = remaining; state.n50 = remaining;
} else if self.n100.is_none() { } else if self.n100.is_none() {
@@ -405,26 +408,10 @@ impl ManiaPpInner {
return 0.0; return 0.0;
} }
let numerator = *n320 * 320 + *n300 * 300 + *n200 * 200 + *n100 * 100 + *n50 * 50; let numerator = *n320 * 32 + *n300 * 30 + *n200 * 20 + *n100 * 10 + *n50 * 5;
let denominator = total_hits as f64 * 320.0; let denominator = total_hits * 32;
numerator as f64 / denominator numerator as f64 / denominator as f64
}
}
/// While generating hitresults that weren't specific, decide how they should be distributed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ManiaHitResultPriority {
/// Prioritize good hitresults over bad ones
BestCase,
/// Prioritize bad hitresults over good ones
WorstCase,
}
impl Default for ManiaHitResultPriority {
#[inline]
fn default() -> Self {
Self::BestCase
} }
} }
+22
View File
@@ -23,9 +23,31 @@ pub struct OsuScoreState {
impl OsuScoreState { impl OsuScoreState {
/// Create a new empty score state. /// Create a new empty score state.
#[inline]
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n300 + self.n100 + self.n50 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * self.n300 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
}
} }
/// Gradually calculate the performance attributes of an osu!standard map. /// Gradually calculate the performance attributes of an osu!standard map.
+194 -233
View File
@@ -2,7 +2,8 @@ use super::{
OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState, PERFORMANCE_BASE_MULTIPLIER, OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState, PERFORMANCE_BASE_MULTIPLIER,
}; };
use crate::{ use crate::{
AnyPP, Beatmap, DifficultyAttributes, GameMode, Mods, OsuStars, PerformanceAttributes, AnyPP, Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuStars,
PerformanceAttributes,
}; };
/// Performance calculator on osu!standard maps. /// Performance calculator on osu!standard maps.
@@ -46,9 +47,10 @@ pub struct OsuPP<'map> {
pub(crate) n300: Option<usize>, pub(crate) n300: Option<usize>,
pub(crate) n100: Option<usize>, pub(crate) n100: Option<usize>,
pub(crate) n50: Option<usize>, pub(crate) n50: Option<usize>,
pub(crate) n_misses: usize, pub(crate) n_misses: Option<usize>,
pub(crate) passed_objects: Option<usize>, pub(crate) passed_objects: Option<usize>,
pub(crate) clock_rate: Option<f64>, pub(crate) clock_rate: Option<f64>,
hitresult_priority: Option<HitResultPriority>,
} }
impl<'map> OsuPP<'map> { impl<'map> OsuPP<'map> {
@@ -65,9 +67,10 @@ impl<'map> OsuPP<'map> {
n300: None, n300: None,
n100: None, n100: None,
n50: None, n50: None,
n_misses: 0, n_misses: None,
passed_objects: None, passed_objects: None,
clock_rate: None, clock_rate: None,
hitresult_priority: None,
} }
} }
@@ -88,7 +91,7 @@ impl<'map> OsuPP<'map> {
#[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(attributes) = attributes.attributes() {
self.attributes.replace(attributes); self.attributes = Some(attributes);
} }
self self
@@ -107,7 +110,17 @@ impl<'map> OsuPP<'map> {
/// Specify the max combo of the play. /// Specify the max combo of the play.
#[inline] #[inline]
pub fn combo(mut self, combo: usize) -> Self { pub fn combo(mut self, combo: usize) -> Self {
self.combo.replace(combo); self.combo = Some(combo);
self
}
/// Specify how hitresults should be generated.
///
/// Defauls to [`HitResultPriority::BestCase`].
#[inline]
pub fn hitresult_priority(mut self, priority: HitResultPriority) -> Self {
self.hitresult_priority = Some(priority);
self self
} }
@@ -115,7 +128,7 @@ impl<'map> OsuPP<'map> {
/// Specify the amount of 300s of a play. /// Specify the amount of 300s of a play.
#[inline] #[inline]
pub fn n300(mut self, n300: usize) -> Self { pub fn n300(mut self, n300: usize) -> Self {
self.n300.replace(n300); self.n300 = Some(n300);
self self
} }
@@ -123,7 +136,7 @@ impl<'map> OsuPP<'map> {
/// Specify the amount of 100s of a play. /// Specify the amount of 100s of a play.
#[inline] #[inline]
pub fn n100(mut self, n100: usize) -> Self { pub fn n100(mut self, n100: usize) -> Self {
self.n100.replace(n100); self.n100 = Some(n100);
self self
} }
@@ -131,7 +144,7 @@ impl<'map> OsuPP<'map> {
/// Specify the amount of 50s of a play. /// Specify the amount of 50s of a play.
#[inline] #[inline]
pub fn n50(mut self, n50: usize) -> Self { pub fn n50(mut self, n50: usize) -> Self {
self.n50.replace(n50); self.n50 = Some(n50);
self self
} }
@@ -139,7 +152,7 @@ impl<'map> OsuPP<'map> {
/// Specify the amount of misses of a play. /// Specify the amount of misses of a 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 = n_misses; self.n_misses = Some(n_misses);
self self
} }
@@ -151,7 +164,7 @@ impl<'map> OsuPP<'map> {
/// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformanceAttributes). /// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformanceAttributes).
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects.replace(passed_objects); self.passed_objects = Some(passed_objects);
self self
} }
@@ -181,161 +194,102 @@ impl<'map> OsuPP<'map> {
self.n300 = Some(n300); self.n300 = Some(n300);
self.n100 = Some(n100); self.n100 = Some(n100);
self.n50 = Some(n50); self.n50 = Some(n50);
self.n_misses = n_misses; self.n_misses = Some(n_misses);
self self
} }
/// Generate the hit results with respect to the given accuracy between `0` and `100`. /// Specify the accuracy of a play between `0` and `100`.
/// /// This will be used to generate matching hitresults.
/// Be sure to set `misses` beforehand!
/// In case of a partial play, be also sure to set `passed_objects` beforehand!
pub fn accuracy(mut self, acc: f64) -> Self { pub fn accuracy(mut self, acc: f64) -> Self {
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len()); self.acc = Some(acc / 100.0);
let mut acc = acc / 100.0;
if self.n100.or(self.n50).is_some() {
let mut n100 = self.n100.unwrap_or(0);
let mut n50 = self.n50.unwrap_or(0);
let placed_points = 2 * n100 + n50 + self.n_misses;
let missing_objects = n_objects - n100 - n50 - self.n_misses;
let missing_points =
((6.0 * acc * n_objects as f64).round() as usize).saturating_sub(placed_points);
let mut n300 = missing_objects.min(missing_points / 6);
n50 += missing_objects - n300;
if let Some(orig_n50) = self.n50.filter(|_| self.n100.is_none()) {
// Only n50s were changed, try to load some off again onto n100s
let difference = n50 - orig_n50;
let n = n300.min(difference / 4);
n300 -= n;
n100 += 5 * n;
n50 -= 4 * n;
}
self.n300 = Some(n300);
self.n100 = Some(n100);
self.n50 = Some(n50);
acc = (6 * n300 + 2 * n100 + n50) as f64 / (6 * n_objects) as f64;
} else {
let misses = self.n_misses.min(n_objects);
let target_total = (acc * n_objects as f64 * 6.0).round() as usize;
let delta = target_total - (n_objects - misses);
let mut n300 = delta / 5;
let mut n100 = (delta % 5).min(n_objects - n300 - misses);
let mut n50 = n_objects - n300 - n100 - misses;
// Sacrifice n300s to transform n50s into n100s
let n = n300.min(n50 / 4);
n300 -= n;
n100 += 5 * n;
n50 -= 4 * n;
self.n300 = Some(n300);
self.n100 = Some(n100);
self.n50 = Some(n50);
acc = (6 * n300 + 2 * n100 + n50) as f64 / (6 * n_objects) as f64;
}
self.acc = Some(acc);
self self
} }
fn assert_hitresults(self, attributes: OsuDifficultyAttributes) -> OsuPPInner { fn generate_hitresults(&self, max_combo: usize) -> OsuScoreState {
let mut n300 = self.n300;
let mut n100 = self.n100;
let mut n50 = self.n50;
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len()); let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
let priority = self.hitresult_priority.unwrap_or_default();
let mut state = OsuScoreState {
max_combo: self.combo.unwrap_or(max_combo),
n300: self.n300.unwrap_or(0),
n100: self.n100.unwrap_or(0),
n50: self.n50.unwrap_or(0),
n_misses: self.n_misses.unwrap_or(0),
};
if let Some(acc) = self.acc { if let Some(acc) = self.acc {
let n300 = n300.unwrap_or(0); // TODO: test
let n100 = n100.unwrap_or(0); let target_total = (acc * (n_objects * 6) as f64).round() as usize;
let n50 = n50.unwrap_or(0);
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f64; let mut delta = target_total.saturating_sub(n_objects.saturating_sub(state.n_misses));
let effective_misses = if self.n50.is_some() {
calculate_effective_misses(&attributes, self.combo, n100, n50, self.n_misses); delta /= 2;
OsuPPInner {
mods: self.mods,
combo: self.combo.unwrap_or(attributes.max_combo),
acc,
n300,
n100,
n50,
n_misses: self.n_misses,
total_hits,
effective_miss_count: effective_misses,
attributes,
} }
} else {
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
let remaining = n_objects if self.n100.is_some() {
.saturating_sub(n300.unwrap_or(0)) delta /= 2;
.saturating_sub(n100.unwrap_or(0)) }
.saturating_sub(n50.unwrap_or(0))
.saturating_sub(self.n_misses);
if remaining > 0 { if let Some(n300) = self.n300 {
if let Some(n300) = n300.as_mut() { delta = delta.saturating_sub(n300 * 6);
if n100.is_none() { } else {
n100 = Some(remaining); state.n300 = delta / 5;
} else if n50.is_none() { }
n50 = Some(remaining);
} else { if self.n100.is_none() {
*n300 += remaining; state.n100 = delta % 5;
} }
} else {
n300 = Some(remaining); state.n50 += n_objects.saturating_sub(state.total_hits() - state.n50);
if let HitResultPriority::BestCase = priority {
// Shift n50 to n100
if self.n300.or(self.n100).or(self.n50).is_none() {
let n = state.n300.min(state.n50 / 4);
state.n300 -= n;
state.n100 += 5 * n;
state.n50 -= 4 * n;
} }
} }
} else {
let remaining = n_objects.saturating_sub(state.total_hits());
let n300 = n300.unwrap_or(0); match priority {
let n100 = n100.unwrap_or(0); HitResultPriority::BestCase => {
let n50 = n50.unwrap_or(0); if self.n300.is_none() {
state.n300 = remaining;
let numerator = n300 * 6 + n100 * 2 + n50; } else if self.n100.is_none() {
state.n100 = remaining;
let acc = if n_objects > 0 { } else if self.n50.is_none() {
numerator as f64 / n_objects as f64 / 6.0 state.n50 = remaining;
} else { } else {
0.0 state.n300 = remaining;
}; }
}
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f64; HitResultPriority::WorstCase => {
if self.n50.is_none() {
let effective_misses = state.n50 = remaining;
calculate_effective_misses(&attributes, self.combo, n100, n50, self.n_misses); } else if self.n100.is_none() {
state.n100 = remaining;
OsuPPInner { } else if self.n300.is_none() {
mods: self.mods, state.n300 = remaining;
combo: self.combo.unwrap_or(attributes.max_combo), } else {
acc, state.n50 = remaining;
n300, }
n100, }
n50,
n_misses: self.n_misses,
total_hits,
effective_miss_count: effective_misses,
attributes,
} }
} }
state
} }
/// Calculate all performance related values, including pp and stars. /// Calculate all performance related values, including pp and stars.
pub fn calculate(mut self) -> OsuPerformanceAttributes { pub fn calculate(mut self) -> OsuPerformanceAttributes {
let attributes = self.attributes.take().unwrap_or_else(|| { let attrs = self.attributes.take().unwrap_or_else(|| {
let mut calculator = OsuStars::new(self.map).mods(self.mods); let mut calculator = OsuStars::new(self.map).mods(self.mods);
if let Some(passed_objects) = self.passed_objects { if let Some(passed_objects) = self.passed_objects {
@@ -349,52 +303,60 @@ impl<'map> OsuPP<'map> {
calculator.calculate() calculator.calculate()
}); });
self.assert_hitresults(attributes).calculate() let state = self.generate_hitresults(attrs.max_combo);
let effective_miss_count = calculate_effective_misses(&attrs, &state);
let inner = OsuPpInner {
attrs,
mods: self.mods,
acc: state.accuracy(),
state,
effective_miss_count,
};
inner.calculate()
} }
} }
struct OsuPPInner { struct OsuPpInner {
attributes: OsuDifficultyAttributes, attrs: OsuDifficultyAttributes,
mods: u32, mods: u32,
acc: f64, acc: f64,
combo: usize, state: OsuScoreState,
n300: usize,
n100: usize,
n50: usize,
n_misses: usize,
total_hits: f64,
effective_miss_count: f64, effective_miss_count: f64,
} }
impl OsuPPInner { impl OsuPpInner {
fn calculate(mut self) -> OsuPerformanceAttributes { fn calculate(mut self) -> OsuPerformanceAttributes {
if self.total_hits.abs() <= f64::EPSILON { let total_hits = self.state.total_hits();
if total_hits == 0 {
return OsuPerformanceAttributes { return OsuPerformanceAttributes {
difficulty: self.attributes, difficulty: self.attrs,
..Default::default() ..Default::default()
}; };
} }
let total_hits = total_hits as f64;
let mut multiplier = PERFORMANCE_BASE_MULTIPLIER; let mut multiplier = PERFORMANCE_BASE_MULTIPLIER;
if self.mods.nf() { if self.mods.nf() {
multiplier *= (1.0 - 0.02 * self.effective_miss_count).max(0.9); multiplier *= (1.0 - 0.02 * self.effective_miss_count).max(0.9);
} }
if self.mods.so() && self.total_hits > 0.0 { if self.mods.so() && total_hits > 0.0 {
multiplier *= 1.0 - (self.attributes.n_spinners as f64 / self.total_hits).powf(0.85); multiplier *= 1.0 - (self.attrs.n_spinners as f64 / total_hits).powf(0.85);
} }
if self.mods.rx() { if self.mods.rx() {
// * https://www.desmos.com/calculator/bc9eybdthb // * https://www.desmos.com/calculator/bc9eybdthb
// * we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0 // * we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
// * this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11) // * this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
let (n100_mult, n50_mult) = if self.attributes.od > 0.0 { let (n100_mult, n50_mult) = if self.attrs.od > 0.0 {
( (
1.0 - (self.attributes.od / 13.33).powf(1.8), 1.0 - (self.attrs.od / 13.33).powf(1.8),
1.0 - (self.attributes.od / 13.33).powi(5), 1.0 - (self.attrs.od / 13.33).powi(5),
) )
} else { } else {
(1.0, 1.0) (1.0, 1.0)
@@ -403,10 +365,10 @@ impl OsuPPInner {
// * As we're adding Oks and Mehs to an approximated number of combo breaks the result can be // * As we're adding Oks and Mehs to an approximated number of combo breaks the result can be
// * higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. // * higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
self.effective_miss_count = (self.effective_miss_count self.effective_miss_count = (self.effective_miss_count
+ self.n100 as f64 + self.state.n100 as f64
+ n100_mult + n100_mult
+ self.n50 as f64 * n50_mult) + self.state.n50 as f64 * n50_mult)
.min(self.total_hits); .min(total_hits);
} }
let aim_value = self.compute_aim_value(); let aim_value = self.compute_aim_value();
@@ -422,7 +384,7 @@ impl OsuPPInner {
* multiplier; * multiplier;
OsuPerformanceAttributes { OsuPerformanceAttributes {
difficulty: self.attributes, difficulty: self.attrs,
pp_acc: acc_value, pp_acc: acc_value,
pp_aim: aim_value, pp_aim: aim_value,
pp_flashlight: flashlight_value, pp_flashlight: flashlight_value,
@@ -433,12 +395,13 @@ impl OsuPPInner {
} }
fn compute_aim_value(&self) -> f64 { fn compute_aim_value(&self) -> f64 {
let mut aim_value = let mut aim_value = (5.0 * (self.attrs.aim / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
(5.0 * (self.attributes.aim / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
let total_hits = self.total_hits();
let len_bonus = 0.95 let len_bonus = 0.95
+ 0.4 * (self.total_hits / 2000.0).min(1.0) + 0.4 * (total_hits / 2000.0).min(1.0)
+ (self.total_hits > 2000.0) as u8 as f64 * (self.total_hits / 2000.0).log10() * 0.5; + (total_hits > 2000.0) as u8 as f64 * (total_hits / 2000.0).log10() * 0.5;
aim_value *= len_bonus; aim_value *= len_bonus;
@@ -446,7 +409,7 @@ impl OsuPPInner {
// * Default a 3% reduction for any # of misses. // * Default a 3% reduction for any # of misses.
if self.effective_miss_count > 0.0 { if self.effective_miss_count > 0.0 {
aim_value *= 0.97 aim_value *= 0.97
* (1.0 - (self.effective_miss_count / self.total_hits).powf(0.775)) * (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
.powf(self.effective_miss_count); .powf(self.effective_miss_count);
} }
@@ -454,10 +417,10 @@ impl OsuPPInner {
let ar_factor = if self.mods.rx() { let ar_factor = if self.mods.rx() {
0.0 0.0
} else if self.attributes.ar > 10.33 { } else if self.attrs.ar > 10.33 {
0.3 * (self.attributes.ar - 10.33) 0.3 * (self.attrs.ar - 10.33)
} else if self.attributes.ar < 8.0 { } else if self.attrs.ar < 8.0 {
0.05 * (8.0 - self.attributes.ar) 0.05 * (8.0 - self.attrs.ar)
} else { } else {
0.0 0.0
}; };
@@ -467,27 +430,27 @@ impl OsuPPInner {
if self.mods.hd() { if self.mods.hd() {
// * We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. // * We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aim_value *= 1.0 + 0.04 * (12.0 - self.attributes.ar); aim_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar);
} }
// * We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. // * We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
let estimate_diff_sliders = self.attributes.n_sliders as f64 * 0.15; let estimate_diff_sliders = self.attrs.n_sliders as f64 * 0.15;
if self.attributes.n_sliders > 0 { if self.attrs.n_sliders > 0 {
let estimate_slider_ends_dropped = ((self.n100 + self.n50 + self.n_misses) let estimate_slider_ends_dropped =
.min(self.attributes.max_combo - self.combo) ((self.state.n100 + self.state.n50 + self.state.n_misses)
as f64) .min(self.attrs.max_combo - self.state.max_combo) as f64)
.clamp(0.0, estimate_diff_sliders); .clamp(0.0, estimate_diff_sliders);
let slider_nerf_factor = (1.0 - self.attributes.slider_factor) let slider_nerf_factor = (1.0 - self.attrs.slider_factor)
* (1.0 - estimate_slider_ends_dropped / estimate_diff_sliders).powi(3) * (1.0 - estimate_slider_ends_dropped / estimate_diff_sliders).powi(3)
+ self.attributes.slider_factor; + self.attrs.slider_factor;
aim_value *= slider_nerf_factor; aim_value *= slider_nerf_factor;
} }
aim_value *= self.acc; aim_value *= self.acc;
// * It is important to consider accuracy difficulty when scaling with accuracy. // * It is important to consider accuracy difficulty when scaling with accuracy.
aim_value *= 0.98 + self.attributes.od * self.attributes.od / 2500.0; aim_value *= 0.98 + self.attrs.od * self.attrs.od / 2500.0;
aim_value aim_value
} }
@@ -498,11 +461,13 @@ impl OsuPPInner {
} }
let mut speed_value = let mut speed_value =
(5.0 * (self.attributes.speed / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0; (5.0 * (self.attrs.speed / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
let total_hits = self.total_hits();
let len_bonus = 0.95 let len_bonus = 0.95
+ 0.4 * (self.total_hits / 2000.0).min(1.0) + 0.4 * (total_hits / 2000.0).min(1.0)
+ (self.total_hits > 2000.0) as u8 as f64 * (self.total_hits / 2000.0).log10() * 0.5; + (total_hits > 2000.0) as u8 as f64 * (total_hits / 2000.0).log10() * 0.5;
speed_value *= len_bonus; speed_value *= len_bonus;
@@ -510,14 +475,14 @@ impl OsuPPInner {
// * Default a 3% reduction for any # of misses. // * Default a 3% reduction for any # of misses.
if self.effective_miss_count > 0.0 { if self.effective_miss_count > 0.0 {
speed_value *= 0.97 speed_value *= 0.97
* (1.0 - (self.effective_miss_count / self.total_hits).powf(0.775)) * (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
.powf(self.effective_miss_count.powf(0.875)); .powf(self.effective_miss_count.powf(0.875));
} }
speed_value *= self.get_combo_scaling_factor(); speed_value *= self.get_combo_scaling_factor();
let ar_factor = if self.attributes.ar > 10.33 { let ar_factor = if self.attrs.ar > 10.33 {
0.3 * (self.attributes.ar - 10.33) 0.3 * (self.attrs.ar - 10.33)
} else { } else {
0.0 0.0
}; };
@@ -528,33 +493,34 @@ impl OsuPPInner {
if self.mods.hd() { if self.mods.hd() {
// * We want to give more reward for lower AR when it comes to aim and HD. // * We want to give more reward for lower AR when it comes to aim and HD.
// * This nerfs high AR and buffs lower AR. // * This nerfs high AR and buffs lower AR.
speed_value *= 1.0 + 0.04 * (12.0 - self.attributes.ar); speed_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar);
} }
// * Calculate accuracy assuming the worst case scenario // * Calculate accuracy assuming the worst case scenario
let relevant_total_diff = self.total_hits - self.attributes.speed_note_count; let relevant_total_diff = total_hits - self.attrs.speed_note_count;
let relevant_n300 = (self.n300 as f64 - relevant_total_diff).max(0.0); let relevant_n300 = (self.state.n300 as f64 - relevant_total_diff).max(0.0);
let relevant_n100 = let relevant_n100 = (self.state.n100 as f64
(self.n100 as f64 - (relevant_total_diff - self.n300 as f64).max(0.0)).max(0.0); - (relevant_total_diff - self.state.n300 as f64).max(0.0))
let relevant_n50 = (self.n50 as f64 .max(0.0);
- (relevant_total_diff - (self.n300 + self.n100) as f64).max(0.0)) let relevant_n50 = (self.state.n50 as f64
- (relevant_total_diff - (self.state.n300 + self.state.n100) as f64).max(0.0))
.max(0.0); .max(0.0);
let relevant_acc = if self.attributes.speed_note_count.abs() <= f64::EPSILON { let relevant_acc = if self.attrs.speed_note_count.abs() <= f64::EPSILON {
0.0 0.0
} else { } else {
(relevant_n300 * 6.0 + relevant_n100 * 2.0 + relevant_n50) (relevant_n300 * 6.0 + relevant_n100 * 2.0 + relevant_n50)
/ (self.attributes.speed_note_count * 6.0) / (self.attrs.speed_note_count * 6.0)
}; };
// * Scale the speed value with accuracy and OD. // * Scale the speed value with accuracy and OD.
speed_value *= (0.95 + self.attributes.od * self.attributes.od / 750.0) speed_value *= (0.95 + self.attrs.od * self.attrs.od / 750.0)
* ((self.acc + relevant_acc) / 2.0).powf((14.5 - (self.attributes.od).max(8.0)) / 2.0); * ((self.acc + relevant_acc) / 2.0).powf((14.5 - (self.attrs.od).max(8.0)) / 2.0);
// * Scale the speed value with # of 50s to punish doubletapping. // * Scale the speed value with # of 50s to punish doubletapping.
speed_value *= 0.99_f64.powf( speed_value *= 0.99_f64.powf(
(self.n50 as f64 >= self.total_hits / 500.0) as u8 as f64 (self.state.n50 as f64 >= total_hits / 500.0) as u8 as f64
* (self.n50 as f64 - self.total_hits / 500.0), * (self.state.n50 as f64 - total_hits / 500.0),
); );
speed_value speed_value
@@ -567,12 +533,12 @@ impl OsuPPInner {
// * This percentage only considers HitCircles of any value - in this part // * This percentage only considers HitCircles of any value - in this part
// * of the calculation we focus on hitting the timing hit window. // * of the calculation we focus on hitting the timing hit window.
let amount_hit_objects_with_acc = self.attributes.n_circles; let amount_hit_objects_with_acc = self.attrs.n_circles;
let mut better_acc_percentage = if amount_hit_objects_with_acc > 0 { let mut better_acc_percentage = if amount_hit_objects_with_acc > 0 {
((self.n300 - (self.total_hits as usize - amount_hit_objects_with_acc)) * 6 ((self.state.n300 - (self.state.total_hits() - amount_hit_objects_with_acc)) * 6
+ self.n100 * 2 + self.state.n100 * 2
+ self.n50) as f64 + self.state.n50) as f64
/ (amount_hit_objects_with_acc * 6) as f64 / (amount_hit_objects_with_acc * 6) as f64
} else { } else {
0.0 0.0
@@ -585,8 +551,7 @@ impl OsuPPInner {
// * Lots of arbitrary values from testing. // * Lots of arbitrary values from testing.
// * Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. // * Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
let mut acc_value = let mut acc_value = 1.52163_f64.powf(self.attrs.od) * better_acc_percentage.powi(24) * 2.83;
1.52163_f64.powf(self.attributes.od) * better_acc_percentage.powi(24) * 2.83;
// * Bonus for many hitcircles - it's harder to keep good accuracy up for longer. // * Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
acc_value *= (amount_hit_objects_with_acc as f64 / 1000.0) acc_value *= (amount_hit_objects_with_acc as f64 / 1000.0)
@@ -610,12 +575,14 @@ impl OsuPPInner {
return 0.0; return 0.0;
} }
let mut flashlight_value = self.attributes.flashlight * self.attributes.flashlight * 25.0; let mut flashlight_value = self.attrs.flashlight * self.attrs.flashlight * 25.0;
let total_hits = self.total_hits();
// * Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // * Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if self.effective_miss_count > 0.0 { if self.effective_miss_count > 0.0 {
flashlight_value *= 0.97 flashlight_value *= 0.97
* (1.0 - (self.effective_miss_count / self.total_hits).powf(0.775)) * (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
.powf(self.effective_miss_count.powf(0.875)); .powf(self.effective_miss_count.powf(0.875));
} }
@@ -623,53 +590,48 @@ impl OsuPPInner {
// * Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. // * Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlight_value *= 0.7 flashlight_value *= 0.7
+ 0.1 * (self.total_hits / 200.0).min(1.0) + 0.1 * (total_hits / 200.0).min(1.0)
+ (self.total_hits > 200.0) as u8 as f64 + (total_hits > 200.0) as u8 as f64 * 0.2 * ((total_hits - 200.0) / 200.0).min(1.0);
* 0.2
* ((self.total_hits - 200.0) / 200.0).min(1.0);
// * Scale the flashlight value with accuracy _slightly_. // * Scale the flashlight value with accuracy _slightly_.
flashlight_value *= 0.5 + self.acc / 2.0; flashlight_value *= 0.5 + self.acc / 2.0;
// * It is important to also consider accuracy difficulty when doing that. // * It is important to also consider accuracy difficulty when doing that.
flashlight_value *= 0.98 + self.attributes.od * self.attributes.od / 2500.0; flashlight_value *= 0.98 + self.attrs.od * self.attrs.od / 2500.0;
flashlight_value flashlight_value
} }
fn get_combo_scaling_factor(&self) -> f64 { fn get_combo_scaling_factor(&self) -> f64 {
if self.attributes.max_combo == 0 { if self.attrs.max_combo == 0 {
1.0 1.0
} else { } else {
((self.combo as f64).powf(0.8) / (self.attributes.max_combo as f64).powf(0.8)).min(1.0) ((self.state.max_combo as f64).powf(0.8) / (self.attrs.max_combo as f64).powf(0.8))
.min(1.0)
} }
} }
fn total_hits(&self) -> f64 {
self.state.total_hits() as f64
}
} }
fn calculate_effective_misses( fn calculate_effective_misses(attrs: &OsuDifficultyAttributes, state: &OsuScoreState) -> f64 {
attrs: &OsuDifficultyAttributes,
combo: Option<usize>,
n100: usize,
n50: usize,
n_misses: usize,
) -> f64 {
// * Guess the number of misses + slider breaks from combo // * Guess the number of misses + slider breaks from combo
let mut combo_based_miss_count = 0.0; let mut combo_based_miss_count = 0.0;
if attrs.n_sliders > 0 { if attrs.n_sliders > 0 {
let full_combo_threshold = attrs.max_combo as f64 - 0.1 * attrs.n_sliders as f64; let full_combo_threshold = attrs.max_combo as f64 - 0.1 * attrs.n_sliders as f64;
if let Some(score_max_combo) = combo if (state.max_combo as f64) < full_combo_threshold {
.map(|combo| combo as f64) combo_based_miss_count = full_combo_threshold / (state.max_combo as f64).max(1.0);
.filter(|&combo| combo < full_combo_threshold)
{
combo_based_miss_count = full_combo_threshold / score_max_combo.max(1.0);
} }
} }
// * Clamp miss count to maximum amount of possible breaks // * Clamp miss count to maximum amount of possible breaks
combo_based_miss_count = combo_based_miss_count.min((n100 + n50 + n_misses) as f64); combo_based_miss_count =
combo_based_miss_count.min((state.n100 + state.n50 + state.n_misses) as f64);
combo_based_miss_count.max(n_misses as f64) combo_based_miss_count.max(state.n_misses as f64)
} }
/// 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.
@@ -783,21 +745,20 @@ mod test {
#[test] #[test]
fn osu_missing_objects() { fn osu_missing_objects() {
let map = Beatmap::default(); let map = Beatmap::default();
let attributes = OsuDifficultyAttributes::default();
let total_objects = 1234; let total_objects = 1234;
let n300 = 1000; let n300 = 1000;
let n100 = 200; let n100 = 200;
let n50 = 30; let n50 = 30;
let calculator = OsuPP::new(&map) let state = OsuPP::new(&map)
.passed_objects(total_objects) .passed_objects(total_objects)
.n300(n300) .n300(n300)
.n100(n100) .n100(n100)
.n50(n50) .n50(n50)
.assert_hitresults(attributes); .generate_hitresults(n300 + n100 + n50 + 50);
let n_objects = calculator.n300 + calculator.n100 + calculator.n50; let n_objects = state.total_hits();
assert_eq!( assert_eq!(
total_objects, n_objects, total_objects, n_objects,
+16
View File
@@ -254,6 +254,22 @@ impl<'map> AnyPP<'map> {
} }
} }
/// While generating remaining hitresults, decide how they should be distributed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum HitResultPriority {
/// Prioritize good hitresults over bad ones
BestCase,
/// Prioritize bad hitresults over good ones
WorstCase,
}
impl Default for HitResultPriority {
#[inline]
fn default() -> Self {
Self::BestCase
}
}
/// 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 AttributeProvider { pub trait AttributeProvider {
/// Provide the actual difficulty attributes. /// Provide the actual difficulty attributes.
+5 -5
View File
@@ -48,7 +48,7 @@ pub struct TaikoPP<'map> {
pub(crate) n300: Option<usize>, pub(crate) n300: Option<usize>,
pub(crate) n100: Option<usize>, pub(crate) n100: Option<usize>,
pub(crate) n_misses: usize, pub(crate) n_misses: Option<usize>,
} }
impl<'map> TaikoPP<'map> { impl<'map> TaikoPP<'map> {
@@ -61,7 +61,7 @@ impl<'map> TaikoPP<'map> {
mods: 0, mods: 0,
combo: None, combo: None,
acc: 1.0, acc: 1.0,
n_misses: 0, n_misses: None,
passed_objects: None, passed_objects: None,
clock_rate: None, clock_rate: None,
n300: None, n300: None,
@@ -118,7 +118,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 = n_misses.min(self.map.n_circles as usize); self.n_misses = Some(n_misses.min(self.map.n_circles as usize));
self self
} }
@@ -168,7 +168,7 @@ impl<'map> TaikoPP<'map> {
self.combo = Some(max_combo); self.combo = Some(max_combo);
self.n300 = Some(n300); self.n300 = Some(n300);
self.n100 = Some(n100); self.n100 = Some(n100);
self.n_misses = n_misses; self.n_misses = Some(n_misses);
self self
} }
@@ -196,7 +196,7 @@ impl<'map> TaikoPP<'map> {
fn assert_hitresults(&'map self, attributes: TaikoDifficultyAttributes) -> TaikoPPInner<'map> { fn assert_hitresults(&'map self, attributes: TaikoDifficultyAttributes) -> TaikoPPInner<'map> {
let total_result_count = attributes.max_combo(); let total_result_count = attributes.max_combo();
let misses = self.n_misses; let misses = self.n_misses.unwrap_or(0);
let (n300, n100) = match (self.n300, self.n100) { let (n300, n100) = match (self.n300, self.n100) {
(Some(n300), Some(n100)) => { (Some(n300), Some(n100)) => {