feat: updated osu!standard difficulate calculation

This commit is contained in:
MaxOhn
2024-09-27 15:46:40 +02:00
parent daf718d74e
commit 3c57b9b409
18 changed files with 268 additions and 159 deletions
+2 -1
View File
@@ -17,7 +17,8 @@ sync = []
tracing = ["rosu-map/tracing"]
[dependencies]
rosu-map = { version = "0.1.1" }
# rosu-map = { version = "0.1.2" }
rosu-map = { git = "https://github.com/MaxOhn/rosu-map", branch = "pp-update" }
rosu-mods = { version = "0.1.0" }
[dev-dependencies]
+2 -2
View File
@@ -11,8 +11,8 @@ with emphasis on a precise translation to Rust for the most [accurate results](#
while also providing a significant [boost in performance](#speed).
Last commits of the ported code:
- [osu!lazer] : `7342fb7f51b34533a42bffda89c3d6c569cc69ce` (2022-10-11)
- [osu!tools] : `146d5916937161ef65906aa97f85d367035f3712` (2022-10-08)
- [osu!lazer] : `f08134f443b2cf255fd19c8bc3ef517b6a3bb8e3` (2024-09-23)
- [osu!tools] : `51965515eb4355fde0591728ef4d38eee119a964` (2024-09-01)
News posts of the latest gamemode updates:
- osu: <https://osu.ppy.sh/home/news/2022-09-30-changes-to-osu-sr-and-pp>
+4 -3
View File
@@ -1,7 +1,8 @@
use std::vec::Drain;
use rosu_map::section::hit_objects::{
CurveBuffers, PathControlPoint, SliderEvent, SliderEventType, SliderEventsIter,
use rosu_map::section::{
general::GameMode,
hit_objects::{CurveBuffers, PathControlPoint, SliderEvent, SliderEventType, SliderEventsIter},
};
use crate::{
@@ -41,7 +42,7 @@ impl<'a> JuiceStream<'a> {
point.slider_velocity
});
let path = slider.curve(&mut bufs.curve);
let path = slider.curve(GameMode::Catch, &mut bufs.curve);
let velocity_factor = JuiceStream::BASE_SCORING_DIST * slider_multiplier / beat_len;
let velocity = velocity_factor * slider_velocity;
+2 -2
View File
@@ -5,8 +5,8 @@
//! while also providing a significant [boost in performance](#speed).
//!
//! Last commits of the ported code:
//! - [osu!lazer] : `7342fb7f51b34533a42bffda89c3d6c569cc69ce` (2022-10-11)
//! - [osu!tools] : `146d5916937161ef65906aa97f85d367035f3712` (2022-10-08)
//! - [osu!lazer] : `f08134f443b2cf255fd19c8bc3ef517b6a3bb8e3` (2024-09-23)
//! - [osu!tools] : `51965515eb4355fde0591728ef4d38eee119a964` (2024-09-01)
//!
//! News posts of the latest gamemode updates:
//! - osu: <https://osu.ppy.sh/home/news/2022-09-30-changes-to-osu-sr-and-pp>
+1 -1
View File
@@ -108,7 +108,7 @@ fn convert(map: &mut Beatmap) {
last_values.pattern = new_pattern;
}
HitObjectKind::Slider(ref slider) => {
let curve = slider.curve(&mut curve_bufs);
let curve = slider.curve(GameMode::Mania, &mut curve_bufs);
let mut gen = DistanceObjectPatternGenerator::new(
&mut random,
+2 -2
View File
@@ -1,4 +1,4 @@
use rosu_map::section::hit_objects::CurveBuffers;
use rosu_map::section::{general::GameMode, hit_objects::CurveBuffers};
use crate::model::{
beatmap::Beatmap,
@@ -26,7 +26,7 @@ impl ManiaObject {
HitObjectKind::Slider(ref slider) => {
const BASE_SCORING_DIST: f32 = 100.0;
let dist = slider.curve(&mut params.curve_bufs).dist();
let dist = slider.curve(GameMode::Mania, &mut params.curve_bufs).dist();
let beat_len = params
.map
+10 -3
View File
@@ -1,6 +1,9 @@
use std::cmp::Ordering;
use rosu_map::section::hit_objects::{BorrowedCurve, CurveBuffers};
use rosu_map::section::{
general::GameMode,
hit_objects::{BorrowedCurve, CurveBuffers},
};
pub use rosu_map::{
section::hit_objects::{hit_samples::HitSoundType, PathControlPoint, PathType, SplineType},
@@ -76,8 +79,12 @@ impl Slider {
self.repeats + 1
}
pub(crate) fn curve<'a>(&self, bufs: &'a mut CurveBuffers) -> BorrowedCurve<'a> {
BorrowedCurve::new(&self.control_points, self.expected_dist, bufs)
pub(crate) fn curve<'a>(
&self,
mode: GameMode,
bufs: &'a mut CurveBuffers,
) -> BorrowedCurve<'a> {
BorrowedCurve::new(mode, &self.control_points, self.expected_dist, bufs)
}
}
+1
View File
@@ -175,6 +175,7 @@ impl_has_mod! {
fl: + Flashlight ["Flashlight"],
so: + SpunOut ["SpunOut"],
bl: - Blinds ["Blinds"],
tc: - Traceable ["Traceable"],
}
impl Default for GameMods {
+5 -5
View File
@@ -1,5 +1,7 @@
use std::{cmp, pin::Pin};
use skills::{flashlight::Flashlight, strain::OsuStrainSkill};
use crate::{
any::difficulty::{skills::Skill, Difficulty},
model::{beatmap::BeatmapAttributes, mods::GameMods},
@@ -174,13 +176,11 @@ impl DifficultyValues {
flashlight_rating *= 0.7;
}
let base_aim_performance =
(5.0 * (aim_rating / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0;
let base_speed_performance =
(5.0 * (speed_rating / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0;
let base_aim_performance = OsuStrainSkill::difficulty_to_performance(aim_rating);
let base_speed_performance = OsuStrainSkill::difficulty_to_performance(speed_rating);
let base_flashlight_performance = if mods.fl() {
flashlight_rating.powf(2.0) * 25.0
Flashlight::difficulty_to_performance(flashlight_rating)
} else {
0.0
};
+11 -5
View File
@@ -1,10 +1,10 @@
use std::pin::Pin;
use std::{borrow::Cow, pin::Pin};
use rosu_map::util::Pos;
use crate::{
any::difficulty::object::IDifficultyObject,
osu::object::{OsuObject, OsuObjectKind},
osu::object::{OsuObject, OsuObjectKind, OsuSlider},
};
use super::{scaling_factor::ScalingFactor, HD_FADE_OUT_DURATION_MULTIPLIER};
@@ -158,20 +158,26 @@ impl<'a> OsuDifficultyObject<'a> {
) -> Pin<&mut OsuObject> {
let pos = h.pos;
let stack_offset = h.stack_offset;
let start_time = h.start_time;
let OsuObjectKind::Slider(ref mut slider) = h.kind else {
return h;
};
let mut nested = Cow::Borrowed(slider.nested_objects.as_slice());
let duration = slider.end_time - start_time;
OsuSlider::lazy_travel_time(start_time, duration, &mut nested);
let nested = nested.as_ref();
let mut curr_cursor_pos = pos + stack_offset;
let scaling_factor = f64::from(OsuDifficultyObject::NORMALIZED_RADIUS) / radius;
for (curr_movement_obj, i) in slider.nested_objects.iter().zip(1..) {
for (curr_movement_obj, i) in nested.iter().zip(1..) {
let mut curr_movement = curr_movement_obj.pos + stack_offset - curr_cursor_pos;
let mut curr_movement_len = scaling_factor * f64::from(curr_movement.length());
let mut required_movement = f64::from(OsuDifficultyObject::ASSUMED_SLIDER_RADIUS);
if i == slider.nested_objects.len() {
if i == nested.len() {
let lazy_movement = slider.lazy_end_pos - curr_cursor_pos;
if lazy_movement.length() < curr_movement.length() {
@@ -190,7 +196,7 @@ impl<'a> OsuDifficultyObject<'a> {
slider.lazy_travel_dist += curr_movement_len as f32;
}
if i == slider.nested_objects.len() {
if i == nested.len() {
slider.lazy_end_pos = curr_cursor_pos;
}
}
+4 -1
View File
@@ -4,6 +4,8 @@ use crate::osu::object::OsuObject;
use super::object::OsuDifficultyObject;
const BROKEN_GAMEFIELD_ROUNDING_ALLOWANCE: f32 = 1.00041;
/// Fields around the scaling of hit objects.
///
/// osu!lazer stores these in each hit object but since all objects share the
@@ -17,7 +19,8 @@ pub struct ScalingFactor {
impl ScalingFactor {
pub fn new(cs: f64) -> Self {
let scale = (1.0 - 0.7 * (cs as f32 - 5.0) / 5.0) / 2.0;
let scale =
(1.0 - 0.7 * ((cs - 5.0) / 5.0)) as f32 / 2.0 * BROKEN_GAMEFIELD_ROUNDING_ALLOWANCE;
let radius = f64::from(OsuObject::OBJECT_RADIUS * scale);
let factor = OsuDifficultyObject::NORMALIZED_RADIUS / radius as f32;
+5 -6
View File
@@ -11,7 +11,7 @@ use crate::{
use super::strain::OsuStrainSkill;
const SKILL_MULTIPLIER: f64 = 23.55;
const SKILL_MULTIPLIER: f64 = 24.963;
const STRAIN_DECAY_BASE: f64 = 0.15;
#[derive(Clone)]
@@ -49,7 +49,6 @@ impl Aim {
OsuStrainSkill::REDUCED_SECTION_COUNT,
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
OsuStrainSkill::DECAY_WEIGHT,
OsuStrainSkill::DIFFICULTY_MULTIPLER,
)
}
}
@@ -121,7 +120,7 @@ impl AimEvaluator {
fn evaluate_diff_of<'a>(
curr: &'a OsuDifficultyObject<'a>,
diff_objects: &'a [OsuDifficultyObject<'a>],
with_sliders: bool,
with_slider_travel_dist: bool,
) -> f64 {
let osu_curr_obj = curr;
@@ -139,7 +138,7 @@ impl AimEvaluator {
// * 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 {
if osu_last_obj.base.is_slider() && with_slider_travel_dist {
// * calculate the slider velocity from slider head to slider end.
let travel_vel = osu_last_obj.travel_dist / osu_last_obj.travel_time;
// * calculate the movement velocity from slider end to current object
@@ -152,7 +151,7 @@ impl AimEvaluator {
// * As above, do the same for the previous hitobject.
let mut prev_vel = osu_last_obj.lazy_jump_dist / osu_last_obj.strain_time;
if osu_last_last_obj.base.is_slider() && with_sliders {
if osu_last_last_obj.base.is_slider() && with_slider_travel_dist {
let travel_vel = osu_last_last_obj.travel_dist / osu_last_last_obj.travel_time;
let movement_vel = osu_last_obj.min_jump_dist / osu_last_obj.min_jump_time;
@@ -254,7 +253,7 @@ impl AimEvaluator {
);
// * Add in additional slider velocity bonus.
if with_sliders {
if with_slider_travel_dist {
aim_strain += slider_bonus * Self::SLIDER_MULTIPLIER;
}
+6 -4
View File
@@ -10,9 +10,7 @@ use crate::{
util::strains_vec::StrainsVec,
};
use super::strain::OsuStrainSkill;
const SKILL_MULTIPLIER: f64 = 0.052;
const SKILL_MULTIPLIER: f64 = 0.05512;
const STRAIN_DECAY_BASE: f64 = 0.15;
pub struct Flashlight {
@@ -49,7 +47,11 @@ impl Flashlight {
}
fn static_difficulty_value(skill: StrainSkill) -> f64 {
skill.get_curr_strain_peaks().sum() * OsuStrainSkill::DIFFICULTY_MULTIPLER
skill.get_curr_strain_peaks().sum()
}
pub fn difficulty_to_performance(difficulty: f64) -> f64 {
25.0 * (difficulty).powf(2.0)
}
}
+104 -93
View File
@@ -11,10 +11,9 @@ use crate::{
use super::strain::OsuStrainSkill;
const SKILL_MULTIPLIER: f64 = 1375.0;
const SKILL_MULTIPLIER: f64 = 1.430;
const STRAIN_DECAY_BASE: f64 = 0.3;
const DIFFICULTY_MULTIPLER: f64 = 1.04;
const REDUCED_SECTION_COUNT: usize = 5;
#[derive(Clone)]
@@ -57,7 +56,6 @@ impl Speed {
REDUCED_SECTION_COUNT,
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
OsuStrainSkill::DECAY_WEIGHT,
DIFFICULTY_MULTIPLER,
)
}
@@ -140,7 +138,7 @@ impl<'a> Skill<'a, Speed> {
struct SpeedEvaluator;
impl SpeedEvaluator {
const SINGLE_SPACING_THRESHOLD: f64 = 125.0;
const SINGLE_SPACING_THRESHOLD: f64 = 125.0; // 1.25 circlers distance between centers
const MIN_SPEED_BONUS: f64 = 75.0; // ~200BPM
const SPEED_BALANCING_FACTOR: f64 = 40.;
@@ -175,21 +173,30 @@ impl SpeedEvaluator {
// * 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 {
// * Add additional scaling bonus for streams/bursts higher than 200bpm
let base = (Self::MIN_SPEED_BONUS - strain_time) / Self::SPEED_BALANCING_FACTOR;
1.0 + 0.75 * base.powf(2.0)
} else {
// * speedBonus will be 1.0 for BPM < 200
1.0
};
let travel_dist = osu_prev_obj.map_or(0.0, |obj| obj.travel_dist);
let dist = Self::SINGLE_SPACING_THRESHOLD.min(travel_dist + osu_curr_obj.min_jump_dist);
let mut dist = travel_dist + osu_curr_obj.min_jump_dist;
(speed_bonus + speed_bonus * (dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.5))
* doubletapness
/ strain_time
// * Cap distance at single_spacing_threshold
dist = Self::SINGLE_SPACING_THRESHOLD.min(dist);
// * Max distance bonus is 2 at single_spacing_threshold
let dist_bonus = 1.0 + (dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.5);
// * Base difficulty with all bonuses
let difficulty = speed_bonus * dist_bonus * 1000.0 / strain_time;
// * Apply penalty if there's doubletappable doubles
return difficulty * doubletapness;
}
}
@@ -233,103 +240,107 @@ impl RhythmEvaluator {
rhythm_start += 1;
}
for i in (1..=rhythm_start).rev() {
let Some(((curr_obj, prev_obj), last_obj)) = curr
.previous(i - 1, diff_objects)
.zip(curr.previous(i, diff_objects))
.zip(curr.previous(i + 1, diff_objects))
else {
break;
};
if let Some((mut prev_obj, mut last_obj)) = curr
.previous(rhythm_start, diff_objects)
.zip(curr.previous(rhythm_start + 1, diff_objects))
{
for i in (1..=rhythm_start).rev() {
let Some(curr_obj) = curr.previous(i - 1, diff_objects) else {
break;
};
// * scales note 0 to 1 from history to now
let mut curr_historical_decay = (f64::from(Self::HISTORY_TIME_MAX)
- (curr.start_time - curr_obj.start_time))
/ f64::from(Self::HISTORY_TIME_MAX);
// * scales note 0 to 1 from history to now
let mut curr_historical_decay = (f64::from(Self::HISTORY_TIME_MAX)
- (curr.start_time - curr_obj.start_time))
/ f64::from(Self::HISTORY_TIME_MAX);
// * 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);
// * 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;
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.powf(2.0).min(0.5);
// * 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.powf(2.0).min(0.5);
let hit_window = u64::from(!curr_obj.base.is_spinner()) as f64 * hit_window;
let hit_window = u64::from(!curr_obj.base.is_spinner()) as f64 * hit_window;
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
.max(0.0))
/ (hit_window * 0.3))
.min(1.0);
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
.max(0.0))
/ (hit_window * 0.3))
.min(1.0);
window_penalty = window_penalty.min(1.0);
window_penalty = window_penalty.min(1.0);
let mut effective_ratio = window_penalty * curr_ratio;
let mut effective_ratio = window_penalty * curr_ratio;
if first_delta_switch {
// Keep in-sync with lazer
#[allow(clippy::if_not_else)]
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;
if first_delta_switch {
// Keep in-sync with lazer
#[allow(clippy::if_not_else)]
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
* f64::from(4 + island_size).sqrt()
/ 2.0
* f64::from(4 + prev_island_size).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 {
// * 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
* f64::from(4 + island_size).sqrt()
/ 2.0
* f64::from(4 + prev_island_size).sqrt()
/ 2.0;
} 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;
// * 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;
last_obj = prev_obj;
prev_obj = curr_obj;
}
}
+5 -3
View File
@@ -8,7 +8,6 @@ pub struct OsuStrainSkill {
impl OsuStrainSkill {
pub const REDUCED_SECTION_COUNT: usize = 10;
pub const REDUCED_STRAIN_BASELINE: f64 = 0.75;
pub const DIFFICULTY_MULTIPLER: f64 = 1.06;
pub const DECAY_WEIGHT: f64 = 0.9;
pub const SECTION_LEN: f64 = 400.0;
@@ -30,7 +29,6 @@ impl OsuStrainSkill {
reduced_section_count: usize,
reduced_strain_baseline: f64,
decay_weight: f64,
difficulty_multiplier: f64,
) -> f64 {
let mut difficulty = 0.0;
let mut weight = 1.0;
@@ -52,7 +50,11 @@ impl OsuStrainSkill {
weight *= decay_weight;
}
difficulty * difficulty_multiplier
difficulty
}
pub fn difficulty_to_performance(difficulty: f64) -> f64 {
(5.0 * (difficulty / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0
}
}
+96 -20
View File
@@ -1,5 +1,10 @@
use std::borrow::Cow;
use rosu_map::{
section::hit_objects::{CurveBuffers, SliderEvent, SliderEventType, SliderEventsIter},
section::{
general::GameMode,
hit_objects::{CurveBuffers, SliderEvent, SliderEventType, SliderEventsIter},
},
util::Pos,
};
@@ -61,6 +66,8 @@ impl OsuObject {
reflect_y(&mut self.pos.y);
if let OsuObjectKind::Slider(ref mut slider) = self.kind {
let repeat_count = slider.repeat_count();
// Requires `stack_offset` so we can't add `h.pos` just yet
slider.lazy_end_pos.y = -slider.lazy_end_pos.y;
@@ -81,6 +88,21 @@ impl OsuObject {
reflect_y(&mut nested.pos.y);
}
// Same for the last repeat point
for nested in nested_iter.by_ref().rev() {
if let NestedSliderObjectKind::Repeat = nested.kind {
nested.pos = if repeat_count % 2 == 0 {
self.pos
} else {
self.pos + Pos::new(slider.path_end_pos.x, -slider.path_end_pos.y)
};
break;
}
reflect_y(&mut nested.pos.y);
}
for nested in nested_iter {
reflect_y(&mut nested.pos.y);
}
@@ -123,12 +145,7 @@ impl OsuObject {
pub fn lazy_travel_time(&self) -> f64 {
match self.kind {
OsuObjectKind::Circle | OsuObjectKind::Spinner(_) => 0.0,
OsuObjectKind::Slider(ref slider) => slider
.nested_objects
// Here we really want the last nested object which is not
// necessarily the tail
.last()
.map_or(0.0, |nested| nested.start_time - self.start_time),
OsuObjectKind::Slider(ref slider) => slider.lazy_travel_time,
}
}
@@ -155,6 +172,10 @@ pub struct OsuSlider {
pub end_time: f64,
pub lazy_end_pos: Pos,
pub lazy_travel_dist: f32,
pub lazy_travel_time: f64,
// Very annoyingly, this position might be needed solely to update the last
// repeat point's position on HR.
pub path_end_pos: Pos,
pub nested_objects: Vec<NestedSliderObject>,
}
@@ -182,13 +203,25 @@ impl OsuSlider {
|point| (point.slider_velocity, point.generate_ticks),
);
let path = slider.curve(curve_bufs);
let path = slider.curve(GameMode::Osu, curve_bufs);
let span_count = slider.span_count() as f64;
let scoring_dist =
f64::from(OsuObject::BASE_SCORING_DIST) * slider_multiplier * slider_velocity;
let velocity = scoring_dist / beat_len;
fn get_precision_adjusted_beat_len(slider_velocity_multiplier: f64, beat_len: f64) -> f64 {
let slider_velocity_as_beat_len = -100.0 / slider_velocity_multiplier;
let bpm_multiplier = if slider_velocity_as_beat_len < 0.0 {
f64::from(((-slider_velocity_as_beat_len) as f32).clamp(10.0, 10_000.0)) / 100.0
} else {
1.0
};
beat_len * bpm_multiplier
}
let velocity = f64::from(OsuObject::BASE_SCORING_DIST) * slider_multiplier
/ get_precision_adjusted_beat_len(slider_velocity, beat_len);
let scoring_dist = velocity * beat_len;
let end_time = start_time + span_count * path.dist() / velocity;
@@ -244,12 +277,14 @@ impl OsuSlider {
start_time: start_time + f64::from(e.span_idx + 1) * span_duration,
kind: NestedSliderObjectKind::Repeat,
},
SliderEventType::LastTick => NestedSliderObject {
pos: end_path_pos, // no `h.pos` yet to keep order of float operations
start_time: e.time,
kind: NestedSliderObjectKind::Tail,
},
SliderEventType::Head | SliderEventType::Tail => return None,
SliderEventType::Tail => {
NestedSliderObject {
pos: end_path_pos, // no `h.pos` yet to keep order of float operations
start_time: e.time,
kind: NestedSliderObjectKind::Tail,
}
}
SliderEventType::Head | SliderEventType::LastTick => return None,
};
Some(obj)
@@ -260,9 +295,8 @@ impl OsuSlider {
a.start_time.total_cmp(&b.start_time)
});
let lazy_travel_time = nested_objects
.last()
.map_or(0.0, |nested| nested.start_time - h.start_time);
let mut nested = Cow::Borrowed(nested_objects.as_slice());
let lazy_travel_time = OsuSlider::lazy_travel_time(start_time, duration, &mut nested);
let mut end_time_min = lazy_travel_time / span_duration;
@@ -273,15 +307,51 @@ impl OsuSlider {
}
let lazy_end_pos = path.position_at(end_time_min);
let path_end_pos = path.position_at(1.0);
Self {
end_time,
lazy_end_pos,
lazy_travel_dist: 0.0,
lazy_travel_time,
path_end_pos,
nested_objects,
}
}
pub fn lazy_travel_time(
start_time: f64,
duration: f64,
nested_objects: &mut Cow<'_, [NestedSliderObject]>,
) -> f64 {
const TAIL_LENIENCY: f64 = -36.0;
let mut tracking_end_time =
(start_time + duration + TAIL_LENIENCY).max(start_time + duration / 2.0);
let last_real_tick = nested_objects
.iter()
.enumerate()
.rfind(|(_, nested)| nested.is_tick());
if let Some((idx, last_real_tick)) =
last_real_tick.filter(|(_, tick)| tick.start_time > tracking_end_time)
{
tracking_end_time = last_real_tick.start_time;
// * When the last tick falls after the tracking end time, we need to re-sort the nested objects
// * based on time. This creates a somewhat weird ordering which is counter to how a user would
// * understand the slider, but allows a zero-diff with known diffcalc output.
// *
// * To reiterate, this is definitely not correct from a difficulty calculation perspective
// * and should be revisited at a later date (likely by replacing this whole code with the commented
// * version above).
nested_objects.to_mut()[idx..].rotate_left(1);
}
tracking_end_time - start_time
}
pub fn repeat_count(&self) -> usize {
self.nested_objects
.iter()
@@ -306,6 +376,7 @@ impl OsuSlider {
}
}
#[derive(Clone, Debug)]
pub struct NestedSliderObject {
pub pos: Pos,
pub start_time: f64,
@@ -316,8 +387,13 @@ impl NestedSliderObject {
pub const fn is_repeat(&self) -> bool {
matches!(self.kind, NestedSliderObjectKind::Repeat)
}
pub const fn is_tick(&self) -> bool {
matches!(self.kind, NestedSliderObjectKind::Tick)
}
}
#[derive(Copy, Clone, Debug)]
pub enum NestedSliderObjectKind {
Repeat,
Tail,
+7 -7
View File
@@ -13,6 +13,7 @@ use crate::{
use super::{
attributes::{OsuDifficultyAttributes, OsuPerformanceAttributes},
difficulty::skills::{flashlight::Flashlight, strain::OsuStrainSkill},
score_state::OsuScoreState,
Osu,
};
@@ -613,7 +614,7 @@ impl OsuPerformanceInner<'_> {
}
fn compute_aim_value(&self) -> f64 {
let mut aim_value = (5.0 * (self.attrs.aim / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0;
let mut aim_value = OsuStrainSkill::difficulty_to_performance(self.attrs.aim);
let total_hits = self.total_hits();
@@ -652,7 +653,7 @@ impl OsuPerformanceInner<'_> {
* (0.0016 / (1.0 + 2.0 * self.effective_miss_count))
* self.acc.powf(16.0))
* (1.0 - 0.003 * self.attrs.hp * self.attrs.hp);
} else if self.mods.hd() {
} else if self.mods.hd() || self.mods.tc() {
// * We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aim_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar);
}
@@ -685,8 +686,7 @@ impl OsuPerformanceInner<'_> {
return 0.0;
}
let mut speed_value =
(5.0 * (self.attrs.speed / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0;
let mut speed_value = OsuStrainSkill::difficulty_to_performance(self.attrs.speed);
let total_hits = self.total_hits();
@@ -719,7 +719,7 @@ impl OsuPerformanceInner<'_> {
// * Increasing the speed value by object count for Blinds isn't
// * ideal, so the minimum buff is given.
speed_value *= 1.12;
} else if self.mods.hd() {
} else if self.mods.hd() || self.mods.tc() {
// * We want to give more reward for lower AR when it comes to aim and HD.
// * This nerfs high AR and buffs lower AR.
speed_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar);
@@ -792,7 +792,7 @@ impl OsuPerformanceInner<'_> {
// * ideal, so the minimum buff is given.
if self.mods.bl() {
acc_value *= 1.14;
} else if self.mods.hd() {
} else if self.mods.hd() || self.mods.tc() {
acc_value *= 1.08;
}
@@ -808,7 +808,7 @@ impl OsuPerformanceInner<'_> {
return 0.0;
}
let mut flashlight_value = self.attrs.flashlight.powf(2.0) * 25.0;
let mut flashlight_value = Flashlight::difficulty_to_performance(self.attrs.flashlight);
let total_hits = self.total_hits();
+1 -1
View File
@@ -131,7 +131,7 @@ fn should_convert_slider_to_taiko_hits(map: &Beatmap, params: &mut SliderParams<
tick_spacing,
} = params;
let curve = slider.curve(bufs);
let curve = slider.curve(GameMode::Taiko, bufs);
// * The true distance, accounting for any repeats. This ends up being the drum roll distance later
let spans = slider.span_count() as f64;