improved osu hitresult generation
This commit is contained in:
+27
-19
@@ -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(|| {
|
||||||
|
attributes
|
||||||
.n_droplets
|
.n_droplets
|
||||||
.unwrap_or_else(|| attributes.n_droplets.saturating_sub(self.n_misses));
|
.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| {
|
||||||
|
*f >= attributes
|
||||||
.n_fruits
|
.n_fruits
|
||||||
.filter(|f| *f >= attributes.n_fruits.saturating_sub(self.n_misses));
|
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||||
|
});
|
||||||
|
|
||||||
let correct_droplets = self
|
let correct_droplets = self.n_droplets.filter(|d| {
|
||||||
|
*d >= attributes
|
||||||
.n_droplets
|
.n_droplets
|
||||||
.filter(|d| *d >= attributes.n_droplets.saturating_sub(self.n_misses));
|
.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
@@ -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,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
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
+198
-237
@@ -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();
|
||||||
|
|
||||||
if let Some(acc) = self.acc {
|
let mut state = OsuScoreState {
|
||||||
let n300 = n300.unwrap_or(0);
|
max_combo: self.combo.unwrap_or(max_combo),
|
||||||
let n100 = n100.unwrap_or(0);
|
n300: self.n300.unwrap_or(0),
|
||||||
let n50 = n50.unwrap_or(0);
|
n100: self.n100.unwrap_or(0),
|
||||||
|
n50: self.n50.unwrap_or(0),
|
||||||
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f64;
|
n_misses: self.n_misses.unwrap_or(0),
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
} 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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
if let Some(acc) = self.acc {
|
||||||
|
// TODO: test
|
||||||
|
let target_total = (acc * (n_objects * 6) as f64).round() as usize;
|
||||||
|
|
||||||
let effective_misses =
|
let mut delta = target_total.saturating_sub(n_objects.saturating_sub(state.n_misses));
|
||||||
calculate_effective_misses(&attributes, self.combo, n100, n50, self.n_misses);
|
|
||||||
|
|
||||||
OsuPPInner {
|
if self.n50.is_some() {
|
||||||
mods: self.mods,
|
delta /= 2;
|
||||||
combo: self.combo.unwrap_or(attributes.max_combo),
|
}
|
||||||
acc,
|
|
||||||
n300,
|
if self.n100.is_some() {
|
||||||
n100,
|
delta /= 2;
|
||||||
n50,
|
}
|
||||||
n_misses: self.n_misses,
|
|
||||||
total_hits,
|
if let Some(n300) = self.n300 {
|
||||||
effective_miss_count: effective_misses,
|
delta = delta.saturating_sub(n300 * 6);
|
||||||
attributes,
|
} 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());
|
||||||
|
|
||||||
|
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.
|
/// 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,
|
||||||
|
|||||||
@@ -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
@@ -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)) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user