Port osu!std updates since f08134f
This commit is contained in:
@@ -299,6 +299,23 @@ impl<'map> Performance<'map> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the calculated attributes belong to an osu!lazer or osu!stable
|
||||||
|
/// score.
|
||||||
|
///
|
||||||
|
/// Defaults to lazer.
|
||||||
|
///
|
||||||
|
/// This affects internal accuracy calculation because lazer considers
|
||||||
|
/// slider heads for accuracy whereas stable does not.
|
||||||
|
///
|
||||||
|
/// Only relevant for osu!standard.
|
||||||
|
pub fn lazer(self, lazer: bool) -> Self {
|
||||||
|
if let Self::Osu(osu) = self {
|
||||||
|
Self::Osu(osu.lazer(lazer))
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Specify the amount of 300s of a play.
|
/// Specify the amount of 300s of a play.
|
||||||
pub fn n300(self, n300: u32) -> Self {
|
pub fn n300(self, n300: u32) -> Self {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -479,6 +479,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for CatchPerformance<'map> {
|
|||||||
n50,
|
n50,
|
||||||
misses,
|
misses,
|
||||||
hitresult_priority: _,
|
hitresult_priority: _,
|
||||||
|
lazer: _,
|
||||||
} = osu;
|
} = osu;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
@@ -832,6 +832,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for ManiaPerformance<'map> {
|
|||||||
n50,
|
n50,
|
||||||
misses,
|
misses,
|
||||||
hitresult_priority,
|
hitresult_priority,
|
||||||
|
lazer: _,
|
||||||
} = osu;
|
} = osu;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
@@ -182,6 +182,26 @@ impl_has_mod! {
|
|||||||
tc: - Traceable ["Traceable"],
|
tc: - Traceable ["Traceable"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GameMods {
|
||||||
|
pub fn no_slider_head_acc(&self, lazer: bool) -> bool {
|
||||||
|
match self.inner {
|
||||||
|
GameModsInner::Lazer(ref mods) => mods
|
||||||
|
.iter()
|
||||||
|
.find_map(|m| match m {
|
||||||
|
GameMod::ClassicOsu(classic) => Some(classic),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map_or(!lazer, |classic| {
|
||||||
|
classic.no_slider_head_accuracy.unwrap_or(true)
|
||||||
|
}),
|
||||||
|
GameModsInner::Intermode(ref mods) => {
|
||||||
|
mods.contains(GameModIntermode::Classic) || !lazer
|
||||||
|
}
|
||||||
|
GameModsInner::Legacy(_) => !lazer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for GameMods {
|
impl Default for GameMods {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::DEFAULT
|
Self::DEFAULT
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ pub struct OsuDifficultyAttributes {
|
|||||||
pub slider_factor: f64,
|
pub slider_factor: f64,
|
||||||
/// The number of clickable objects weighted by difficulty.
|
/// The number of clickable objects weighted by difficulty.
|
||||||
pub speed_note_count: f64,
|
pub speed_note_count: f64,
|
||||||
|
/// Weighted sum of aim strains.
|
||||||
|
pub aim_difficult_strain_count: f64,
|
||||||
|
/// Weighted sum of speed strains.
|
||||||
|
pub speed_difficult_strain_count: f64,
|
||||||
/// The approach rate.
|
/// The approach rate.
|
||||||
pub ar: f64,
|
pub ar: f64,
|
||||||
/// The overall difficulty
|
/// The overall difficulty
|
||||||
|
|||||||
+10
-3
@@ -67,7 +67,7 @@ pub fn convert_objects(
|
|||||||
.iter_mut()
|
.iter_mut()
|
||||||
.for_each(OsuObject::reflect_vertically);
|
.for_each(OsuObject::reflect_vertically);
|
||||||
} else {
|
} else {
|
||||||
osu_objects.iter_mut().for_each(OsuObject::finalize_tail);
|
osu_objects.iter_mut().for_each(OsuObject::finalize_nested);
|
||||||
}
|
}
|
||||||
|
|
||||||
let stack_threshold = time_preempt * f64::from(converted.stack_leniency);
|
let stack_threshold = time_preempt * f64::from(converted.stack_leniency);
|
||||||
@@ -246,13 +246,20 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// * Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`.
|
||||||
|
// * This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
|
||||||
|
// * and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`.
|
||||||
|
// *
|
||||||
|
// * Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
||||||
|
// * if we use `EndTime` here it would result in unexpected stacking.
|
||||||
|
|
||||||
if hit_objects[j].pos.distance(hit_objects[i].pos) < STACK_DISTANCE {
|
if hit_objects[j].pos.distance(hit_objects[i].pos) < STACK_DISTANCE {
|
||||||
hit_objects[i].stack_height += 1;
|
hit_objects[i].stack_height += 1;
|
||||||
start_time = hit_objects[j].end_time();
|
start_time = hit_objects[j].start_time;
|
||||||
} else if hit_objects[j].pos.distance(pos2) < STACK_DISTANCE {
|
} else if hit_objects[j].pos.distance(pos2) < STACK_DISTANCE {
|
||||||
slider_stack += 1;
|
slider_stack += 1;
|
||||||
hit_objects[j].stack_height -= slider_stack;
|
hit_objects[j].stack_height -= slider_stack;
|
||||||
start_time = hit_objects[j].end_time();
|
start_time = hit_objects[j].start_time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use std::{cmp, pin::Pin};
|
use std::{cmp, pin::Pin};
|
||||||
|
|
||||||
use skills::{flashlight::Flashlight, strain::OsuStrainSkill};
|
use skills::{
|
||||||
|
flashlight::Flashlight,
|
||||||
|
strain::{DifficultyValue, OsuStrainSkill, UsedOsuStrainSkills},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::{skills::Skill, Difficulty},
|
any::difficulty::{skills::Skill, Difficulty},
|
||||||
@@ -148,15 +151,16 @@ impl DifficultyValues {
|
|||||||
pub fn eval(
|
pub fn eval(
|
||||||
attrs: &mut OsuDifficultyAttributes,
|
attrs: &mut OsuDifficultyAttributes,
|
||||||
mods: &GameMods,
|
mods: &GameMods,
|
||||||
aim_difficulty_value: f64,
|
aim: UsedOsuStrainSkills<DifficultyValue>,
|
||||||
aim_no_sliders_difficulty_value: f64,
|
aim_no_sliders: UsedOsuStrainSkills<DifficultyValue>,
|
||||||
speed_difficulty_value: f64,
|
speed: UsedOsuStrainSkills<DifficultyValue>,
|
||||||
speed_relevant_note_count: f64,
|
speed_relevant_note_count: f64,
|
||||||
flashlight_difficulty_value: f64,
|
flashlight_difficulty_value: f64,
|
||||||
) {
|
) {
|
||||||
let mut aim_rating = aim_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
let mut aim_rating = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
let aim_rating_no_sliders = aim_no_sliders_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
let aim_rating_no_sliders =
|
||||||
let mut speed_rating = speed_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
aim_no_sliders.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
|
let mut speed_rating = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
let mut flashlight_rating = flashlight_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
let mut flashlight_rating = flashlight_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
|
|
||||||
let slider_factor = if aim_rating > 0.0 {
|
let slider_factor = if aim_rating > 0.0 {
|
||||||
@@ -165,6 +169,9 @@ impl DifficultyValues {
|
|||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let aim_difficult_strain_count = aim.count_difficult_strains();
|
||||||
|
let speed_difficult_strain_count = speed.count_difficult_strains();
|
||||||
|
|
||||||
if mods.td() {
|
if mods.td() {
|
||||||
aim_rating = aim_rating.powf(0.8);
|
aim_rating = aim_rating.powf(0.8);
|
||||||
flashlight_rating = flashlight_rating.powf(0.8);
|
flashlight_rating = flashlight_rating.powf(0.8);
|
||||||
@@ -207,6 +214,8 @@ impl DifficultyValues {
|
|||||||
attrs.speed = speed_rating;
|
attrs.speed = speed_rating;
|
||||||
attrs.flashlight = flashlight_rating;
|
attrs.flashlight = flashlight_rating;
|
||||||
attrs.slider_factor = slider_factor;
|
attrs.slider_factor = slider_factor;
|
||||||
|
attrs.aim_difficult_strain_count = aim_difficult_strain_count;
|
||||||
|
attrs.speed_difficult_strain_count = speed_difficult_strain_count;
|
||||||
attrs.stars = star_rating;
|
attrs.stars = star_rating;
|
||||||
attrs.speed_note_count = speed_relevant_note_count;
|
attrs.speed_note_count = speed_relevant_note_count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub struct OsuDifficultyObject<'a> {
|
|||||||
impl<'a> OsuDifficultyObject<'a> {
|
impl<'a> OsuDifficultyObject<'a> {
|
||||||
pub const NORMALIZED_RADIUS: f32 = 50.0;
|
pub const NORMALIZED_RADIUS: f32 = 50.0;
|
||||||
|
|
||||||
const MIN_DELTA_TIME: f64 = 25.0;
|
pub const MIN_DELTA_TIME: f64 = 25.0;
|
||||||
const MAX_SLIDER_RADIUS: f32 = Self::NORMALIZED_RADIUS * 2.4;
|
const MAX_SLIDER_RADIUS: f32 = Self::NORMALIZED_RADIUS * 2.4;
|
||||||
const ASSUMED_SLIDER_RADIUS: f32 = Self::NORMALIZED_RADIUS * 1.8;
|
const ASSUMED_SLIDER_RADIUS: f32 = Self::NORMALIZED_RADIUS * 1.8;
|
||||||
|
|
||||||
@@ -86,6 +86,24 @@ impl<'a> OsuDifficultyObject<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_doubletapness(&self, next: Option<&Self>, hit_window: f64) -> f64 {
|
||||||
|
let Some(next) = next else { return 0.0 };
|
||||||
|
|
||||||
|
let hit_window = if self.base.is_spinner() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
hit_window
|
||||||
|
};
|
||||||
|
|
||||||
|
let curr_delta_time = self.delta_time.max(1.0);
|
||||||
|
let next_delta_time = next.delta_time.max(1.0);
|
||||||
|
let delta_diff = (next_delta_time - curr_delta_time).abs();
|
||||||
|
let speed_ratio = curr_delta_time / curr_delta_time.max(delta_diff);
|
||||||
|
let window_ratio = (curr_delta_time / hit_window).min(1.0).powf(2.0);
|
||||||
|
|
||||||
|
1.0 - (speed_ratio).powf(1.0 - window_ratio)
|
||||||
|
}
|
||||||
|
|
||||||
fn set_distances(
|
fn set_distances(
|
||||||
&mut self,
|
&mut self,
|
||||||
last_object: &OsuObject,
|
last_object: &OsuObject,
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ pub struct ScalingFactor {
|
|||||||
|
|
||||||
impl ScalingFactor {
|
impl ScalingFactor {
|
||||||
pub fn new(cs: f64) -> Self {
|
pub fn new(cs: f64) -> Self {
|
||||||
let scale =
|
let scale = (f64::from(1.0_f32) - f64::from(0.7_f32) * ((cs - 5.0) / 5.0)) as f32 / 2.0
|
||||||
(1.0 - 0.7 * ((cs - 5.0) / 5.0)) as f32 / 2.0 * BROKEN_GAMEFIELD_ROUNDING_ALLOWANCE;
|
* BROKEN_GAMEFIELD_ROUNDING_ALLOWANCE;
|
||||||
|
|
||||||
let radius = f64::from(OsuObject::OBJECT_RADIUS * scale);
|
let radius = f64::from(OsuObject::OBJECT_RADIUS * scale);
|
||||||
let factor = OsuDifficultyObject::NORMALIZED_RADIUS / radius as f32;
|
let factor = OsuDifficultyObject::NORMALIZED_RADIUS / radius as f32;
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ use crate::{
|
|||||||
util::{float_ext::FloatExt, strains_vec::StrainsVec},
|
util::{float_ext::FloatExt, strains_vec::StrainsVec},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::strain::OsuStrainSkill;
|
use super::strain::{DifficultyValue, OsuStrainSkill, UsedOsuStrainSkills};
|
||||||
|
|
||||||
const SKILL_MULTIPLIER: f64 = 24.963;
|
const SKILL_MULTIPLIER: f64 = 25.18;
|
||||||
const STRAIN_DECAY_BASE: f64 = 0.15;
|
const STRAIN_DECAY_BASE: f64 = 0.15;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -31,20 +31,20 @@ impl Aim {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks().strains()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn difficulty_value(self) -> f64 {
|
pub fn difficulty_value(self) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||||
Self::static_difficulty_value(self.inner)
|
Self::static_difficulty_value(self.inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use [`difficulty_value`] instead whenever possible because
|
/// Use [`difficulty_value`] instead whenever possible because
|
||||||
/// [`as_difficulty_value`] clones internally.
|
/// [`as_difficulty_value`] clones internally.
|
||||||
pub fn as_difficulty_value(&self) -> f64 {
|
pub fn as_difficulty_value(&self) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||||
Self::static_difficulty_value(self.inner.clone())
|
Self::static_difficulty_value(self.inner.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn static_difficulty_value(skill: OsuStrainSkill) -> f64 {
|
fn static_difficulty_value(skill: OsuStrainSkill) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||||
skill.difficulty_value(
|
skill.difficulty_value(
|
||||||
OsuStrainSkill::REDUCED_SECTION_COUNT,
|
OsuStrainSkill::REDUCED_SECTION_COUNT,
|
||||||
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
|
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
|
||||||
@@ -104,6 +104,7 @@ impl<'a> Skill<'a, Aim> {
|
|||||||
self.inner.curr_strain +=
|
self.inner.curr_strain +=
|
||||||
AimEvaluator::evaluate_diff_of(curr, self.diff_objects, self.inner.with_sliders)
|
AimEvaluator::evaluate_diff_of(curr, self.diff_objects, self.inner.with_sliders)
|
||||||
* SKILL_MULTIPLIER;
|
* SKILL_MULTIPLIER;
|
||||||
|
self.inner.inner.object_strains.push(self.inner.curr_strain);
|
||||||
|
|
||||||
self.inner.curr_strain
|
self.inner.curr_strain
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use std::{cmp, f64::consts::PI};
|
use std::{
|
||||||
|
cmp,
|
||||||
|
f64::consts::{E, PI},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::{
|
any::difficulty::{
|
||||||
@@ -10,7 +13,7 @@ use crate::{
|
|||||||
GameMods,
|
GameMods,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::strain::OsuStrainSkill;
|
use super::strain::{DifficultyValue, OsuStrainSkill, UsedOsuStrainSkills};
|
||||||
|
|
||||||
const SKILL_MULTIPLIER: f64 = 1.430;
|
const SKILL_MULTIPLIER: f64 = 1.430;
|
||||||
const STRAIN_DECAY_BASE: f64 = 0.3;
|
const STRAIN_DECAY_BASE: f64 = 0.3;
|
||||||
@@ -21,7 +24,6 @@ const REDUCED_SECTION_COUNT: usize = 5;
|
|||||||
pub struct Speed {
|
pub struct Speed {
|
||||||
curr_strain: f64,
|
curr_strain: f64,
|
||||||
curr_rhythm: f64,
|
curr_rhythm: f64,
|
||||||
object_strains: Vec<f64>,
|
|
||||||
hit_window: f64,
|
hit_window: f64,
|
||||||
has_autopilot_mod: bool,
|
has_autopilot_mod: bool,
|
||||||
inner: OsuStrainSkill,
|
inner: OsuStrainSkill,
|
||||||
@@ -32,8 +34,6 @@ impl Speed {
|
|||||||
Self {
|
Self {
|
||||||
curr_strain: 0.0,
|
curr_strain: 0.0,
|
||||||
curr_rhythm: 0.0,
|
curr_rhythm: 0.0,
|
||||||
// mean=406.72 | median=307
|
|
||||||
object_strains: Vec::with_capacity(256),
|
|
||||||
hit_window,
|
hit_window,
|
||||||
has_autopilot_mod: mods.ap(),
|
has_autopilot_mod: mods.ap(),
|
||||||
inner: OsuStrainSkill::default(),
|
inner: OsuStrainSkill::default(),
|
||||||
@@ -41,20 +41,20 @@ impl Speed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks().strains()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn difficulty_value(self) -> f64 {
|
pub fn difficulty_value(self) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||||
Self::static_difficulty_value(self.inner)
|
Self::static_difficulty_value(self.inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use [`difficulty_value`] instead whenever possible because
|
/// Use [`difficulty_value`] instead whenever possible because
|
||||||
/// [`as_difficulty_value`] clones internally.
|
/// [`as_difficulty_value`] clones internally.
|
||||||
pub fn as_difficulty_value(&self) -> f64 {
|
pub fn as_difficulty_value(&self) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||||
Self::static_difficulty_value(self.inner.clone())
|
Self::static_difficulty_value(self.inner.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn static_difficulty_value(skill: OsuStrainSkill) -> f64 {
|
fn static_difficulty_value(skill: OsuStrainSkill) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||||
skill.difficulty_value(
|
skill.difficulty_value(
|
||||||
REDUCED_SECTION_COUNT,
|
REDUCED_SECTION_COUNT,
|
||||||
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
|
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
|
||||||
@@ -63,13 +63,14 @@ impl Speed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn relevant_note_count(&self) -> f64 {
|
pub fn relevant_note_count(&self) -> f64 {
|
||||||
self.object_strains
|
self.inner
|
||||||
|
.object_strains
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.max_by(f64::total_cmp)
|
.max_by(f64::total_cmp)
|
||||||
.filter(|&n| n > 0.0)
|
.filter(|&n| n > 0.0)
|
||||||
.map_or(0.0, |max_strain| {
|
.map_or(0.0, |max_strain| {
|
||||||
self.object_strains.iter().fold(0.0, |sum, strain| {
|
self.inner.object_strains.iter().fold(0.0, |sum, strain| {
|
||||||
sum + (1.0 + (-(strain / max_strain * 12.0 - 6.0)).exp()).recip()
|
sum + (1.0 + (-(strain / max_strain * 12.0 - 6.0)).exp()).recip()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -135,7 +136,7 @@ impl<'a> Skill<'a, Speed> {
|
|||||||
RhythmEvaluator::evaluate_diff_of(curr, self.diff_objects, self.inner.hit_window);
|
RhythmEvaluator::evaluate_diff_of(curr, self.diff_objects, self.inner.hit_window);
|
||||||
|
|
||||||
let total_strain = self.inner.curr_strain * self.inner.curr_rhythm;
|
let total_strain = self.inner.curr_strain * self.inner.curr_rhythm;
|
||||||
self.inner.object_strains.push(total_strain);
|
self.inner.inner.object_strains.push(total_strain);
|
||||||
|
|
||||||
total_strain
|
total_strain
|
||||||
}
|
}
|
||||||
@@ -146,7 +147,8 @@ struct SpeedEvaluator;
|
|||||||
impl SpeedEvaluator {
|
impl SpeedEvaluator {
|
||||||
const SINGLE_SPACING_THRESHOLD: f64 = 125.0; // 1.25 circlers distance between centers
|
const SINGLE_SPACING_THRESHOLD: f64 = 125.0; // 1.25 circlers distance between centers
|
||||||
const MIN_SPEED_BONUS: f64 = 75.0; // ~200BPM
|
const MIN_SPEED_BONUS: f64 = 75.0; // ~200BPM
|
||||||
const SPEED_BALANCING_FACTOR: f64 = 40.;
|
const SPEED_BALANCING_FACTOR: f64 = 40.0;
|
||||||
|
const DIST_MULTIPLIER: f64 = 0.94;
|
||||||
|
|
||||||
fn evaluate_diff_of<'a>(
|
fn evaluate_diff_of<'a>(
|
||||||
curr: &'a OsuDifficultyObject<'a>,
|
curr: &'a OsuDifficultyObject<'a>,
|
||||||
@@ -164,17 +166,10 @@ impl SpeedEvaluator {
|
|||||||
let osu_next_obj = curr.next(0, diff_objects);
|
let osu_next_obj = curr.next(0, diff_objects);
|
||||||
|
|
||||||
let mut strain_time = curr.strain_time;
|
let mut strain_time = curr.strain_time;
|
||||||
let mut doubletapness = 1.0;
|
// Note: Technically `osu_next_obj` is never `None` but instead the
|
||||||
|
// default value. This could maybe invalidate the `get_doubletapness`
|
||||||
// * Nerf doubletappable doubles.
|
// result.
|
||||||
if let Some(osu_next_obj) = osu_next_obj {
|
let doubletapness = 1.0 - osu_curr_obj.get_doubletapness(osu_next_obj, hit_window);
|
||||||
let curr_delta_time = osu_curr_obj.delta_time.max(1.0);
|
|
||||||
let next_delta_time = osu_next_obj.delta_time.max(1.0);
|
|
||||||
let delta_diff = (next_delta_time - curr_delta_time).abs();
|
|
||||||
let speed_ratio = curr_delta_time / curr_delta_time.max(delta_diff);
|
|
||||||
let window_ratio = (curr_delta_time / hit_window).min(1.0).powf(2.0);
|
|
||||||
doubletapness = speed_ratio.powf(1.0 - window_ratio);
|
|
||||||
}
|
|
||||||
|
|
||||||
// * Cap deltatime to the OD 300 hitwindow.
|
// * Cap deltatime to the OD 300 hitwindow.
|
||||||
// * 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
// * 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||||
@@ -184,10 +179,10 @@ impl SpeedEvaluator {
|
|||||||
// * Add additional scaling bonus for streams/bursts higher than 200bpm
|
// * Add additional scaling bonus for streams/bursts higher than 200bpm
|
||||||
let base = (Self::MIN_SPEED_BONUS - strain_time) / Self::SPEED_BALANCING_FACTOR;
|
let base = (Self::MIN_SPEED_BONUS - strain_time) / Self::SPEED_BALANCING_FACTOR;
|
||||||
|
|
||||||
1.0 + 0.75 * base.powf(2.0)
|
0.75 * base.powf(2.0)
|
||||||
} else {
|
} else {
|
||||||
// * speedBonus will be 1.0 for BPM < 200
|
// * speedBonus will be 0.0 for BPM < 200
|
||||||
1.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let travel_dist = osu_prev_obj.map_or(0.0, |obj| obj.travel_dist);
|
let travel_dist = osu_prev_obj.map_or(0.0, |obj| obj.travel_dist);
|
||||||
@@ -196,15 +191,15 @@ impl SpeedEvaluator {
|
|||||||
// * Cap distance at single_spacing_threshold
|
// * Cap distance at single_spacing_threshold
|
||||||
dist = Self::SINGLE_SPACING_THRESHOLD.min(dist);
|
dist = Self::SINGLE_SPACING_THRESHOLD.min(dist);
|
||||||
|
|
||||||
// * Max distance bonus is 2 at single_spacing_threshold
|
// * Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||||
let dist_bonus = if has_autopilot_mod {
|
let dist_bonus = if has_autopilot_mod {
|
||||||
1.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
1.0 + (dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.5)
|
(dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.95) * Self::DIST_MULTIPLIER
|
||||||
};
|
};
|
||||||
|
|
||||||
// * Base difficulty with all bonuses
|
// * Base difficulty with all bonuses
|
||||||
let difficulty = speed_bonus * dist_bonus * 1000.0 / strain_time;
|
let difficulty = (1.0 + speed_bonus + dist_bonus) * 1000.0 / strain_time;
|
||||||
|
|
||||||
// * Apply penalty if there's doubletappable doubles
|
// * Apply penalty if there's doubletappable doubles
|
||||||
difficulty * doubletapness
|
difficulty * doubletapness
|
||||||
@@ -214,9 +209,10 @@ impl SpeedEvaluator {
|
|||||||
struct RhythmEvaluator;
|
struct RhythmEvaluator;
|
||||||
|
|
||||||
impl RhythmEvaluator {
|
impl RhythmEvaluator {
|
||||||
// * 5 seconds of calculatingRhythmBonus max.
|
const HISTORY_TIME_MAX: u32 = 5 * 1000; // 5 seconds
|
||||||
const HISTORY_TIME_MAX: u32 = 5000;
|
const HISTORY_OBJECTS_MAX: usize = 32;
|
||||||
const RHYTHM_MULTIPLIER: f64 = 0.75;
|
const RHYTHM_OVERALL_MULTIPLIER: f64 = 0.95;
|
||||||
|
const RHYTHM_RATIO_MULTIPLIER: f64 = 12.0;
|
||||||
|
|
||||||
fn evaluate_diff_of<'a>(
|
fn evaluate_diff_of<'a>(
|
||||||
curr: &'a OsuDifficultyObject<'a>,
|
curr: &'a OsuDifficultyObject<'a>,
|
||||||
@@ -227,16 +223,23 @@ impl RhythmEvaluator {
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut prev_island_size = 0;
|
|
||||||
|
|
||||||
let mut rhythm_complexity_sum = 0.0;
|
let mut rhythm_complexity_sum = 0.0;
|
||||||
let mut island_size = 1;
|
|
||||||
|
let delta_difference_eps = hit_window * 0.3;
|
||||||
|
|
||||||
|
let mut island = RhythmIsland::new(delta_difference_eps);
|
||||||
|
let mut prev_island = RhythmIsland::new(delta_difference_eps);
|
||||||
|
|
||||||
|
// * we can't use dictionary here because we need to compare island with a tolerance
|
||||||
|
// * which is impossible to pass into the hash comparer
|
||||||
|
let mut island_counts = Vec::<IslandCount>::new();
|
||||||
|
|
||||||
// * store the ratio of the current start of an island to buff for tighter rhythms
|
// * store the ratio of the current start of an island to buff for tighter rhythms
|
||||||
let mut start_ratio = 0.0;
|
let mut start_ratio = 0.0;
|
||||||
|
|
||||||
let mut first_delta_switch = false;
|
let mut first_delta_switch = false;
|
||||||
|
|
||||||
let historical_note_count = cmp::min(curr.idx, 32);
|
let historical_note_count = cmp::min(curr.idx, Self::HISTORY_OBJECTS_MAX);
|
||||||
|
|
||||||
let mut rhythm_start = 0;
|
let mut rhythm_start = 0;
|
||||||
|
|
||||||
@@ -255,47 +258,50 @@ impl RhythmEvaluator {
|
|||||||
.previous(rhythm_start, diff_objects)
|
.previous(rhythm_start, diff_objects)
|
||||||
.zip(curr.previous(rhythm_start + 1, diff_objects))
|
.zip(curr.previous(rhythm_start + 1, diff_objects))
|
||||||
{
|
{
|
||||||
|
// * we go from the furthest object back to the current one
|
||||||
for i in (1..=rhythm_start).rev() {
|
for i in (1..=rhythm_start).rev() {
|
||||||
let Some(curr_obj) = curr.previous(i - 1, diff_objects) else {
|
let Some(curr_obj) = curr.previous(i - 1, diff_objects) else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
// * scales note 0 to 1 from history to now
|
// * scales note 0 to 1 from history to now
|
||||||
let mut curr_historical_decay = (f64::from(Self::HISTORY_TIME_MAX)
|
let time_decay = (f64::from(Self::HISTORY_TIME_MAX)
|
||||||
- (curr.start_time - curr_obj.start_time))
|
- (curr.start_time - curr_obj.start_time))
|
||||||
/ f64::from(Self::HISTORY_TIME_MAX);
|
/ f64::from(Self::HISTORY_TIME_MAX);
|
||||||
|
let note_decay = (historical_note_count - i) as f64 / historical_note_count as f64;
|
||||||
|
|
||||||
// * either we're limited by time or limited by object count.
|
// * either we're limited by time or limited by object count.
|
||||||
curr_historical_decay = curr_historical_decay
|
let curr_historical_decay = note_decay.min(time_decay);
|
||||||
.min((historical_note_count - i) as f64 / historical_note_count as f64);
|
|
||||||
|
|
||||||
let curr_delta = curr_obj.strain_time;
|
let curr_delta = curr_obj.strain_time;
|
||||||
let prev_delta = prev_obj.strain_time;
|
let prev_delta = prev_obj.strain_time;
|
||||||
let last_delta = last_obj.strain_time;
|
let last_delta = last_obj.strain_time;
|
||||||
|
|
||||||
// * fancy function to calculate rhythmbonuses.
|
// * calculate how much current delta difference deserves a rhythm bonus
|
||||||
let base = (PI / (prev_delta.min(curr_delta) / prev_delta.max(curr_delta))).sin();
|
// * this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
||||||
let curr_ratio = 1.0 + 6.0 * base.powf(2.0).min(0.5);
|
let delta_difference_ratio =
|
||||||
|
prev_delta.min(curr_delta) / prev_delta.max(curr_delta);
|
||||||
|
let curr_ratio = 1.0
|
||||||
|
+ Self::RHYTHM_RATIO_MULTIPLIER
|
||||||
|
* (PI / delta_difference_ratio).sin().powf(2.0).min(0.5);
|
||||||
|
|
||||||
let hit_window = u64::from(!curr_obj.base.is_spinner()) as f64 * hit_window;
|
// reduce ratio bonus if delta difference is too big
|
||||||
|
let fraction = (prev_delta / curr_delta).max(curr_delta / prev_delta);
|
||||||
|
let fraction_multiplier = (2.0 - fraction / 8.0).clamp(0.0, 1.0);
|
||||||
|
|
||||||
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
|
let window_penalty = (((prev_delta - curr_delta).abs() - delta_difference_eps)
|
||||||
.max(0.0))
|
.max(0.0)
|
||||||
/ (hit_window * 0.3))
|
/ delta_difference_eps)
|
||||||
.min(1.0);
|
.min(1.0);
|
||||||
|
|
||||||
window_penalty = window_penalty.min(1.0);
|
let mut effective_ratio = window_penalty * curr_ratio * fraction_multiplier;
|
||||||
|
|
||||||
let mut effective_ratio = window_penalty * curr_ratio;
|
|
||||||
|
|
||||||
if first_delta_switch {
|
if first_delta_switch {
|
||||||
// Keep in-sync with lazer
|
// Keep in-sync with lazer
|
||||||
#[allow(clippy::if_not_else)]
|
#[allow(clippy::if_not_else)]
|
||||||
if !(prev_delta > 1.25 * curr_delta || prev_delta * 1.25 < curr_delta) {
|
if (prev_delta - curr_delta).abs() < delta_difference_eps {
|
||||||
if island_size < 7 {
|
// * island is still progressing
|
||||||
// * island is still progressing, count size.
|
island.add_delta(curr_delta as i32);
|
||||||
island_size += 1;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// * bpm change is into slider, this is easy acc window
|
// * bpm change is into slider, this is easy acc window
|
||||||
if curr_obj.base.is_slider() {
|
if curr_obj.base.is_slider() {
|
||||||
@@ -303,51 +309,86 @@ impl RhythmEvaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// * bpm change was from a slider, this is easier typically than circle -> circle
|
// * bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
|
// * unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||||
if prev_obj.base.is_slider() {
|
if prev_obj.base.is_slider() {
|
||||||
effective_ratio *= 0.25;
|
effective_ratio *= 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// * repeated island size (ex: triplet -> triplet)
|
// * repeated island polarity (2 -> 4, 3 -> 5)
|
||||||
if prev_island_size == island_size {
|
if island.is_similar_polarity(&prev_island) {
|
||||||
effective_ratio *= 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
// * repeated island polartiy (2 -> 4, 3 -> 5)
|
|
||||||
if prev_island_size % 2 == island_size % 2 {
|
|
||||||
effective_ratio *= 0.5;
|
effective_ratio *= 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// * previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
// * previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||||
if last_delta > prev_delta + 10.0 && prev_delta > curr_delta + 10.0 {
|
if last_delta > prev_delta + delta_difference_eps
|
||||||
|
&& prev_delta > curr_delta + delta_difference_eps
|
||||||
|
{
|
||||||
effective_ratio *= 0.125;
|
effective_ratio *= 0.125;
|
||||||
}
|
}
|
||||||
|
|
||||||
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
|
// * repeated island size (ex: triplet -> triplet)
|
||||||
* curr_historical_decay
|
// * TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
|
||||||
* f64::from(4 + island_size).sqrt()
|
if prev_island.delta_count == island.delta_count {
|
||||||
/ 2.0
|
effective_ratio *= 0.5;
|
||||||
* f64::from(4 + prev_island_size).sqrt()
|
}
|
||||||
/ 2.0;
|
|
||||||
|
if let Some(island_count) = island_counts
|
||||||
|
.iter_mut()
|
||||||
|
.find(|entry| entry.island == island)
|
||||||
|
.filter(|entry| !entry.island.is_default())
|
||||||
|
{
|
||||||
|
// * only add island to island counts if they're going one after another
|
||||||
|
if prev_island == island {
|
||||||
|
island_count.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * repeated island (ex: triplet -> triplet)
|
||||||
|
let power = logistic(f64::from(island.delta), 2.75, 0.24, 14.0);
|
||||||
|
effective_ratio *= (3.0 / island_count.count as f64)
|
||||||
|
.min((island_count.count as f64).recip().powf(power));
|
||||||
|
} else {
|
||||||
|
island_counts.push(IslandCount { island, count: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// * scale down the difficulty if the object is doubletappable
|
||||||
|
let doubletapness = prev_obj.get_doubletapness(Some(curr_obj), hit_window);
|
||||||
|
effective_ratio *= 1.0 - doubletapness * 0.75;
|
||||||
|
|
||||||
|
rhythm_complexity_sum +=
|
||||||
|
(effective_ratio * start_ratio).sqrt() * curr_historical_decay;
|
||||||
|
|
||||||
start_ratio = effective_ratio;
|
start_ratio = effective_ratio;
|
||||||
|
|
||||||
// * log the last island size.
|
prev_island = island;
|
||||||
prev_island_size = island_size;
|
|
||||||
|
|
||||||
// * we're slowing down, stop counting
|
// * we're slowing down, stop counting
|
||||||
if prev_delta * 1.25 < curr_delta {
|
if prev_delta + delta_difference_eps < curr_delta {
|
||||||
// * if we're speeding up, this stays true and we keep counting island size.
|
// * if we're speeding up, this stays true and we keep counting island size.
|
||||||
first_delta_switch = false;
|
first_delta_switch = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
island_size = 1;
|
island =
|
||||||
|
RhythmIsland::new_with_delta(curr_delta as i32, delta_difference_eps);
|
||||||
}
|
}
|
||||||
} else if prev_delta > 1.25 * curr_delta {
|
} else if prev_delta > curr_delta + delta_difference_eps {
|
||||||
// * we want to be speeding up.
|
// * we're speeding up.
|
||||||
// * Begin counting island until we change speed again.
|
// * Begin counting island until we change speed again.
|
||||||
first_delta_switch = true;
|
first_delta_switch = true;
|
||||||
|
|
||||||
|
// * bpm change is into slider, this is easy acc window
|
||||||
|
if curr_obj.base.is_slider() {
|
||||||
|
effective_ratio *= 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
|
// * unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||||
|
if prev_obj.base.is_slider() {
|
||||||
|
effective_ratio *= 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
start_ratio = effective_ratio;
|
start_ratio = effective_ratio;
|
||||||
island_size = 1;
|
|
||||||
|
island = RhythmIsland::new_with_delta(curr_delta as i32, delta_difference_eps);
|
||||||
}
|
}
|
||||||
|
|
||||||
last_obj = prev_obj;
|
last_obj = prev_obj;
|
||||||
@@ -356,6 +397,74 @@ impl RhythmEvaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// * produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
// * produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
(4.0 + rhythm_complexity_sum * Self::RHYTHM_MULTIPLIER).sqrt() / 2.0
|
(4.0 + rhythm_complexity_sum * Self::RHYTHM_OVERALL_MULTIPLIER).sqrt() / 2.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn logistic(x: f64, max_value: f64, multiplier: f64, offset: f64) -> f64 {
|
||||||
|
max_value / (1.0 + E.powf(offset - (multiplier * x)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
struct RhythmIsland {
|
||||||
|
delta_difference_eps: f64,
|
||||||
|
delta: i32,
|
||||||
|
delta_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_DELTA_TIME: i32 = 25;
|
||||||
|
|
||||||
|
// Compile-time check in case `OsuDifficultyObject::MIN_DELTA_TIME` changes
|
||||||
|
// but we forget to update this value.
|
||||||
|
const _: [(); 0 - !{ MIN_DELTA_TIME - OsuDifficultyObject::MIN_DELTA_TIME as i32 == 0 } as usize] =
|
||||||
|
[];
|
||||||
|
|
||||||
|
impl RhythmIsland {
|
||||||
|
fn new(delta_difference_eps: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
delta_difference_eps,
|
||||||
|
delta: 0,
|
||||||
|
delta_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_with_delta(delta: i32, delta_difference_eps: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
delta_difference_eps,
|
||||||
|
delta: delta.max(MIN_DELTA_TIME),
|
||||||
|
delta_count: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_delta(&mut self, delta: i32) {
|
||||||
|
if self.delta == i32::MAX {
|
||||||
|
self.delta = delta.max(MIN_DELTA_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.delta_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_similar_polarity(&self, other: &Self) -> bool {
|
||||||
|
// * TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
|
||||||
|
// * naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
||||||
|
self.delta_count % 2 == other.delta_count % 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_default(&self) -> bool {
|
||||||
|
self.delta_difference_eps.abs() < f64::EPSILON
|
||||||
|
&& self.delta == i32::MAX
|
||||||
|
&& self.delta_count == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for RhythmIsland {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
f64::from((self.delta - other.delta).abs()) < self.delta_difference_eps
|
||||||
|
&& self.delta_count == other.delta_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IslandCount {
|
||||||
|
island: RhythmIsland,
|
||||||
|
count: usize,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
use crate::{any::difficulty::skills::StrainSkill, util::strains_vec::StrainsVec};
|
use crate::{any::difficulty::skills::StrainSkill, util::strains_vec::StrainsVec};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone)]
|
||||||
pub struct OsuStrainSkill {
|
pub struct OsuStrainSkill {
|
||||||
|
pub object_strains: Vec<f64>,
|
||||||
pub inner: StrainSkill,
|
pub inner: StrainSkill,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for OsuStrainSkill {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
// mean=406.72 | median=307
|
||||||
|
object_strains: Vec::with_capacity(256),
|
||||||
|
inner: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl OsuStrainSkill {
|
impl OsuStrainSkill {
|
||||||
pub const REDUCED_SECTION_COUNT: usize = 10;
|
pub const REDUCED_SECTION_COUNT: usize = 10;
|
||||||
pub const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
pub const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||||
@@ -20,8 +31,11 @@ impl OsuStrainSkill {
|
|||||||
self.inner.start_new_section_from(initial_strain);
|
self.inner.start_new_section_from(initial_strain);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
pub fn get_curr_strain_peaks(self) -> UsedOsuStrainSkills<StrainsVec> {
|
||||||
self.inner.get_curr_strain_peaks()
|
UsedOsuStrainSkills {
|
||||||
|
value: self.inner.get_curr_strain_peaks(),
|
||||||
|
object_strains: self.object_strains,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn difficulty_value(
|
pub fn difficulty_value(
|
||||||
@@ -29,11 +43,14 @@ impl OsuStrainSkill {
|
|||||||
reduced_section_count: usize,
|
reduced_section_count: usize,
|
||||||
reduced_strain_baseline: f64,
|
reduced_strain_baseline: f64,
|
||||||
decay_weight: f64,
|
decay_weight: f64,
|
||||||
) -> f64 {
|
) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
let mut peaks = self.get_curr_strain_peaks();
|
let UsedOsuStrainSkills {
|
||||||
|
value: mut peaks,
|
||||||
|
object_strains,
|
||||||
|
} = self.get_curr_strain_peaks();
|
||||||
|
|
||||||
let peaks_iter = peaks.sorted_non_zero_iter_mut().take(reduced_section_count);
|
let peaks_iter = peaks.sorted_non_zero_iter_mut().take(reduced_section_count);
|
||||||
|
|
||||||
@@ -50,7 +67,10 @@ impl OsuStrainSkill {
|
|||||||
weight *= decay_weight;
|
weight *= decay_weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
difficulty
|
UsedOsuStrainSkills {
|
||||||
|
value: DifficultyValue(difficulty),
|
||||||
|
object_strains,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn difficulty_to_performance(difficulty: f64) -> f64 {
|
pub fn difficulty_to_performance(difficulty: f64) -> f64 {
|
||||||
@@ -61,3 +81,39 @@ impl OsuStrainSkill {
|
|||||||
fn lerp(start: f64, end: f64, amount: f64) -> f64 {
|
fn lerp(start: f64, end: f64, amount: f64) -> f64 {
|
||||||
start + (end - start) * amount
|
start + (end - start) * amount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DifficultyValue(f64);
|
||||||
|
|
||||||
|
pub struct UsedOsuStrainSkills<T> {
|
||||||
|
value: T,
|
||||||
|
object_strains: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UsedOsuStrainSkills<DifficultyValue> {
|
||||||
|
pub fn difficulty_value(&self) -> f64 {
|
||||||
|
self.value.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count_difficult_strains(&self) -> f64 {
|
||||||
|
let DifficultyValue(diff) = self.value;
|
||||||
|
|
||||||
|
if diff.abs() < f64::EPSILON {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * What would the top strain be if all strain values were identical
|
||||||
|
let consistent_top_strain = diff / 10.0;
|
||||||
|
|
||||||
|
// * Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||||
|
self.object_strains
|
||||||
|
.iter()
|
||||||
|
.map(|s| 1.1 / (1.0 + (-10.0 * (s / consistent_top_strain - 0.88)).exp()))
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UsedOsuStrainSkills<StrainsVec> {
|
||||||
|
pub fn strains(self) -> StrainsVec {
|
||||||
|
self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+9
-54
@@ -66,53 +66,21 @@ impl OsuObject {
|
|||||||
reflect_y(&mut self.pos.y);
|
reflect_y(&mut self.pos.y);
|
||||||
|
|
||||||
if let OsuObjectKind::Slider(ref mut slider) = self.kind {
|
if let OsuObjectKind::Slider(ref mut slider) = self.kind {
|
||||||
let repeat_count = slider.repeat_count();
|
|
||||||
|
|
||||||
// Requires `stack_offset` so we can't add `h.pos` just yet
|
// Requires `stack_offset` so we can't add `h.pos` just yet
|
||||||
slider.lazy_end_pos.y = -slider.lazy_end_pos.y;
|
slider.lazy_end_pos.y = -slider.lazy_end_pos.y;
|
||||||
|
|
||||||
let mut nested_iter = slider.nested_objects.iter_mut();
|
for nested in slider.nested_objects.iter_mut() {
|
||||||
|
let mut nested_pos = self.pos; // already reflected at this point
|
||||||
// Since the tail is handled differently but it's not necessarily
|
nested_pos += Pos::new(nested.pos.x, -nested.pos.y);
|
||||||
// the last object, we first search for it, and then handle the
|
nested.pos = nested_pos;
|
||||||
// other nested objects
|
|
||||||
for nested in nested_iter.by_ref().rev() {
|
|
||||||
if let NestedSliderObjectKind::Tail = nested.kind {
|
|
||||||
let mut tail_pos = self.pos; // already reflected at this point
|
|
||||||
tail_pos += Pos::new(nested.pos.x, -nested.pos.y);
|
|
||||||
nested.pos = tail_pos;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
reflect_y(&mut nested.pos.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same for the last repeat point
|
|
||||||
for nested in nested_iter.by_ref().rev() {
|
|
||||||
if let NestedSliderObjectKind::Repeat = nested.kind {
|
|
||||||
nested.pos = if repeat_count % 2 == 0 {
|
|
||||||
self.pos
|
|
||||||
} else {
|
|
||||||
self.pos + Pos::new(slider.path_end_pos.x, -slider.path_end_pos.y)
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
reflect_y(&mut nested.pos.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
for nested in nested_iter {
|
|
||||||
reflect_y(&mut nested.pos.y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finalize_tail(&mut self) {
|
pub fn finalize_nested(&mut self) {
|
||||||
if let OsuObjectKind::Slider(ref mut slider) = self.kind {
|
if let OsuObjectKind::Slider(ref mut slider) = self.kind {
|
||||||
if let Some(tail) = slider.tail_mut() {
|
for nested in slider.nested_objects.iter_mut() {
|
||||||
tail.pos += self.pos;
|
nested.pos += self.pos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,9 +141,6 @@ pub struct OsuSlider {
|
|||||||
pub lazy_end_pos: Pos,
|
pub lazy_end_pos: Pos,
|
||||||
pub lazy_travel_dist: f32,
|
pub lazy_travel_dist: f32,
|
||||||
pub lazy_travel_time: f64,
|
pub lazy_travel_time: f64,
|
||||||
// Very annoyingly, this position might be needed solely to update the last
|
|
||||||
// repeat point's position on HR.
|
|
||||||
pub path_end_pos: Pos,
|
|
||||||
pub nested_objects: Vec<NestedSliderObject>,
|
pub nested_objects: Vec<NestedSliderObject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,12 +221,12 @@ impl OsuSlider {
|
|||||||
.filter_map(|e| {
|
.filter_map(|e| {
|
||||||
let obj = match e.kind {
|
let obj = match e.kind {
|
||||||
SliderEventType::Tick => NestedSliderObject {
|
SliderEventType::Tick => NestedSliderObject {
|
||||||
pos: h.pos + path.position_at(e.path_progress),
|
pos: path.position_at(e.path_progress),
|
||||||
start_time: e.time,
|
start_time: e.time,
|
||||||
kind: NestedSliderObjectKind::Tick,
|
kind: NestedSliderObjectKind::Tick,
|
||||||
},
|
},
|
||||||
SliderEventType::Repeat => NestedSliderObject {
|
SliderEventType::Repeat => NestedSliderObject {
|
||||||
pos: h.pos + path.position_at(e.path_progress),
|
pos: path.position_at(e.path_progress),
|
||||||
start_time: start_time + f64::from(e.span_idx + 1) * span_duration,
|
start_time: start_time + f64::from(e.span_idx + 1) * span_duration,
|
||||||
kind: NestedSliderObjectKind::Repeat,
|
kind: NestedSliderObjectKind::Repeat,
|
||||||
},
|
},
|
||||||
@@ -293,14 +258,12 @@ impl OsuSlider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let lazy_end_pos = path.position_at(end_time_min);
|
let lazy_end_pos = path.position_at(end_time_min);
|
||||||
let path_end_pos = path.position_at(1.0);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
end_time,
|
end_time,
|
||||||
lazy_end_pos,
|
lazy_end_pos,
|
||||||
lazy_travel_dist: 0.0,
|
lazy_travel_dist: 0.0,
|
||||||
lazy_travel_time,
|
lazy_travel_time,
|
||||||
path_end_pos,
|
|
||||||
nested_objects,
|
nested_objects,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,14 +315,6 @@ impl OsuSlider {
|
|||||||
// short and fast buzz sliders (/b/1001757)
|
// short and fast buzz sliders (/b/1001757)
|
||||||
.rfind(|nested| matches!(nested.kind, NestedSliderObjectKind::Tail))
|
.rfind(|nested| matches!(nested.kind, NestedSliderObjectKind::Tail))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tail_mut(&mut self) -> Option<&mut NestedSliderObject> {
|
|
||||||
self.nested_objects
|
|
||||||
.iter_mut()
|
|
||||||
// The tail is not necessarily the last nested object, e.g. on very
|
|
||||||
// short and fast buzz sliders (/b/1001757)
|
|
||||||
.rfind(|nested| matches!(nested.kind, NestedSliderObjectKind::Tail))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
+44
-19
@@ -33,6 +33,7 @@ pub struct OsuPerformance<'map> {
|
|||||||
pub(crate) n50: Option<u32>,
|
pub(crate) n50: Option<u32>,
|
||||||
pub(crate) misses: Option<u32>,
|
pub(crate) misses: Option<u32>,
|
||||||
pub(crate) hitresult_priority: HitResultPriority,
|
pub(crate) hitresult_priority: HitResultPriority,
|
||||||
|
pub(crate) lazer: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> OsuPerformance<'map> {
|
impl<'map> OsuPerformance<'map> {
|
||||||
@@ -152,6 +153,19 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the calculated attributes belong to an osu!lazer or osu!stable
|
||||||
|
/// score.
|
||||||
|
///
|
||||||
|
/// Defaults to lazer.
|
||||||
|
///
|
||||||
|
/// This affects internal accuracy calculation because lazer considers
|
||||||
|
/// slider heads for accuracy whereas stable does not.
|
||||||
|
pub const fn lazer(mut self, lazer: bool) -> Self {
|
||||||
|
self.lazer = Some(lazer);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Specify the amount of 300s of a play.
|
/// Specify the amount of 300s of a play.
|
||||||
pub const fn n300(mut self, n300: u32) -> Self {
|
pub const fn n300(mut self, n300: u32) -> Self {
|
||||||
self.n300 = Some(n300);
|
self.n300 = Some(n300);
|
||||||
@@ -509,6 +523,7 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
acc: state.accuracy(),
|
acc: state.accuracy(),
|
||||||
state,
|
state,
|
||||||
effective_miss_count,
|
effective_miss_count,
|
||||||
|
lazer: self.lazer.unwrap_or(true),
|
||||||
};
|
};
|
||||||
|
|
||||||
inner.calculate()
|
inner.calculate()
|
||||||
@@ -525,6 +540,7 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
n50: None,
|
n50: None,
|
||||||
misses: None,
|
misses: None,
|
||||||
hitresult_priority: HitResultPriority::DEFAULT,
|
hitresult_priority: HitResultPriority::DEFAULT,
|
||||||
|
lazer: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,7 +551,8 @@ impl<'map, T: IntoModePerformance<'map, Osu>> From<T> for OsuPerformance<'map> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14;
|
// * This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||||
|
pub const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.15;
|
||||||
|
|
||||||
struct OsuPerformanceInner<'mods> {
|
struct OsuPerformanceInner<'mods> {
|
||||||
attrs: OsuDifficultyAttributes,
|
attrs: OsuDifficultyAttributes,
|
||||||
@@ -543,10 +560,13 @@ struct OsuPerformanceInner<'mods> {
|
|||||||
acc: f64,
|
acc: f64,
|
||||||
state: OsuScoreState,
|
state: OsuScoreState,
|
||||||
effective_miss_count: f64,
|
effective_miss_count: f64,
|
||||||
|
lazer: 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 {
|
||||||
@@ -592,7 +612,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();
|
let acc_value = self.compute_accuracy_value(using_classic_slider_acc);
|
||||||
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)
|
||||||
@@ -628,16 +648,13 @@ impl OsuPerformanceInner<'_> {
|
|||||||
|
|
||||||
aim_value *= len_bonus;
|
aim_value *= len_bonus;
|
||||||
|
|
||||||
// * 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 {
|
||||||
aim_value *= 0.97
|
aim_value *= self.calculate_miss_penalty(
|
||||||
* (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
|
self.effective_miss_count,
|
||||||
.powf(self.effective_miss_count);
|
self.attrs.aim_difficult_strain_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
aim_value *= self.get_combo_scaling_factor();
|
|
||||||
|
|
||||||
let ar_factor = if self.mods.rx() {
|
let ar_factor = if self.mods.rx() {
|
||||||
0.0
|
0.0
|
||||||
} else if self.attrs.ar > 10.33 {
|
} else if self.attrs.ar > 10.33 {
|
||||||
@@ -700,16 +717,13 @@ impl OsuPerformanceInner<'_> {
|
|||||||
|
|
||||||
speed_value *= len_bonus;
|
speed_value *= len_bonus;
|
||||||
|
|
||||||
// * 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 {
|
||||||
speed_value *= 0.97
|
speed_value *= self.calculate_miss_penalty(
|
||||||
* (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
|
self.effective_miss_count,
|
||||||
.powf(self.effective_miss_count.powf(0.875));
|
self.attrs.speed_difficult_strain_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
speed_value *= self.get_combo_scaling_factor();
|
|
||||||
|
|
||||||
let ar_factor = if self.mods.ap() {
|
let ar_factor = if self.mods.ap() {
|
||||||
0.0
|
0.0
|
||||||
} else if self.attrs.ar > 10.33 {
|
} else if self.attrs.ar > 10.33 {
|
||||||
@@ -750,7 +764,7 @@ impl OsuPerformanceInner<'_> {
|
|||||||
|
|
||||||
// * Scale the speed value with accuracy and OD.
|
// * Scale the speed value with accuracy and OD.
|
||||||
speed_value *= (0.95 + self.attrs.od * self.attrs.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.attrs.od).max(8.0)) / 2.0);
|
* ((self.acc + relevant_acc) / 2.0).powf((14.5 - self.attrs.od) / 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(
|
||||||
@@ -761,14 +775,18 @@ impl OsuPerformanceInner<'_> {
|
|||||||
speed_value
|
speed_value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_accuracy_value(&self) -> f64 {
|
fn compute_accuracy_value(&self, using_classic_slider_acc: bool) -> f64 {
|
||||||
if self.mods.rx() {
|
if self.mods.rx() {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// * 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.attrs.n_circles;
|
let mut amount_hit_objects_with_acc = self.attrs.n_circles;
|
||||||
|
|
||||||
|
if using_classic_slider_acc {
|
||||||
|
amount_hit_objects_with_acc += self.attrs.n_sliders;
|
||||||
|
}
|
||||||
|
|
||||||
let better_acc_percentage = if amount_hit_objects_with_acc > 0 {
|
let better_acc_percentage = if amount_hit_objects_with_acc > 0 {
|
||||||
let sub = self.state.total_hits() - amount_hit_objects_with_acc;
|
let sub = self.state.total_hits() - amount_hit_objects_with_acc;
|
||||||
@@ -842,6 +860,13 @@ impl OsuPerformanceInner<'_> {
|
|||||||
flashlight_value
|
flashlight_value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// * Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||||
|
// * so we use the amount of relatively difficult sections to adjust miss penalty
|
||||||
|
// * to make it more punishing on maps with lower amount of hard sections.
|
||||||
|
fn calculate_miss_penalty(&self, miss_count: f64, diff_strain_count: f64) -> f64 {
|
||||||
|
0.96 / ((miss_count / (4.0 * diff_strain_count.ln().powf(0.94))) + 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_combo_scaling_factor(&self) -> f64 {
|
fn get_combo_scaling_factor(&self) -> f64 {
|
||||||
if self.attrs.max_combo == 0 {
|
if self.attrs.max_combo == 0 {
|
||||||
1.0
|
1.0
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for TaikoPerformance<'map> {
|
|||||||
n50: _,
|
n50: _,
|
||||||
misses,
|
misses,
|
||||||
hitresult_priority,
|
hitresult_priority,
|
||||||
|
lazer: _,
|
||||||
} = osu;
|
} = osu;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
Reference in New Issue
Block a user