wip osu pp update

This commit is contained in:
MaxOhn
2022-10-07 18:46:17 +02:00
parent fae3a0359c
commit 5eb8dab588
34 changed files with 2415 additions and 1931 deletions
+273
View File
@@ -0,0 +1,273 @@
use std::{
any::Any,
f64::consts::{FRAC_PI_2, FRAC_PI_6, PI},
mem,
};
use crate::osu::difficulty_object::OsuDifficultyObject;
use super::{previous, previous_start_time, OsuStrainSkill, Skill, StrainSkill};
#[derive(Clone)]
pub(crate) struct Aim {
curr_strain: f64,
curr_section_peak: f64,
curr_section_end: f64,
strain_peaks: Vec<f64>,
with_sliders: bool,
}
impl Aim {
const SKILL_MULTIPLIER: f64 = 23.55;
const STRAIN_DECAY_BASE: f64 = 0.15;
pub(crate) fn new(with_sliders: bool) -> Self {
Self {
curr_strain: 0.0,
curr_section_peak: 0.0,
curr_section_end: 0.0,
strain_peaks: Vec::new(),
with_sliders,
}
}
fn strain_decay(ms: f64) -> f64 {
Self::STRAIN_DECAY_BASE.powf(ms / 1000.0)
}
}
impl Skill for Aim {
fn process(
&mut self,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
hit_window: f64,
) {
<Self as StrainSkill>::process(self, curr, diff_objects, hit_window)
}
fn difficulty_value(&mut self) -> f64 {
<Self as OsuStrainSkill>::difficulty_value(self)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn take_strain_peaks(&mut self) -> Vec<f64> {
mem::take(&mut self.strain_peaks)
}
}
impl StrainSkill for Aim {
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
fn strain_value_at(
&mut self,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
_hit_window: f64,
) -> f64 {
self.curr_strain *= Self::strain_decay(curr.delta_time);
self.curr_strain += AimEvaluator::evaluate_diff_of(curr, diff_objects, self.with_sliders)
* Self::SKILL_MULTIPLIER;
self.curr_strain
}
fn calculate_initial_strain(
&self,
time: f64,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
) -> f64 {
self.curr_strain * Self::strain_decay(time - previous_start_time(diff_objects, curr.idx, 0))
}
fn difficulty_value(&mut self) -> f64 {
<Self as OsuStrainSkill>::difficulty_value(self)
}
}
impl OsuStrainSkill for Aim {}
struct AimEvaluator;
impl AimEvaluator {
const WIDE_ANGLE_MULTIPLIER: f64 = 1.5;
const ACUTE_ANGLE_MULTIPLIER: f64 = 1.95;
const SLIDER_MULTIPLIER: f64 = 1.35;
const VELOCITY_CHANGE_MULTIPLIER: f64 = 0.75;
fn evaluate_diff_of(
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
with_sliders: bool,
) -> f64 {
let osu_curr_obj = curr;
let (osu_last_last_obj, osu_last_obj) = if let Some(tuple) =
previous(diff_objects, curr.idx, 1)
.zip(previous(diff_objects, curr.idx, 0))
.filter(|(_, last)| !(curr.base.is_spinner() || last.base.is_spinner()))
{
tuple
} else {
return 0.0;
};
// * Calculate the velocity to the current hitobject, which starts
// * with a base distance / time assuming the last object is a hitcircle.
let mut curr_vel = osu_curr_obj.dists.lazy_jump_dist / osu_curr_obj.strain_time;
// * But if the last object is a slider, then we extend the travel
// * velocity through the slider into the current object.
if osu_last_obj.base.is_slider() && with_sliders {
// * calculate the slider velocity from slider head to slider end.
let travel_vel = osu_last_obj.dists.travel_dist / osu_last_obj.dists.travel_time;
// * calculate the movement velocity from slider end to current object
let movement_vel = osu_curr_obj.dists.min_jump_dist / osu_curr_obj.dists.min_jump_time;
// * take the larger total combined velocity.
curr_vel = curr_vel.max(movement_vel + travel_vel);
}
// * As above, do the same for the previous hitobject.
let mut prev_vel = osu_last_obj.dists.lazy_jump_dist / osu_last_obj.strain_time;
if osu_last_last_obj.base.is_slider() && with_sliders {
let travel_vel =
osu_last_last_obj.dists.travel_dist / osu_last_last_obj.dists.travel_time;
let movement_vel = osu_last_obj.dists.min_jump_dist / osu_last_obj.dists.min_jump_time;
prev_vel = prev_vel.max(movement_vel + travel_vel);
}
let mut wide_angle_bonus = 0.0;
let mut acute_angle_bonus = 0.0;
let mut slider_bonus = 0.0;
let mut vel_change_bonus = 0.0;
// * Start strain with regular velocity.
let mut aim_strain = curr_vel;
// * If rhythms are the same.
if osu_curr_obj.strain_time.max(osu_last_obj.strain_time)
< 1.25 * osu_curr_obj.strain_time.min(osu_last_obj.strain_time)
{
if let Some(((curr_angle, last_angle), last_last_angle)) = osu_curr_obj
.dists
.angle
.zip(osu_last_obj.dists.angle)
.zip(osu_last_last_obj.dists.angle)
{
// * Rewarding angles, take the smaller velocity as base.
let angle_bonus = curr_vel.min(prev_vel);
wide_angle_bonus = Self::calc_wide_angle_bonus(curr_angle);
acute_angle_bonus = Self::calc_acute_angle_bonus(curr_angle);
// * Only buff deltaTime exceeding 300 bpm 1/2.
if osu_curr_obj.strain_time > 100.0 {
acute_angle_bonus = 0.0;
} else {
let base1 =
(FRAC_PI_2 * ((100.0 - osu_curr_obj.strain_time) / 25.0).min(1.0)).sin();
let base2 = (FRAC_PI_2
* ((osu_curr_obj.dists.lazy_jump_dist).clamp(50.0, 100.0) - 50.0)
/ 50.0)
.sin();
// * Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
acute_angle_bonus *= Self::calc_acute_angle_bonus(last_angle)
// * The maximum velocity we buff is equal to 125 / strainTime
* angle_bonus.min(125.0 / osu_curr_obj.strain_time)
// * scale buff from 150 bpm 1/4 to 200 bpm 1/4
* base1
* base1
// * Buff distance exceeding 50 (radius) up to 100 (diameter).
* base2
* base2;
}
// * Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
wide_angle_bonus *= angle_bonus
* (1.0 - wide_angle_bonus.min(Self::calc_wide_angle_bonus(last_angle).powi(3)));
// * Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
acute_angle_bonus *= 0.5
+ 0.5
* (1.0
- acute_angle_bonus
.min(Self::calc_acute_angle_bonus(last_last_angle).powi(3)));
}
}
if prev_vel.max(curr_vel).abs() > f64::EPSILON {
// * We want to use the average velocity over the whole object when awarding
// * differences, not the individual jump and slider path velocities.
prev_vel = (osu_last_obj.dists.lazy_jump_dist + osu_last_last_obj.dists.travel_dist)
/ osu_last_obj.strain_time;
curr_vel = (osu_curr_obj.dists.lazy_jump_dist + osu_last_obj.dists.travel_dist)
/ osu_curr_obj.strain_time;
// * Scale with ratio of difference compared to 0.5 * max dist.
let dist_ratio_base =
(FRAC_PI_2 * (prev_vel - curr_vel).abs() / prev_vel.max(curr_vel)).sin();
let dist_ratio = dist_ratio_base * dist_ratio_base;
// * Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
let overlap_vel_buff = (125.0 / osu_curr_obj.strain_time.min(osu_last_obj.strain_time))
.min((prev_vel - curr_vel).abs());
vel_change_bonus = overlap_vel_buff * dist_ratio;
// * Penalize for rhythm changes.
let bonus_base = (osu_curr_obj.strain_time).min(osu_last_obj.strain_time)
/ (osu_curr_obj.strain_time).max(osu_last_obj.strain_time);
vel_change_bonus *= bonus_base * bonus_base;
}
if osu_last_obj.base.is_slider() {
// * Reward sliders based on velocity.
slider_bonus = osu_last_obj.dists.travel_dist / osu_last_obj.dists.travel_time
}
// * Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
aim_strain += (acute_angle_bonus * Self::ACUTE_ANGLE_MULTIPLIER).max(
wide_angle_bonus * Self::WIDE_ANGLE_MULTIPLIER
+ vel_change_bonus * Self::VELOCITY_CHANGE_MULTIPLIER,
);
// * Add in additional slider velocity bonus.
if with_sliders {
aim_strain += slider_bonus * Self::SLIDER_MULTIPLIER;
}
aim_strain
}
fn calc_wide_angle_bonus(angle: f64) -> f64 {
let base = (3.0 / 4.0 * ((5.0 / 6.0 * PI).min(angle.max(FRAC_PI_6)) - FRAC_PI_6)).sin();
base * base
}
fn calc_acute_angle_bonus(angle: f64) -> f64 {
1.0 - Self::calc_wide_angle_bonus(angle)
}
}
+235
View File
@@ -0,0 +1,235 @@
use std::{any::Any, mem};
use crate::{
osu::{
difficulty_object::OsuDifficultyObject,
osu_object::{NestedObjectKind, OsuObjectKind},
},
Mods,
};
use super::{previous, previous_start_time, OsuStrainSkill, Skill, StrainSkill};
#[derive(Clone)]
pub(crate) struct Flashlight {
curr_strain: f64,
curr_section_peak: f64,
curr_section_end: f64,
strain_peaks: Vec<f64>,
has_hidden_mod: bool,
scaling_factor: f64,
}
impl Flashlight {
const SKILL_MULTIPLIER: f64 = 0.052;
const STRAIN_DECAY_BASE: f64 = 0.15;
pub(crate) fn new(mods: u32, radius: f32) -> Self {
Self {
curr_strain: 0.0,
curr_section_peak: 0.0,
curr_section_end: 0.0,
strain_peaks: Vec::new(),
has_hidden_mod: mods.hd(),
scaling_factor: 52.0 / radius as f64,
}
}
fn strain_decay(ms: f64) -> f64 {
Self::STRAIN_DECAY_BASE.powf(ms / 1000.0)
}
}
impl Skill for Flashlight {
fn process(
&mut self,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
hit_window: f64,
) {
<Self as StrainSkill>::process(self, curr, diff_objects, hit_window)
}
fn difficulty_value(&mut self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn take_strain_peaks(&mut self) -> Vec<f64> {
mem::take(&mut self.strain_peaks)
}
}
impl StrainSkill for Flashlight {
const DECAY_WEIGHT: f64 = 0.9;
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
fn strain_value_at(
&mut self,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
_hit_window: f64,
) -> f64 {
self.curr_strain *= Self::strain_decay(curr.delta_time);
self.curr_strain += FlashlightEvaluator::evaluate_diff_of(
curr,
diff_objects,
self.has_hidden_mod,
self.scaling_factor,
) * Self::SKILL_MULTIPLIER;
self.curr_strain
}
fn calculate_initial_strain(
&self,
time: f64,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
) -> f64 {
self.curr_strain * Self::strain_decay(time - previous_start_time(diff_objects, curr.idx, 0))
}
fn difficulty_value(&mut self) -> f64 {
self.get_curr_strain_peaks().into_iter().sum::<f64>() * Self::DIFFICULTY_MULTIPLER
}
}
impl OsuStrainSkill for Flashlight {}
struct FlashlightEvaluator;
impl FlashlightEvaluator {
const MAX_OPACITY_BONUS: f64 = 0.4;
const HIDDEN_BONUS: f64 = 0.2;
const MIN_VELOCITY: f64 = 0.5;
const SLIDER_MULTIPLIER: f64 = 1.3;
const MIN_ANGLE_MULTIPLIER: f64 = 0.2;
fn evaluate_diff_of(
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
hidden: bool,
scaling_factor: f64,
) -> f64 {
if curr.base.is_spinner() {
return 0.0;
}
let osu_curr = curr;
let osu_hit_obj = curr.base;
let mut small_dist_nerf = 1.0;
let mut cumulative_strain_time = 0.0;
let mut result = 0.0;
let mut last_obj = osu_curr;
let mut angle_repeat_count = 0.0;
// * This is iterating backwards in time from the current object.
for i in 0..curr.idx.min(10) {
let curr_obj = if let Some(curr_obj) = previous(diff_objects, curr.idx, i) {
curr_obj
} else {
break;
};
let curr_hit_obj = curr_obj.base;
if !curr_obj.base.is_spinner() {
let jump_dist = (osu_hit_obj.pos - curr_hit_obj.end_pos()).length() as f64;
cumulative_strain_time += last_obj.strain_time;
// * We want to nerf objects that can be easily seen within the Flashlight circle radius.
if i == 0 {
small_dist_nerf = (jump_dist / 75.0).min(1.0);
}
// * We also want to nerf stacks so that only the first object of the stack is accounted for.
let stack_nerf = ((curr_obj.dists.lazy_jump_dist / scaling_factor) / 25.0).min(1.0);
// * Bonus based on how visible the object is.
let opacity_bonus = 1.0
+ Self::MAX_OPACITY_BONUS
* (1.0 - osu_curr.opacity_at(curr_hit_obj.start_time, hidden));
result += stack_nerf * opacity_bonus * scaling_factor * jump_dist
/ cumulative_strain_time;
if let Some((curr_obj_angle, osu_curr_angle)) =
curr_obj.dists.angle.zip(osu_curr.dists.angle)
{
// * Objects further back in time should count less for the nerf.
if (curr_obj_angle - osu_curr_angle).abs() < 0.02 {
angle_repeat_count += (1.0 - 0.1 * i as f64).max(0.0);
}
}
}
last_obj = curr_obj;
}
let base = small_dist_nerf * result;
result = base * base;
// * Additional bonus for Hidden due to there being no approach circles.
if hidden {
result *= 1.0 + Self::HIDDEN_BONUS;
}
// * Nerf patterns with repeated angles.
result *= Self::MIN_ANGLE_MULTIPLIER
+ (1.0 - Self::MIN_ANGLE_MULTIPLIER) / (angle_repeat_count + 1.0);
let mut slider_bonus = 0.0;
if let OsuObjectKind::Slider { nested_objects, .. } = &osu_curr.base.kind {
// * Invert the scaling factor to determine the true travel distance independent of circle size.
let pixel_travel_dist = osu_curr.dists.lazy_travel_dist as f64 / scaling_factor;
// * Reward sliders based on velocity.
slider_bonus = ((pixel_travel_dist / osu_curr.dists.travel_time as f64
- Self::MIN_VELOCITY)
.max(0.0))
.sqrt();
// * Longer sliders require more memorisation.
slider_bonus *= pixel_travel_dist;
// * Nerf sliders with repeats, as less memorisation is required.
let repeat_count = nested_objects.iter().fold(0, |count, nested| {
count + matches!(nested.kind, NestedObjectKind::Repeat) as usize
});
if repeat_count > 0 {
slider_bonus /= (repeat_count + 1) as f64;
}
}
result += slider_bonus * Self::SLIDER_MULTIPLIER;
result
}
}
+166
View File
@@ -0,0 +1,166 @@
mod aim;
mod flashlight;
mod speed;
use std::{any::Any, cmp::Ordering, mem};
pub(crate) use self::{aim::Aim, flashlight::Flashlight, speed::Speed};
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>;
}
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());
}
fn save_curr_peak(&mut self) {
let peak = *self.curr_section_peak();
self.strain_peaks_mut().push(peak);
}
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;
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
}
}
fn previous<'map, 'objects>(
diff_objects: &'objects [OsuDifficultyObject<'map>],
curr: usize,
backwards_idx: usize,
) -> Option<&'objects OsuDifficultyObject<'map>> {
curr.checked_sub(backwards_idx + 1)
.and_then(|idx| diff_objects.get(idx))
}
fn previous_start_time(
diff_objects: &[OsuDifficultyObject<'_>],
curr: usize,
backwards_idx: usize,
) -> f64 {
previous(diff_objects, curr, backwards_idx).map_or(0.0, |h| h.start_time)
}
fn next<'map, 'objects>(
diff_objects: &'objects [OsuDifficultyObject<'map>],
curr: usize,
forwards_idx: usize,
) -> Option<&'objects OsuDifficultyObject<'map>> {
diff_objects.get(curr + (forwards_idx + 1))
}
+331
View File
@@ -0,0 +1,331 @@
use std::{any::Any, cmp::Ordering, f64::consts::PI, mem};
use crate::osu::difficulty_object::OsuDifficultyObject;
use super::{next, previous, previous_start_time, OsuStrainSkill, Skill, StrainSkill};
#[derive(Clone)]
pub(crate) struct Speed {
curr_strain: f64,
curr_section_peak: f64,
curr_section_end: f64,
curr_rhythm: f64,
strain_peaks: Vec<f64>,
object_strains: Vec<f64>,
}
impl Speed {
const SKILL_MULTIPLIER: f64 = 1375.0;
const STRAIN_DECAY_BASE: f64 = 0.3;
pub(crate) fn new() -> Self {
Self {
curr_strain: 0.0,
curr_section_peak: 0.0,
curr_section_end: 0.0,
curr_rhythm: 0.0,
strain_peaks: Vec::new(),
object_strains: Vec::new(),
}
}
fn strain_decay(ms: f64) -> f64 {
Self::STRAIN_DECAY_BASE.powf(ms / 1000.0)
}
pub(crate) fn relevant_note_count(&self) -> f64 {
self.object_strains
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
.copied()
.filter(|&n| n > 0.0)
.map_or(0.0, |max_strain| {
self.object_strains.iter().fold(0.0, |sum, strain| {
sum + (1.0 + (-(strain / max_strain * 12.0 - 6.0)).exp()).recip()
})
})
}
}
impl Skill for Speed {
fn process(
&mut self,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
hit_window: f64,
) {
<Self as StrainSkill>::process(self, curr, diff_objects, hit_window)
}
fn difficulty_value(&mut self) -> f64 {
<Self as OsuStrainSkill>::difficulty_value(self)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn take_strain_peaks(&mut self) -> Vec<f64> {
mem::take(&mut self.strain_peaks)
}
}
impl StrainSkill for Speed {
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
fn strain_value_at(
&mut self,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
hit_window: f64,
) -> f64 {
self.curr_strain *= Self::strain_decay(curr.strain_time);
self.curr_strain += SpeedEvaluator::evaluate_diff_of(curr, diff_objects, hit_window)
* Self::SKILL_MULTIPLIER;
self.curr_rhythm = RhythmEvaluator::evaluate_diff_of(curr, diff_objects, hit_window);
let total_strain = self.curr_strain * self.curr_rhythm;
self.object_strains.push(total_strain);
total_strain
}
fn calculate_initial_strain(
&self,
time: f64,
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
) -> f64 {
(self.curr_strain * self.curr_rhythm)
* Self::strain_decay(time - previous_start_time(diff_objects, curr.idx, 0))
}
fn difficulty_value(&mut self) -> f64 {
<Self as OsuStrainSkill>::difficulty_value(self)
}
}
impl OsuStrainSkill for Speed {
const REDUCED_SECTION_COUNT: usize = 5;
const DIFFICULTY_MULTIPLER: f64 = 1.04;
}
struct SpeedEvaluator;
impl SpeedEvaluator {
const SINGLE_SPACING_THRESHOLD: f64 = 125.0;
const MIN_SPEED_BONUS: f64 = 75.0; // ~200BPM
const SPEED_BALANCING_FACTOR: f64 = 40.;
fn evaluate_diff_of(
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
hit_window: f64,
) -> f64 {
if curr.base.is_spinner() {
return 0.0;
}
// * derive strainTime for calculation
let osu_curr_obj = curr;
let osu_prev_obj = previous(diff_objects, curr.idx, 0);
let osu_next_obj = next(diff_objects, curr.idx, 0);
let mut strain_time = curr.strain_time;
let mut doubletapness = 1.0;
// * Nerf doubletappable doubles.
if let Some(osu_next_obj) = osu_next_obj {
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_base = (curr_delta_time / hit_window).min(1.0);
let window_ratio = window_ratio_base * window_ratio_base;
doubletapness = speed_ratio.powf(1.0 - window_ratio);
}
// * 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.
strain_time /= ((strain_time / hit_window) / 0.93).clamp(0.92, 1.0);
// * derive speedBonus for calculation
let speed_bonus = if strain_time < Self::MIN_SPEED_BONUS {
let base = (Self::MIN_SPEED_BONUS - strain_time) / Self::SPEED_BALANCING_FACTOR;
1.0 + 0.75 * base * base
} else {
1.0
};
let travel_dist = osu_prev_obj.map_or(0.0, |obj| obj.dists.travel_dist);
let dist =
Self::SINGLE_SPACING_THRESHOLD.min(travel_dist + osu_curr_obj.dists.min_jump_dist);
(speed_bonus + speed_bonus * (dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.5))
* doubletapness
/ strain_time
}
}
struct RhythmEvaluator;
impl RhythmEvaluator {
// * 5 seconds of calculatingRhythmBonus max.
const HISTORY_TIME_MAX: u32 = 5000;
const RHYTHM_MULTIPLIER: f64 = 0.75;
fn evaluate_diff_of(
curr: &OsuDifficultyObject<'_>,
diff_objects: &[OsuDifficultyObject<'_>],
hit_window: f64,
) -> f64 {
if curr.base.is_spinner() {
return 0.0;
}
let mut prev_island_size = 0;
let mut rhythm_complexity_sum = 0.0;
let mut island_size = 1;
// * store the ratio of the current start of an island to buff for tighter rhythms
let mut start_ratio = 0.0;
let mut first_delta_switch = false;
let historical_note_count = curr.idx.min(32);
let mut rhythm_start = 0;
while previous(diff_objects, curr.idx, rhythm_start)
.filter(|prev| {
rhythm_start + 2 < historical_note_count
&& curr.start_time - prev.start_time < Self::HISTORY_TIME_MAX as f64
})
.is_some()
{
rhythm_start += 1;
}
for i in (1..=rhythm_start).rev() {
let (curr_obj, prev_obj, last_obj) = if let Some(((curr, prev), last)) =
previous(diff_objects, curr.idx, i - 1)
.zip(previous(diff_objects, curr.idx, i))
.zip(previous(diff_objects, curr.idx, i + 1))
{
(curr, prev, last)
} else {
break;
};
// * scales note 0 to 1 from history to now
let mut curr_historical_decay = (Self::HISTORY_TIME_MAX as f64
- (curr.start_time - curr_obj.start_time))
/ Self::HISTORY_TIME_MAX as f64;
// * either we're limited by time or limited by object count.
curr_historical_decay = curr_historical_decay
.min((historical_note_count - i) as f64 / historical_note_count as f64);
let curr_delta = curr_obj.strain_time;
let prev_delta = prev_obj.strain_time;
let last_delta = last_obj.strain_time;
// * fancy function to calculate rhythmbonuses.
let base = (PI / (prev_delta.min(curr_delta) / prev_delta.max(curr_delta))).sin();
let curr_ratio = 1.0 + 6.0 * (base * base).min(0.5);
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
.max(0.0))
/ (hit_window * 0.3))
.min(1.0);
println!("window_penalty: prev={prev_delta} | curr={curr_delta} | window={hit_window} => {window_penalty}");
window_penalty = window_penalty.min(1.0);
let mut effective_ratio = window_penalty * curr_ratio;
if first_delta_switch {
if !(prev_delta > 1.25 * curr_delta || prev_delta * 1.25 < curr_delta) {
if island_size < 7 {
// * island is still progressing, count size.
island_size += 1;
}
} else {
// * bpm change is into slider, this is easy acc window
if curr_obj.base.is_slider() {
effective_ratio *= 0.125;
}
// * bpm change was from a slider, this is easier typically than circle -> circle
if prev_obj.base.is_slider() {
effective_ratio *= 0.25;
}
// * repeated island size (ex: triplet -> triplet)
if prev_island_size == island_size {
effective_ratio *= 0.25;
}
// * repeated island polartiy (2 -> 4, 3 -> 5)
if prev_island_size % 2 == island_size % 2 {
effective_ratio *= 0.5;
}
// * 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 {
effective_ratio *= 0.125;
}
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
* curr_historical_decay
* ((4 + island_size) as f64).sqrt()
/ 2.0
* ((4 + prev_island_size) as f64).sqrt()
/ 2.0;
start_ratio = effective_ratio;
// * log the last island size.
prev_island_size = island_size;
// * we're slowing down, stop counting
if prev_delta * 1.25 < curr_delta {
// * if we're speeding up, this stays true and we keep counting island size.
first_delta_switch = false;
}
island_size = 1;
}
} else if prev_delta > 1.25 * curr_delta {
// * we want to be speeding up.
// * Begin counting island until we change speed again.
first_delta_switch = true;
start_ratio = effective_ratio;
island_size = 1;
}
}
// * produces multiplier that can be applied to strain. range [1, infinity) (not really though)
let res = (4.0 + rhythm_complexity_sum * Self::RHYTHM_MULTIPLIER).sqrt() / 2.0;
println!("res={res}");
res
}
}