store osu skills in struct instead of array
This commit is contained in:
@@ -6,12 +6,11 @@ use std::{
|
||||
use crate::{curve::CurveBuffers, Beatmap, Mods};
|
||||
|
||||
use super::{
|
||||
create_skills,
|
||||
difficulty_object::{Distances, OsuDifficultyObject},
|
||||
old_stacking,
|
||||
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
|
||||
scaling_factor::ScalingFactor,
|
||||
skills::{Aim, Flashlight, Skill, Speed},
|
||||
skills::{Skill, Skills},
|
||||
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER,
|
||||
PERFORMANCE_BASE_MULTIPLIER, PREEMPT_MIN,
|
||||
};
|
||||
@@ -46,6 +45,7 @@ use super::{
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct OsuGradualDifficultyAttributes {
|
||||
pub(crate) idx: usize,
|
||||
mods: u32,
|
||||
@@ -54,7 +54,7 @@ pub struct OsuGradualDifficultyAttributes {
|
||||
#[allow(unused)]
|
||||
hit_objects: Vec<OsuObject>,
|
||||
diff_objects: Vec<OsuDifficultyObject<'static>>,
|
||||
skills: [Box<dyn Skill>; 4],
|
||||
skills: Skills,
|
||||
hit_window: f64,
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ impl Debug for OsuGradualDifficultyAttributes {
|
||||
.field("idx", &self.idx)
|
||||
.field("attrs", &self.attrs)
|
||||
.field("diff_objects", &self.diff_objects)
|
||||
.field("skills", &"<cannot be displayed>")
|
||||
.field("skills", &self.skills)
|
||||
.field("hit_window", &self.hit_window)
|
||||
.finish()
|
||||
}
|
||||
@@ -133,7 +133,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
h
|
||||
});
|
||||
|
||||
let skills = create_skills(mods, scaling_factor.radius, time_preempt, time_fade_in);
|
||||
let skills = Skills::new(mods, scaling_factor.radius, time_preempt, time_fade_in);
|
||||
|
||||
let last = match hit_objects_iter.next() {
|
||||
Some(prev) => prev,
|
||||
@@ -222,36 +222,25 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
let curr = self.diff_objects.get(self.idx)?;
|
||||
self.idx += 1;
|
||||
|
||||
for skill in self.skills.iter_mut() {
|
||||
skill.process(curr, &self.diff_objects, self.hit_window);
|
||||
}
|
||||
self.skills
|
||||
.process(curr, &self.diff_objects, self.hit_window);
|
||||
|
||||
Self::increment_combo(curr.base, &mut self.attrs);
|
||||
|
||||
let [aim, aim_no_sliders, speed, flashlight] = &self.skills;
|
||||
|
||||
let mut aim = aim.as_any().downcast_ref::<Aim>().unwrap().clone();
|
||||
|
||||
let mut aim_no_sliders = aim_no_sliders
|
||||
.as_any()
|
||||
.downcast_ref::<Aim>()
|
||||
.unwrap()
|
||||
.clone();
|
||||
let Skills {
|
||||
mut aim,
|
||||
mut aim_no_sliders,
|
||||
mut speed,
|
||||
mut flashlight,
|
||||
} = self.skills.clone();
|
||||
|
||||
let mut aim_rating = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let aim_rating_no_sliders =
|
||||
aim_no_sliders.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let mut speed = speed.as_any().downcast_ref::<Speed>().unwrap().clone();
|
||||
let speed_notes = speed.relevant_note_count();
|
||||
let mut speed_rating = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let mut flashlight = flashlight
|
||||
.as_any()
|
||||
.downcast_ref::<Flashlight>()
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let mut flashlight_rating = flashlight.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let slider_factor = if aim_rating > 0.0 {
|
||||
@@ -319,9 +308,8 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
let curr = self.diff_objects.get(self.idx)?;
|
||||
self.idx += 1;
|
||||
|
||||
for skill in self.skills.iter_mut() {
|
||||
skill.process(curr, &self.diff_objects, self.hit_window);
|
||||
}
|
||||
self.skills
|
||||
.process(curr, &self.diff_objects, self.hit_window);
|
||||
|
||||
Self::increment_combo(curr.base, &mut self.attrs);
|
||||
}
|
||||
|
||||
+22
-35
@@ -12,7 +12,7 @@ use self::{
|
||||
difficulty_object::{Distances, OsuDifficultyObject},
|
||||
osu_object::{ObjectParameters, OsuObject},
|
||||
scaling_factor::ScalingFactor,
|
||||
skills::{Aim, Flashlight, Skill, Speed},
|
||||
skills::{Skill, Skills},
|
||||
};
|
||||
|
||||
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
|
||||
@@ -116,21 +116,19 @@ impl<'map> OsuStars<'map> {
|
||||
|
||||
let (skills, mut attrs) = calculate_skills(self);
|
||||
|
||||
let [mut aim, mut aim_no_sliders, mut speed, mut flashlight] = skills;
|
||||
let Skills {
|
||||
mut aim,
|
||||
mut aim_no_sliders,
|
||||
mut speed,
|
||||
mut flashlight,
|
||||
} = skills;
|
||||
|
||||
let mut aim_rating = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let aim_rating_no_sliders =
|
||||
aim_no_sliders.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let (mut speed_rating, speed_notes) =
|
||||
if let Some(speed) = speed.as_any_mut().downcast_mut::<Speed>() {
|
||||
let notes = speed.relevant_note_count();
|
||||
let rating = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
(rating, notes)
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
let speed_notes = speed.relevant_note_count();
|
||||
let mut speed_rating = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let mut flashlight_rating = flashlight.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
@@ -192,14 +190,19 @@ impl<'map> OsuStars<'map> {
|
||||
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
|
||||
let (skills, _) = calculate_skills(self);
|
||||
|
||||
let [mut aim, mut aim_no_sliders, mut speed, mut flashlight] = skills;
|
||||
let Skills {
|
||||
aim,
|
||||
aim_no_sliders,
|
||||
speed,
|
||||
flashlight,
|
||||
} = skills;
|
||||
|
||||
OsuStrains {
|
||||
section_len: SECTION_LEN * clock_rate,
|
||||
aim: aim.take_strain_peaks(),
|
||||
aim_no_sliders: aim_no_sliders.take_strain_peaks(),
|
||||
speed: speed.take_strain_peaks(),
|
||||
flashlight: flashlight.take_strain_peaks(),
|
||||
aim: aim.strain_peaks,
|
||||
aim_no_sliders: aim_no_sliders.strain_peaks,
|
||||
speed: speed.strain_peaks,
|
||||
flashlight: flashlight.strain_peaks,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +232,7 @@ impl OsuStrains {
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficultyAttributes) {
|
||||
fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
|
||||
let OsuStars {
|
||||
map,
|
||||
mods,
|
||||
@@ -295,7 +298,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
h
|
||||
});
|
||||
|
||||
let mut skills = create_skills(mods, scaling_factor.radius, time_preempt, time_fade_in);
|
||||
let mut skills = Skills::new(mods, scaling_factor.radius, time_preempt, time_fade_in);
|
||||
|
||||
let last = match hit_objects.next() {
|
||||
Some(prev) => prev,
|
||||
@@ -333,9 +336,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
}
|
||||
|
||||
for curr in diff_objects.iter() {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(curr, &diff_objects, hit_window);
|
||||
}
|
||||
skills.process(curr, &diff_objects, hit_window);
|
||||
}
|
||||
|
||||
(skills, attrs)
|
||||
@@ -491,20 +492,6 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_skills(
|
||||
mods: u32,
|
||||
radius: f32,
|
||||
time_preempt: f64,
|
||||
time_fade_in: f64,
|
||||
) -> [Box<dyn Skill>; 4] {
|
||||
[
|
||||
Box::new(Aim::new(true)) as Box<dyn Skill>,
|
||||
Box::new(Aim::new(false)) as Box<dyn Skill>,
|
||||
Box::new(Speed::new()) as Box<dyn Skill>,
|
||||
Box::new(Flashlight::new(mods, radius, time_preempt, time_fade_in)) as Box<dyn Skill>,
|
||||
]
|
||||
}
|
||||
|
||||
/// The result of a difficulty calculation on an osu!standard map.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct OsuDifficultyAttributes {
|
||||
|
||||
+3
-22
@@ -1,19 +1,15 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
f64::consts::{FRAC_PI_2, PI},
|
||||
mem,
|
||||
};
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
|
||||
use crate::osu::difficulty_object::OsuDifficultyObject;
|
||||
|
||||
use super::{previous, previous_start_time, OsuStrainSkill, Skill, StrainSkill};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Aim {
|
||||
curr_strain: f64,
|
||||
curr_section_peak: f64,
|
||||
curr_section_end: f64,
|
||||
strain_peaks: Vec<f64>,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
with_sliders: bool,
|
||||
}
|
||||
|
||||
@@ -51,21 +47,6 @@ impl Skill for Aim {
|
||||
fn difficulty_value(&mut self) -> f64 {
|
||||
<Self as OsuStrainSkill>::difficulty_value(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_strain_peaks(&mut self) -> Vec<f64> {
|
||||
mem::take(&mut self.strain_peaks)
|
||||
}
|
||||
}
|
||||
|
||||
impl StrainSkill for Aim {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::{any::Any, mem};
|
||||
|
||||
use crate::{
|
||||
osu::{difficulty_object::OsuDifficultyObject, osu_object::OsuObjectKind},
|
||||
Mods,
|
||||
@@ -7,12 +5,12 @@ use crate::{
|
||||
|
||||
use super::{previous, previous_start_time, OsuStrainSkill, Skill, StrainSkill};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Flashlight {
|
||||
curr_strain: f64,
|
||||
curr_section_peak: f64,
|
||||
curr_section_end: f64,
|
||||
strain_peaks: Vec<f64>,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
has_hidden_mod: bool,
|
||||
scaling_factor: f64,
|
||||
time_preempt: f64,
|
||||
@@ -56,21 +54,6 @@ impl Skill for Flashlight {
|
||||
fn difficulty_value(&mut self) -> f64 {
|
||||
<Self as StrainSkill>::difficulty_value(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_strain_peaks(&mut self) -> Vec<f64> {
|
||||
mem::take(&mut self.strain_peaks)
|
||||
}
|
||||
}
|
||||
|
||||
impl StrainSkill for Flashlight {
|
||||
|
||||
+28
-128
@@ -1,145 +1,45 @@
|
||||
mod aim;
|
||||
mod flashlight;
|
||||
mod speed;
|
||||
mod traits;
|
||||
|
||||
use std::{any::Any, cmp::Ordering, mem};
|
||||
use crate::osu::difficulty_object::OsuDifficultyObject;
|
||||
|
||||
pub(crate) use self::{aim::Aim, flashlight::Flashlight, speed::Speed};
|
||||
pub(crate) use self::{
|
||||
aim::Aim,
|
||||
flashlight::Flashlight,
|
||||
speed::Speed,
|
||||
traits::{OsuStrainSkill, Skill, StrainSkill},
|
||||
};
|
||||
|
||||
use super::{difficulty_object::OsuDifficultyObject, SECTION_LEN};
|
||||
|
||||
pub(crate) trait Skill {
|
||||
fn process(
|
||||
&mut self,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
hit_window: f64,
|
||||
);
|
||||
fn difficulty_value(&mut self) -> f64;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn take_strain_peaks(&mut self) -> Vec<f64>;
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Skills {
|
||||
pub aim: Aim,
|
||||
pub aim_no_sliders: Aim,
|
||||
pub speed: Speed,
|
||||
pub flashlight: Flashlight,
|
||||
}
|
||||
|
||||
pub(crate) trait StrainSkill: Skill + Sized {
|
||||
const DECAY_WEIGHT: f64 = 0.9;
|
||||
impl Skills {
|
||||
pub(crate) fn new(mods: u32, radius: f32, time_preempt: f64, time_fade_in: f64) -> Self {
|
||||
Self {
|
||||
aim: Aim::new(true),
|
||||
aim_no_sliders: Aim::new(false),
|
||||
speed: Speed::new(),
|
||||
flashlight: Flashlight::new(mods, radius, time_preempt, time_fade_in),
|
||||
}
|
||||
}
|
||||
|
||||
fn strain_peaks_mut(&mut self) -> &mut Vec<f64>;
|
||||
fn curr_section_peak(&mut self) -> &mut f64;
|
||||
fn curr_section_end(&mut self) -> &mut f64;
|
||||
|
||||
fn strain_value_at(
|
||||
&mut self,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
hit_window: f64,
|
||||
) -> f64;
|
||||
|
||||
fn calculate_initial_strain(
|
||||
&self,
|
||||
time: f64,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
) -> f64;
|
||||
|
||||
fn process(
|
||||
pub(crate) fn process(
|
||||
&mut self,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
hit_window: f64,
|
||||
) {
|
||||
// * The first object doesn't generate a strain, so we begin with an incremented section end
|
||||
if curr.idx == 0 {
|
||||
let section_len = SECTION_LEN as f64;
|
||||
*self.curr_section_end() = (curr.start_time / section_len).ceil() * section_len;
|
||||
}
|
||||
|
||||
while curr.start_time > *self.curr_section_end() {
|
||||
self.save_curr_peak();
|
||||
|
||||
{
|
||||
let section_end = *self.curr_section_end();
|
||||
self.start_new_section_from(section_end, curr, diff_objects);
|
||||
}
|
||||
|
||||
*self.curr_section_end() += SECTION_LEN as f64;
|
||||
}
|
||||
|
||||
*self.curr_section_peak() = self
|
||||
.strain_value_at(curr, diff_objects, hit_window)
|
||||
.max(*self.curr_section_peak());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn save_curr_peak(&mut self) {
|
||||
let peak = *self.curr_section_peak();
|
||||
self.strain_peaks_mut().push(peak);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn start_new_section_from(
|
||||
&mut self,
|
||||
time: f64,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
) {
|
||||
// * The maximum strain of the new section is not zero by default
|
||||
// * This means we need to capture the strain level at the beginning of the new section,
|
||||
// * and use that as the initial peak level.
|
||||
*self.curr_section_peak() = self.calculate_initial_strain(time, curr, diff_objects);
|
||||
}
|
||||
|
||||
fn difficulty_value(&mut self) -> f64;
|
||||
|
||||
#[inline]
|
||||
fn get_curr_strain_peaks(&mut self) -> Vec<f64> {
|
||||
let curr_peak = *self.curr_section_peak();
|
||||
let mut strain_peaks = mem::take(self.strain_peaks_mut());
|
||||
strain_peaks.push(curr_peak);
|
||||
|
||||
strain_peaks
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait OsuStrainSkill: StrainSkill + Sized {
|
||||
const REDUCED_SECTION_COUNT: usize = 10;
|
||||
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||
const DIFFICULTY_MULTIPLER: f64 = 1.06;
|
||||
|
||||
fn difficulty_value(&mut self) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
// * Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
// * These sections will not contribute to the difficulty.
|
||||
let mut peaks = self.get_curr_strain_peaks();
|
||||
|
||||
peaks.retain(|&peak| peak > 0.0);
|
||||
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
let peak_iter = peaks.iter_mut().take(Self::REDUCED_SECTION_COUNT);
|
||||
|
||||
fn lerp(start: f64, end: f64, amount: f64) -> f64 {
|
||||
start + (end - start) * amount
|
||||
}
|
||||
|
||||
// * We are reducing the highest strains first to account for extreme difficulty spikes
|
||||
for (i, strain) in peak_iter.enumerate() {
|
||||
let clamped = (i as f32 / Self::REDUCED_SECTION_COUNT as f32).clamp(0.0, 1.0) as f64;
|
||||
let scale = (lerp(1.0, 10.0, clamped)).log10();
|
||||
*strain *= lerp(Self::REDUCED_STRAIN_BASELINE, 1.0, scale);
|
||||
}
|
||||
|
||||
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
// * Difficulty is the weighted sum of the highest strains from every section.
|
||||
// * We're sorting from highest to lowest strain.
|
||||
for strain in peaks {
|
||||
difficulty += strain * weight;
|
||||
weight *= Self::DECAY_WEIGHT;
|
||||
}
|
||||
|
||||
difficulty * Self::DIFFICULTY_MULTIPLER
|
||||
<Aim as Skill>::process(&mut self.aim, curr, diff_objects, hit_window);
|
||||
<Aim as Skill>::process(&mut self.aim_no_sliders, curr, diff_objects, hit_window);
|
||||
<Speed as Skill>::process(&mut self.speed, curr, diff_objects, hit_window);
|
||||
<Flashlight as Skill>::process(&mut self.flashlight, curr, diff_objects, hit_window);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-18
@@ -1,16 +1,16 @@
|
||||
use std::{any::Any, cmp::Ordering, f64::consts::PI, mem};
|
||||
use std::{cmp::Ordering, f64::consts::PI};
|
||||
|
||||
use crate::osu::difficulty_object::OsuDifficultyObject;
|
||||
|
||||
use super::{next, previous, previous_start_time, OsuStrainSkill, Skill, StrainSkill};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Speed {
|
||||
curr_strain: f64,
|
||||
curr_section_peak: f64,
|
||||
curr_section_end: f64,
|
||||
curr_rhythm: f64,
|
||||
strain_peaks: Vec<f64>,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
object_strains: Vec<f64>,
|
||||
}
|
||||
|
||||
@@ -62,21 +62,6 @@ impl Skill for Speed {
|
||||
fn difficulty_value(&mut self) -> f64 {
|
||||
<Self as OsuStrainSkill>::difficulty_value(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_strain_peaks(&mut self) -> Vec<f64> {
|
||||
mem::take(&mut self.strain_peaks)
|
||||
}
|
||||
}
|
||||
|
||||
impl StrainSkill for Speed {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
use std::{cmp::Ordering, mem};
|
||||
|
||||
use crate::osu::{difficulty_object::OsuDifficultyObject, SECTION_LEN};
|
||||
|
||||
pub(crate) trait Skill {
|
||||
fn process(
|
||||
&mut self,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
hit_window: f64,
|
||||
);
|
||||
fn difficulty_value(&mut self) -> f64;
|
||||
}
|
||||
|
||||
pub(crate) trait StrainSkill: Skill + Sized {
|
||||
const DECAY_WEIGHT: f64 = 0.9;
|
||||
|
||||
fn strain_peaks_mut(&mut self) -> &mut Vec<f64>;
|
||||
fn curr_section_peak(&mut self) -> &mut f64;
|
||||
fn curr_section_end(&mut self) -> &mut f64;
|
||||
|
||||
fn strain_value_at(
|
||||
&mut self,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
hit_window: f64,
|
||||
) -> f64;
|
||||
|
||||
fn calculate_initial_strain(
|
||||
&self,
|
||||
time: f64,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
) -> f64;
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
hit_window: f64,
|
||||
) {
|
||||
// * The first object doesn't generate a strain, so we begin with an incremented section end
|
||||
if curr.idx == 0 {
|
||||
let section_len = SECTION_LEN as f64;
|
||||
*self.curr_section_end() = (curr.start_time / section_len).ceil() * section_len;
|
||||
}
|
||||
|
||||
while curr.start_time > *self.curr_section_end() {
|
||||
self.save_curr_peak();
|
||||
|
||||
{
|
||||
let section_end = *self.curr_section_end();
|
||||
self.start_new_section_from(section_end, curr, diff_objects);
|
||||
}
|
||||
|
||||
*self.curr_section_end() += SECTION_LEN as f64;
|
||||
}
|
||||
|
||||
*self.curr_section_peak() = self
|
||||
.strain_value_at(curr, diff_objects, hit_window)
|
||||
.max(*self.curr_section_peak());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn save_curr_peak(&mut self) {
|
||||
let peak = *self.curr_section_peak();
|
||||
self.strain_peaks_mut().push(peak);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn start_new_section_from(
|
||||
&mut self,
|
||||
time: f64,
|
||||
curr: &OsuDifficultyObject<'_>,
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
) {
|
||||
// * The maximum strain of the new section is not zero by default
|
||||
// * This means we need to capture the strain level at the beginning of the new section,
|
||||
// * and use that as the initial peak level.
|
||||
*self.curr_section_peak() = self.calculate_initial_strain(time, curr, diff_objects);
|
||||
}
|
||||
|
||||
fn difficulty_value(&mut self) -> f64;
|
||||
|
||||
#[inline]
|
||||
fn get_curr_strain_peaks(&mut self) -> Vec<f64> {
|
||||
let curr_peak = *self.curr_section_peak();
|
||||
let mut strain_peaks = mem::take(self.strain_peaks_mut());
|
||||
strain_peaks.push(curr_peak);
|
||||
|
||||
strain_peaks
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait OsuStrainSkill: StrainSkill + Sized {
|
||||
const REDUCED_SECTION_COUNT: usize = 10;
|
||||
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||
const DIFFICULTY_MULTIPLER: f64 = 1.06;
|
||||
|
||||
fn difficulty_value(&mut self) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
// * Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
// * These sections will not contribute to the difficulty.
|
||||
let mut peaks = self.get_curr_strain_peaks();
|
||||
|
||||
peaks.retain(|&peak| peak > 0.0);
|
||||
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
let peak_iter = peaks.iter_mut().take(Self::REDUCED_SECTION_COUNT);
|
||||
|
||||
fn lerp(start: f64, end: f64, amount: f64) -> f64 {
|
||||
start + (end - start) * amount
|
||||
}
|
||||
|
||||
// * We are reducing the highest strains first to account for extreme difficulty spikes
|
||||
for (i, strain) in peak_iter.enumerate() {
|
||||
let clamped = (i as f32 / Self::REDUCED_SECTION_COUNT as f32).clamp(0.0, 1.0) as f64;
|
||||
let scale = (lerp(1.0, 10.0, clamped)).log10();
|
||||
*strain *= lerp(Self::REDUCED_STRAIN_BASELINE, 1.0, scale);
|
||||
}
|
||||
|
||||
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
// * Difficulty is the weighted sum of the highest strains from every section.
|
||||
// * We're sorting from highest to lowest strain.
|
||||
for strain in peaks {
|
||||
difficulty += strain * weight;
|
||||
weight *= Self::DECAY_WEIGHT;
|
||||
}
|
||||
|
||||
difficulty * Self::DIFFICULTY_MULTIPLER
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user