feat: avoid using missed slider estimates

This commit is contained in:
MaxOhn
2024-10-30 10:15:46 +01:00
parent 1ee664ae62
commit ad2609782b
+68 -32
View File
@@ -623,9 +623,42 @@ impl<'map> OsuPerformance<'map> {
MapOrAttrs::Attrs(attrs) => attrs, MapOrAttrs::Attrs(attrs) => attrs,
}; };
let effective_miss_count = calculate_effective_misses(&attrs, &state); let mods = self.difficulty.get_mods();
let lazer = self.difficulty.get_lazer(); let lazer = self.difficulty.get_lazer();
let using_classic_slider_acc = mods.no_slider_head_acc(lazer);
let mut effective_miss_count = 0.0;
if attrs.n_sliders > 0 {
if using_classic_slider_acc {
// * Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
// * In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
let full_combo_threshold =
f64::from(attrs.max_combo) - 0.1 * f64::from(attrs.n_sliders);
if f64::from(state.max_combo) < full_combo_threshold {
effective_miss_count =
full_combo_threshold / total_imperfect_hits(&state).max(1.0);
}
// * In classic scores there can't be more misses than a sum of all non-perfect judgements
effective_miss_count = effective_miss_count.min(total_imperfect_hits(&state));
} else {
let full_combo_threshold =
f64::from(attrs.max_combo - n_slider_ends_dropped(&attrs, &state));
if f64::from(state.max_combo) < full_combo_threshold {
effective_miss_count =
full_combo_threshold / total_imperfect_hits(&state).max(1.0);
}
// * Combine regular misses with tick misses since tick misses break combo as well
effective_miss_count = effective_miss_count
.min(f64::from(n_slider_tick_miss(&attrs, &state) + state.misses));
}
}
effective_miss_count = effective_miss_count.max(f64::from(state.misses));
let (n_slider_ends, n_slider_ticks) = if lazer { let (n_slider_ends, n_slider_ticks) = if lazer {
(attrs.n_sliders, attrs.n_slider_ticks) (attrs.n_sliders, attrs.n_slider_ticks)
@@ -635,11 +668,11 @@ impl<'map> OsuPerformance<'map> {
let inner = OsuPerformanceInner { let inner = OsuPerformanceInner {
attrs, attrs,
mods: self.difficulty.get_mods(), mods,
acc: state.accuracy(n_slider_ticks, n_slider_ends), acc: state.accuracy(n_slider_ticks, n_slider_ends),
state, state,
effective_miss_count, effective_miss_count,
lazer, using_classic_slider_acc,
}; };
inner.calculate() inner.calculate()
@@ -677,13 +710,11 @@ struct OsuPerformanceInner<'mods> {
acc: f64, acc: f64,
state: OsuScoreState, state: OsuScoreState,
effective_miss_count: f64, effective_miss_count: f64,
lazer: bool, using_classic_slider_acc: bool,
} }
impl OsuPerformanceInner<'_> { impl OsuPerformanceInner<'_> {
fn calculate(mut self) -> OsuPerformanceAttributes { fn calculate(mut self) -> OsuPerformanceAttributes {
let using_classic_slider_acc = self.mods.no_slider_head_acc(self.lazer);
let total_hits = self.state.total_hits(); let total_hits = self.state.total_hits();
if total_hits == 0 { if total_hits == 0 {
@@ -729,7 +760,7 @@ impl OsuPerformanceInner<'_> {
let aim_value = self.compute_aim_value(); let aim_value = self.compute_aim_value();
let speed_value = self.compute_speed_value(); let speed_value = self.compute_speed_value();
let acc_value = self.compute_accuracy_value(using_classic_slider_acc); let acc_value = self.compute_accuracy_value();
let flashlight_value = self.compute_flashlight_value(); let flashlight_value = self.compute_flashlight_value();
let pp = (aim_value.powf(1.1) let pp = (aim_value.powf(1.1)
@@ -796,15 +827,27 @@ impl OsuPerformanceInner<'_> {
let estimate_diff_sliders = f64::from(self.attrs.n_sliders) * 0.15; let estimate_diff_sliders = f64::from(self.attrs.n_sliders) * 0.15;
if self.attrs.n_sliders > 0 { if self.attrs.n_sliders > 0 {
let estimate_slider_ends_dropped = f64::from(cmp::min( let estimate_improperly_followed_difficult_sliders = if self.using_classic_slider_acc {
self.state.n100 + self.state.n50 + self.state.misses, // * When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
self.attrs.max_combo.saturating_sub(self.state.max_combo), let maximum_possible_droppled_sliders = total_imperfect_hits(&self.state);
))
.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).powf(3.0)
+ self.attrs.slider_factor;
maximum_possible_droppled_sliders
.min(f64::from(self.attrs.max_combo - self.state.max_combo))
.clamp(0.0, estimate_diff_sliders)
} else {
// * We add tick misses here since they too mean that the player didn't follow the slider properly
// * We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
(f64::from(
n_slider_ends_dropped(&self.attrs, &self.state)
+ n_slider_tick_miss(&self.attrs, &self.state),
))
.min(estimate_diff_sliders)
};
let slider_nerf_factor = (1.0 - self.attrs.slider_factor)
* (1.0 - estimate_improperly_followed_difficult_sliders / estimate_diff_sliders)
.powf(3.0)
+ self.attrs.slider_factor;
aim_value *= slider_nerf_factor; aim_value *= slider_nerf_factor;
} }
@@ -886,7 +929,7 @@ impl OsuPerformanceInner<'_> {
speed_value speed_value
} }
fn compute_accuracy_value(&self, using_classic_slider_acc: bool) -> f64 { fn compute_accuracy_value(&self) -> f64 {
if self.mods.rx() { if self.mods.rx() {
return 0.0; return 0.0;
} }
@@ -895,7 +938,7 @@ impl OsuPerformanceInner<'_> {
// * of the calculation we focus on hitting the timing hit window. // * of the calculation we focus on hitting the timing hit window.
let mut amount_hit_objects_with_acc = self.attrs.n_circles; let mut amount_hit_objects_with_acc = self.attrs.n_circles;
if !using_classic_slider_acc { if !self.using_classic_slider_acc {
amount_hit_objects_with_acc += self.attrs.n_sliders; amount_hit_objects_with_acc += self.attrs.n_sliders;
} }
@@ -992,23 +1035,16 @@ impl OsuPerformanceInner<'_> {
} }
} }
fn calculate_effective_misses(attrs: &OsuDifficultyAttributes, state: &OsuScoreState) -> f64 { fn total_imperfect_hits(state: &OsuScoreState) -> f64 {
// * Guess the number of misses + slider breaks from combo f64::from(state.n100 + state.n50 + state.misses)
let mut combo_based_miss_count = 0.0;
if attrs.n_sliders > 0 {
let full_combo_threshold = f64::from(attrs.max_combo) - 0.1 * f64::from(attrs.n_sliders);
if f64::from(state.max_combo) < full_combo_threshold {
combo_based_miss_count = full_combo_threshold / f64::from(state.max_combo).max(1.0);
}
} }
// * Clamp miss count to maximum amount of possible breaks fn n_slider_ends_dropped(attrs: &OsuDifficultyAttributes, state: &OsuScoreState) -> u32 {
combo_based_miss_count = attrs.n_sliders - state.slider_end_hits
combo_based_miss_count.min(f64::from(state.n100 + state.n50 + state.misses)); }
combo_based_miss_count.max(f64::from(state.misses)) fn n_slider_tick_miss(attrs: &OsuDifficultyAttributes, state: &OsuScoreState) -> u32 {
attrs.n_slider_ticks - state.slider_tick_hits
} }
struct NoComboState { struct NoComboState {