improved osu hitresult generation
This commit is contained in:
+30
-22
@@ -42,7 +42,7 @@ pub struct CatchPP<'map> {
|
||||
pub(crate) n_droplets: Option<usize>,
|
||||
pub(crate) n_tiny_droplets: 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>,
|
||||
clock_rate: Option<f64>,
|
||||
}
|
||||
@@ -61,7 +61,7 @@ impl<'map> CatchPP<'map> {
|
||||
n_droplets: None,
|
||||
n_tiny_droplets: None,
|
||||
n_tiny_droplet_misses: None,
|
||||
n_misses: 0,
|
||||
n_misses: None,
|
||||
passed_objects: None,
|
||||
clock_rate: None,
|
||||
}
|
||||
@@ -132,7 +132,7 @@ impl<'map> CatchPP<'map> {
|
||||
/// Specify the amount of fruit / droplet misses of the play.
|
||||
#[inline]
|
||||
pub fn misses(mut self, n_misses: usize) -> Self {
|
||||
self.n_misses = n_misses;
|
||||
self.n_misses = Some(n_misses);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -176,7 +176,7 @@ impl<'map> CatchPP<'map> {
|
||||
self.n_droplets = Some(n_droplets);
|
||||
self.n_tiny_droplets = Some(n_tiny_droplets);
|
||||
self.n_tiny_droplet_misses = Some(n_tiny_droplet_misses);
|
||||
self.n_misses = n_misses;
|
||||
self.n_misses = Some(n_misses);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -201,15 +201,17 @@ impl<'map> CatchPP<'map> {
|
||||
|
||||
let attributes = self.attributes.as_ref().unwrap();
|
||||
|
||||
let n_droplets = self
|
||||
.n_droplets
|
||||
.unwrap_or_else(|| attributes.n_droplets.saturating_sub(self.n_misses));
|
||||
let n_droplets = self.n_droplets.unwrap_or_else(|| {
|
||||
attributes
|
||||
.n_droplets
|
||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||
});
|
||||
|
||||
let max_combo = attributes.max_combo();
|
||||
|
||||
let n_fruits = self.n_fruits.unwrap_or_else(|| {
|
||||
max_combo
|
||||
.saturating_sub(self.n_misses)
|
||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||
.saturating_sub(n_droplets)
|
||||
});
|
||||
|
||||
@@ -237,16 +239,20 @@ impl<'map> CatchPP<'map> {
|
||||
|
||||
let correct_combo_hits = self
|
||||
.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);
|
||||
|
||||
let correct_fruits = self
|
||||
.n_fruits
|
||||
.filter(|f| *f >= attributes.n_fruits.saturating_sub(self.n_misses));
|
||||
let correct_fruits = self.n_fruits.filter(|f| {
|
||||
*f >= attributes
|
||||
.n_fruits
|
||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||
});
|
||||
|
||||
let correct_droplets = self
|
||||
.n_droplets
|
||||
.filter(|d| *d >= attributes.n_droplets.saturating_sub(self.n_misses));
|
||||
let correct_droplets = self.n_droplets.filter(|d| {
|
||||
*d >= attributes
|
||||
.n_droplets
|
||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||
});
|
||||
|
||||
let correct_tinies = self
|
||||
.n_tiny_droplets
|
||||
@@ -267,7 +273,7 @@ impl<'map> CatchPP<'map> {
|
||||
let missing = max_combo
|
||||
.saturating_sub(n_fruits)
|
||||
.saturating_sub(n_droplets)
|
||||
.saturating_sub(self.n_misses);
|
||||
.saturating_sub(self.n_misses.unwrap_or(0));
|
||||
|
||||
let missing_fruits =
|
||||
missing.saturating_sub(attributes.n_droplets.saturating_sub(n_droplets));
|
||||
@@ -287,7 +293,7 @@ impl<'map> CatchPP<'map> {
|
||||
n_droplets,
|
||||
n_tiny_droplets,
|
||||
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_tiny_droplets: self.n_tiny_droplets.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)
|
||||
+ calculator.n_droplets.unwrap_or(0)
|
||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
||||
let denominator =
|
||||
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
|
||||
let denominator = numerator
|
||||
+ calculator.n_tiny_droplet_misses.unwrap_or(0)
|
||||
+ calculator.n_misses.unwrap_or(0);
|
||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||
|
||||
assert!(
|
||||
@@ -582,8 +589,9 @@ mod test {
|
||||
let numerator = calculator.n_fruits.unwrap_or(0)
|
||||
+ calculator.n_droplets.unwrap_or(0)
|
||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
||||
let denominator =
|
||||
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
|
||||
let denominator = numerator
|
||||
+ calculator.n_tiny_droplet_misses.unwrap_or(0)
|
||||
+ calculator.n_misses.unwrap_or(0);
|
||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||
|
||||
assert!(
|
||||
|
||||
+1
-1
@@ -200,7 +200,7 @@ mod gradual;
|
||||
pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, ScoreState};
|
||||
|
||||
mod pp;
|
||||
pub use pp::{AnyPP, AttributeProvider};
|
||||
pub use pp::{AnyPP, AttributeProvider, HitResultPriority};
|
||||
|
||||
mod stars;
|
||||
pub use stars::AnyStars;
|
||||
|
||||
@@ -20,16 +20,31 @@ pub struct 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.
|
||||
#[inline]
|
||||
pub fn total_hits(&self) -> usize {
|
||||
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
|
||||
}
|
||||
}
|
||||
|
||||
impl ManiaScoreState {
|
||||
/// Create a new empty score state.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
/// 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.n320 + self.n300) + 4 * self.n200 + 2 * self.n100 + self.n50;
|
||||
let denominator = 6 * total_hits;
|
||||
|
||||
numerator as f64 / denominator as f64
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-28
@@ -1,7 +1,9 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
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
|
||||
/// Performance calculator on osu!mania maps.
|
||||
@@ -48,7 +50,7 @@ pub struct ManiaPP<'map> {
|
||||
pub(crate) n_misses: Option<usize>,
|
||||
|
||||
acc: Option<f64>,
|
||||
hitresult_priority: Option<ManiaHitResultPriority>,
|
||||
hitresult_priority: Option<HitResultPriority>,
|
||||
}
|
||||
|
||||
impl<'map> ManiaPP<'map> {
|
||||
@@ -120,7 +122,7 @@ impl<'map> ManiaPP<'map> {
|
||||
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.
|
||||
#[inline]
|
||||
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||
@@ -131,9 +133,9 @@ impl<'map> ManiaPP<'map> {
|
||||
|
||||
/// Specify how hitresults should be generated.
|
||||
///
|
||||
/// Defauls to [`ManiaHitResultPriority::BestCase`].
|
||||
/// Defauls to [`HitResultPriority::BestCase`].
|
||||
#[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
|
||||
@@ -237,7 +239,7 @@ impl<'map> ManiaPP<'map> {
|
||||
}
|
||||
|
||||
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 mut state = ManiaScoreState {
|
||||
@@ -250,6 +252,7 @@ impl<'map> ManiaPP<'map> {
|
||||
};
|
||||
|
||||
if let Some(acc) = self.acc {
|
||||
// TODO: test
|
||||
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));
|
||||
@@ -274,7 +277,7 @@ impl<'map> ManiaPP<'map> {
|
||||
|
||||
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
|
||||
if self.n320.or(self.n300).or(self.n200).or(self.n50).is_none() {
|
||||
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());
|
||||
|
||||
match priority {
|
||||
ManiaHitResultPriority::BestCase => {
|
||||
HitResultPriority::BestCase => {
|
||||
if self.n320.is_none() {
|
||||
state.n320 = remaining;
|
||||
} else if self.n300.is_none() {
|
||||
@@ -324,7 +327,7 @@ impl<'map> ManiaPP<'map> {
|
||||
state.n320 = remaining;
|
||||
}
|
||||
}
|
||||
ManiaHitResultPriority::WorstCase => {
|
||||
HitResultPriority::WorstCase => {
|
||||
if self.n50.is_none() {
|
||||
state.n50 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
@@ -405,26 +408,10 @@ impl ManiaPpInner {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let numerator = *n320 * 320 + *n300 * 300 + *n200 * 200 + *n100 * 100 + *n50 * 50;
|
||||
let denominator = total_hits as f64 * 320.0;
|
||||
let numerator = *n320 * 32 + *n300 * 30 + *n200 * 20 + *n100 * 10 + *n50 * 5;
|
||||
let denominator = total_hits * 32;
|
||||
|
||||
numerator as f64 / denominator
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
numerator as f64 / denominator as f64
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,31 @@ pub struct OsuScoreState {
|
||||
|
||||
impl OsuScoreState {
|
||||
/// Create a new empty score state.
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
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.
|
||||
|
||||
+194
-233
@@ -2,7 +2,8 @@ use super::{
|
||||
OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState, PERFORMANCE_BASE_MULTIPLIER,
|
||||
};
|
||||
use crate::{
|
||||
AnyPP, Beatmap, DifficultyAttributes, GameMode, Mods, OsuStars, PerformanceAttributes,
|
||||
AnyPP, Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuStars,
|
||||
PerformanceAttributes,
|
||||
};
|
||||
|
||||
/// Performance calculator on osu!standard maps.
|
||||
@@ -46,9 +47,10 @@ pub struct OsuPP<'map> {
|
||||
pub(crate) n300: Option<usize>,
|
||||
pub(crate) n100: 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) clock_rate: Option<f64>,
|
||||
hitresult_priority: Option<HitResultPriority>,
|
||||
}
|
||||
|
||||
impl<'map> OsuPP<'map> {
|
||||
@@ -65,9 +67,10 @@ impl<'map> OsuPP<'map> {
|
||||
n300: None,
|
||||
n100: None,
|
||||
n50: None,
|
||||
n_misses: 0,
|
||||
n_misses: None,
|
||||
passed_objects: None,
|
||||
clock_rate: None,
|
||||
hitresult_priority: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +91,7 @@ impl<'map> OsuPP<'map> {
|
||||
#[inline]
|
||||
pub fn attributes(mut self, attributes: impl OsuAttributeProvider) -> Self {
|
||||
if let Some(attributes) = attributes.attributes() {
|
||||
self.attributes.replace(attributes);
|
||||
self.attributes = Some(attributes);
|
||||
}
|
||||
|
||||
self
|
||||
@@ -107,7 +110,17 @@ impl<'map> OsuPP<'map> {
|
||||
/// Specify the max combo of the play.
|
||||
#[inline]
|
||||
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
|
||||
}
|
||||
@@ -115,7 +128,7 @@ impl<'map> OsuPP<'map> {
|
||||
/// Specify the amount of 300s of a play.
|
||||
#[inline]
|
||||
pub fn n300(mut self, n300: usize) -> Self {
|
||||
self.n300.replace(n300);
|
||||
self.n300 = Some(n300);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -123,7 +136,7 @@ impl<'map> OsuPP<'map> {
|
||||
/// Specify the amount of 100s of a play.
|
||||
#[inline]
|
||||
pub fn n100(mut self, n100: usize) -> Self {
|
||||
self.n100.replace(n100);
|
||||
self.n100 = Some(n100);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -131,7 +144,7 @@ impl<'map> OsuPP<'map> {
|
||||
/// Specify the amount of 50s of a play.
|
||||
#[inline]
|
||||
pub fn n50(mut self, n50: usize) -> Self {
|
||||
self.n50.replace(n50);
|
||||
self.n50 = Some(n50);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -139,7 +152,7 @@ impl<'map> OsuPP<'map> {
|
||||
/// Specify the amount of misses of a play.
|
||||
#[inline]
|
||||
pub fn n_misses(mut self, n_misses: usize) -> Self {
|
||||
self.n_misses = n_misses;
|
||||
self.n_misses = Some(n_misses);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -151,7 +164,7 @@ impl<'map> OsuPP<'map> {
|
||||
/// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformanceAttributes).
|
||||
#[inline]
|
||||
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||
self.passed_objects.replace(passed_objects);
|
||||
self.passed_objects = Some(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -181,161 +194,102 @@ impl<'map> OsuPP<'map> {
|
||||
self.n300 = Some(n300);
|
||||
self.n100 = Some(n100);
|
||||
self.n50 = Some(n50);
|
||||
self.n_misses = n_misses;
|
||||
self.n_misses = Some(n_misses);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
|
||||
///
|
||||
/// Be sure to set `misses` beforehand!
|
||||
/// In case of a partial play, be also sure to set `passed_objects` beforehand!
|
||||
/// Specify the accuracy of a play between `0` and `100`.
|
||||
/// This will be used to generate matching hitresults.
|
||||
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
|
||||
|
||||
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.acc = Some(acc / 100.0);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn assert_hitresults(self, attributes: OsuDifficultyAttributes) -> OsuPPInner {
|
||||
let mut n300 = self.n300;
|
||||
let mut n100 = self.n100;
|
||||
let mut n50 = self.n50;
|
||||
|
||||
fn generate_hitresults(&self, max_combo: usize) -> OsuScoreState {
|
||||
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 {
|
||||
let n300 = n300.unwrap_or(0);
|
||||
let n100 = n100.unwrap_or(0);
|
||||
let n50 = n50.unwrap_or(0);
|
||||
// TODO: test
|
||||
let target_total = (acc * (n_objects * 6) as f64).round() as usize;
|
||||
|
||||
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 =
|
||||
calculate_effective_misses(&attributes, self.combo, n100, n50, self.n_misses);
|
||||
|
||||
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,
|
||||
if self.n50.is_some() {
|
||||
delta /= 2;
|
||||
}
|
||||
} else {
|
||||
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
|
||||
|
||||
let remaining = n_objects
|
||||
.saturating_sub(n300.unwrap_or(0))
|
||||
.saturating_sub(n100.unwrap_or(0))
|
||||
.saturating_sub(n50.unwrap_or(0))
|
||||
.saturating_sub(self.n_misses);
|
||||
if self.n100.is_some() {
|
||||
delta /= 2;
|
||||
}
|
||||
|
||||
if remaining > 0 {
|
||||
if let Some(n300) = n300.as_mut() {
|
||||
if n100.is_none() {
|
||||
n100 = Some(remaining);
|
||||
} else if n50.is_none() {
|
||||
n50 = Some(remaining);
|
||||
} else {
|
||||
*n300 += remaining;
|
||||
}
|
||||
} else {
|
||||
n300 = Some(remaining);
|
||||
if let Some(n300) = self.n300 {
|
||||
delta = delta.saturating_sub(n300 * 6);
|
||||
} else {
|
||||
state.n300 = delta / 5;
|
||||
}
|
||||
|
||||
if self.n100.is_none() {
|
||||
state.n100 = delta % 5;
|
||||
}
|
||||
|
||||
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);
|
||||
let n100 = n100.unwrap_or(0);
|
||||
let n50 = n50.unwrap_or(0);
|
||||
|
||||
let numerator = n300 * 6 + n100 * 2 + n50;
|
||||
|
||||
let acc = if n_objects > 0 {
|
||||
numerator as f64 / n_objects as f64 / 6.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f64;
|
||||
|
||||
let effective_misses =
|
||||
calculate_effective_misses(&attributes, self.combo, n100, n50, self.n_misses);
|
||||
|
||||
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,
|
||||
match priority {
|
||||
HitResultPriority::BestCase => {
|
||||
if self.n300.is_none() {
|
||||
state.n300 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
state.n100 = remaining;
|
||||
} else if self.n50.is_none() {
|
||||
state.n50 = remaining;
|
||||
} else {
|
||||
state.n300 = remaining;
|
||||
}
|
||||
}
|
||||
HitResultPriority::WorstCase => {
|
||||
if self.n50.is_none() {
|
||||
state.n50 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
state.n100 = remaining;
|
||||
} else if self.n300.is_none() {
|
||||
state.n300 = remaining;
|
||||
} else {
|
||||
state.n50 = remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// Calculate all performance related values, including pp and stars.
|
||||
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);
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
@@ -349,52 +303,60 @@ impl<'map> OsuPP<'map> {
|
||||
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 {
|
||||
attributes: OsuDifficultyAttributes,
|
||||
struct OsuPpInner {
|
||||
attrs: OsuDifficultyAttributes,
|
||||
mods: u32,
|
||||
acc: f64,
|
||||
combo: usize,
|
||||
|
||||
n300: usize,
|
||||
n100: usize,
|
||||
n50: usize,
|
||||
n_misses: usize,
|
||||
|
||||
total_hits: f64,
|
||||
state: OsuScoreState,
|
||||
effective_miss_count: f64,
|
||||
}
|
||||
|
||||
impl OsuPPInner {
|
||||
impl OsuPpInner {
|
||||
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 {
|
||||
difficulty: self.attributes,
|
||||
difficulty: self.attrs,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
let total_hits = total_hits as f64;
|
||||
|
||||
let mut multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
||||
|
||||
if self.mods.nf() {
|
||||
multiplier *= (1.0 - 0.02 * self.effective_miss_count).max(0.9);
|
||||
}
|
||||
|
||||
if self.mods.so() && self.total_hits > 0.0 {
|
||||
multiplier *= 1.0 - (self.attributes.n_spinners as f64 / self.total_hits).powf(0.85);
|
||||
if self.mods.so() && total_hits > 0.0 {
|
||||
multiplier *= 1.0 - (self.attrs.n_spinners as f64 / total_hits).powf(0.85);
|
||||
}
|
||||
|
||||
if self.mods.rx() {
|
||||
// * https://www.desmos.com/calculator/bc9eybdthb
|
||||
// * 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)
|
||||
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.attributes.od / 13.33).powi(5),
|
||||
1.0 - (self.attrs.od / 13.33).powf(1.8),
|
||||
1.0 - (self.attrs.od / 13.33).powi(5),
|
||||
)
|
||||
} else {
|
||||
(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
|
||||
// * 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.n100 as f64
|
||||
+ self.state.n100 as f64
|
||||
+ n100_mult
|
||||
+ self.n50 as f64 * n50_mult)
|
||||
.min(self.total_hits);
|
||||
+ self.state.n50 as f64 * n50_mult)
|
||||
.min(total_hits);
|
||||
}
|
||||
|
||||
let aim_value = self.compute_aim_value();
|
||||
@@ -422,7 +384,7 @@ impl OsuPPInner {
|
||||
* multiplier;
|
||||
|
||||
OsuPerformanceAttributes {
|
||||
difficulty: self.attributes,
|
||||
difficulty: self.attrs,
|
||||
pp_acc: acc_value,
|
||||
pp_aim: aim_value,
|
||||
pp_flashlight: flashlight_value,
|
||||
@@ -433,12 +395,13 @@ impl OsuPPInner {
|
||||
}
|
||||
|
||||
fn compute_aim_value(&self) -> f64 {
|
||||
let mut aim_value =
|
||||
(5.0 * (self.attributes.aim / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
||||
let mut aim_value = (5.0 * (self.attrs.aim / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
||||
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
let len_bonus = 0.95
|
||||
+ 0.4 * (self.total_hits / 2000.0).min(1.0)
|
||||
+ (self.total_hits > 2000.0) as u8 as f64 * (self.total_hits / 2000.0).log10() * 0.5;
|
||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||
+ (total_hits > 2000.0) as u8 as f64 * (total_hits / 2000.0).log10() * 0.5;
|
||||
|
||||
aim_value *= len_bonus;
|
||||
|
||||
@@ -446,7 +409,7 @@ impl OsuPPInner {
|
||||
// * Default a 3% reduction for any # of misses.
|
||||
if self.effective_miss_count > 0.0 {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -454,10 +417,10 @@ impl OsuPPInner {
|
||||
|
||||
let ar_factor = if self.mods.rx() {
|
||||
0.0
|
||||
} else if self.attributes.ar > 10.33 {
|
||||
0.3 * (self.attributes.ar - 10.33)
|
||||
} else if self.attributes.ar < 8.0 {
|
||||
0.05 * (8.0 - self.attributes.ar)
|
||||
} else if self.attrs.ar > 10.33 {
|
||||
0.3 * (self.attrs.ar - 10.33)
|
||||
} else if self.attrs.ar < 8.0 {
|
||||
0.05 * (8.0 - self.attrs.ar)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
@@ -467,27 +430,27 @@ impl OsuPPInner {
|
||||
|
||||
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.
|
||||
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.
|
||||
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 {
|
||||
let estimate_slider_ends_dropped = ((self.n100 + self.n50 + self.n_misses)
|
||||
.min(self.attributes.max_combo - self.combo)
|
||||
as f64)
|
||||
.clamp(0.0, estimate_diff_sliders);
|
||||
let slider_nerf_factor = (1.0 - self.attributes.slider_factor)
|
||||
if self.attrs.n_sliders > 0 {
|
||||
let estimate_slider_ends_dropped =
|
||||
((self.state.n100 + self.state.n50 + self.state.n_misses)
|
||||
.min(self.attrs.max_combo - self.state.max_combo) as f64)
|
||||
.clamp(0.0, estimate_diff_sliders);
|
||||
let slider_nerf_factor = (1.0 - self.attrs.slider_factor)
|
||||
* (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 *= self.acc;
|
||||
// * 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
|
||||
}
|
||||
@@ -498,11 +461,13 @@ impl OsuPPInner {
|
||||
}
|
||||
|
||||
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
|
||||
+ 0.4 * (self.total_hits / 2000.0).min(1.0)
|
||||
+ (self.total_hits > 2000.0) as u8 as f64 * (self.total_hits / 2000.0).log10() * 0.5;
|
||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||
+ (total_hits > 2000.0) as u8 as f64 * (total_hits / 2000.0).log10() * 0.5;
|
||||
|
||||
speed_value *= len_bonus;
|
||||
|
||||
@@ -510,14 +475,14 @@ impl OsuPPInner {
|
||||
// * Default a 3% reduction for any # of misses.
|
||||
if self.effective_miss_count > 0.0 {
|
||||
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));
|
||||
}
|
||||
|
||||
speed_value *= self.get_combo_scaling_factor();
|
||||
|
||||
let ar_factor = if self.attributes.ar > 10.33 {
|
||||
0.3 * (self.attributes.ar - 10.33)
|
||||
let ar_factor = if self.attrs.ar > 10.33 {
|
||||
0.3 * (self.attrs.ar - 10.33)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
@@ -528,33 +493,34 @@ impl OsuPPInner {
|
||||
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.
|
||||
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
|
||||
let relevant_total_diff = self.total_hits - self.attributes.speed_note_count;
|
||||
let relevant_n300 = (self.n300 as f64 - relevant_total_diff).max(0.0);
|
||||
let relevant_n100 =
|
||||
(self.n100 as f64 - (relevant_total_diff - self.n300 as f64).max(0.0)).max(0.0);
|
||||
let relevant_n50 = (self.n50 as f64
|
||||
- (relevant_total_diff - (self.n300 + self.n100) as f64).max(0.0))
|
||||
let relevant_total_diff = total_hits - self.attrs.speed_note_count;
|
||||
let relevant_n300 = (self.state.n300 as f64 - relevant_total_diff).max(0.0);
|
||||
let relevant_n100 = (self.state.n100 as f64
|
||||
- (relevant_total_diff - self.state.n300 as f64).max(0.0))
|
||||
.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);
|
||||
|
||||
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
|
||||
} else {
|
||||
(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.
|
||||
speed_value *= (0.95 + self.attributes.od * self.attributes.od / 750.0)
|
||||
* ((self.acc + relevant_acc) / 2.0).powf((14.5 - (self.attributes.od).max(8.0)) / 2.0);
|
||||
speed_value *= (0.95 + self.attrs.od * self.attrs.od / 750.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.
|
||||
speed_value *= 0.99_f64.powf(
|
||||
(self.n50 as f64 >= self.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) as u8 as f64
|
||||
* (self.state.n50 as f64 - total_hits / 500.0),
|
||||
);
|
||||
|
||||
speed_value
|
||||
@@ -567,12 +533,12 @@ impl OsuPPInner {
|
||||
|
||||
// * This percentage only considers HitCircles of any value - in this part
|
||||
// * 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 {
|
||||
((self.n300 - (self.total_hits as usize - amount_hit_objects_with_acc)) * 6
|
||||
+ self.n100 * 2
|
||||
+ self.n50) as f64
|
||||
((self.state.n300 - (self.state.total_hits() - amount_hit_objects_with_acc)) * 6
|
||||
+ self.state.n100 * 2
|
||||
+ self.state.n50) as f64
|
||||
/ (amount_hit_objects_with_acc * 6) as f64
|
||||
} else {
|
||||
0.0
|
||||
@@ -585,8 +551,7 @@ impl OsuPPInner {
|
||||
|
||||
// * Lots of arbitrary values from testing.
|
||||
// * Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
|
||||
let mut acc_value =
|
||||
1.52163_f64.powf(self.attributes.od) * better_acc_percentage.powi(24) * 2.83;
|
||||
let mut acc_value = 1.52163_f64.powf(self.attrs.od) * better_acc_percentage.powi(24) * 2.83;
|
||||
|
||||
// * 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)
|
||||
@@ -610,12 +575,14 @@ impl OsuPPInner {
|
||||
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.
|
||||
if self.effective_miss_count > 0.0 {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -623,53 +590,48 @@ impl OsuPPInner {
|
||||
|
||||
// * Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
||||
flashlight_value *= 0.7
|
||||
+ 0.1 * (self.total_hits / 200.0).min(1.0)
|
||||
+ (self.total_hits > 200.0) as u8 as f64
|
||||
* 0.2
|
||||
* ((self.total_hits - 200.0) / 200.0).min(1.0);
|
||||
+ 0.1 * (total_hits / 200.0).min(1.0)
|
||||
+ (total_hits > 200.0) as u8 as f64 * 0.2 * ((total_hits - 200.0) / 200.0).min(1.0);
|
||||
|
||||
// * Scale the flashlight value with accuracy _slightly_.
|
||||
flashlight_value *= 0.5 + self.acc / 2.0;
|
||||
// * 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
|
||||
}
|
||||
|
||||
fn get_combo_scaling_factor(&self) -> f64 {
|
||||
if self.attributes.max_combo == 0 {
|
||||
if self.attrs.max_combo == 0 {
|
||||
1.0
|
||||
} 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(
|
||||
attrs: &OsuDifficultyAttributes,
|
||||
combo: Option<usize>,
|
||||
n100: usize,
|
||||
n50: usize,
|
||||
n_misses: usize,
|
||||
) -> f64 {
|
||||
fn calculate_effective_misses(attrs: &OsuDifficultyAttributes, state: &OsuScoreState) -> f64 {
|
||||
// * Guess the number of misses + slider breaks from combo
|
||||
let mut combo_based_miss_count = 0.0;
|
||||
|
||||
if attrs.n_sliders > 0 {
|
||||
let full_combo_threshold = attrs.max_combo as f64 - 0.1 * attrs.n_sliders as f64;
|
||||
|
||||
if let Some(score_max_combo) = combo
|
||||
.map(|combo| combo as f64)
|
||||
.filter(|&combo| combo < full_combo_threshold)
|
||||
{
|
||||
combo_based_miss_count = full_combo_threshold / score_max_combo.max(1.0);
|
||||
if (state.max_combo as f64) < full_combo_threshold {
|
||||
combo_based_miss_count = full_combo_threshold / (state.max_combo as f64).max(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// * 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.
|
||||
@@ -783,21 +745,20 @@ mod test {
|
||||
#[test]
|
||||
fn osu_missing_objects() {
|
||||
let map = Beatmap::default();
|
||||
let attributes = OsuDifficultyAttributes::default();
|
||||
|
||||
let total_objects = 1234;
|
||||
let n300 = 1000;
|
||||
let n100 = 200;
|
||||
let n50 = 30;
|
||||
|
||||
let calculator = OsuPP::new(&map)
|
||||
let state = OsuPP::new(&map)
|
||||
.passed_objects(total_objects)
|
||||
.n300(n300)
|
||||
.n100(n100)
|
||||
.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!(
|
||||
total_objects, n_objects,
|
||||
|
||||
@@ -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.
|
||||
pub trait AttributeProvider {
|
||||
/// Provide the actual difficulty attributes.
|
||||
|
||||
+5
-5
@@ -48,7 +48,7 @@ pub struct TaikoPP<'map> {
|
||||
|
||||
pub(crate) n300: Option<usize>,
|
||||
pub(crate) n100: Option<usize>,
|
||||
pub(crate) n_misses: usize,
|
||||
pub(crate) n_misses: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'map> TaikoPP<'map> {
|
||||
@@ -61,7 +61,7 @@ impl<'map> TaikoPP<'map> {
|
||||
mods: 0,
|
||||
combo: None,
|
||||
acc: 1.0,
|
||||
n_misses: 0,
|
||||
n_misses: None,
|
||||
passed_objects: None,
|
||||
clock_rate: None,
|
||||
n300: None,
|
||||
@@ -118,7 +118,7 @@ impl<'map> TaikoPP<'map> {
|
||||
/// Specify the amount of misses of the play.
|
||||
#[inline]
|
||||
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
|
||||
}
|
||||
@@ -168,7 +168,7 @@ impl<'map> TaikoPP<'map> {
|
||||
self.combo = Some(max_combo);
|
||||
self.n300 = Some(n300);
|
||||
self.n100 = Some(n100);
|
||||
self.n_misses = n_misses;
|
||||
self.n_misses = Some(n_misses);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -196,7 +196,7 @@ impl<'map> TaikoPP<'map> {
|
||||
|
||||
fn assert_hitresults(&'map self, attributes: TaikoDifficultyAttributes) -> TaikoPPInner<'map> {
|
||||
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) {
|
||||
(Some(n300), Some(n100)) => {
|
||||
|
||||
Reference in New Issue
Block a user