store osu skills in struct instead of array

This commit is contained in:
MaxOhn
2022-10-22 19:42:16 +02:00
parent ea23a8948b
commit 9acd6efe50
7 changed files with 208 additions and 249 deletions
+15 -27
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+2 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+135
View File
@@ -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
}
}