osu: updated up to commit from 2021-10-27

This commit is contained in:
MaxOhn
2021-10-30 19:08:24 +02:00
parent 6ce71cf01c
commit d8f7608ad7
9 changed files with 528 additions and 352 deletions
+2
View File
@@ -1,5 +1,7 @@
## Upcoming
- osu: Updated up to commit [6944151486e677bfd11f2390163aca9161defbbf](https://github.com/ppy/osu/commit/6944151486e677bfd11f2390163aca9161defbbf) (2021-10-27)
# v0.2.3
- Reduced amount of required features of `async_std` and `async_tokio`
+3
View File
@@ -14,6 +14,7 @@ pub trait Mods: Copy {
const HD: u32 = 1 << 3;
const HR: u32 = 1 << 4;
const DT: u32 = 1 << 6;
const RX: u32 = 1 << 7;
const HT: u32 = 1 << 8;
const FL: u32 = 1 << 10;
const SO: u32 = 1 << 12;
@@ -28,6 +29,7 @@ pub trait Mods: Copy {
fn hd(self) -> bool;
fn hr(self) -> bool;
fn dt(self) -> bool;
fn rx(self) -> bool;
fn ht(self) -> bool;
fn fl(self) -> bool;
fn so(self) -> bool;
@@ -72,6 +74,7 @@ impl Mods for u32 {
impl_mods!(hd, HD);
impl_mods!(hr, HR);
impl_mods!(dt, DT);
impl_mods!(rx, RX);
impl_mods!(ht, HT);
impl_mods!(fl, FL);
impl_mods!(so, SO);
+7 -5
View File
@@ -10,13 +10,15 @@ pub use versions::*;
/// This data is necessary to calculate PP.
#[derive(Clone, Debug, Default)]
pub struct DifficultyAttributes {
pub stars: f32,
pub aim_strain: f32,
pub speed_strain: f32,
pub flashlight_rating: f32,
pub ar: f32,
pub od: f32,
pub speed_strain: f32,
pub aim_strain: f32,
pub flashlight_strain: f32,
pub max_combo: usize,
pub hp: f32,
pub n_circles: usize,
pub n_sliders: usize,
pub n_spinners: usize,
pub stars: f32,
pub max_combo: usize,
}
+98 -56
View File
@@ -42,6 +42,8 @@ pub struct OsuPP<'m> {
n50: Option<usize>,
n_misses: usize,
passed_objects: Option<usize>,
effective_misses: Option<usize>,
}
impl<'m> OsuPP<'m> {
@@ -59,6 +61,8 @@ impl<'m> OsuPP<'m> {
n50: None,
n_misses: 0,
passed_objects: None,
effective_misses: None,
}
}
@@ -279,9 +283,11 @@ impl<'m> OsuPP<'m> {
let total_hits = self.total_hits() as f32;
let mut multiplier = 1.12;
self.calculate_effective_misses(total_hits);
// NF penalty
if self.mods.nf() {
multiplier *= (1.0 - 0.02 * self.n_misses as f32).max(0.9);
multiplier *= (1.0 - 0.02 * self.effective_misses.map_or(0.0, |m| m as f32)).max(0.9);
}
// SO penalty
@@ -290,6 +296,14 @@ impl<'m> OsuPP<'m> {
multiplier *= 1.0 - (n_spinners as f32 / total_hits).powf(0.85);
}
// Relax penalty
if self.mods.rx() {
*self.effective_misses.as_mut().unwrap() +=
self.n100.unwrap_or(0) + self.n50.unwrap_or(0);
multiplier *= 0.6;
}
let aim_value = self.compute_aim_value(total_hits);
let speed_value = self.compute_speed_value(total_hits);
let acc_value = self.compute_accuracy_value(total_hits);
@@ -326,10 +340,10 @@ impl<'m> OsuPP<'m> {
aim_value *= len_bonus;
// Penalize misses
if self.n_misses > 0 {
let effective_misses = self.effective_misses.map_or(0, |m| m as i32);
if effective_misses > 0 {
aim_value *= 0.97
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
.powi(self.n_misses as i32);
* (1.0 - (effective_misses as f32 / total_hits).powf(0.775)).powi(effective_misses);
}
// Combo scaling
@@ -349,7 +363,7 @@ impl<'m> OsuPP<'m> {
let ar_total_hits_factor = (1.0 + (-(0.007 * (total_hits - 400.0))).exp()).recip();
let ar_bonus = 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
// HD bonus
// HD bonus (this would include the Blinds mod but it's currently not representable)
if self.mods.hd() {
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
}
@@ -376,10 +390,11 @@ impl<'m> OsuPP<'m> {
speed_value *= len_bonus;
// Penalize misses
if self.n_misses > 0 {
let effective_misses = self.effective_misses.map_or(0.0, |m| m as f32);
if effective_misses > 0.0 {
speed_value *= 0.97
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
.powf((self.n_misses as f32).powf(0.875));
* (1.0 - (effective_misses / total_hits).powf(0.775))
.powf(effective_misses.powf(0.875));
}
// Combo scaling
@@ -398,7 +413,7 @@ impl<'m> OsuPP<'m> {
speed_value *= 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
// HD bonus
// HD bonus (this would include the Blinds mod but it's currently not representable)
if self.mods.hd() {
speed_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
}
@@ -421,6 +436,10 @@ impl<'m> OsuPP<'m> {
}
fn compute_accuracy_value(&self, total_hits: f32) -> f32 {
if self.mods.rx() {
return 0.0;
}
let attributes = self.attributes.as_ref().unwrap();
let n_circles = attributes.n_circles as f32;
let n300 = self.n300.unwrap_or(0) as f32;
@@ -436,7 +455,7 @@ impl<'m> OsuPP<'m> {
// Bonus for many hitcircles
acc_value *= ((n_circles as f32 / 1000.0).powf(0.3)).min(1.15);
// HD bonus
// HD bonus (this would include the Blinds mod but it's currently not representable)
if self.mods.hd() {
acc_value *= 1.08;
}
@@ -450,53 +469,76 @@ impl<'m> OsuPP<'m> {
}
fn compute_flashlight_value(&self, total_hits: f32) -> f32 {
if self.mods.fl() {
let attributes = self.attributes.as_ref().unwrap();
// TD penalty
let raw_flashlight = if self.mods.td() {
attributes.flashlight_strain.powf(0.8)
} else {
attributes.flashlight_strain
};
let mut flashlight_value = raw_flashlight * raw_flashlight * 25.0;
// Add an additional bonus for HDFL
if self.mods.hd() {
flashlight_value *= 1.3;
}
// Penalize misses by assessing # of misses relative to the total # of objects.
// Default a 3% reduction for any # of misses
if self.n_misses > 0 {
flashlight_value *= 0.97
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
.powf((self.n_misses as f32).powf(0.875));
}
// Combo scaling
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
flashlight_value *=
((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
}
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
flashlight_value *= 0.7
+ 0.1 * (total_hits / 200.0).min(1.0)
+ (total_hits > 200.0) as u8 as f32
* (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
// Scale the aim value with accuracy _slightly_
flashlight_value *= 0.5 + self.acc.unwrap() / 2.0;
// It is important to also consider accuracy difficulty when doing that
flashlight_value *= 0.98 + attributes.od * attributes.od / 2500.0;
flashlight_value
} else {
0.0
if !self.mods.fl() {
return 0.0;
}
let attributes = self.attributes.as_ref().unwrap();
// TD penalty
let raw_flashlight = if self.mods.td() {
attributes.flashlight_rating.powf(0.8)
} else {
attributes.flashlight_rating
};
let mut flashlight_value = raw_flashlight * raw_flashlight * 25.0;
// Add an additional bonus for HDFL
if self.mods.hd() {
flashlight_value *= 1.3;
}
// Penalize misses by assessing # of misses relative to the total # of objects.
// Default a 3% reduction for any # of misses
let effective_misses = self.effective_misses.map_or(0.0, |m| m as f32);
if effective_misses > 0.0 {
flashlight_value *= 0.97
* (1.0 - (effective_misses / total_hits).powf(0.775))
.powf(effective_misses.powf(0.875));
}
// Combo scaling
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
flashlight_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
}
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
flashlight_value *= 0.7
+ 0.1 * (total_hits / 200.0).min(1.0)
+ (total_hits > 200.0) as u8 as f32 * (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
// Scale the aim value with accuracy _slightly_
flashlight_value *= 0.5 + self.acc.unwrap() / 2.0;
// It is important to also consider accuracy difficulty when doing that
flashlight_value *= 0.98 + attributes.od * attributes.od / 2500.0;
flashlight_value
}
fn calculate_effective_misses(&mut self, total_hits: f32) {
// Guess the number of misses + slider breaks from combo
let mut combo_based_misses: f32 = 0.0;
let attributes = self.attributes.as_ref().unwrap();
if attributes.n_sliders > 0 {
let full_combo_threshold =
attributes.max_combo as f32 - 0.1 * attributes.n_sliders as f32;
let f32_combo = self.combo.map(|c| c as f32);
if let Some(combo) = f32_combo.filter(|&c| c < full_combo_threshold) {
combo_based_misses = full_combo_threshold / combo.max(1.0);
}
}
// We're clamping misses because since it's derived from combo it
// can be higher than total hits and that breaks some calculations
combo_based_misses = combo_based_misses.min(total_hits);
self.effective_misses = Some(self.n_misses.max(combo_based_misses.floor() as usize))
}
#[inline]
+2 -6
View File
@@ -10,11 +10,7 @@ pub mod no_leniency;
#[cfg_attr(docsrs, doc(cfg(feature = "no_sliders_no_leniency")))]
pub mod no_sliders_no_leniency;
const OSU_OD_MAX: f32 = 20.0;
const OSU_OD_AVG: f32 = 50.0;
const OSU_OD_MIN: f32 = 80.0;
#[inline]
fn difficulty_range(od: f32) -> f32 {
super::super::difficulty_range(od, OSU_OD_MAX, OSU_OD_AVG, OSU_OD_MIN)
fn difficulty_range_od(od: f32) -> f32 {
super::super::difficulty_range(od, 20.0, 50.0, 80.0)
}
@@ -21,30 +21,36 @@ impl<'h> DifficultyObject<'h> {
scaling_factor: f32,
) -> Self {
let delta = base.time - prev.time;
let strain_time = delta.max(50.0);
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
let strain_time = delta.max(25.0);
let pos = base.pos;
let travel_dist = prev.travel_dist();
let prev_cursor_pos = prev.lazy_end_pos();
let jump_dist = if base.is_spinner() {
0.0
// We don't need to calculate either angle or distance
// when one of the last->curr objects is a spinner
let (jump_dist, angle) = if base.is_spinner() {
(0.0, None)
} else {
((pos - prev_cursor_pos) * scaling_factor).length()
let jump_dist = ((pos - prev_cursor_pos) * scaling_factor).length();
let angle = prev_prev.map(|prev_prev| {
let prev_prev_cursor_pos = prev_prev.lazy_end_pos();
let v1 = prev_prev_cursor_pos - prev.pos;
let v2 = pos - prev_cursor_pos;
let dot = v1.dot(v2);
let det = v1.x * v2.y - v1.y * v2.x;
det.atan2(dot).abs()
});
(jump_dist, angle)
};
let angle = prev_prev.map(|prev_prev| {
let prev_prev_cursor_pos = prev_prev.lazy_end_pos();
let v1 = prev_prev_cursor_pos - prev.pos;
let v2 = pos - prev_cursor_pos;
let dot = v1.dot(v2);
let det = v1.x * v2.y - v1.y * v2.x;
det.atan2(dot).abs()
});
Self {
base,
prev: prev_vals,
+99 -32
View File
@@ -5,6 +5,8 @@
#![cfg(feature = "no_leniency")]
use std::mem;
use super::super::DifficultyAttributes;
mod difficulty_object;
@@ -39,11 +41,12 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
let map_attributes = map.attributes().mods(mods);
let hitwindow = super::difficulty_range(map_attributes.od).floor() / map_attributes.clock_rate;
let od = (80.0 - hitwindow) / 6.0;
let hit_window = super::difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
let od = (80.0 - hit_window) / 6.0;
let mut diff_attributes = DifficultyAttributes {
ar: map_attributes.ar,
hp: map_attributes.hp,
od,
..Default::default()
};
@@ -88,7 +91,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
let mut skills = Vec::with_capacity(2 + fl as usize);
skills.push(Skill::new(SkillKind::Aim));
skills.push(Skill::new(SkillKind::speed()));
skills.push(Skill::new(SkillKind::speed(hit_window)));
if fl {
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
@@ -106,6 +109,10 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
while h.base.time > current_section_end {
for skill in skills.iter_mut() {
skill.start_new_section_from(current_section_end);
}
current_section_end += SECTION_LEN;
}
@@ -144,7 +151,12 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
}
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_rating = if mods.rx() {
0.0
} else {
skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
};
let flashlight_rating = if let Some(skill) = skills.get_mut(2) {
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
@@ -152,30 +164,61 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
0.0
};
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
let base_aim_performance = {
let base = 5.0 * (aim_rating / 0.0675).max(1.0) - 4.0;
base * base * base / 100_000.0
};
let base_speed_performance = {
let base = 5.0 * (speed_rating / 0.0675).max(1.0) - 4.0;
base * base * base / 100_000.0
};
let base_flashlight_performance = if fl {
flashlight_rating * flashlight_rating * 25.0
} else {
0.0
};
let base_performance = (base_aim_performance.powf(1.1)
+ base_speed_performance.powf(1.1)
+ base_flashlight_performance.powf(1.1))
.powf(1.0 / 1.1);
let star_rating = if base_performance > 0.00001 {
1.12_f32.cbrt()
* 0.027
* ((100_000.0 / (1.0_f32 / 1.1).exp2() * base_performance).cbrt() + 4.0)
} else {
0.0
};
diff_attributes.n_circles = map.n_circles as usize;
diff_attributes.n_spinners = map.n_spinners as usize;
diff_attributes.stars = stars;
diff_attributes.speed_strain = speed_rating;
diff_attributes.aim_strain = aim_rating;
diff_attributes.flashlight_strain = flashlight_rating;
diff_attributes.speed_strain = speed_rating;
diff_attributes.flashlight_rating = flashlight_rating;
diff_attributes.n_circles = map.n_circles as usize;
diff_attributes.n_sliders = map.n_sliders as usize;
diff_attributes.n_spinners = map.n_spinners as usize;
diff_attributes.stars = star_rating;
StarResult::Osu(diff_attributes)
}
// TODO: Add flashlight strains
/// Essentially the same as the `stars` function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let map_attributes = map.attributes().mods(mods);
let hitwindow = super::difficulty_range(map_attributes.od).floor() / map_attributes.clock_rate;
let od = (80.0 - hitwindow) / 6.0;
let hit_window =
super::difficulty_range_od(map_attributes.od).floor() / map_attributes.clock_rate;
let od = (80.0 - hit_window) / 6.0;
let mut diff_attributes = DifficultyAttributes {
ar: map_attributes.ar,
hp: map_attributes.hp,
od,
..Default::default()
};
@@ -207,8 +250,15 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
)
});
let mut aim = Skill::new(SkillKind::Aim);
let mut speed = Skill::new(SkillKind::speed());
let fl = mods.fl();
let mut skills = Vec::with_capacity(2 + fl as usize);
skills.push(Skill::new(SkillKind::Aim));
skills.push(Skill::new(SkillKind::speed(hit_window)));
if fl {
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
};
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
@@ -222,11 +272,16 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
while h.base.time > current_section_end {
for skill in skills.iter_mut() {
skill.start_new_section_from(current_section_end);
}
current_section_end += SECTION_LEN;
}
aim.process(&h);
speed.process(&h);
for skill in skills.iter_mut() {
skill.process(&h);
}
prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
@@ -237,31 +292,43 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
while h.base.time > current_section_end {
aim.save_current_peak();
aim.start_new_section_from(current_section_end);
speed.save_current_peak();
speed.start_new_section_from(current_section_end);
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
}
current_section_end += SECTION_LEN;
}
aim.process(&h);
speed.process(&h);
prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr;
}
aim.save_current_peak();
speed.save_current_peak();
for skill in skills.iter_mut() {
skill.save_current_peak();
}
let strains = aim
.strain_peaks
.into_iter()
.zip(speed.strain_peaks.into_iter())
.map(|(aim, speed)| aim + speed)
.collect();
let mut speed_strains = skills.pop().unwrap().strain_peaks;
let mut aim_strains = skills.pop().unwrap().strain_peaks;
let strains = if let Some(mut flashlight_strains) = skills.pop().map(|s| s.strain_peaks) {
mem::swap(&mut speed_strains, &mut aim_strains);
mem::swap(&mut aim_strains, &mut flashlight_strains);
aim_strains
.into_iter()
.zip(speed_strains)
.zip(flashlight_strains)
.map(|((aim, speed), flashlight)| aim + speed + flashlight)
.collect()
} else {
aim_strains
.into_iter()
.zip(speed_strains)
.map(|(aim, speed)| aim + speed)
.collect()
};
Strains {
section_length: SECTION_LEN,
+47 -76
View File
@@ -1,25 +1,14 @@
use crate::math_util;
use super::{DifficultyObject, SkillKind};
use super::{skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKind};
use std::cmp::Ordering;
const SPEED_SKILL_MULTIPLIER: f32 = 1400.0;
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
const SPEED_DECAY_WEIGHT: f32 = 0.9;
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
const AIM_DECAY_WEIGHT: f32 = 0.9;
const FLASHLIGHT_SKILL_MULTIPLIER: f32 = 0.13;
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15;
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0;
pub(crate) struct Skill {
current_strain: f32,
current_section_peak: f32,
curr_strain: f32,
curr_section_peak: f32,
kind: SkillKind,
pub(crate) strain_peaks: Vec<f32>,
@@ -31,8 +20,8 @@ impl Skill {
#[inline]
pub(crate) fn new(kind: SkillKind) -> Self {
Self {
current_strain: 1.0,
current_section_peak: 1.0,
curr_strain: 1.0,
curr_section_peak: 0.0,
kind,
strain_peaks: Vec::with_capacity(128),
@@ -41,39 +30,33 @@ impl Skill {
}
}
pub(crate) fn process(&mut self, curr: &DifficultyObject) {
self.kind.pre_process();
self.curr_section_peak = self.strain_value_at(curr).max(self.curr_section_peak);
self.prev_time = Some(curr.base.time);
self.kind.post_process(curr);
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
self.strain_peaks.push(self.current_section_peak);
self.strain_peaks.push(self.curr_section_peak);
}
#[inline]
pub(crate) fn start_new_section_from(&mut self, time: f32) {
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
}
#[inline]
pub(crate) fn process(&mut self, current: &DifficultyObject) {
self.kind.pre_process();
self.current_strain *= self.strain_decay(current.delta);
self.current_strain += self.kind.strain_value_of(&current) * self.skill_multiplier();
self.current_section_peak = self
.current_section_peak
.max(self.total_current_strain(current));
self.prev_time.replace(current.base.time);
self.kind.post_process(current);
}
#[inline]
fn total_current_strain(&self, current: &DifficultyObject) -> f32 {
self.kind.total_current_strain(self.current_strain, current)
// The maximum strain of the new section is not zero by default
self.curr_section_peak = self.calculate_initial_strain(time);
}
pub(crate) fn difficulty_value(&mut self) -> f32 {
// TODO: Remove
// for (i, strain) in self.strain_peaks.iter().enumerate() {
// println!("[{}] {}", i, strain);
// }
let mut difficulty = 0.0;
let mut weight = 1.0;
let decay_weight = self.decay_weight();
let decay_weight = self.kind.decay_weight();
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
let reduced_section_count_f32 = reduced_section_count as f32;
@@ -81,12 +64,9 @@ impl Skill {
self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for (i, strain) in self
.strain_peaks
.iter_mut()
.take(reduced_section_count)
.enumerate()
{
let peaks = self.strain_peaks.iter_mut();
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
let clamped = (i as f32 / reduced_section_count_f32).clamp(0.0, 1.0);
let scale = (math_util::lerp(1.0, 10.0, clamped)).log10();
*strain *= math_util::lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
@@ -95,7 +75,7 @@ impl Skill {
self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in self.strain_peaks.iter() {
for &strain in &self.strain_peaks {
difficulty += strain * weight;
weight *= decay_weight;
}
@@ -103,40 +83,31 @@ impl Skill {
difficulty * difficulty_multiplier
}
#[inline]
fn skill_multiplier(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER,
pub(crate) fn calculate_initial_strain(&self, time: f32) -> f32 {
let prev_time = self.prev_time.unwrap_or(0.0);
let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time);
match &self.kind {
SkillKind::Aim | SkillKind::Flashlight { .. } => decayed_strain,
SkillKind::Speed { curr_rhythm, .. } => curr_rhythm * decayed_strain,
}
}
#[inline]
fn strain_decay_base(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
pub(crate) fn strain_value_at(&mut self, curr: &DifficultyObject) -> f32 {
self.curr_strain *= self.kind.strain_decay(curr.delta);
self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier();
match &mut self.kind {
SkillKind::Aim | SkillKind::Flashlight { .. } => self.curr_strain,
SkillKind::Speed {
curr_rhythm,
history,
hit_window,
} => {
*curr_rhythm = calculate_speed_rhythm_bonus(curr, history, *hit_window);
self.curr_strain * *curr_rhythm
}
}
}
#[inline]
fn decay_weight(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_DECAY_WEIGHT,
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
}
}
#[inline]
fn peak_strain(&self, delta_time: f32) -> f32 {
self.current_strain * self.strain_decay(delta_time)
}
#[inline]
fn strain_decay(&self, ms: f32) -> f32 {
self.strain_decay_base().powf(ms / 1000.0)
}
}
+248 -161
View File
@@ -1,47 +1,76 @@
use std::collections::VecDeque;
use std::{collections::VecDeque, f32::consts::PI, iter};
use crate::parse::Pos2;
use crate::{math_util, parse::Pos2};
use super::DifficultyObject;
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
const SPEED_ANGLE_BONUS_BEGIN: f32 = 5.0 * std::f32::consts::FRAC_PI_6;
const PI_OVER_4: f32 = std::f32::consts::FRAC_PI_4;
const PI_OVER_2: f32 = std::f32::consts::FRAC_PI_2;
const MIN_SPEED_BONUS: f32 = 75.0;
const MAX_SPEED_BONUS: f32 = 45.0;
const SPEED_BALANCING_FACTOR: f32 = 40.0;
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
const TIMING_THRESHOLD: f32 = 107.0;
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
const AIM_DECAY_WEIGHT: f32 = 0.9;
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
const AIM_REDUCED_SECTION_COUNT: usize = 10;
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
const SPEED_SKILL_MULTIPLIER: f32 = 1375.0;
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
const SPEED_DECAY_WEIGHT: f32 = 0.9;
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
const FLASHLIGHT_HISTORY_LENGTH: usize = 10;
const SPEED_HISTORY_LENGTH: usize = 32;
const SPEED_HISTORY_TIME_MAX: f32 = 5000.0;
const SPEED_RHYTHM_MULTIPLIER: f32 = 0.75;
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
const FLASHLIGHT_SKILL_MULTIPLIER: f32 = 0.15;
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15;
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0;
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f32 = 1.06;
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
const SPEED_HISTORY_TIME_MAX: f32 = 3000.0;
const SPEED_RHYTHM_MULTIPLIER: f32 = 1.5;
const FLASHLIGHT_HISTORY_LENGTH: usize = 10;
pub(crate) struct FlashlightHistoryEntry {
is_spinner: bool,
end_pos: Pos2,
is_spinner: bool,
jump_dist: f32,
strain_time: f32,
}
impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
fn from(h: &DifficultyObject) -> Self {
Self {
end_pos: h.base.end_pos(),
is_spinner: h.base.is_spinner(),
jump_dist: h.jump_dist,
strain_time: h.strain_time,
}
}
}
pub(crate) struct SpeedHistoryEntry {
is_slider: bool,
start_time: f32,
strain_time: f32,
}
impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
fn from(h: &DifficultyObject) -> Self {
Self {
is_slider: h.base.is_slider(),
start_time: h.base.time,
strain_time: h.strain_time,
}
}
}
pub(crate) enum SkillKind {
Aim,
Flashlight {
@@ -49,7 +78,9 @@ pub(crate) enum SkillKind {
scaling_factor: f32,
},
Speed {
curr_rhythm: f32,
history: VecDeque<SpeedHistoryEntry>,
hit_window: f32,
},
}
@@ -61,9 +92,11 @@ impl SkillKind {
}
}
pub(crate) fn speed() -> Self {
pub(crate) fn speed(hit_window: f32) -> Self {
Self::Speed {
curr_rhythm: 1.0,
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH),
hit_window,
}
}
@@ -71,99 +104,92 @@ impl SkillKind {
match self {
Self::Aim => {}
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
Self::Speed { history } => history.truncate(SPEED_HISTORY_LENGTH),
Self::Speed { history, .. } => history.truncate(SPEED_HISTORY_LENGTH),
}
}
pub(crate) fn post_process(&mut self, current: &DifficultyObject) {
match self {
Self::Aim => {}
Self::Flashlight { history, .. } => {
let entry = FlashlightHistoryEntry {
is_spinner: current.base.is_spinner(),
end_pos: current.base.end_pos(),
strain_time: current.strain_time,
};
history.push_front(entry);
}
Self::Speed { history } => {
let entry = SpeedHistoryEntry {
is_slider: current.base.is_slider(),
start_time: current.base.time,
strain_time: current.strain_time,
};
history.push_front(entry);
}
Self::Flashlight { history, .. } => history.push_front(current.into()),
Self::Speed { history, .. } => history.push_front(current.into()),
}
}
pub(crate) fn strain_value_of(&self, current: &DifficultyObject) -> f32 {
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject) -> f32 {
match self {
Self::Aim => {
if current.base.is_spinner() {
if curr.base.is_spinner() {
return 0.0;
}
let mut result = 0.0;
let mut aim_strain = 0.0;
if let Some((prev_jump_dist, prev_strain_time)) = current.prev {
if let Some(angle) = current.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
if let Some((prev_jump_dist, prev_strain_time)) = curr.prev {
if let Some(angle) = curr.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
let scale = 90.0;
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
* (prev_jump_dist - scale).max(0.0)
* (current.jump_dist - scale).max(0.0))
* (curr.jump_dist - scale).max(0.0))
.sqrt();
result = 1.4 * apply_diminishing_exp(angle_bonus.max(0.0))
aim_strain = 1.4 * apply_diminishing_exp(angle_bonus.max(0.0))
/ (TIMING_THRESHOLD).max(prev_strain_time)
}
}
let jump_dist_exp = apply_diminishing_exp(current.jump_dist);
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
let jump_dist_exp = apply_diminishing_exp(curr.jump_dist);
let travel_dist_exp = apply_diminishing_exp(curr.travel_dist);
let dist_exp =
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / current.strain_time)
let res = (aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / curr.strain_time);
res
}
Self::Flashlight {
history,
scaling_factor,
} => {
if current.base.is_spinner() {
if curr.base.is_spinner() {
return 0.0;
}
let mut small_dist_nerf = 1.0;
let mut result = 0.0;
let mut cumulative_strain_time = 0.0;
let mut history = history.iter();
if let Some(entry) = history.next() {
if !entry.is_spinner {
let jump_dist = (current.base.pos - entry.end_pos).length();
cumulative_strain_time += entry.strain_time;
if let Some(prev) = history.next() {
// Handle first entry distinctly for slight optimization
if !prev.is_spinner {
let jump_dist = (curr.base.pos - prev.end_pos).length();
cumulative_strain_time += prev.strain_time;
// We want to nerf objects that can be easily seen within the Flashlight circle radius
if jump_dist < 50.0 {
small_dist_nerf = jump_dist / 50.0;
}
small_dist_nerf = (jump_dist / 75.0).min(1.0);
result += scaling_factor * jump_dist / cumulative_strain_time;
// We also want to nerf stacks so that only the first object of the stack is accounted for
// -- since jump distance is 0 on stacked notes in this version, approximate value as 0.2
let stack_nerf =
((prev.jump_dist / scaling_factor) / 25.0).min(1.0).max(0.2);
result += stack_nerf * scaling_factor * jump_dist / cumulative_strain_time;
}
for (i, entry) in (1..).zip(history) {
if !entry.is_spinner {
let jump_dist = (current.base.pos - entry.end_pos).length();
cumulative_strain_time += entry.strain_time;
let factors = iter::successors(Some(0.8), |s| Some(s * 0.8));
result += 0.8_f32.powi(i) * scaling_factor * jump_dist
for (factor, prev) in factors.zip(history) {
if !prev.is_spinner {
let jump_dist = (curr.base.pos - prev.end_pos).length();
cumulative_strain_time += prev.strain_time;
let stack_nerf =
((prev.jump_dist / scaling_factor) / 25.0).min(1.0).max(0.2);
result += factor * stack_nerf * scaling_factor * jump_dist
/ cumulative_strain_time;
}
}
@@ -173,57 +199,45 @@ impl SkillKind {
result * result
}
Self::Speed { .. } => {
if current.base.is_spinner() {
Self::Speed {
history,
hit_window,
..
} => {
if curr.base.is_spinner() {
return 0.0;
}
let dist = SINGLE_SPACING_TRESHOLD.min(current.travel_dist + current.jump_dist);
let delta_time = MAX_SPEED_BONUS.max(current.delta);
let mut strain_time = curr.strain_time;
let hit_window_full = hit_window * 2.0;
let speed_window_ratio = strain_time / hit_window_full;
let prev = history.front();
// Aim to nerf cheesy rhythms (very fast consecutive doubles with large delta times between)
if let Some(prev) =
prev.filter(|p| strain_time < hit_window_full && p.strain_time > strain_time)
{
strain_time =
math_util::lerp(prev.strain_time, strain_time, speed_window_ratio);
}
// Cap delta time to the OD 300 hit window
// 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_full / 0.93).clamp(0.92, 1.0);
// Derive speed bonus for calculation
let mut speed_bonus = 1.0;
if delta_time < MIN_SPEED_BONUS {
let exp_base = (MIN_SPEED_BONUS - delta_time) / SPEED_BALANCING_FACTOR;
speed_bonus = 1.0 + exp_base * exp_base;
if strain_time < MIN_SPEED_BONUS {
let base = (MIN_SPEED_BONUS - strain_time) / SPEED_BALANCING_FACTOR;
speed_bonus = 1.0 + 0.75 * base * base;
}
let mut angle_bonus = 1.0;
let dist = SINGLE_SPACING_TRESHOLD.min(curr.travel_dist + curr.jump_dist);
if let Some(angle) = current.angle.filter(|a| *a < SPEED_ANGLE_BONUS_BEGIN) {
let exp_base = (1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)).sin();
angle_bonus = 1.0 + exp_base * exp_base / 3.57;
if angle < PI_OVER_2 {
angle_bonus = 1.28;
if dist < 90.0 && angle < PI_OVER_4 {
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
} else if dist < 90.0 {
angle_bonus += (1.0 - angle_bonus)
* ((90.0 - dist) / 10.0).min(1.0)
* ((PI_OVER_2 - angle) / PI_OVER_4).sin();
}
}
}
(1.0 + (speed_bonus - 1.0) * 0.75)
* angle_bonus
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ current.strain_time
}
}
}
pub(crate) fn total_current_strain(
&self,
current_strain: f32,
current: &DifficultyObject,
) -> f32 {
match self {
SkillKind::Aim | SkillKind::Flashlight { .. } => current_strain,
SkillKind::Speed { history } => {
current_strain * calculate_speed_rhythm_bonus(history, current.base.time)
(speed_bonus + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ strain_time
}
}
}
@@ -239,65 +253,143 @@ impl SkillKind {
Self::Speed { .. } => (SPEED_REDUCED_SECTION_COUNT, SPEED_DIFFICULTY_MULTIPLIER),
}
}
}
fn calculate_speed_rhythm_bonus(history: &VecDeque<SpeedHistoryEntry>, start_time: f32) -> f32 {
let mut previous_island_size = usize::MAX;
let mut island_times = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let mut island_size = 0;
let mut first_delta_switch = false;
for (prev, curr) in history.iter().skip(1).zip(history).rev() {
let prev_delta = prev.strain_time;
let curr_delta = curr.strain_time;
let mut effective_ratio = prev_delta.min(curr_delta) / prev_delta.max(curr_delta);
if effective_ratio > 0.5 {
effective_ratio = 0.5 + (effective_ratio - 0.5) * 5.0;
}
let curr_historical_decay = (SPEED_HISTORY_TIME_MAX - (start_time - curr.start_time))
.max(0.0)
/ SPEED_HISTORY_TIME_MAX;
if first_delta_switch {
if is_ratio_equal(1.0, prev_delta, curr_delta) {
island_size += 1;
} else {
if island_size > 6 {
island_size = 6;
}
if curr.is_slider {
effective_ratio /= 2.0;
}
if prev.is_slider {
effective_ratio *= 0.75;
}
if previous_island_size == island_size {
effective_ratio /= 0.5;
}
island_times[island_size] += effective_ratio * curr_historical_decay;
previous_island_size = island_size;
if prev_delta * 1.25 < curr_delta {
first_delta_switch = false;
}
island_size = 6;
}
} else if prev_delta > 1.25 * curr_delta {
first_delta_switch = true;
island_size = 0;
#[inline]
pub(crate) fn skill_multiplier(&self) -> f32 {
match self {
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER,
}
}
let rhythm_complexity_sum: f32 = island_times.iter().sum();
#[inline]
pub(crate) fn strain_decay_base(&self) -> f32 {
match self {
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
}
}
#[inline]
pub(crate) fn decay_weight(&self) -> f32 {
match self {
SkillKind::Aim => AIM_DECAY_WEIGHT,
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
}
}
#[inline]
pub(crate) fn strain_decay(&self, ms: f32) -> f32 {
self.strain_decay_base().powf(ms / 1000.0)
}
}
pub(crate) fn calculate_speed_rhythm_bonus(
current: &DifficultyObject,
history: &VecDeque<SpeedHistoryEntry>,
hit_window: f32,
) -> f32 {
if current.base.is_spinner() {
return 0.0;
}
let mut prev_island_size = 0;
let mut rhythm_complexity_sum = 0.0;
let mut island_size = 1;
let mut first_delta_switch = false;
let adjusted_hit_window = hit_window * 0.6;
let history_len = history.len() as f32;
// Store the ratio of the current start of an island to buff for tighter rhythms
let mut start_ratio = 0.0;
let currs = history.iter();
let prevs = history.iter().skip(1);
let lasts = history.iter().skip(2);
for (((prev, curr), last), i) in prevs.zip(currs).zip(lasts).rev().zip(2..) {
let mut curr_historical_decay =
(SPEED_HISTORY_TIME_MAX - (current.base.time - curr.start_time)).max(0.0)
/ SPEED_HISTORY_TIME_MAX;
if curr_historical_decay.abs() > f32::EPSILON {
// Either we're limited by time or limited by object count
curr_historical_decay = curr_historical_decay.min(i as f32 / history_len);
let curr_delta = curr.strain_time;
let prev_delta = prev.strain_time;
let last_delta = last.strain_time;
// Fancy function to calculate rhythm bonuses
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 lower_penalty = ((prev_delta - curr_delta).abs() - adjusted_hit_window).max(0.0);
let window_penalty = (lower_penalty / adjusted_hit_window).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_size += 1;
}
} else {
if curr.is_slider {
// bpm change is into slider, this is easy acc window
effective_ratio *= 0.125;
}
if prev.is_slider {
// bpm change was from a slider, this is easier typically than circle -> circle
effective_ratio *= 0.25;
}
if prev_island_size == island_size {
// repeated island size (ex: triplet -> triplet)
effective_ratio *= 0.25;
}
if prev_island_size % 2 == island_size % 2 {
// repeated island polarity (2 -> 4, 3 -> 5)
effective_ratio *= 0.5;
}
if last_delta > prev_delta + 10.0 && prev_delta > curr_delta + 10.0 {
// previous increase happened a note ago, 1/1 -> 1/2-1/4, don't want to buff this
effective_ratio *= 0.125;
}
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
* curr_historical_decay
* ((4 + island_size) as f32).sqrt()
* ((4 + prev_island_size) as f32).sqrt()
/ 4.0;
start_ratio = effective_ratio;
prev_island_size = island_size;
island_size = 1;
// 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;
}
}
} 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)
(4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
}
@@ -305,8 +397,3 @@ fn calculate_speed_rhythm_bonus(history: &VecDeque<SpeedHistoryEntry>, start_tim
fn apply_diminishing_exp(val: f32) -> f32 {
val.powf(0.99)
}
#[inline]
fn is_ratio_equal(ratio: f32, a: f32, b: f32) -> bool {
a + 15.0 > ratio * b && a - 15.0 < ratio * b
}