From 3a597205a9412ee9da8d7096fe6e436d92232376 Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 27 Jul 2026 20:37:43 -0500 Subject: [PATCH 01/27] fix: update OsuDifficultyObject calculations --- src/osu/difficulty/object.rs | 72 ++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/src/osu/difficulty/object.rs b/src/osu/difficulty/object.rs index 7cddf0de..a65ce8b0 100644 --- a/src/osu/difficulty/object.rs +++ b/src/osu/difficulty/object.rs @@ -16,6 +16,8 @@ pub struct OsuDifficultyObject<'a> { pub delta_time: f64, pub adjusted_delta_time: f64, + pub last_obj_end_delta_time: f64, + pub jump_dist: f64, pub lazy_jump_dist: f64, pub min_jump_dist: f64, pub min_jump_time: f64, @@ -25,6 +27,7 @@ pub struct OsuDifficultyObject<'a> { pub lazy_travel_dist: f64, pub lazy_travel_time: f64, pub angle: Option, + pub normalized_vector_angle: Option, pub small_circle_bonus: f64, } @@ -49,15 +52,23 @@ impl<'a> OsuDifficultyObject<'a> { let delta_time = (hit_object.start_time - last_object.start_time) / clock_rate; let start_time = hit_object.start_time / clock_rate; - let strain_time = delta_time.max(Self::MIN_DELTA_TIME); - let small_circle_bonus = (1.0 + (30.0 - scaling_factor.radius) / 40.0).max(1.0); + let adjusted_delta_time = delta_time.max(Self::MIN_DELTA_TIME); + let last_obj_end_delta_time = if let Some(last) = last_diff_obj { + (start_time - last.start_time + last.delta_time).max(Self::MIN_DELTA_TIME) + } else { + adjusted_delta_time + }; + + let small_circle_bonus = (1.0 + (30.0 - scaling_factor.radius) / 70.0).max(1.0); let mut this = Self { idx, base: hit_object, start_time, delta_time, - adjusted_delta_time: strain_time, + adjusted_delta_time, + last_obj_end_delta_time, + jump_dist: 0.0, lazy_jump_dist: 0.0, min_jump_dist: 0.0, min_jump_time: 0.0, @@ -67,6 +78,7 @@ impl<'a> OsuDifficultyObject<'a> { lazy_travel_dist: 0.0, lazy_travel_time: 0.0, angle: None, + normalized_vector_angle: None, small_circle_bonus, }; @@ -91,7 +103,7 @@ impl<'a> OsuDifficultyObject<'a> { } let fade_in_start_time = self.base.start_time - time_preempt; - let fade_in_duration = time_fade_in; + let fade_in_duration = 400.0 * (time_preempt / OsuObject::PREEMPT_MIN).min(1.0); if hidden { // * Taken from OsuModHidden. @@ -132,20 +144,21 @@ impl<'a> OsuDifficultyObject<'a> { scaling_factor: &ScalingFactor, ) { if let OsuObjectKind::Slider(ref slider) = self.base.kind { - self.travel_dist = self.lazy_travel_dist - * ((1.0 + slider.repeat_count() as f64 / 2.5).powf(1.0 / 2.5)); + self.travel_dist = self.lazy_travel_dist * (slider.repeat_count() as f64).powf(0.3).max(1.0); self.travel_time = (self.lazy_travel_time / clock_rate).max(OsuDifficultyObject::MIN_DELTA_TIME); } + self.min_jump_time = self.adjusted_delta_time; + if self.base.is_spinner() || last_object.is_spinner() { return; } let scaling_factor = scaling_factor.factor; - let last_cursor_pos = if let Some(last_diff_obj) = last_diff_obj { + let mut last_cursor_pos = if let Some(last_diff_obj) = last_diff_obj { Self::get_end_cursor_pos(last_diff_obj) } else { last_object.stacked_pos() @@ -154,13 +167,14 @@ impl<'a> OsuDifficultyObject<'a> { self.lazy_jump_dist = f64::from( (self.base.stacked_pos() * scaling_factor - last_cursor_pos * scaling_factor).length(), ); - self.min_jump_time = self.adjusted_delta_time; self.min_jump_dist = self.lazy_jump_dist; let Some(last_diff_obj) = last_diff_obj else { return; }; + self.jump_dist = f64::from((last_diff_obj.base.stacked_pos() - self.base.stacked_pos()).length() * scaling_factor); + if let OsuObjectKind::Slider(ref last_slider) = last_object.kind { let last_travel_time = (last_diff_obj.lazy_travel_time / clock_rate) .max(OsuDifficultyObject::MIN_DELTA_TIME); @@ -184,17 +198,20 @@ impl<'a> OsuDifficultyObject<'a> { let Some(last_last_diff_obj) = last_last_diff_obj else { return; }; - + if !last_last_diff_obj.base.is_spinner() { - let last_last_cursor_pos = Self::get_end_cursor_pos(last_last_diff_obj); + if last_object.is_slider() && last_diff_obj.travel_dist > 0.0 { + last_cursor_pos = last_object.stacked_pos(); + } - let v1 = last_last_cursor_pos - last_object.stacked_pos(); - let v2 = self.base.stacked_pos() - last_cursor_pos; + let last_last_cursor_pos = Self::get_end_cursor_pos(last_last_diff_obj); - let dot = v1.dot(v2); - let det = v1.x * v2.y - v1.y * v2.x; + let angle = self.calculate_angle(last_cursor_pos, last_last_cursor_pos); + let slider_angle = self.calculate_slider_angle(last_diff_obj, last_last_cursor_pos); + let v = self.base.stacked_pos() - last_cursor_pos; - self.angle = Some((f64::from(det).atan2(f64::from(dot))).abs()); + self.normalized_vector_angle = Some(f64::from(v.y).abs().atan2(f64::from(v.x).abs())); + self.angle = Some(angle.min(slider_angle)); } } @@ -290,6 +307,31 @@ impl<'a> OsuDifficultyObject<'a> { self.lazy_end_pos = Some(lazy_end_pos); } + fn calculate_slider_angle(&self, last_diff_obj: &OsuDifficultyObject, last_last_pos: Pos) -> f64 { + let last_pos = Self::get_end_cursor_pos(last_diff_obj); + + let adjusted_last_last_pos = if let OsuObjectKind::Slider(ref last_slider) = last_diff_obj.base.kind && last_diff_obj.travel_dist > 0.0 { + if let Some(second_last_nested) = last_slider.nested_objects.get(last_slider.nested_objects.len().saturating_sub(2)) { + second_last_nested.pos + last_diff_obj.base.stack_offset + } else { + last_last_pos + } + } else { + last_last_pos + }; + + self.calculate_angle(last_pos, adjusted_last_last_pos) + } + + fn calculate_angle(&self, last_pos: Pos, last_last_pos: Pos) -> f64 { + let v1 = last_last_pos - last_pos; + let v2 = self.base.stacked_pos() - last_pos; + let dot = v1.dot(v2); + let det = v1.x * v2.y - v1.y * v2.x; + + f64::from(det).atan2(f64::from(dot)).abs() + } + const fn get_end_cursor_pos(hit_object: &OsuDifficultyObject) -> Pos { if let Some(lazy_end_pos) = hit_object.lazy_end_pos { lazy_end_pos From 6441acd7e8a35178d062868aa70af8f9c1dafbfb Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 27 Jul 2026 23:00:25 -0500 Subject: [PATCH 02/27] feat: impl osu SnapAimEvaluator --- src/osu/difficulty/evaluators/aim/mod.rs | 1 + src/osu/difficulty/evaluators/aim/snap_aim.rs | 254 ++++++++++++++++++ .../evaluators/{aim.rs => aim_old.rs} | 0 src/osu/difficulty/evaluators/mod.rs | 3 +- 4 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 src/osu/difficulty/evaluators/aim/mod.rs create mode 100644 src/osu/difficulty/evaluators/aim/snap_aim.rs rename src/osu/difficulty/evaluators/{aim.rs => aim_old.rs} (100%) diff --git a/src/osu/difficulty/evaluators/aim/mod.rs b/src/osu/difficulty/evaluators/aim/mod.rs new file mode 100644 index 00000000..42982793 --- /dev/null +++ b/src/osu/difficulty/evaluators/aim/mod.rs @@ -0,0 +1 @@ +mod snap_aim; \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/aim/snap_aim.rs b/src/osu/difficulty/evaluators/aim/snap_aim.rs new file mode 100644 index 00000000..eaa0df29 --- /dev/null +++ b/src/osu/difficulty/evaluators/aim/snap_aim.rs @@ -0,0 +1,254 @@ +use crate::{ + any::difficulty::object::IDifficultyObject, + osu::difficulty::object::OsuDifficultyObject, + util::{ + difficulty::{milliseconds_to_bpm, reverse_lerp, smootherstep, smoothstep}, + float_ext::FloatExt + } +}; + +pub struct SnapAimEvaluator; + +impl SnapAimEvaluator { + const WIDE_ANGLE_MULTIPLIER: f64 = 9.67; + const ACUTE_ANGLE_MULTIPLIER: f64 = 2.41; + const SLIDER_MULTIPLIER: f64 = 1.5; + const VELOCITY_CHANGE_MULTIPLIER: f64 = 0.9; + const WIGGLE_MULTIPLIER: f64 = 1.02; + + const WIDE_ANGLE_TIME_SCALE: f64 = 1.45; + + const REPETITION_NOTE_LIMIT: usize = 6; + const REPETITION_MAX_NERF: f64 = 0.15; + const REPETITION_MAX_VECTOR_INFLUENCE: f64 = 0.5; + + #[expect(clippy::too_many_lines, reason = "staying in-sync with lazer")] + pub fn evaluate_diff_of<'a>( + curr: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>], + with_slider_travel_dist: bool, + ) -> f64 { + let osu_curr_obj = curr; + + let Some(osu_last_obj) = curr + .previous(0, diff_objects) + .filter(|last| !(curr.base.is_spinner() || last.base.is_spinner())) + else { + return 0.0; + }; + + #[expect(clippy::items_after_statements, reason = "staying in-sync with lazer")] + const RADIUS: i32 = OsuDifficultyObject::NORMALIZED_RADIUS; + #[expect(clippy::items_after_statements, reason = "staying in-sync with lazer")] + const DIAMETER: i32 = OsuDifficultyObject::NORMALIZED_DIAMETER; + + // * Calculate the velocity to the current hitobject, which starts + // * with a base distance / time assuming the last object is a hitcircle. + let curr_dist = if with_slider_travel_dist { osu_curr_obj.lazy_jump_dist } else { osu_curr_obj.jump_dist }; + let mut curr_vel = osu_curr_obj.lazy_jump_dist / osu_curr_obj.adjusted_delta_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_slider_travel_dist { + let slider_distance = osu_last_obj.lazy_travel_dist + osu_curr_obj.lazy_jump_dist; + curr_vel = curr_vel.max(slider_distance / osu_curr_obj.adjusted_delta_time); + } + + let prev_dist = if with_slider_travel_dist { osu_last_obj.lazy_jump_dist } else { osu_last_obj.jump_dist }; + let prev_vel = prev_dist / osu_last_obj.adjusted_delta_time; + + // * Start difficulty with regular velocity. + let mut snap_diff = curr_vel; + + // * Penalize angle repetition. + snap_diff *= Self::vector_angle_repetition(osu_curr_obj, osu_last_obj, diff_objects); + + if let (Some(curr_angle), Some(last_angle)) = (osu_curr_obj.angle, osu_last_obj.angle) { + // * Rewarding angles, take the smaller velocity as base. + let vel_influence = curr_vel.min(prev_vel); + let mut acute_angle_bonus = 0.0; + + // * If rhythms are the same. + if osu_curr_obj.adjusted_delta_time.max(osu_last_obj.adjusted_delta_time) < + 1.25 * osu_curr_obj.adjusted_delta_time.min(osu_last_obj.adjusted_delta_time) { + acute_angle_bonus = Self::calc_angle_acuteness(curr_angle); + + // * Penalize angle repetition. It is important to do it _before_ + // * multiplying by anything because we compare raw acuteness here. + acute_angle_bonus *= 0.08 + + 0.92 + * (1.0 - f64::min( + acute_angle_bonus, + f64::powf(Self::calc_angle_acuteness(last_angle), 3.0) + )); + + // * Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter. + acute_angle_bonus *= vel_influence + * smootherstep( + milliseconds_to_bpm(osu_curr_obj.adjusted_delta_time, Some(2)), + 300.0, + 400.0 + ) + * smootherstep(curr_dist, 0.0, f64::from(DIAMETER * 2)); + } + + let mut wide_angle_bonus = Self::calc_angle_wideness(curr_angle); + + // * Penalize angle repetition. It is important to do it _before_ + // * multiplying by anything because we compare raw wideness here. + wide_angle_bonus *= 0.25 + + 0.75 + * (1.0 - f64::min( + wide_angle_bonus, + f64::powf(Self::calc_angle_wideness(last_angle), 3.0) + )); + + // * Rescaling velocity for the wide angle bonus + let mut wide_angle_curr_vel = curr_dist / + osu_curr_obj.adjusted_delta_time.powf(Self::WIDE_ANGLE_TIME_SCALE); + let wide_angle_prev_vel = prev_dist / + osu_last_obj.adjusted_delta_time.powf(Self::WIDE_ANGLE_TIME_SCALE); + + if osu_last_obj.base.is_slider() && with_slider_travel_dist { + let slider_dist = osu_last_obj.lazy_travel_dist + osu_curr_obj.lazy_jump_dist; + wide_angle_curr_vel = f64::max( + wide_angle_curr_vel, + slider_dist / f64::powf(osu_curr_obj.adjusted_delta_time, Self::WIDE_ANGLE_TIME_SCALE) + ); + } + + wide_angle_bonus *= wide_angle_curr_vel.min(wide_angle_prev_vel); + + if let Some(osu_last_2_obj) = curr.previous(2, diff_objects) { + // * If objects just go back and forth through a middle point - don't give as much wide bonus. + // * Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, + // * so any object's angle's center point is always the previous object. + let dist = (osu_last_2_obj.base.stacked_pos() - osu_last_obj.base.stacked_pos()).length(); + + if dist < 1.0 { + wide_angle_bonus *= 1.0 - 0.55 * (1.0 - f64::from(dist)); + } + } + + // * Add in acute angle bonus or wide angle bonus, whichever is larger. + snap_diff += f64::max( + acute_angle_bonus * Self::ACUTE_ANGLE_MULTIPLIER, + wide_angle_bonus * Self::WIDE_ANGLE_MULTIPLIER + ); + + // * Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle + // * https://www.desmos.com/calculator/dp0v0nvowc + let wiggle_bonus = vel_influence + * smootherstep(curr_dist, f64::from(RADIUS), f64::from(DIAMETER)) + * f64::powf( + reverse_lerp(curr_dist, f64::from(DIAMETER * 3), f64::from(DIAMETER)), + 1.8 + ) + * smootherstep(curr_angle, f64::to_radians(110.0), f64::to_radians(60.0)) + * smootherstep(prev_dist, f64::from(RADIUS), f64::from(DIAMETER)) + * f64::powf( + reverse_lerp(prev_dist, f64::from(DIAMETER * 3), f64::from(DIAMETER)), + 1.8 + ) + * smootherstep(last_angle, f64::to_radians(110.0), f64::to_radians(60.0)); + + snap_diff += wiggle_bonus * Self::WIGGLE_MULTIPLIER; + } + + if prev_vel.max(curr_vel).not_eq(0.0) { + if with_slider_travel_dist { + // * We want to use just the object jump without slider velocity when awarding differences + curr_vel = curr_dist / osu_curr_obj.adjusted_delta_time; + } + + // * Scale with ratio of difference compared to 0.5 * max dist. + let dist_ratio = smoothstep( + (prev_vel - curr_vel).abs() / prev_vel.max(curr_vel), + 0.0, + 1.0 + ); + + // * Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. + let overlap_vel_buff = (f64::from(DIAMETER) * 1.25 / osu_curr_obj.adjusted_delta_time + .min(osu_last_obj.adjusted_delta_time) + ).min((prev_vel - curr_vel).abs()); + + let mut vel_change_bonus = overlap_vel_buff * dist_ratio; + + // * Penalize for rhythm changes. + vel_change_bonus *= f64::powf( + osu_curr_obj.adjusted_delta_time.min(osu_last_obj.adjusted_delta_time) + / osu_curr_obj.adjusted_delta_time.max(osu_last_obj.adjusted_delta_time), + 2.0 + ); + + snap_diff += vel_change_bonus * Self::VELOCITY_CHANGE_MULTIPLIER; + } + + // * Reward sliders based on velocity. + if osu_curr_obj.base.is_slider() && with_slider_travel_dist { + let slider_bonus = osu_curr_obj.travel_dist / osu_curr_obj.travel_time; + snap_diff += if slider_bonus.lt(&1.0) + { slider_bonus } + else { slider_bonus.powf(0.75) * Self::SLIDER_MULTIPLIER }; + } + + // * Apply high circle size bonus + snap_diff *= osu_curr_obj.small_circle_bonus; + + snap_diff *= Self::high_bpm_bonus(osu_curr_obj.adjusted_delta_time); + + snap_diff + } + + fn high_bpm_bonus(ms: f64) -> f64 { + 1.0 / (1.0 - f64::powf(0.03, (ms / 1000.0).powf(0.65))) + } + + fn vector_angle_repetition<'a>( + curr: &'a OsuDifficultyObject<'a>, + prev: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>] + ) -> f64 { + let (Some(curr_angle), Some(prev_angle)) = (curr.angle, prev.angle) else { + return 1.0; + }; + + let mut const_angle_count: f64 = 0.0; + for i in 0..Self::REPETITION_NOTE_LIMIT { + let Some(prev_obj) = curr.previous(i, diff_objects) else { + break + }; + + // * Only consider vectors in the same jump section, + // * stopping to change rhythm ruins momentum + if curr.adjusted_delta_time.max(prev_obj.adjusted_delta_time) > + 1.1 * curr.adjusted_delta_time.min(prev_obj.adjusted_delta_time) { + break + } + + if let (Some(curr_vec_angle), Some(prev_vec_angle)) = (curr.normalized_vector_angle, prev.normalized_vector_angle) { + let angle_diff = curr_vec_angle - prev_vec_angle; + // * Refer to this desmos for tuning, constants need to be precise + // * so that values stay within the range of 0 and 1. + // * https://www.desmos.com/calculator/a8jesv5sv2 + const_angle_count += (8.0 * f64::to_radians(11.25).min(angle_diff)).cos(); + } + } + + let vec_repetition = (0.5 / const_angle_count).min(1.0).powf(2.0); + let stack_factor = smootherstep(curr.lazy_jump_dist, 0.0, f64::from(OsuDifficultyObject::NORMALIZED_DIAMETER)); + let angle_diff_adjusted = (2.0 * f64::to_radians(45.0).min((curr_angle - prev_angle).abs() * stack_factor)).cos(); + let base_nerf = 1.0 - Self::REPETITION_MAX_NERF * Self::calc_angle_acuteness(prev_angle) * angle_diff_adjusted; + + (base_nerf + (1.0 - base_nerf) * vec_repetition * Self::REPETITION_MAX_VECTOR_INFLUENCE * stack_factor).powf(2.0) + } + + const fn calc_angle_wideness(angle: f64) -> f64 { + smoothstep(angle, f64::to_radians(140.0), f64::to_radians(40.0)) + } + + const fn calc_angle_acuteness(angle: f64) -> f64 { + smoothstep(angle, f64::to_radians(140.0), f64::to_radians(40.0)) + } +} \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/aim.rs b/src/osu/difficulty/evaluators/aim_old.rs similarity index 100% rename from src/osu/difficulty/evaluators/aim.rs rename to src/osu/difficulty/evaluators/aim_old.rs diff --git a/src/osu/difficulty/evaluators/mod.rs b/src/osu/difficulty/evaluators/mod.rs index 3a6d1f51..9153a1bc 100644 --- a/src/osu/difficulty/evaluators/mod.rs +++ b/src/osu/difficulty/evaluators/mod.rs @@ -1,9 +1,10 @@ pub use self::{ - aim::AimEvaluator, flashlight::FlashlightEvaluator, rhythm::RhythmEvaluator, + aim_old::AimEvaluator, flashlight::FlashlightEvaluator, rhythm::RhythmEvaluator, speed::SpeedEvaluator, }; mod aim; +mod aim_old; mod flashlight; mod rhythm; mod speed; From 436c6c22c5e3c73283e71521b31172f63f4c8337 Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 27 Jul 2026 23:17:17 -0500 Subject: [PATCH 03/27] feat: impl osu AgilityEvaluator --- src/osu/difficulty/evaluators/aim/agility.rs | 40 ++++++++++++++++++++ src/osu/difficulty/evaluators/aim/mod.rs | 1 + 2 files changed, 41 insertions(+) create mode 100644 src/osu/difficulty/evaluators/aim/agility.rs diff --git a/src/osu/difficulty/evaluators/aim/agility.rs b/src/osu/difficulty/evaluators/aim/agility.rs new file mode 100644 index 00000000..d59ce505 --- /dev/null +++ b/src/osu/difficulty/evaluators/aim/agility.rs @@ -0,0 +1,40 @@ +use crate::{ + any::difficulty::object::IDifficultyObject, + osu::difficulty::object::OsuDifficultyObject +}; + +pub struct AgilityEvaluator; + +impl AgilityEvaluator { + // * 1.2 circles distance between centers + const DISTANCE_CAP: f64 = (OsuDifficultyObject::NORMALIZED_DIAMETER as f64) * 1.2; + + pub fn evaluate_diff_of<'a>( + curr: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>] + ) -> f64 { + if curr.base.is_spinner() { + return 0.0; + } + + let osu_curr_obj = curr; + let osu_prev_obj = curr.previous(0, diff_objects); + + let travel_dist = osu_prev_obj.map_or(0.0, |obj| obj.lazy_travel_dist); + let dist = travel_dist + osu_curr_obj.lazy_jump_dist; + + let dist_scaled = dist.min(Self::DISTANCE_CAP) / Self::DISTANCE_CAP; + + let mut agility_diff = dist_scaled * 1000.0 / osu_curr_obj.adjusted_delta_time; + + agility_diff *= osu_curr_obj.small_circle_bonus.powf(1.5); + + agility_diff *= Self::high_bpm_bonus(osu_curr_obj.adjusted_delta_time); + + agility_diff + } + + fn high_bpm_bonus(ms: f64) -> f64 { + 1.0 / (1.0 - f64::powf(0.2, ms / 1000.0)) + } +} \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/aim/mod.rs b/src/osu/difficulty/evaluators/aim/mod.rs index 42982793..0769265d 100644 --- a/src/osu/difficulty/evaluators/aim/mod.rs +++ b/src/osu/difficulty/evaluators/aim/mod.rs @@ -1 +1,2 @@ +mod agility; mod snap_aim; \ No newline at end of file From 0d876c24c97b0466202e700b17f7d340828fc484 Mon Sep 17 00:00:00 2001 From: myssto Date: Tue, 28 Jul 2026 00:00:25 -0500 Subject: [PATCH 04/27] feat: impl osu FlowAimEvaluator --- src/osu/difficulty/evaluators/aim/flow_aim.rs | 138 ++++++++++++++++++ src/osu/difficulty/evaluators/aim/mod.rs | 1 + src/osu/difficulty/evaluators/aim/snap_aim.rs | 2 +- src/osu/difficulty/evaluators/mod.rs | 4 +- src/osu/difficulty/object.rs | 3 + 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/osu/difficulty/evaluators/aim/flow_aim.rs diff --git a/src/osu/difficulty/evaluators/aim/flow_aim.rs b/src/osu/difficulty/evaluators/aim/flow_aim.rs new file mode 100644 index 00000000..fb47f07d --- /dev/null +++ b/src/osu/difficulty/evaluators/aim/flow_aim.rs @@ -0,0 +1,138 @@ +use rosu_map::util::Pos; + +use crate::{ + any::difficulty::object::IDifficultyObject, osu::difficulty::{ + evaluators::aim::snap_aim::SnapAimEvaluator, + object::OsuDifficultyObject + }, + util::{ + difficulty::{smootherstep, smoothstep}, + float_ext::FloatExt + } +}; + +pub struct FlowAimEvaluator; + +impl FlowAimEvaluator { + const VELOCITY_CHANGE_MULTIPLIER: f64 = 0.52; + + pub fn evaluate_diff_of<'a>( + curr: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>], + with_slider_travel_dist: bool, + ) -> f64 { + let osu_curr_obj = curr; + + let Some(osu_last_obj) = curr + .previous(0, diff_objects) + .filter(|last| !(curr.base.is_spinner() || last.base.is_spinner())) + else { + return 0.0; + }; + + let curr_dist = if with_slider_travel_dist { osu_curr_obj.lazy_jump_dist } else { osu_curr_obj.jump_dist }; + let prev_dist = if with_slider_travel_dist { osu_last_obj.lazy_jump_dist } else { osu_last_obj.jump_dist }; + + let mut curr_vel = curr_dist / osu_curr_obj.adjusted_delta_time; + + if osu_last_obj.base.is_slider() && with_slider_travel_dist { + // * If the last object is a slider, then we extend the travel velocity through the slider into the current object. + let slider_dist = osu_last_obj.lazy_travel_dist + osu_curr_obj.lazy_jump_dist; + curr_vel = curr_vel.max(slider_dist / osu_curr_obj.adjusted_delta_time); + } + + let prev_vel = prev_dist / osu_last_obj.adjusted_delta_time; + + let mut flow_diff = curr_vel; + + // * Apply high circle size bonus to the base velocity. + // * We use reduced CS bonus here because the bonus was made for an evaluator with a different d/t scaling. + flow_diff *= osu_curr_obj.small_circle_bonus.sqrt(); + + // * Rhythm changes are harder to flow. + flow_diff *= 1.0 + f64::min( + 0.25, + f64::powf( + (osu_curr_obj.adjusted_delta_time.max(osu_last_obj.adjusted_delta_time) - osu_curr_obj.adjusted_delta_time.min(osu_last_obj.adjusted_delta_time)) / 50.0, + 4.0 + ) + ); + + if let (Some(curr_angle), Some(last_angle)) = (osu_curr_obj.angle, osu_last_obj.angle) { + let angle_diff = (curr_angle - last_angle).abs(); + let angle_diff_adjusted = (angle_diff / 2.0).sin() * 180.0; + let angular_vel = angle_diff_adjusted / (osu_curr_obj.adjusted_delta_time * 0.1); + + // * Low angular velocity flow (angles are consistent) is easier to follow than erratic flow. + flow_diff *= 0.8 + (angular_vel / 270.0).sqrt(); + } + + // * If all three notes are overlapping - don't reward bonuses as you don't have to do additional movement. + let mut overlapped_notes_weight = 1.0; + + // NOTE: Source does not null check osuLastLastObj + // instead current.Index > 2 is checked. + if let Some(osu_last_last_obj) = curr.previous(1, diff_objects) { + overlapped_notes_weight = 1.0 + - Self::calc_overlap_factor(osu_curr_obj, osu_last_obj) + * Self::calc_overlap_factor(osu_curr_obj, osu_last_last_obj) + * Self::calc_overlap_factor(osu_last_obj, osu_last_last_obj); + } + + if let Some(curr_angle) = osu_curr_obj.angle { + // * Acute angles are also hard to flow. + flow_diff += curr_vel + * SnapAimEvaluator::calc_angle_acuteness(curr_angle) + * overlapped_notes_weight; + } + + if prev_vel.max(curr_vel).not_eq(0.0) { + if with_slider_travel_dist { + curr_vel = curr_dist / osu_curr_obj.adjusted_delta_time; + } + + // * Scale with ratio of difference compared to 0.5 * max dist. + let dist_ratio = smoothstep( + (prev_vel - curr_vel).abs() / prev_vel.max(curr_vel), + 0.0, + 1.0 + ); + + // * Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. + let overlap_vel_buff = f64::min( + f64::from(OsuDifficultyObject::NORMALIZED_DIAMETER) * 1.25 / osu_curr_obj.adjusted_delta_time.min(osu_last_obj.adjusted_delta_time), + (prev_vel - curr_vel).abs() + ); + + flow_diff += overlap_vel_buff + * dist_ratio + * overlapped_notes_weight + * Self::VELOCITY_CHANGE_MULTIPLIER; + } + + if osu_curr_obj.base.is_slider() && with_slider_travel_dist { + // * Include slider velocity to make velocity more consistent with snap. + flow_diff += osu_curr_obj.travel_dist / osu_curr_obj.travel_time; + } + + // * Final velocity is being raised to a power because flow difficulty scales harder with + // * both high distance and time, and we want to account for that. + flow_diff = flow_diff.powf(1.45); + + // * Reduce difficulty for low spacing since spacing below radius is always to be flowed + flow_diff * smootherstep(curr_dist, 0.0, f64::from(OsuDifficultyObject::NORMALIZED_RADIUS)) + } + + fn calc_overlap_factor<'a>( + first: &'a OsuDifficultyObject<'a>, + second: &'a OsuDifficultyObject<'a> + ) -> f64 { + let dist = Pos::distance(&first.base.stacked_pos(), second.base.stacked_pos()); + + f64::clamp( + 1.0 - (f64::from(dist) - first.radius).max(0.0).powf(2.0), + 0.0, + 1.0 + ) + } +} \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/aim/mod.rs b/src/osu/difficulty/evaluators/aim/mod.rs index 0769265d..d1d5b95f 100644 --- a/src/osu/difficulty/evaluators/aim/mod.rs +++ b/src/osu/difficulty/evaluators/aim/mod.rs @@ -1,2 +1,3 @@ mod agility; +mod flow_aim; mod snap_aim; \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/aim/snap_aim.rs b/src/osu/difficulty/evaluators/aim/snap_aim.rs index eaa0df29..827786de 100644 --- a/src/osu/difficulty/evaluators/aim/snap_aim.rs +++ b/src/osu/difficulty/evaluators/aim/snap_aim.rs @@ -248,7 +248,7 @@ impl SnapAimEvaluator { smoothstep(angle, f64::to_radians(140.0), f64::to_radians(40.0)) } - const fn calc_angle_acuteness(angle: f64) -> f64 { + pub const fn calc_angle_acuteness(angle: f64) -> f64 { smoothstep(angle, f64::to_radians(140.0), f64::to_radians(40.0)) } } \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/mod.rs b/src/osu/difficulty/evaluators/mod.rs index 9153a1bc..ccdea99f 100644 --- a/src/osu/difficulty/evaluators/mod.rs +++ b/src/osu/difficulty/evaluators/mod.rs @@ -1,5 +1,7 @@ pub use self::{ - aim_old::AimEvaluator, flashlight::FlashlightEvaluator, rhythm::RhythmEvaluator, + aim_old::AimEvaluator, + flashlight::FlashlightEvaluator, + rhythm::RhythmEvaluator, speed::SpeedEvaluator, }; diff --git a/src/osu/difficulty/object.rs b/src/osu/difficulty/object.rs index a65ce8b0..6c13bd78 100644 --- a/src/osu/difficulty/object.rs +++ b/src/osu/difficulty/object.rs @@ -12,6 +12,7 @@ use super::{HD_FADE_OUT_DURATION_MULTIPLIER, scaling_factor::ScalingFactor}; pub struct OsuDifficultyObject<'a> { pub idx: usize, pub base: &'a OsuObject, + pub radius: f64, pub start_time: f64, pub delta_time: f64, @@ -49,6 +50,7 @@ impl<'a> OsuDifficultyObject<'a> { idx: usize, scaling_factor: &ScalingFactor, ) -> Self { + let radius = f64::from(OsuObject::OBJECT_RADIUS) * scaling_factor.radius; let delta_time = (hit_object.start_time - last_object.start_time) / clock_rate; let start_time = hit_object.start_time / clock_rate; @@ -64,6 +66,7 @@ impl<'a> OsuDifficultyObject<'a> { let mut this = Self { idx, base: hit_object, + radius, start_time, delta_time, adjusted_delta_time, From 7cb0bc0fb282234f189b97c479310f484b0e6cc1 Mon Sep 17 00:00:00 2001 From: myssto Date: Tue, 28 Jul 2026 09:00:17 -0500 Subject: [PATCH 05/27] feat: impl osu speed evaluator group --- src/osu/difficulty/evaluators/aim/mod.rs | 6 +- src/osu/difficulty/evaluators/aim_old.rs | 228 --------------- src/osu/difficulty/evaluators/mod.rs | 18 +- src/osu/difficulty/evaluators/rhythm.rs | 272 ------------------ src/osu/difficulty/evaluators/speed.rs | 73 ----- src/osu/difficulty/evaluators/speed/mod.rs | 2 + src/osu/difficulty/evaluators/speed/rhythm.rs | 262 +++++++++++++++++ src/osu/difficulty/evaluators/speed/speed.rs | 51 ++++ src/osu/difficulty/object.rs | 21 +- src/util/difficulty.rs | 15 +- 10 files changed, 348 insertions(+), 600 deletions(-) delete mode 100644 src/osu/difficulty/evaluators/aim_old.rs delete mode 100644 src/osu/difficulty/evaluators/rhythm.rs delete mode 100644 src/osu/difficulty/evaluators/speed.rs create mode 100644 src/osu/difficulty/evaluators/speed/mod.rs create mode 100644 src/osu/difficulty/evaluators/speed/rhythm.rs create mode 100644 src/osu/difficulty/evaluators/speed/speed.rs diff --git a/src/osu/difficulty/evaluators/aim/mod.rs b/src/osu/difficulty/evaluators/aim/mod.rs index d1d5b95f..3264b91c 100644 --- a/src/osu/difficulty/evaluators/aim/mod.rs +++ b/src/osu/difficulty/evaluators/aim/mod.rs @@ -1,3 +1,3 @@ -mod agility; -mod flow_aim; -mod snap_aim; \ No newline at end of file +pub mod agility; +pub mod flow_aim; +pub mod snap_aim; \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/aim_old.rs b/src/osu/difficulty/evaluators/aim_old.rs deleted file mode 100644 index e306d400..00000000 --- a/src/osu/difficulty/evaluators/aim_old.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::{ - any::difficulty::object::IDifficultyObject, - osu::difficulty::object::OsuDifficultyObject, - util::{ - difficulty::{milliseconds_to_bpm, reverse_lerp, smootherstep, smoothstep}, - float_ext::FloatExt, - }, -}; - -pub struct AimEvaluator; - -impl AimEvaluator { - const WIDE_ANGLE_MULTIPLIER: f64 = 1.5; - const ACUTE_ANGLE_MULTIPLIER: f64 = 2.55; - const SLIDER_MULTIPLIER: f64 = 1.35; - const VELOCITY_CHANGE_MULTIPLIER: f64 = 0.75; - const WIGGLE_MULTIPLIER: f64 = 1.02; - - #[expect(clippy::too_many_lines, reason = "staying in-sync with lazer")] - pub fn evaluate_diff_of<'a>( - curr: &'a OsuDifficultyObject<'a>, - diff_objects: &'a [OsuDifficultyObject<'a>], - with_slider_travel_dist: bool, - ) -> f64 { - let osu_curr_obj = curr; - - let Some((osu_last_last_obj, osu_last_obj)) = curr - .previous(1, diff_objects) - .zip(curr.previous(0, diff_objects)) - .filter(|(_, last)| !(curr.base.is_spinner() || last.base.is_spinner())) - else { - return 0.0; - }; - - #[expect(clippy::items_after_statements, reason = "staying in-sync with lazer")] - const RADIUS: i32 = OsuDifficultyObject::NORMALIZED_RADIUS; - #[expect(clippy::items_after_statements, reason = "staying in-sync with lazer")] - const DIAMETER: i32 = OsuDifficultyObject::NORMALIZED_DIAMETER; - - // * 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.lazy_jump_dist / osu_curr_obj.adjusted_delta_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_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 - let movement_vel = osu_curr_obj.min_jump_dist / osu_curr_obj.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.lazy_jump_dist / osu_last_obj.adjusted_delta_time; - - 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; - - 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; - let mut wiggle_bonus = 0.0; - - // * Start strain with regular velocity. - let mut aim_strain = curr_vel; - - if let Some((curr_angle, last_angle)) = osu_curr_obj.angle.zip(osu_last_obj.angle) { - // * Rewarding angles, take the smaller velocity as base. - let angle_bonus = curr_vel.min(prev_vel); - - // * If rhythms are the same. - if osu_curr_obj - .adjusted_delta_time - .max(osu_last_obj.adjusted_delta_time) - < 1.25 - * osu_curr_obj - .adjusted_delta_time - .min(osu_last_obj.adjusted_delta_time) - { - acute_angle_bonus = Self::calc_acute_angle_bonus(curr_angle); - - // * Penalize angle repetition. - acute_angle_bonus *= 0.08 - + 0.92 - * (1.0 - - f64::min( - acute_angle_bonus, - f64::powf(Self::calc_acute_angle_bonus(last_angle), 3.0), - )); - - // * Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter - acute_angle_bonus *= angle_bonus - * smootherstep( - milliseconds_to_bpm(osu_curr_obj.adjusted_delta_time, Some(2)), - 300.0, - 400.0, - ) - * smootherstep( - osu_curr_obj.lazy_jump_dist, - f64::from(DIAMETER), - f64::from(DIAMETER * 2), - ); - } - - wide_angle_bonus = Self::calc_wide_angle_bonus(curr_angle); - - // * Penalize angle repetition. - wide_angle_bonus *= 1.0 - - f64::min( - wide_angle_bonus, - f64::powf(Self::calc_wide_angle_bonus(last_angle), 3.0), - ); - - // * Apply full wide angle bonus for distance more than one diameter - wide_angle_bonus *= - angle_bonus * smootherstep(osu_curr_obj.lazy_jump_dist, 0.0, f64::from(DIAMETER)); - - // * Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle - // * https://www.desmos.com/calculator/dp0v0nvowc - wiggle_bonus = angle_bonus - * smootherstep( - osu_curr_obj.lazy_jump_dist, - f64::from(RADIUS), - f64::from(DIAMETER), - ) - * f64::powf( - reverse_lerp( - osu_curr_obj.lazy_jump_dist, - f64::from(DIAMETER * 3), - f64::from(DIAMETER), - ), - 1.8, - ) - * smootherstep(curr_angle, f64::to_radians(110.0), f64::to_radians(60.0)) - * smootherstep( - osu_last_obj.lazy_jump_dist, - f64::from(RADIUS), - f64::from(DIAMETER), - ) - * f64::powf( - reverse_lerp( - osu_last_obj.lazy_jump_dist, - f64::from(DIAMETER * 3), - f64::from(DIAMETER), - ), - 1.8, - ) - * smootherstep(last_angle, f64::to_radians(110.0), f64::to_radians(60.0)); - - if let Some(osu_last_2_obj) = curr.previous(2, diff_objects) { - let distance = - (osu_last_2_obj.base.stacked_pos() - osu_last_obj.base.stacked_pos()).length(); - - if distance < 1.0 { - wide_angle_bonus *= 1.0 - 0.35 * f64::from(1.0 - distance); - } - } - } - - if prev_vel.max(curr_vel).not_eq(0.0) { - // * 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.lazy_jump_dist + osu_last_last_obj.travel_dist) - / osu_last_obj.adjusted_delta_time; - curr_vel = (osu_curr_obj.lazy_jump_dist + osu_last_obj.travel_dist) - / osu_curr_obj.adjusted_delta_time; - - // * Scale with ratio of difference compared to 0.5 * max dist. - let dist_ratio = smoothstep( - (prev_vel - curr_vel).abs() / prev_vel.max(curr_vel), - 0.0, - 1.0, - ); - - // * Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. - let overlap_vel_buff = (f64::from(DIAMETER) * 1.25 - / osu_curr_obj - .adjusted_delta_time - .min(osu_last_obj.adjusted_delta_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.adjusted_delta_time) - .min(osu_last_obj.adjusted_delta_time) - / (osu_curr_obj.adjusted_delta_time).max(osu_last_obj.adjusted_delta_time); - vel_change_bonus *= bonus_base.powf(2.0); - } - - if osu_last_obj.base.is_slider() { - // * Reward sliders based on velocity. - slider_bonus = osu_last_obj.travel_dist / osu_last_obj.travel_time; - } - - aim_strain += wiggle_bonus * Self::WIGGLE_MULTIPLIER; - aim_strain += vel_change_bonus * Self::VELOCITY_CHANGE_MULTIPLIER; - - // * Add in acute angle bonus or wide angle bonus, whichever is larger. - aim_strain += (acute_angle_bonus * Self::ACUTE_ANGLE_MULTIPLIER) - .max(wide_angle_bonus * Self::WIDE_ANGLE_MULTIPLIER); - - aim_strain *= osu_curr_obj.small_circle_bonus; - - // * Add in additional slider velocity bonus. - if with_slider_travel_dist { - aim_strain += slider_bonus * Self::SLIDER_MULTIPLIER; - } - - aim_strain - } - - const fn calc_wide_angle_bonus(angle: f64) -> f64 { - smoothstep(angle, f64::to_radians(40.0), f64::to_radians(140.0)) - } - - const fn calc_acute_angle_bonus(angle: f64) -> f64 { - smoothstep(angle, f64::to_radians(140.0), f64::to_radians(40.0)) - } -} diff --git a/src/osu/difficulty/evaluators/mod.rs b/src/osu/difficulty/evaluators/mod.rs index ccdea99f..78231ed6 100644 --- a/src/osu/difficulty/evaluators/mod.rs +++ b/src/osu/difficulty/evaluators/mod.rs @@ -1,12 +1,16 @@ pub use self::{ - aim_old::AimEvaluator, - flashlight::FlashlightEvaluator, - rhythm::RhythmEvaluator, - speed::SpeedEvaluator, + aim::{ + agility::AgilityEvaluator, + flow_aim::FlowAimEvaluator, + snap_aim::SnapAimEvaluator, + }, + speed::{ + rhythm::RhythmEvaluator, + speed::SpeedEvaluator, + }, + flashlight::FlashlightEvaluator }; mod aim; -mod aim_old; -mod flashlight; -mod rhythm; mod speed; +mod flashlight; diff --git a/src/osu/difficulty/evaluators/rhythm.rs b/src/osu/difficulty/evaluators/rhythm.rs deleted file mode 100644 index 101d4c9e..00000000 --- a/src/osu/difficulty/evaluators/rhythm.rs +++ /dev/null @@ -1,272 +0,0 @@ -use std::cmp; - -use crate::{ - any::difficulty::object::IDifficultyObject, - osu::difficulty::object::OsuDifficultyObject, - util::difficulty::{logistic, smoothstep_bell_curve}, -}; - -pub struct RhythmEvaluator; - -impl RhythmEvaluator { - const HISTORY_TIME_MAX: u32 = 5 * 1000; // 5 seconds - const HISTORY_OBJECTS_MAX: usize = 32; - const RHYTHM_OVERALL_MULTIPLIER: f64 = 1.0; - const RHYTHM_RATIO_MULTIPLIER: f64 = 15.0; - - #[expect(clippy::too_many_lines, reason = "staying in-sync with lazer")] - pub fn evaluate_diff_of<'a>( - curr: &'a OsuDifficultyObject<'a>, - diff_objects: &'a [OsuDifficultyObject<'a>], - hit_window: f64, - ) -> f64 { - if curr.base.is_spinner() { - return 0.0; - } - - let mut rhythm_complexity_sum = 0.0; - - let delta_difference_eps = hit_window * 0.3; - - let mut island = RhythmIsland::new(delta_difference_eps); - let mut prev_island = RhythmIsland::new(delta_difference_eps); - - // * we can't use dictionary here because we need to compare island with a tolerance - // * which is impossible to pass into the hash comparer - let mut island_counts = Vec::::new(); - - // * 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 = cmp::min(curr.idx, Self::HISTORY_OBJECTS_MAX); - - let mut rhythm_start = 0; - - while curr - .previous(rhythm_start, diff_objects) - .filter(|prev| { - rhythm_start + 2 < historical_note_count - && curr.start_time - prev.start_time < f64::from(Self::HISTORY_TIME_MAX) - }) - .is_some() - { - rhythm_start += 1; - } - - if let Some((mut prev_obj, mut last_obj)) = curr - .previous(rhythm_start, diff_objects) - .zip(curr.previous(rhythm_start + 1, diff_objects)) - { - // * we go from the furthest object back to the current one - 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 time_decay = (f64::from(Self::HISTORY_TIME_MAX) - - (curr.start_time - curr_obj.start_time)) - / f64::from(Self::HISTORY_TIME_MAX); - let note_decay = (historical_note_count - i) as f64 / historical_note_count as f64; - - // * either we're limited by time or limited by object count. - let curr_historical_decay = note_decay.min(time_decay); - - // * Use custom cap value to ensure that at this point delta time is actually zero - let curr_delta = curr_obj.delta_time.max(1e-7); - let prev_delta = prev_obj.delta_time.max(1e-7); - let last_delta = last_obj.delta_time.max(1e-7); - - // * calculate how much current delta difference deserves a rhythm bonus - // * this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200) - let delta_difference = prev_delta.max(curr_delta) / prev_delta.min(curr_delta); - - // * Take only the fractional part of the value since we're only interested in punishing multiples - let delta_difference_fraction = delta_difference - delta_difference.trunc(); - - let curr_ratio = 1.0 - + Self::RHYTHM_RATIO_MULTIPLIER - * smoothstep_bell_curve(delta_difference_fraction, 0.5, 0.5).min(0.5); - - // * reduce ratio bonus if delta difference is too big - let difference_multiplier = (2.0 - delta_difference / 8.0).clamp(0.0, 1.0); - - let window_penalty = (((prev_delta - curr_delta).abs() - delta_difference_eps) - .max(0.0) - / delta_difference_eps) - .min(1.0); - - let mut effective_ratio = window_penalty * curr_ratio * difference_multiplier; - - if first_delta_switch { - if (prev_delta - curr_delta).abs() < delta_difference_eps { - // * island is still progressing - island.add_delta(curr_delta as i32); - } 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 - // * unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders - if prev_obj.base.is_slider() { - effective_ratio *= 0.3; - } - - // * repeated island polarity (2 -> 4, 3 -> 5) - if island.is_similar_polarity(&prev_island) { - 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 + delta_difference_eps - && prev_delta > curr_delta + delta_difference_eps - { - effective_ratio *= 0.125; - } - - // * repeated island size (ex: triplet -> triplet) - // * TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation - if prev_island.delta_count == island.delta_count { - effective_ratio *= 0.5; - } - - if let Some(island_count) = island_counts - .iter_mut() - .find(|entry| entry.island == island) - .filter(|entry| !entry.island.is_default()) - { - // * only add island to island counts if they're going one after another - if prev_island == island { - island_count.count += 1; - } - - // * repeated island (ex: triplet -> triplet) - let power = logistic(f64::from(island.delta), 58.33, 0.24, Some(2.75)); - effective_ratio *= (3.0 / island_count.count as f64) - .min((island_count.count as f64).recip().powf(power)); - } else { - island_counts.push(IslandCount { island, count: 1 }); - } - - // * scale down the difficulty if the object is doubletappable - let doubletapness = prev_obj.get_doubletapness(Some(curr_obj), hit_window); - effective_ratio *= 1.0 - doubletapness * 0.75; - - rhythm_complexity_sum += - (effective_ratio * start_ratio).sqrt() * curr_historical_decay; - - start_ratio = effective_ratio; - - prev_island = island; - - // * we're slowing down, stop counting - if prev_delta + delta_difference_eps < curr_delta { - // * if we're speeding up, this stays true and we keep counting island size. - first_delta_switch = false; - } - - island = - RhythmIsland::new_with_delta(curr_delta as i32, delta_difference_eps); - } - } else if prev_delta > curr_delta + delta_difference_eps { - // * we're speeding up. - // * Begin counting island until we change speed again. - first_delta_switch = true; - - // * bpm change is into slider, this is easy acc window - if curr_obj.base.is_slider() { - effective_ratio *= 0.6; - } - - // * bpm change was from a slider, this is easier typically than circle -> circle - // * unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders - if prev_obj.base.is_slider() { - effective_ratio *= 0.6; - } - - start_ratio = effective_ratio; - - island = RhythmIsland::new_with_delta(curr_delta as i32, delta_difference_eps); - } - - last_obj = prev_obj; - prev_obj = curr_obj; - } - } - - // * produces multiplier that can be applied to strain. range [1, infinity) (not really though) - let mut rhythm_difficulty = - (4.0 + rhythm_complexity_sum * Self::RHYTHM_OVERALL_MULTIPLIER).sqrt() / 2.0; - rhythm_difficulty *= 1.0 - curr.get_doubletapness(curr.next(0, diff_objects), hit_window); - - rhythm_difficulty - } -} - -#[derive(Copy, Clone)] -struct RhythmIsland { - delta_difference_eps: f64, - delta: i32, - delta_count: i32, -} - -const MIN_DELTA_TIME: i32 = 25; - -// Compile-time check in case `OsuDifficultyObject::MIN_DELTA_TIME` changes -// but we forget to update this value. -const _: [(); 0 - !{ MIN_DELTA_TIME - OsuDifficultyObject::MIN_DELTA_TIME as i32 == 0 } as usize] = - []; - -impl RhythmIsland { - const fn new(delta_difference_eps: f64) -> Self { - Self { - delta_difference_eps, - delta: 0, - delta_count: 0, - } - } - - fn new_with_delta(delta: i32, delta_difference_eps: f64) -> Self { - Self { - delta_difference_eps, - delta: cmp::max(delta, MIN_DELTA_TIME), - delta_count: 1, - } - } - - fn add_delta(&mut self, delta: i32) { - if self.delta == i32::MAX { - self.delta = cmp::max(delta, MIN_DELTA_TIME); - } - - self.delta_count += 1; - } - - const fn is_similar_polarity(&self, other: &Self) -> bool { - // * TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple) - // * naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation - self.delta_count % 2 == other.delta_count % 2 - } - - fn is_default(&self) -> bool { - self.delta_difference_eps.abs() < f64::EPSILON - && self.delta == i32::MAX - && self.delta_count == 0 - } -} - -impl PartialEq for RhythmIsland { - fn eq(&self, other: &Self) -> bool { - f64::from((self.delta - other.delta).abs()) < self.delta_difference_eps - && self.delta_count == other.delta_count - } -} - -struct IslandCount { - island: RhythmIsland, - count: usize, -} diff --git a/src/osu/difficulty/evaluators/speed.rs b/src/osu/difficulty/evaluators/speed.rs deleted file mode 100644 index 961d2dd5..00000000 --- a/src/osu/difficulty/evaluators/speed.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{ - any::difficulty::object::IDifficultyObject, - osu::difficulty::object::OsuDifficultyObject, - util::difficulty::{bpm_to_milliseconds, milliseconds_to_bpm}, -}; - -pub struct SpeedEvaluator; - -impl SpeedEvaluator { - const SINGLE_SPACING_THRESHOLD: f64 = OsuDifficultyObject::NORMALIZED_DIAMETER as f64 * 1.25; // 1.25 circlers distance between centers - const MIN_SPEED_BONUS: f64 = 200.0; // 200 BPM 1/4th - const SPEED_BALANCING_FACTOR: f64 = 40.0; - const DIST_MULTIPLIER: f64 = 0.8; - - pub fn evaluate_diff_of<'a>( - curr: &'a OsuDifficultyObject<'a>, - diff_objects: &'a [OsuDifficultyObject<'a>], - hit_window: f64, - autopilot: bool, - ) -> f64 { - if curr.base.is_spinner() { - return 0.0; - } - - // * derive strainTime for calculation - let osu_curr_obj = curr; - let osu_prev_obj = curr.previous(0, diff_objects); - let osu_next_obj = curr.next(0, diff_objects); - - let mut strain_time = curr.adjusted_delta_time; - // Note: Technically `osu_next_obj` is never `None` but instead the - // default value. This could maybe invalidate the `get_doubletapness` - // result. - let doubletapness = 1.0 - osu_curr_obj.get_doubletapness(osu_next_obj, hit_window); - - // * 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); - - let speed_bonus = if milliseconds_to_bpm(strain_time, None) > Self::MIN_SPEED_BONUS { - // * Add additional scaling bonus for streams/bursts higher than 200bpm - let base = (bpm_to_milliseconds(Self::MIN_SPEED_BONUS, None) - strain_time) - / Self::SPEED_BALANCING_FACTOR; - - 0.75 * base.powf(2.0) - } else { - // * speedBonus will be 0.0 for BPM < 200 - 0.0 - }; - - let travel_dist = osu_prev_obj.map_or(0.0, |obj| obj.travel_dist); - let mut dist = travel_dist + osu_curr_obj.min_jump_dist; - - // * Cap distance at single_spacing_threshold - dist = Self::SINGLE_SPACING_THRESHOLD.min(dist); - - // * Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold - let mut dist_bonus = - (dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.95) * Self::DIST_MULTIPLIER; - - dist_bonus *= osu_curr_obj.small_circle_bonus.sqrt(); - - if autopilot { - dist_bonus = 0.0; - } - - // * Base difficulty with all bonuses - let difficulty = (1.0 + speed_bonus + dist_bonus) * 1000.0 / strain_time; - - // * Apply penalty if there's doubletappable doubles - difficulty * doubletapness - } -} diff --git a/src/osu/difficulty/evaluators/speed/mod.rs b/src/osu/difficulty/evaluators/speed/mod.rs new file mode 100644 index 00000000..0e7957d1 --- /dev/null +++ b/src/osu/difficulty/evaluators/speed/mod.rs @@ -0,0 +1,2 @@ +pub mod rhythm; +pub mod speed; \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/speed/rhythm.rs b/src/osu/difficulty/evaluators/speed/rhythm.rs new file mode 100644 index 00000000..bd5c79c1 --- /dev/null +++ b/src/osu/difficulty/evaluators/speed/rhythm.rs @@ -0,0 +1,262 @@ +use crate::{ + any::difficulty::object::IDifficultyObject, + osu::difficulty::object::OsuDifficultyObject, + util::difficulty::{logistic, reverse_lerp, smoothstep_bell_curve}, +}; + +pub struct RhythmEvaluator; + +impl RhythmEvaluator { + const HISTORY_TIME_MAX: u32 = 5 * 1000; // 5 seconds + const HISTORY_OBJECTS_MAX: usize = 32; + const RHYTHM_OVERALL_MULTIPLIER: f64 = 1.0; + const RHYTHM_RATIO_MULTIPLIER: f64 = 15.0; + const RHYTHM_RATIO_DIFF_MULTIPLIER: f64 = 26.0; + const DELTA_MIN_VALUE: f64 = 1e-7; + + pub fn evaluate_diff_of<'a>( + curr: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>], + hit_window: f64, + ) -> f64 { + if curr.base.is_spinner() { + return 0.0; + } + + let mut rhythm_complexity_sum = 0.0; + let delta_difference_eps = hit_window * 0.3; + + let mut island = RhythmIsland::new(i32::MAX); + let mut prev_island = RhythmIsland::new(i32::MAX); + let mut islands = Vec::::new(); + + // * Store the difficulty of the current start of an island to buff for tighter rhythms. + let mut start_difficulty = 0.0; + let mut first_delta_switch = false; + let historical_note_count = curr.idx.max(Self::HISTORY_OBJECTS_MAX); + let mut rhythm_start = 0; + + while curr + .previous(rhythm_start, diff_objects) + .filter(|prev| { + rhythm_start + 2 < historical_note_count + && curr.start_time - prev.start_time < f64::from(Self::HISTORY_TIME_MAX) + }) + .is_some() + { + rhythm_start += 1; + } + + if let Some((mut prev_obj, mut prev_prev_obj)) = curr + .previous(rhythm_start, diff_objects) + .zip(curr.previous(rhythm_start + 1, diff_objects)) + { + // * We go from the furthest object back to the current one. + for i in (1..=rhythm_start).rev() { + let Some(curr_obj) = curr.previous(i - 1, diff_objects) else { + break; + }; + + if curr_obj.base.is_spinner() { + continue + } + + // * Scales note 0 to 1 from history to now + let time_decay = (f64::from(Self::HISTORY_TIME_MAX) - (curr.start_time - curr_obj.start_time)) / f64::from(Self::HISTORY_TIME_MAX); + let note_decay = ((historical_note_count - i) / historical_note_count) as f64; + + let curr_historical_decay = note_decay.min(time_decay); + + // * Use custom cap value to ensure that at this point delta time is actually zero. + let curr_delta = curr_obj.delta_time.max(Self::DELTA_MIN_VALUE); + let prev_delta = prev_obj.delta_time.max(Self::DELTA_MIN_VALUE); + let delta_difference = (prev_delta - curr_delta).abs(); + + // * Make sure to always have the current island initialised - if we don't do it here it will only initialise on the next rhythm change. + if island.delta == i32::MAX { + island = RhythmIsland::new(curr_delta as i32); + } + + // * Calculate how much current delta difference deserves a rhythm bonus. + // * this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200). + let delta_difference_ratio = prev_delta.max(curr_delta) / prev_delta.min(curr_delta); + + // * reduce ratio bonus if delta difference is too big + let difference_multiplier = (2.0 - delta_difference / 8.0).clamp(0.0, 1.0); + + let window_penalty = ((delta_difference - delta_difference_eps) / delta_difference_eps).clamp(0.0, 1.0); + + let mut effective_difficulty = Self::get_effective_diff(delta_difference_ratio) * window_penalty * difference_multiplier; + + // * If previous object is a slider it might be easier to tap since you don't have to do a whole tapping motion + // * while a full deltatime might end up some weird ratio the "unpress->tap" motion might be simple + // * for example a slider-circle-circle pattern should be evaluated as a regular triple and not as a single->double + if prev_obj.base.is_slider() { + let slider_lazy_end_delta = curr_obj.min_jump_time; + let slider_lazy_delta_diff_ratio = slider_lazy_end_delta.max(curr_delta) / slider_lazy_end_delta.min(curr_delta); + + let slider_real_end_delta = curr_obj.last_obj_end_delta_time; + let slider_real_delta_diff_ratio = slider_real_end_delta.max(curr_delta) / slider_real_end_delta.min(curr_delta); + + let slider_effective_difficulty = f64::min( + Self::get_effective_diff(slider_lazy_delta_diff_ratio), + Self::get_effective_diff(slider_real_delta_diff_ratio) + ); + effective_difficulty = slider_effective_difficulty.min(effective_difficulty); + } + + if delta_difference < delta_difference_eps { + // * Island is still progressing + island.add_delta(curr_delta as i32); + } + + if first_delta_switch && delta_difference > delta_difference_eps { + // * bpm change is into slider, this is easy acc window + if curr_obj.base.is_slider() { + effective_difficulty *= 0.5; + } + + // * repeated island polarity (2 -> 4, 3 -> 5) + if island.is_similar_polarity(&prev_island, delta_difference_eps) { + effective_difficulty *= 0.5; + } + + // * previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. + if prev_prev_obj.delta_time.max(Self::DELTA_MIN_VALUE) > prev_delta + delta_difference_eps && prev_delta > curr_delta + delta_difference_eps { + effective_difficulty *= 0.125; + } + + // * repeated island size (ex: triplet -> triplet) + // * TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation + if prev_island.delta_count == island.delta_count { + effective_difficulty *= 0.5; + } + + // isSpeedingUp + if prev_delta > curr_delta + delta_difference_eps { + effective_difficulty *= 0.65; + } + + let mut found = false; + + for existing_island in islands + .iter_mut() + .filter(|i| i.almost_eq(&island, delta_difference_eps)) { + + // * only increase island occurrences if they're going one after another + if prev_island.almost_eq(&island, delta_difference_eps) { + existing_island.occurrences += 1; + } + + // * repeated island (ex: triplet -> triplet) + let power = logistic(f64::from(island.delta), 58.33, 0.24, Some(2.75)); + effective_difficulty *= f64::min( + 3.0 / f64::from(existing_island.occurrences), + (1.0 / f64::from(existing_island.occurrences)).powf(power) + ); + + found = true; + break + } + + if !found && island.delta_count > 0 { + islands.push(island); + } + + // * scale down the difficulty if the object is double-tappable + effective_difficulty *= 1.0 - prev_obj.calc_double_tap_feasibility(Some(curr_obj), hit_window) * 0.75; + + if island.delta_count > 1 { + rhythm_complexity_sum += (effective_difficulty * start_difficulty).sqrt() * curr_historical_decay; + } else { + // * constant difficulty for single-note islands + rhythm_complexity_sum += 0.7 * curr_historical_decay; + } + + start_difficulty = effective_difficulty; + + // * we're slowing down, stop counting + if prev_delta + delta_difference_eps < curr_delta { + // * if we're speeding up, this stays true and we keep counting island size. + first_delta_switch = false; + } + + prev_island = island; + island = RhythmIsland::new(curr_delta as i32); + + } else if prev_delta > curr_delta + delta_difference_eps { + // * we're speeding up + // * Begin counting island until we change speed again. + first_delta_switch = true; + + // * bpm change is into slider, this is easy acc window + if curr_obj.base.is_slider() { + effective_difficulty *= 0.6; + } + + // * bpm change was from a slider, this is easier typically than circle -> circle + // * unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders + if prev_obj.base.is_slider() { + effective_difficulty *= 0.6; + } + + start_difficulty = effective_difficulty; + island = RhythmIsland::new(curr_delta as i32); + } + + prev_prev_obj = prev_obj; + prev_obj = curr_obj; + } + } + + // * If the current island is long we don't want the sum to have as big of an effect + rhythm_complexity_sum *= reverse_lerp(f64::from(island.delta_count), 22.0, 3.0); + + (4.0 + rhythm_complexity_sum * Self::RHYTHM_OVERALL_MULTIPLIER).sqrt() / 2.0 + } + + fn get_effective_diff(delta_diff_ratio: f64) -> f64 { + // * Take only the fractional part of the value since we're only interested in punishing multiples + let delta_diff_fraction = delta_diff_ratio - delta_diff_ratio.trunc(); + 1.0 + Self::RHYTHM_RATIO_DIFF_MULTIPLIER * f64::min(0.5, smoothstep_bell_curve(delta_diff_fraction)) + } +} + +#[derive(Copy, Clone)] +struct RhythmIsland { + delta: i32, + delta_count: i32, + occurrences: i32 +} + +impl RhythmIsland { + pub fn new(delta: i32) -> Self { + Self { + delta: delta.max(OsuDifficultyObject::MIN_DELTA_TIME as i32), + delta_count: 1, + occurrences: 1 + } + } + + fn add_delta(&mut self, delta: i32) { + if delta == i32::MAX { + self.delta = delta.max(OsuDifficultyObject::MIN_DELTA_TIME as i32); + } + + self.delta_count += 1; + } + + fn is_similar_polarity(&self, other: &Self, epsilon: f64) -> bool { + // * Single delta islands shouldn't be compared. + if self.delta_count <= 1 || other.delta_count <= 1 { + false + } else { + f64::from((self.delta - other.delta).abs()) < epsilon + && self.delta_count % 2 == other.delta_count % 2 + } + } + + fn almost_eq(&self, other: &Self, epsilon: f64) -> bool { + f64::from((self.delta - other.delta).abs()) < epsilon && self.delta_count == other.delta_count + } +} \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/speed/speed.rs b/src/osu/difficulty/evaluators/speed/speed.rs new file mode 100644 index 00000000..6c4c976b --- /dev/null +++ b/src/osu/difficulty/evaluators/speed/speed.rs @@ -0,0 +1,51 @@ +use crate::{ + any::difficulty::object::IDifficultyObject, + osu::difficulty::object::OsuDifficultyObject, + util::difficulty::{bpm_to_milliseconds, milliseconds_to_bpm}, +}; + +pub struct SpeedEvaluator; + +impl SpeedEvaluator { + const MIN_SPEED_BONUS: f64 = 200.0; // 200 BPM 1/4th + const SPEED_BALANCING_FACTOR: f64 = 40.0; + + pub fn evaluate_diff_of<'a>( + curr: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>], + hit_window: f64, + ) -> f64 { + if curr.base.is_spinner() { + return 0.0; + } + + let osu_curr_obj = curr; + + let mut strain_time = osu_curr_obj.adjusted_delta_time; + let double_tap_feasibility = 1.0 - osu_curr_obj.calc_double_tap_feasibility(osu_curr_obj.next(0, diff_objects), hit_window); + + // * 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); + + let speed_bonus = if milliseconds_to_bpm(strain_time, None) > Self::MIN_SPEED_BONUS { + // * Add additional scaling bonus for streams/bursts higher than 200bpm + 0.75 * ((bpm_to_milliseconds(Self::MIN_SPEED_BONUS, None) - strain_time) / Self::SPEED_BALANCING_FACTOR).powf(2.0) + } else { + // * speedBonus will be 0.0 for BPM < 200 + 0.0 + }; + + // * Base difficulty with all bonuses + let mut speed_difficulty = (1.0 + speed_bonus) * 1000.0 / strain_time; + + speed_difficulty *= Self::high_bpm_bonus(osu_curr_obj.adjusted_delta_time); + + // * Apply penalty if there's doubletappable doubles + speed_difficulty * double_tap_feasibility + } + + fn high_bpm_bonus(ms: f64) -> f64 { + 1.0 / (1.0 - f64::powf(0.3, ms / 1000.0)) + } +} \ No newline at end of file diff --git a/src/osu/difficulty/object.rs b/src/osu/difficulty/object.rs index 6c13bd78..1abf6531 100644 --- a/src/osu/difficulty/object.rs +++ b/src/osu/difficulty/object.rs @@ -3,8 +3,7 @@ use std::borrow::Cow; use rosu_map::util::Pos; use crate::{ - any::difficulty::object::{HasStartTime, IDifficultyObject}, - osu::object::{OsuObject, OsuObjectKind}, + any::difficulty::object::{HasStartTime, IDifficultyObject}, osu::object::{OsuObject, OsuObjectKind}, util::difficulty::reverse_lerp, }; use super::{HD_FADE_OUT_DURATION_MULTIPLIER, scaling_factor::ScalingFactor}; @@ -120,8 +119,10 @@ impl<'a> OsuDifficultyObject<'a> { } } - pub fn get_doubletapness(&self, next: Option<&Self>, hit_window: f64) -> f64 { - let Some(next) = next else { return 0.0 }; + pub fn calc_double_tap_feasibility(&self, next: Option<&Self>, hit_window: f64) -> f64 { + let Some(next) = next else { + return 0.0 + }; let hit_window = if self.base.is_spinner() { 0.0 @@ -132,10 +133,18 @@ impl<'a> OsuDifficultyObject<'a> { let curr_delta_time = self.delta_time.max(1.0); let next_delta_time = next.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 = (curr_delta_time / hit_window).min(1.0).powf(2.0); + let window_ratio = (curr_delta_time / hit_window).min(1.0).powf(5.0); + + // * Can't doubletap if circles don't intersect + let distance_factor = reverse_lerp( + self.lazy_jump_dist, + f64::from(Self::NORMALIZED_DIAMETER), + f64::from(Self::NORMALIZED_RADIUS) + ).powf(2.0); - 1.0 - (speed_ratio).powf(1.0 - window_ratio) + 1.0 - speed_ratio.powf(distance_factor * (1.0 - window_ratio)) } fn set_distances( diff --git a/src/util/difficulty.rs b/src/util/difficulty.rs index bba7abab..eb866658 100644 --- a/src/util/difficulty.rs +++ b/src/util/difficulty.rs @@ -40,17 +40,10 @@ pub fn bell_curve(x: f64, mean: f64, width: f64, multiplier: Option) -> f64 multiplier.unwrap_or(1.0) * f64::exp(E * -(f64::powf(x - mean, 2.0) / f64::powf(width, 2.0))) } -pub fn smoothstep_bell_curve(x: f64, mean: f64, width: f64) -> f64 { - let mut new_x = x; - - new_x -= mean; - new_x = if new_x > 0.0 { - width - new_x - } else { - width + new_x - }; - - smoothstep(new_x, 0.0, width) +pub const fn smoothstep_bell_curve(x: f64) -> f64 { + let mut new_x = 0.5 - (x - 0.5).abs(); + new_x = (new_x * 2.0).clamp(0.0, 1.0); + new_x * new_x * (3.0 - 2.0 * new_x) } pub const fn smoothstep(x: f64, start: f64, end: f64) -> f64 { From 88238045bd06a7f1bc708f8c7e89d0fd4a90a2cd Mon Sep 17 00:00:00 2001 From: myssto Date: Tue, 28 Jul 2026 09:22:40 -0500 Subject: [PATCH 06/27] fix: update osu flashlight evaluator --- src/osu/difficulty/evaluators/flashlight.rs | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/osu/difficulty/evaluators/flashlight.rs b/src/osu/difficulty/evaluators/flashlight.rs index 92308f05..5d80b98f 100644 --- a/src/osu/difficulty/evaluators/flashlight.rs +++ b/src/osu/difficulty/evaluators/flashlight.rs @@ -1,7 +1,8 @@ use std::cmp; use crate::{ - any::difficulty::object::IDifficultyObject, + GameMods, + any::difficulty::object::IDifficultyObject, osu::{difficulty::object::OsuDifficultyObject, object::OsuObjectKind}, }; @@ -32,7 +33,7 @@ impl FlashlightEvaluator { &self, curr: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>], - hidden: bool, + mods: &GameMods, ) -> f64 { if curr.base.is_spinner() { return 0.0; @@ -44,7 +45,7 @@ impl FlashlightEvaluator { let mut small_dist_nerf = 1.0; let mut cumulative_strain_time = 0.0; - let mut result = 0.0; + let mut flashlight_difficulty = 0.0; let mut last_obj = osu_curr; @@ -55,11 +56,10 @@ impl FlashlightEvaluator { let Some(curr_obj) = curr.previous(i, diff_objects) else { break; }; + let curr_hit_obj = curr_obj.base; cumulative_strain_time += last_obj.adjusted_delta_time; - let curr_hit_obj = curr_obj.base; - if !curr_obj.base.is_spinner() { let jump_dist = f64::from( (osu_hit_obj.stacked_pos() - curr_hit_obj.stacked_end_pos()).length(), @@ -79,12 +79,12 @@ impl FlashlightEvaluator { * (1.0 - osu_curr.opacity_at( curr_hit_obj.start_time, - hidden, + mods.hd_only_fade_approach_circles() == Some(false), self.time_preempt, self.time_fade_in, )); - result += stack_nerf * opacity_bonus * self.scaling_factor * jump_dist + flashlight_difficulty += stack_nerf * opacity_bonus * self.scaling_factor * jump_dist / cumulative_strain_time; if let Some((curr_obj_angle, osu_curr_angle)) = curr_obj.angle.zip(osu_curr.angle) { @@ -98,15 +98,15 @@ impl FlashlightEvaluator { last_obj = curr_obj; } - result = (small_dist_nerf * result).powf(2.0); + flashlight_difficulty = (small_dist_nerf * flashlight_difficulty).powf(2.0); // * Additional bonus for Hidden due to there being no approach circles. - if hidden { - result *= 1.0 + Self::HIDDEN_BONUS; + if mods.hd() { + flashlight_difficulty *= 1.0 + Self::HIDDEN_BONUS; } // * Nerf patterns with repeated angles. - result *= Self::MIN_ANGLE_MULTIPLIER + flashlight_difficulty *= Self::MIN_ANGLE_MULTIPLIER + (1.0 - Self::MIN_ANGLE_MULTIPLIER) / (angle_repeat_count + 1.0); let mut slider_bonus = 0.0; @@ -131,8 +131,8 @@ impl FlashlightEvaluator { } } - result += slider_bonus * Self::SLIDER_MULTIPLIER; + flashlight_difficulty += slider_bonus * Self::SLIDER_MULTIPLIER; - result + flashlight_difficulty } } From 857dac76ddae159221f890c55153716ad285fdcb Mon Sep 17 00:00:00 2001 From: myssto Date: Wed, 29 Jul 2026 18:36:47 -0500 Subject: [PATCH 07/27] feat: impl osu ReadingEvaluator --- src/osu/difficulty/evaluators/mod.rs | 4 +- src/osu/difficulty/evaluators/reading.rs | 274 +++++++++++++++++++++++ 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/osu/difficulty/evaluators/reading.rs diff --git a/src/osu/difficulty/evaluators/mod.rs b/src/osu/difficulty/evaluators/mod.rs index 78231ed6..ad1812b5 100644 --- a/src/osu/difficulty/evaluators/mod.rs +++ b/src/osu/difficulty/evaluators/mod.rs @@ -8,9 +8,11 @@ pub use self::{ rhythm::RhythmEvaluator, speed::SpeedEvaluator, }, - flashlight::FlashlightEvaluator + flashlight::FlashlightEvaluator, + reading::ReadingEvaluator, }; mod aim; mod speed; mod flashlight; +mod reading; \ No newline at end of file diff --git a/src/osu/difficulty/evaluators/reading.rs b/src/osu/difficulty/evaluators/reading.rs new file mode 100644 index 00000000..97749c3e --- /dev/null +++ b/src/osu/difficulty/evaluators/reading.rs @@ -0,0 +1,274 @@ +use core::f64; + +use crate::{ + any::difficulty::object::IDifficultyObject, + osu::difficulty::object::OsuDifficultyObject, + util::{ + difficulty::{norm, reverse_lerp, smootherstep}, + float_ext::FloatExt + }, +}; + +pub struct ReadingEvaluator { + time_preempt: f64, + time_fade_in: f64, +} + +impl ReadingEvaluator { + const READING_WINDOW_SIZE: f64 = 3000.0; // * 3 seconds + const DISTANCE_INFLUENCE_THRESHOLD: f64 = (OsuDifficultyObject::NORMALIZED_DIAMETER as f64) * 1.5; + + const DENSITY_MULTIPLIER: f64 = 2.4; + const DESNITY_DIFFICULTY_BASE: f64 = 2.5; + + const PREEMPT_BALANCING_FACTOR: f64 = 14_0000.0; + const PREEMPT_STARTING_POINT: f64 = 500.0; // * AR 9.66 in milliseconds + + const HIDDEN_MULTIPLIER: f64 = 0.28; + + const MINIMUM_ANGLE_RELEVANCY_TIME: f64 = 2000.0; // * 2 seconds + const MAXIMUM_ANGLE_RELEVANCY_TIME: f64 = 200.0; + + pub fn evaluate_diff_of<'a>( + &self, + curr: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>], + hidden: bool, + ) -> f64 { + if curr.base.is_spinner() || curr.idx == 0 { + return 0.0; + } + + let curr_obj = curr; + let next_obj = curr.next(0, diff_objects); + + // * Only allow velocity to buff + let velocity = f64::max(1.0, curr_obj.lazy_jump_dist / curr_obj.adjusted_delta_time); + + let curr_visible_obj_density = self.retrieve_current_visible_object_density(curr_obj, diff_objects); + let past_obj_difficulty_influence = self.get_past_obj_difficulty_influence(curr_obj, diff_objects); + + let constant_angle_nerf_factor = Self::get_constant_angle_nerf_factor(curr_obj, diff_objects); + + let note_density_difficulty = Self::calc_density_difficulty( + next_obj, + velocity, + constant_angle_nerf_factor, + past_obj_difficulty_influence, + curr_visible_obj_density + ); + + let hidden_difficulty = if hidden { + self.calc_hidden_difficulty(curr_obj, diff_objects, past_obj_difficulty_influence, curr_visible_obj_density, velocity, constant_angle_nerf_factor) + } else { + 0.0 + }; + + let preempt_difficulty = Self::calc_preempt_difficulty(velocity, constant_angle_nerf_factor, self.time_preempt); + + let mut reading_difficulty = norm(1.5, [preempt_difficulty, hidden_difficulty, note_density_difficulty]); + + // Having less time to process information is harder + reading_difficulty *= Self::high_bpm_bonus(curr_obj.adjusted_delta_time); + + reading_difficulty + } + + fn calc_density_difficulty<'a>( + next_obj: Option<&'a OsuDifficultyObject<'a>>, + velocity: f64, + constant_angle_nerf_factor: f64, + past_obj_difficulty_influence: f64, + curr_visible_obj_density: f64, + ) -> f64 { + // * Consider future densities too because it can make the path the cursor takes less clear + let mut fut_obj_difficulty_influence = curr_visible_obj_density.sqrt(); + + if let Some(next_obj) = next_obj { + // * Reduce difficulty if movement to next object is small + fut_obj_difficulty_influence = smootherstep(next_obj.lazy_jump_dist, 15.0, Self::DISTANCE_INFLUENCE_THRESHOLD); + } + + // * Value higher note densities exponentially + let mut note_density_difficulty = (past_obj_difficulty_influence + fut_obj_difficulty_influence) + .powf(1.7) + * 0.4 + * constant_angle_nerf_factor + * velocity; + + // * Award only denser than average maps. + note_density_difficulty = (note_density_difficulty - Self::DESNITY_DIFFICULTY_BASE).max(0.0); + + // Apply a soft cap to general density reading to account for partial memorization + note_density_difficulty.powf(0.45) * Self::DENSITY_MULTIPLIER + } + + fn calc_preempt_difficulty( + velocity: f64, + constant_angle_nerf_factor: f64, + preempt: f64, + ) -> f64 { + // * Arbitrary curve for the base value preempt difficulty should have as approach rate increases. + // * https://www.desmos.com/calculator/c175335a71 + ((Self::PREEMPT_STARTING_POINT - preempt + (preempt - Self::PREEMPT_STARTING_POINT).abs()) / 2.0) + .powf(2.5) + / Self::PREEMPT_BALANCING_FACTOR + * constant_angle_nerf_factor + * velocity + } + + fn calc_hidden_difficulty<'a>( + &self, + curr_obj: &'a OsuDifficultyObject<'a>, + // TODO: pass this OR see comment on l#109 + // curr_obj_preempt: f64, + diff_objects: &'a [OsuDifficultyObject<'a>], + past_obj_difficulty_influence: f64, + curr_visible_obj_density: f64, + velocity: f64, + constant_angle_nerf_factor: f64, + ) -> f64 { + // * Higher preempt means that time spent invisible is higher too, we want to reward that + let preempt_factor = self.time_preempt.powf(2.2) * 0.01; + + // * Account for both past and current densities + let density_factor = (curr_visible_obj_density + past_obj_difficulty_influence).powf(3.3) * 3.0; + + let mut hidden_difficulty = (preempt_factor + density_factor) * constant_angle_nerf_factor * velocity * 0.01; + + // * Apply a soft cap to general HD reading to account for partial memorization + hidden_difficulty = hidden_difficulty.powf(0.4) * Self::HIDDEN_MULTIPLIER; + + if let Some(prev_obj) = curr_obj.previous(0, diff_objects) { + if curr_obj.lazy_jump_dist.eq(0.0) + && curr_obj.opacity_at(prev_obj.start_time, true, self.time_preempt, self.time_fade_in).eq(0.0) + // TODO: Double check using this preempt over OsuDifficultyHitObject.Preempt + && prev_obj.start_time > curr_obj.start_time - self.time_preempt { + + // * Perfect stacks are harder the less time between notes + hidden_difficulty += Self::HIDDEN_MULTIPLIER * 2500.0 / curr_obj.adjusted_delta_time.powf(1.5); + } + } + + hidden_difficulty + } + + fn get_past_obj_difficulty_influence<'a>(&self, curr_obj: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>]) -> f64 { + diff_objects + .iter() + // Note: This achieves the same as retrievePastVisibleObjects + .filter(|d| { + d.idx < curr_obj.idx + && curr_obj.start_time - d.start_time > Self::READING_WINDOW_SIZE + && d.start_time < curr_obj.start_time - self.time_preempt + }) + .fold(0.0, |past_obj_difficulty_influence, loop_obj| { + let mut loop_difficulty = loop_obj.opacity_at(loop_obj.start_time, false, self.time_preempt, self.time_fade_in); + + // * When aiming an object small distances mean previous objects may be cheesed, so it doesn't matter whether they were arranged confusingly. + loop_difficulty *= smootherstep(loop_obj.lazy_jump_dist, 15.0, Self::DISTANCE_INFLUENCE_THRESHOLD); + + // * Account less for objects close to the max reading window + let delta_time = curr_obj.start_time - loop_obj.start_time; + let time_nerf_factor = Self::get_time_nerf_factor(delta_time); + + loop_difficulty *= time_nerf_factor; + past_obj_difficulty_influence + loop_difficulty + }) + } + + // * Returns the density of objects visible at the point in time the current object needs to be clicked capped by the reading window. + fn retrieve_current_visible_object_density<'a>(&self, curr_obj: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>]) -> f64 { + let mut forwards_idx = 0; + let mut visible_object_count = 0.0; + + while let Some(hit_obj) = curr_obj.next(forwards_idx, diff_objects).filter(|next| { + next.start_time - curr_obj.start_time > Self::READING_WINDOW_SIZE + // * Object not visible at the time current object needs to be clicked. + || curr_obj.start_time < next.start_time - self.time_preempt + }) { + let delta_time = hit_obj.start_time - curr_obj.start_time; + let time_nerf_factor = Self::get_time_nerf_factor(delta_time); + + visible_object_count += hit_obj.opacity_at(curr_obj.start_time, false, self.time_preempt, self.time_fade_in) * time_nerf_factor; + + forwards_idx += 1; + } + + visible_object_count + } + + // * Returns a factor of how often the current object's angle has been repeated in a certain time frame. + // * It does this by checking the difference in angle between current and past objects and sums them based on a range of similarity. + // * https://www.desmos.com/calculator/eb057a4822 + fn get_constant_angle_nerf_factor<'a>(curr_obj: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>]) -> f64 { + let mut constant_angle_count = 0.0; + let mut backwards_idx = 0; + let mut curr_time_gap = 0.0; + + let mut loop_obj_prev0 = curr_obj; + let mut loop_obj_prev1: Option<&OsuDifficultyObject<'a>> = None; + let mut loop_obj_prev2: Option<&OsuDifficultyObject<'a>> = None; + + while let Some(loop_obj) = curr_obj.previous(backwards_idx, diff_objects).filter(|_| curr_time_gap < Self::MINIMUM_ANGLE_RELEVANCY_TIME) { + // * Account less for objects that are close to the time limit. + let long_interval_factor = reverse_lerp( + loop_obj.adjusted_delta_time, + Self::MAXIMUM_ANGLE_RELEVANCY_TIME, + Self::MINIMUM_ANGLE_RELEVANCY_TIME + ); + + if let (Some(loop_obj_angle), Some(curr_obj_angle)) = (loop_obj.angle, curr_obj.angle) { + let angle_diff = (curr_obj_angle - loop_obj_angle).abs(); + let mut angle_diff_alternating = f64::consts::PI; + + if let (Some(loop_obj_prev0_angle), Some(loop_obj_prev1_angle), Some(loop_obj_prev2_angle)) + = (loop_obj_prev0.angle, loop_obj_prev1.map_or(None, |o| o.angle), loop_obj_prev2.map_or(None, |o| o.angle)) { + angle_diff_alternating = (loop_obj_prev1_angle - loop_obj_angle).abs(); + angle_diff_alternating += (loop_obj_prev2_angle - loop_obj_prev1_angle).abs(); + + let mut weight = 1.0; + + // * Be sure that one of the angles is very sharp, when other is wide + weight *= reverse_lerp( + loop_obj_angle.min(loop_obj_prev0_angle) * 100.0 / f64::consts::PI, + 20.0, + 5.0 + ); + weight *= reverse_lerp( + loop_obj_angle.min(loop_obj_prev0_angle) * 100.0 / f64::consts::PI, + 60.0, + 120.0 + ); + + // * Lerp between max angle difference and rescaled alternating difference, with more harsh scaling compared to normal difference + angle_diff_alternating = f64::lerp(f64::consts::PI, 0.1 * angle_diff_alternating, weight); + } + + let stack_factor = smootherstep(loop_obj.lazy_jump_dist, 0.0, f64::from(OsuDifficultyObject::NORMALIZED_RADIUS)); + + constant_angle_count += ( + 3.0 * (f64::to_radians(30.0).min((angle_diff).min(angle_diff_alternating) * stack_factor)) + ).cos() * long_interval_factor; + } + + curr_time_gap = curr_obj.start_time - loop_obj.start_time; + backwards_idx += 1; + + loop_obj_prev2 = loop_obj_prev1; + loop_obj_prev1 = Some(loop_obj_prev0); + loop_obj_prev0 = loop_obj; + } + + (2.0 / constant_angle_count).clamp(0.2, 1.0) + } + + // * Returns a nerfing factor for when objects are very distant in time, affecting reading less. + const fn get_time_nerf_factor(delta_time: f64) -> f64 { + (2.0 - delta_time / (Self::READING_WINDOW_SIZE / 2.0)).clamp(0.0, 1.0) + } + + fn high_bpm_bonus(ms: f64) -> f64 { + 1.0 / (1.0 - f64::powf(0.8, ms / 1000.0)) + } +} \ No newline at end of file From f49347af93049012793388a9abfea2b545fc5c3e Mon Sep 17 00:00:00 2001 From: myssto Date: Fri, 31 Jul 2026 01:32:24 -0500 Subject: [PATCH 08/27] refactor: wip refactor of skill traits --- src/any/difficulty/mod.rs | 1 + .../difficulty/skills_new/harmonic_skill.rs | 60 ++ src/any/difficulty/skills_new/mod.rs | 59 ++ src/any/difficulty/skills_new/skill.rs | 12 + .../skills_new/strain_decay_skill.rs | 14 + src/any/difficulty/skills_new/strain_skill.rs | 71 +++ .../variable_length_strain_skill.rs | 102 ++++ src/osu/difficulty/gradual.rs | 3 +- src/osu/difficulty/mod.rs | 11 +- src/osu/difficulty/skills/aim.rs | 279 ++++++++- src/osu/difficulty/skills/flashlight.rs | 126 +++- src/osu/difficulty/skills/mod.rs | 12 +- src/osu/strains.rs | 3 +- src/util/difficulty.rs | 4 + src/util/macros_new.rs | 575 ++++++++++++++++++ src/util/mod.rs | 3 + src/util/traits.rs | 16 + 17 files changed, 1292 insertions(+), 59 deletions(-) create mode 100644 src/any/difficulty/skills_new/harmonic_skill.rs create mode 100644 src/any/difficulty/skills_new/mod.rs create mode 100644 src/any/difficulty/skills_new/skill.rs create mode 100644 src/any/difficulty/skills_new/strain_decay_skill.rs create mode 100644 src/any/difficulty/skills_new/strain_skill.rs create mode 100644 src/any/difficulty/skills_new/variable_length_strain_skill.rs create mode 100644 src/util/macros_new.rs diff --git a/src/any/difficulty/mod.rs b/src/any/difficulty/mod.rs index 4782991b..aa7afc3e 100644 --- a/src/any/difficulty/mod.rs +++ b/src/any/difficulty/mod.rs @@ -25,6 +25,7 @@ pub mod gradual; pub mod inspect; pub mod object; pub mod skills; +pub mod skills_new; use crate::model::mode::IGameMode; diff --git a/src/any/difficulty/skills_new/harmonic_skill.rs b/src/any/difficulty/skills_new/harmonic_skill.rs new file mode 100644 index 00000000..af3192d5 --- /dev/null +++ b/src/any/difficulty/skills_new/harmonic_skill.rs @@ -0,0 +1,60 @@ +use crate::{any::difficulty::skills_new::skill::Skill, util::traits::{IEnumerable, IOrderedEnumerable}}; + +pub trait NewHarmonicSkill: Skill { + const HARMONIC_SCALE: f64 = 1.0; + const DECAY_EXPONENT: f64 = 0.9; + + fn object_difficulty_of<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64; + + fn process_internal<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64 { + self.object_difficulty_of(curr, objects) + } + + fn get_transformed_difficulties(difficulties: &[f64]) -> &[f64] { + difficulties + } + + fn count_top_weighted_object_difficulties(difficulty_value: f64) -> f64; + + fn difficulty_to_performance(difficulty: f64) -> f64 { + 4.0 * difficulty.powf(3.0) + } +} + +pub fn harmonic_skill_difficulty_value( + transformed_object_difficulties: &[f64], + object_weight_sum: &mut f64, + harmonic_scale: f64, + decay_exponent: f64, +) -> f64 { + *object_weight_sum = 0.0; + + if transformed_object_difficulties.is_empty() { + return 0.0; + } + + let mut difficulty = 0.0; + let mut index = 0; + + // * Objects with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). + // * These objects will not contribute to the difficulty. + for obj in transformed_object_difficulties.to_vec().cs_order_descending().cs_where(|v| *v > 0.0) { + // * Use a harmonic sum that considers each object of the map according to a predefined weight. + let weight = (1.0 + (harmonic_scale / f64::from(1 + index))) / (f64::from(index).powf(decay_exponent) + 1.0 + (harmonic_scale / f64::from(1 + index))); + + *object_weight_sum += weight; + + difficulty += obj * weight; + index += 1; + } + + difficulty +} diff --git a/src/any/difficulty/skills_new/mod.rs b/src/any/difficulty/skills_new/mod.rs new file mode 100644 index 00000000..29b62086 --- /dev/null +++ b/src/any/difficulty/skills_new/mod.rs @@ -0,0 +1,59 @@ +use crate::util::{ + difficulty::logistic, + float_ext::FloatExt, + hint::unlikely, +}; + +pub mod harmonic_skill; +pub mod skill; +pub mod strain_skill; +pub mod strain_decay_skill; +pub mod variable_length_strain_skill; + +pub fn count_top_weighted_object_difficulties( + difficulty_value: f64, + object_difficulties: &[f64], + object_weight_sum: f64, +) -> f64 { + if object_difficulties.is_empty() || FloatExt::eq(object_weight_sum, 0.0) { + return 0.0; + } + + // * What would the top difficulty be if all object difficulties were identical + let consistent_top_obj = difficulty_value / object_weight_sum; + + if consistent_top_obj == 0.0 { + 0.0 + } else { + object_difficulties + .iter() + .fold(0.0, |acc, od| acc + logistic(od / consistent_top_obj, 0.88, 10.0, Some(1.1))) + } +} + +pub fn count_top_weighted_strains( + object_difficulties: &[f64], + difficulty_value: f64, + decay_weight: f64, +) -> f64 { + if unlikely(object_difficulties.is_empty()) { + return 0.0; + } + + // * What would the top strain be if all strain values were identical + let consistent_top_strain = difficulty_value * (1.0 - decay_weight); + + if unlikely(FloatExt::eq(consistent_top_strain, 0.0)) { + return object_difficulties.len() as f64; + } + + // * Use a weighted sum of all strains. Constants are arbitrary and give nice values + object_difficulties + .iter() + .map(|s| logistic(s / consistent_top_strain, 0.88, 10.0, Some(1.1))) + .sum() +} + +pub fn strain_decay_base(ms: f64, strain_decay_base: f64) -> f64 { + f64::powf(strain_decay_base, ms / 1000.0) +} \ No newline at end of file diff --git a/src/any/difficulty/skills_new/skill.rs b/src/any/difficulty/skills_new/skill.rs new file mode 100644 index 00000000..8885cd4c --- /dev/null +++ b/src/any/difficulty/skills_new/skill.rs @@ -0,0 +1,12 @@ +pub trait Skill: Sized { + type DifficultyObject<'a>; + type DifficultyObjects<'a>: ?Sized; + + fn process<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ); + + fn get_object_difficulties(&self) -> &[f64]; +} \ No newline at end of file diff --git a/src/any/difficulty/skills_new/strain_decay_skill.rs b/src/any/difficulty/skills_new/strain_decay_skill.rs new file mode 100644 index 00000000..345e8805 --- /dev/null +++ b/src/any/difficulty/skills_new/strain_decay_skill.rs @@ -0,0 +1,14 @@ +use crate::any::difficulty::skills_new::strain_skill::NewStrainSkill; + +pub trait NewStrainDecaySkill: NewStrainSkill { + const SKILL_MULTIPLIER: f64 = 0.0; + const STRAIN_DECAY_BASE: f64 = 0.0; + + fn strain_value_of<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64; + + fn strain_decay(ms: f64) -> f64; +} diff --git a/src/any/difficulty/skills_new/strain_skill.rs b/src/any/difficulty/skills_new/strain_skill.rs new file mode 100644 index 00000000..8d34d372 --- /dev/null +++ b/src/any/difficulty/skills_new/strain_skill.rs @@ -0,0 +1,71 @@ +use crate::{ + any::difficulty::skills_new::skill::Skill, + util::traits::{IEnumerable, IOrderedEnumerable}, +}; + +pub trait NewStrainSkill: Skill { + const DECAY_WEIGHT: f64 = 0.9; + const SECTION_LENGTH: i32 = 400; + + fn process_internal<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64; + + fn strain_value_at<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64; + + fn count_top_weighted_strains(&self, difficulty_value: f64) -> f64; + + fn save_current_peak(&mut self); + + fn start_new_section_from<'a>( + &mut self, + time: f64, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ); + + fn calculate_initial_strain<'a>( + &self, + time: f64, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64; + + fn into_current_strain_peaks(self) -> Vec; + + fn get_current_strain_peaks(mut strain_peaks: Vec, current_section_peak: f64) -> Vec { + strain_peaks.push(current_section_peak); + + strain_peaks + } + + fn difficulty_value(current_strain_peaks: Vec) -> f64; + + fn into_difficulty_value(self) -> f64; + + fn cloned_difficulty_value(&self) -> f64; +} + +pub fn strain_skill_difficulty_value(current_strain_peaks: Vec, decay_weight: f64) -> 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 peaks = current_strain_peaks.cs_where(|&p| p > 0.0); + + // * Difficulty is the weighted sum of the highest strains from every section. + // * We're sorting from highest to lowest strain. + for strain in peaks.cs_order_descending() { + difficulty += strain * weight; + weight *= decay_weight; + } + + difficulty +} \ No newline at end of file diff --git a/src/any/difficulty/skills_new/variable_length_strain_skill.rs b/src/any/difficulty/skills_new/variable_length_strain_skill.rs new file mode 100644 index 00000000..4327bd53 --- /dev/null +++ b/src/any/difficulty/skills_new/variable_length_strain_skill.rs @@ -0,0 +1,102 @@ +use crate::{ + any::difficulty::skills_new::skill::Skill, + util::traits::IEnumerable, +}; + +pub trait VariableLengthStrainSkill: Skill { + const DECAY_WEIGHT: f64 = 0.9; + const MAX_SECTION_LENGTH: f64 = 400.0; + const MAX_STORED_LENGTH: f64 = 11.0 / (1.0 - Self::DECAY_WEIGHT); + + fn process_internal<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64; + + fn strain_value_at<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64; + + fn backfill_peaks<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ); + + fn save_current_peak(&mut self, section_length: f64); + + fn start_new_section_from<'a>( + &mut self, + time: f64, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ); + + fn calculate_initial_strain<'a>( + &self, + time: f64, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64; + + fn into_current_strain_peaks(self) -> Vec; + + fn get_current_strain_peaks( + mut strain_peaks: Vec, + final_peak: Option, + current_section_peak: f64, + current_section_begin: f64, + current_section_end: f64, + ) -> Vec { + if final_peak.is_none() { + let final_peak = StrainPeak::new( + current_section_peak, + current_section_end - current_section_begin + ); + strain_peaks.cs_add_in_place(final_peak); + } + + strain_peaks + } + + fn count_top_weighted_strains(&self, difficulty_value: f64) -> f64; +} + +#[derive(Debug, Clone, Copy)] +pub struct StrainPeak { + pub value: f64, + pub section_length: f64, +} + +impl StrainPeak { + pub const fn new(value: f64, section_length: f64) -> Self { + Self { + value, + section_length: section_length.round() + } + } +} + +impl PartialEq for StrainPeak { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Eq for StrainPeak {} + +impl Ord for StrainPeak { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // * Reverse sort, highest is first. + other.value.total_cmp(&self.value) + } +} + +impl PartialOrd for StrainPeak { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} \ No newline at end of file diff --git a/src/osu/difficulty/gradual.rs b/src/osu/difficulty/gradual.rs index 20f4fce1..beceec2f 100644 --- a/src/osu/difficulty/gradual.rs +++ b/src/osu/difficulty/gradual.rs @@ -1,3 +1,4 @@ +use crate::any::difficulty::skills_new::skill::Skill; use std::{cmp, mem}; use rosu_map::section::general::GameMode; @@ -147,7 +148,7 @@ fn new(difficulty: Difficulty, map: &Beatmap) -> OsuGradualDifficulty { let great_hit_window = map_attrs.hit_windows().od_great.unwrap_or(0.0); - let skills = OsuSkills::new(mods, &scaling_factor, great_hit_window, time_preempt); + let skills = OsuSkills::new(mods, &scaling_factor, great_hit_window, time_preempt, diff_objects.len(), attrs.od()); let diff_objects = extend_lifetime(diff_objects.into_boxed_slice()); let score_simulator = GradualLegacyScoreSimulator::new(map, map_attrs); diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index d38b5ad5..cde4d2c7 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -160,11 +160,18 @@ impl DifficultyValues { let great_hit_window = map_attrs.hit_windows().od_great.unwrap_or(0.0); - let mut skills = OsuSkills::new(mods, &scaling_factor, great_hit_window, time_preempt); - // The first hit object has no difficulty object let take_diff_objects = cmp::min(map.hit_objects.len(), take).saturating_sub(1); + let mut skills = OsuSkills::new( + mods, + &scaling_factor, + great_hit_window, + time_preempt, + take_diff_objects, + attrs.od() + ); + for hit_object in diff_objects.iter().take(take_diff_objects) { skills.process(hit_object, &diff_objects); } diff --git a/src/osu/difficulty/skills/aim.rs b/src/osu/difficulty/skills/aim.rs index 8bf71452..824ea33d 100644 --- a/src/osu/difficulty/skills/aim.rs +++ b/src/osu/difficulty/skills/aim.rs @@ -1,26 +1,66 @@ +use core::f64; + use crate::{ + GameMods, any::difficulty::{ object::{HasStartTime, IDifficultyObject}, - skills::{StrainSkill, strain_decay}, + skills_new::{ + strain_decay_base, + variable_length_strain_skill::{ + VariableLengthStrainSkill, + StrainPeak, + }, + }, + }, + osu::difficulty::{ + evaluators::{ + AgilityEvaluator, + FlowAimEvaluator, + SnapAimEvaluator, + }, + object::OsuDifficultyObject, + }, + util::{ + difficulty::{lerp, logistic, logistic_exp, norm}, + float_ext::FloatExt, + traits::IEnumerable, }, - osu::difficulty::{evaluators::AimEvaluator, object::OsuDifficultyObject}, - util::float_ext::FloatExt, }; -use super::strain::OsuStrainSkill; - -define_skill! { - #[derive(Clone)] - pub struct Aim: StrainSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] { - include_sliders: bool, +define_new_skill! { + pub struct Aim: VariableLengthStrainSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] { current_strain: f64 = 0.0, slider_strains: Vec = Vec::with_capacity(64), + include_sliders: bool, + mods: GameMods, + overall_difficulty: f64, + } + + pub fn new(mods: &GameMods, include_sliders: bool, overall_difficulty: f64) -> Self { + Self { + current_strain: 0.0, + slider_strains: Vec::with_capacity(64), + mods: mods.clone(), + include_sliders: include_sliders, + overall_difficulty: overall_difficulty, + } } } impl Aim { + const SKILL_MULTIPLIER_SNAP: f64 = 70.9; + const SKILL_MULTIPLIER_AGILITY: f64 = 2.35; + const SKILL_MULTIPLIER_FLOW: f64 = 242.0; + + const SKILL_MULTIPLIER_TOTAL: f64 = 1.12; + const COMBINED_SNAP_NORM_EXPONENT: f64 = 1.2; + const SKILL_MULTIPLIER: f64 = 26.0; - const STRAIN_DECAY_BASE: f64 = 0.15; + const STRAIN_DECAY_BASE: f64 = 0.2; + + fn strain_decay(ms: f64) -> f64 { + strain_decay_base(ms, 0.15) + } fn calculate_initial_strain( &mut self, @@ -32,7 +72,7 @@ impl Aim { .previous(0, objects) .map_or(0.0, HasStartTime::start_time); - self.current_strain * strain_decay(time - prev_start_time, Self::STRAIN_DECAY_BASE) + self.current_strain * Self::strain_decay(time - prev_start_time) } fn strain_value_at( @@ -40,9 +80,14 @@ impl Aim { curr: &OsuDifficultyObject<'_>, objects: &[OsuDifficultyObject<'_>], ) -> f64 { - self.current_strain *= strain_decay(curr.delta_time, Self::STRAIN_DECAY_BASE); - self.current_strain += AimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) - * Self::SKILL_MULTIPLIER; + if self.mods.ap() { + return 0.0; + } + + let decay = Self::strain_decay(curr.adjusted_delta_time); + + self.current_strain *= decay; + self.current_strain += self.calculate_adjusted_difficulty(curr, objects) * (1.0 - decay); if curr.base.is_slider() { self.slider_strains.push(self.current_strain); @@ -51,6 +96,72 @@ impl Aim { self.current_strain } + fn calculate_adjusted_difficulty(&self, curr: &OsuDifficultyObject<'_>, objects: &[OsuDifficultyObject<'_>]) -> f64 { + let snap_difficulty = SnapAimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) * Self::SKILL_MULTIPLIER_SNAP; + let agility_difficulty = AgilityEvaluator::evaluate_diff_of(curr, objects) * Self::SKILL_MULTIPLIER_AGILITY; + let flow_difficulty = FlowAimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) * Self::SKILL_MULTIPLIER_FLOW; + + let mut total_difficulty = self.calculate_total_value(snap_difficulty, agility_difficulty, flow_difficulty); + + if let Some(attraction_strength) = self.mods.attraction_strength() { + total_difficulty *= 1.0 - attraction_strength; + } + + total_difficulty *= 0.985 + self.overall_difficulty.max(0.0).powf(2.0) / 4000.0; + + total_difficulty + } + + fn calculate_total_value(&self, snap_difficulty: f64, agility_difficulty: f64, flow_difficulty: f64) -> f64 { + let mut snap_difficulty_new = snap_difficulty; + let mut flow_difficulty_new = flow_difficulty; + + // * We compare flow to combined snap and agility because snap by itself doesn't have enough difficulty to be above flow on streams + // * Agility on the other hand is supposed to measure the rate of cursor velocity changes while snapping + // * So snapping every circle on a stream requires an enormous amount of agility at which point it's easier to flow + let mut combined_snap_difficulty = norm(Self::COMBINED_SNAP_NORM_EXPONENT, [snap_difficulty_new, agility_difficulty]); + + let p_snap = Self::calculate_snap_flow_probability(flow_difficulty / combined_snap_difficulty); + let p_flow = 1.0 - p_snap; + + if self.mods.td() { + // * we don't adjust agility here since agility represents TD difficulty in a decent enough way + snap_difficulty_new = snap_difficulty_new.powf(0.89); + combined_snap_difficulty = norm(Self::COMBINED_SNAP_NORM_EXPONENT, [snap_difficulty_new, agility_difficulty]); + } + + if self.mods.rx() { + combined_snap_difficulty *= 0.75; + flow_difficulty_new *= 0.6; + } + + let total_difficulty = combined_snap_difficulty * p_snap * flow_difficulty_new * p_flow; + let total_strain = total_difficulty * Self::SKILL_MULTIPLIER_TOTAL; + + total_strain + } + + fn calculate_snap_flow_probability(ratio: f64) -> f64 { + // * A function that turns the ratio of snap : flow into the probability of snapping/flowing + // * It has the constraints: + // * P(snap) + P(flow) = 1 (the object is always either snapped or flowed) + // * P(snap) = f(snap/flow), P(flow) = f(flow/snap) (ie snap and flow are symmetric and reversible) + // * Therefore: f(x) + f(1/x) = 1 + // * 0 <= f(x) <= 1 (cannot have negative or greater than 100% probability of snapping or flowing) + // * This logistic function is a solution, which fits nicely with the general idea of interpolation and provides a tuneable constant + const K: f64 = 7.27; + + if FloatExt::eq(ratio, 0.0) { + return 0.0; + } + + if ratio.is_nan() { + return 1.0; + } + + logistic_exp(-K * ratio.log(f64::consts::E), None) + } + pub fn get_difficult_sliders(&self) -> f64 { if self.slider_strains.is_empty() { return 0.0; @@ -65,24 +176,140 @@ impl Aim { self.slider_strains .iter() .copied() - .map(|strain| 1.0 / (1.0 + f64::exp(-(strain / max_slider_strain * 12.0 - 6.0)))) + .map(|strain| logistic(strain / max_slider_strain, 0.5, 12.0, None)) .sum() } - pub fn slider_strains(&self) -> &[f64] { - &self.slider_strains + pub fn count_top_weighted_sliders(&self, difficulty_value: f64) -> f64 { + if self.slider_strains.is_empty() { + return 0.0; + } + + // * What would the top strain be if all strain values were identical + let consistent_top_strain = difficulty_value / 10.0; + + if FloatExt::eq(consistent_top_strain, 0.0) { + return 0.0; + } + + // * Use a weighted sum of all strains. Constants are arbitrary and give nice values + self.slider_strains + .iter() + .map(|s| logistic(*s / consistent_top_strain, 0.88, 10.0, Some(1.1))) + .sum() + } + + pub fn difficulty_value(current_strain_peaks: Vec) -> f64 { + aim_difficulty_value( + Self::get_reduced_strain_peaks(current_strain_peaks), + Self::MAX_SECTION_LENGTH, + Self::DECAY_WEIGHT + ) + } + + pub fn into_difficulty_value(self) -> f64 { + aim_difficulty_value( + Self::get_reduced_strain_peaks( + Self::get_current_strain_peaks( + self.skill_strain_peaks, + self.skill_final_peak, + self.skill_current_section_peak, + self.skill_current_section_begin, + self.skill_current_section_end + ) + ), + Self::MAX_SECTION_LENGTH, + Self::DECAY_WEIGHT + ) } - // From `OsuStrainSkill`; native rather than trait function so that it has - // priority over `StrainSkill::difficulty_value` - fn difficulty_value(current_strain_peaks: Vec) -> f64 { - super::strain::difficulty_value( - current_strain_peaks, - Self::REDUCED_SECTION_COUNT, - Self::REDUCED_STRAIN_BASELINE, - Self::DECAY_WEIGHT, + pub fn cloned_difficulty_value(&self) -> f64 { + aim_difficulty_value( + Self::get_reduced_strain_peaks( + Self::get_current_strain_peaks( + self.skill_strain_peaks.clone(), + self.skill_final_peak, + self.skill_current_section_peak, + self.skill_current_section_begin, + self.skill_current_section_end + ) + ), + Self::MAX_SECTION_LENGTH, + Self::DECAY_WEIGHT ) } + + fn get_reduced_strain_peaks(current_strain_peaks: Vec) -> Vec { + const REDUCED_SECTION_TIME: f64 = 4000.0; + const REDUCED_STRAIN_BASELINE: f64 = 0.727; + const CHUNK_SIZE: f64 = 20.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 strains = current_strain_peaks.cs_where(|p| p.value > 0.0); + + let mut time = 0.0; + let mut skip_count = 0; + + // * We are reducing the highest strains first to account for extreme difficulty spikes + // * Strains are split into 20ms chunks to try to mitigate inconsistencies caused by reducing strains + while strains.len() > skip_count && time < REDUCED_SECTION_TIME { + let strain = strains[skip_count]; + + let mut added_time = 0.0; + while added_time < strain.section_length { + let scale = lerp(1.0, 10.0, ((time + added_time) / REDUCED_SECTION_TIME).clamp(0.0, 1.0)).log10(); + + // * intentionally add at end and sort afterwards, should be cheaper. + strains.push(StrainPeak::new( + strain.value * lerp(REDUCED_STRAIN_BASELINE, 1.0, scale), + CHUNK_SIZE.min(strain.section_length - added_time) + )); + + added_time += CHUNK_SIZE; + } + + time += strain.section_length; + skip_count += 1; + } + + let mut reduced = strains.split_off(skip_count); + reduced.sort_by(|a, b| b.cmp(a)); + + reduced + } } -impl OsuStrainSkill for Aim {} +fn aim_difficulty_value(reduced_strain_peaks: Vec, max_section_length: f64, decay_weight: f64) -> f64 { + let mut difficulty = 0.0; + let mut time = 0.0; + + // * Difficulty is a continuous weighted sum of the sorted strains + for strain in reduced_strain_peaks { + /* Weighting function can be thought of as: + b + ∫ DecayWeight^x dx + a + where a = startTime and b = endTime + + Technically, the function below has been slightly modified from the equation above. + The real function would be + double weight = DiffUtils.Pow(DecayWeight, startTime) - DiffUtils.Pow(DecayWeight, endTime); + ... + return difficulty / Math.Log(1 / DecayWeight); + E.g. for a DecayWeight of 0.9, we're multiplying by 10 instead of 9.49122... + + This change makes it so that a map composed solely of MaxSectionLength chunks will have the exact same value when summed in this class and StrainSkill. + Doing this ensures the relationship between strain values and difficulty values remains the same between the two classes. + */ + let start_time = time; + let end_time = time + strain.section_length / max_section_length; + + let weight = decay_weight.powf(start_time) - decay_weight.powf(end_time); + + difficulty += strain.value * weight; + time = end_time; + } + + difficulty / (1.0 - decay_weight) +} \ No newline at end of file diff --git a/src/osu/difficulty/skills/flashlight.rs b/src/osu/difficulty/skills/flashlight.rs index 9ca244e0..f4457819 100644 --- a/src/osu/difficulty/skills/flashlight.rs +++ b/src/osu/difficulty/skills/flashlight.rs @@ -2,36 +2,49 @@ use crate::{ GameMods, any::difficulty::{ object::{HasStartTime, IDifficultyObject}, - skills::strain_decay, + skills_new::{ + strain_decay_base, + strain_skill::NewStrainSkill, + }, }, - osu::difficulty::{evaluators::FlashlightEvaluator, object::OsuDifficultyObject}, - util::traits::IEnumerable, + osu::difficulty::{ + evaluators::FlashlightEvaluator, + object::OsuDifficultyObject + }, + util::difficulty::reverse_lerp, }; -define_skill! { - pub struct Flashlight: StrainSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] { +define_new_skill! { + pub struct Flashlight: NewStrainSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] { current_strain: f64, - has_hidden_mod: bool, + total_objects: i32, + overall_difficulty: f64, + mods: GameMods, evaluator: FlashlightEvaluator, } - pub fn new(mods: &GameMods, radius: f64, time_preempt: f64, time_fade_in: f64) -> Self { + pub fn new(mods: &GameMods, total_objects: i32, overall_difficulty: f64, radius: f64, time_preempt: f64, time_fade_in: f64) -> Self { let scaling_factor = 52.0 / radius; Self { current_strain: 0.0, - has_hidden_mod: mods.hd(), + total_objects: total_objects, + mods: mods.clone(), + overall_difficulty: overall_difficulty, evaluator: FlashlightEvaluator::new(scaling_factor, time_preempt, time_fade_in), } } } impl Flashlight { - const SKILL_MULTIPLIER: f64 = 0.05512; - const STRAIN_DECAY_BASE: f64 = 0.15; + const SKILL_MULTIPLIER: f64 = 0.058; + + fn strain_decay(ms: f64) -> f64 { + strain_decay_base(ms, 0.15) + } - fn calculate_initial_strain( - &mut self, + fn calculate_initial_strain<'a>( + &self, time: f64, curr: &OsuDifficultyObject<'_>, objects: &[OsuDifficultyObject<'_>], @@ -40,32 +53,91 @@ impl Flashlight { .previous(0, objects) .map_or(0.0, HasStartTime::start_time); - self.current_strain * strain_decay(time - prev_start_time, Self::STRAIN_DECAY_BASE) + self.current_strain * Self::strain_decay(time - prev_start_time) } - fn strain_value_at( + fn strain_value_at<'a>( &mut self, curr: &OsuDifficultyObject<'_>, objects: &[OsuDifficultyObject<'_>], ) -> f64 { - self.current_strain *= strain_decay(curr.delta_time, Self::STRAIN_DECAY_BASE); - self.current_strain += self - .evaluator - .evaluate_diff_of(curr, objects, self.has_hidden_mod) - * Self::SKILL_MULTIPLIER; + if !self.mods.fl() { + return 0.0; + } + self.current_strain *= Self::strain_decay(curr.delta_time); + self.current_strain += self.calculate_adjusted_difficulty(curr, objects) * Self::SKILL_MULTIPLIER; + self.current_strain } - #[expect( - clippy::needless_pass_by_value, - reason = "function definition needs to stay in-sync with `StrainSkill::difficulty_value`" - )] - fn difficulty_value(current_strain_peaks: Vec) -> f64 { - current_strain_peaks.cs_sum() + fn calculate_adjusted_difficulty( + &self, + curr: &OsuDifficultyObject<'_>, + objects: &[OsuDifficultyObject<'_>], + ) -> f64 { + let mut difficulty = self.evaluator.evaluate_diff_of(curr, objects, &self.mods); + + if self.mods.td() { + difficulty = difficulty.powf(0.9); + } + + if let Some(attraction_strength) = self.mods.attraction_strength() { + difficulty *= 1.0 - attraction_strength; + } + + if let Some(start_scale) = self.mods.deflate_start_scale() { + difficulty *= reverse_lerp(start_scale, 11.0, 1.0).clamp(0.1, 1.0); + } + + if self.mods.rx() { + difficulty *= 0.7; + } + + if self.mods.ap() { + difficulty *= 0.4; + } + + difficulty *= 0.985 + self.overall_difficulty.max(0.0).powf(2.0) / 4000.0; + + difficulty + } + + // NOTE: + // Flashlight is (currently) the only skill needing to override `StrainSkill.difficulty_value(current_strain_peaks) -> f64` + // and requires `self.total_objects`. Since the static method isn't ever used as far as I can tell, it can remain default for now. + // As long as `into_difficulty_value` and `cloned_difficulty_value` are correct it should not affect elsewhere. + + fn into_difficulty_value(self) -> f64 { + flashlight_difficulty_value( + Self::get_current_strain_peaks( + self.skill_strain_peaks, + self.skill_current_section_peak, + ), + self.total_objects + ) } - pub fn difficulty_to_performance(difficulty: f64) -> f64 { - 25.0 * f64::powf(difficulty, 2.0) + pub fn cloned_difficulty_value(&self) -> f64 { + flashlight_difficulty_value( + Self::get_current_strain_peaks( + self.skill_strain_peaks.clone(), + self.skill_current_section_peak, + ), + self.total_objects + ) } } + +fn flashlight_difficulty_value(current_strain_peaks: Vec, total_objects: i32) -> f64 { + let sum: f64 = current_strain_peaks.iter().sum(); + + sum * 0.7 + + 0.1 + * (f64::from(total_objects) / 200.0).min(1.0) + + (if total_objects > 200 { + 0.2 * f64::from((total_objects - 200).min(1) / 200) + } else { + 0.0 + }) +} \ No newline at end of file diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index c338b4d2..e3a771b2 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -1,3 +1,4 @@ +use crate::any::difficulty::skills_new::skill::Skill; use crate::{any::difficulty::skills::StrainSkill, model::mods::GameMods, osu::object::OsuObject}; use self::{aim::Aim, flashlight::Flashlight, speed::Speed}; @@ -24,6 +25,8 @@ impl OsuSkills { scaling_factor: &ScalingFactor, great_hit_window: f64, time_preempt: f64, + total_objects: usize, + overall_difficulty: f64, ) -> Self { let hit_window = 2.0 * great_hit_window; @@ -44,7 +47,14 @@ impl OsuSkills { let aim = Aim::new(true); let aim_no_sliders = Aim::new(false); let speed = Speed::new(hit_window, mods.ap()); - let flashlight = Flashlight::new(mods, scaling_factor.radius, time_preempt, time_fade_in); + let flashlight = Flashlight::new( + mods, + total_objects as i32, + overall_difficulty, + scaling_factor.radius, + time_preempt, + time_fade_in + ); Self { aim, diff --git a/src/osu/strains.rs b/src/osu/strains.rs index 6cfbf24b..62ab6d65 100644 --- a/src/osu/strains.rs +++ b/src/osu/strains.rs @@ -1,6 +1,5 @@ use crate::{ - Beatmap, Difficulty, any::difficulty::skills::StrainSkill, model::mode::ConvertError, - osu::convert::prepare_map, + Beatmap, Difficulty, any::difficulty::{skills::StrainSkill, skills_new::strain_skill::NewStrainSkill}, model::mode::ConvertError, osu::convert::prepare_map, }; use super::difficulty::{DifficultyValues, skills::OsuSkills}; diff --git a/src/util/difficulty.rs b/src/util/difficulty.rs index eb866658..760a3c5c 100644 --- a/src/util/difficulty.rs +++ b/src/util/difficulty.rs @@ -58,6 +58,10 @@ pub const fn smootherstep(x: f64, start: f64, end: f64) -> f64 { x * x * x * (x * (6.0 * x - 15.0) + 10.0) } +pub const fn lerp(start: f64, end: f64, amount: f64) -> f64 { + start + (end - start) * amount +} + pub const fn reverse_lerp(x: f64, start: f64, end: f64) -> f64 { f64::clamp((x - start) / (end - start), 0.0, 1.0) } diff --git a/src/util/macros_new.rs b/src/util/macros_new.rs new file mode 100644 index 00000000..cbd0c71e --- /dev/null +++ b/src/util/macros_new.rs @@ -0,0 +1,575 @@ +macro_rules! define_new_skill { + // Entry point without `new` function + ( + $( #[$meta:meta] )* + $vis:vis struct $skill:ident: $trait:ident => $objects:ty[$object:ty] { + $( $field_name:ident: $field_type:ty $( = $field_default:expr )?, )* + } + ) => { + define_new_skill! { + @$trait $objects[$object] + extend_fields $trait + fields { $( $field_name $field_type $( = $field_default )?, )* } + struct { $( #[$meta] )* $vis $skill } + new { + setup {} + args {} + assigns {} + } + } + }; + + // Entry point with `new` function + ( + $( #[$meta:meta] )* + $vis:vis struct $skill:ident: $trait:ident => $objects:ty[$object:ty] { + $( $field_name:ident: $field_type:ty $( = $field_default:expr )?, )* + } + + $_new_vis:vis fn new( $( $arg_name:ident: $arg_type:ty ),* ) -> Self { + $( $body:tt )* + } + ) => { + define_new_skill! { + @$trait $objects[$object] + extend_fields $trait + fields { $( $field_name $field_type |, )* } + struct { $( #[$meta] )* $vis $skill } + new { + setup_body { $( $body )* } + setup {} + args { $( $arg_name $arg_type, )* } + } + } + }; + + // Processing `new` function: Found the final `Self` return value + ( + @$trait:ident $objects:ty[$object:ty] + extend_fields $extend_fields:ident + fields { $( $fields:tt )* } + struct { $( $struct:tt )* } + new { + setup_body { + Self { $( $assign_name:ident: $assign_expr:expr, )* } // <- + } + setup { $( $setup:tt )* } + args { $( $args:tt )* } + } + ) => { + define_new_skill! { + @$trait $objects[$object] + extend_fields $trait + fields { $( $fields )* } + struct { $( $struct )* } + new { + setup { $( $setup )* } + args { $( $args )* } + assigns { $( $assign_name $assign_expr, )* } // <- + } + } + }; + + // Processing `new` function: Pop next statement from body and continue + ( + @$trait:ident $objects:ty[$object:ty] + extend_fields $extend_fields:ident + fields { $( $fields:tt )* } + struct { $( $struct:tt )* } + new { + setup_body { + $stmt:stmt; // <- + $( $rest:tt )+ + } + setup { $( $setup:tt )* } + args { $( $args:tt )* } + } + ) => { + define_new_skill! { + @$trait $objects[$object] + extend_fields $trait + fields { $( $fields )* } + struct { $( $struct )* } + new { + setup_body { $( $rest )* } // <- + setup { $( $setup )* $stmt } // <- + args { $( $args )* } + } + } + }; + + // Extend `VariableLengthStrainSkill`'s fields + ( + @$trait:ident $objects:ty[$object:ty] + extend_fields VariableLengthStrainSkill // <- + fields { $( $fields:tt )* } + $( $rest:tt )* + ) => { + define_new_skill! { + @$trait $objects[$object] + extend_fields Skill + fields { + $( $fields )* + skill_current_section_peak f64 = 0.0, // <- + skill_current_section_end f64 = 0.0, // <- + skill_current_section_begin f64 = 0.0, // <- + skill_total_length f64 = 0.0, // <- + skill_strain_peaks Vec = Vec::with_capacity(256), // <- + skill_final_peak Option = None, // <- + skill_queued_strains Vec<(f64, f64)> = Vec::with_capacity(256), // <- + } + $( $rest )* + } + }; + + // Extend `NewStrainSkill`'s fields + ( + @$trait:ident $objects:ty[$object:ty] + extend_fields NewStrainSkill // <- + fields { $( $fields:tt )* } + $( $rest:tt )* + ) => { + define_new_skill! { + @$trait $objects[$object] + extend_fields Skill + fields { + $( $fields )* + skill_current_section_peak f64 = 0.0, // <- + skill_current_section_end f64 = 0.0, // <- + skill_strain_peaks Vec = Vec::with_capacity(256), // <- + } + $( $rest )* + } + }; + + // Extend `Skill`'s fields + ( + @$trait:ident $objects:ty[$object:ty] + extend_fields Skill // <- + fields { $( $fields:tt )* } + $( $rest:tt )* + ) => { + define_new_skill! { + @$trait $objects[$object] + fields { + $( $fields )* + skill_object_difficulties Vec = Vec::with_capacity(256), // <- + } + $( $rest )* + } + }; + + // Parse field without default + ( + @$trait:ident $objects:ty[$object:ty] + fields { + $field_name:ident $field_type:ty, // <- + $( $fields:tt )* + } + struct { $( $struct:tt )* } + new { + setup { $( $setup:tt )* } + args { $( $args:tt )* } + assigns { $( $assigns:tt )* } + } + ) => { + define_new_skill! { + @$trait $objects[$object] + fields { $( $fields )* } + struct { $( $struct )* $field_name $field_type, } // <- + new { + setup { $( $setup )* } + args { $( $args )* $field_name $field_type, } // <- + assigns { $( $assigns )* $field_name, } // <- + } + } + }; + + // Parse field with default + ( + @$trait:ident $objects:ty[$object:ty] + fields { + $field_name:ident $field_type:ty = $field_default:expr, // <- + $( $fields:tt )* + } + struct { $( $struct:tt )* } + new { + setup { $( $setup:tt )* } + args { $( $args:tt )* } + assigns { $( $assigns:tt )* } + } + ) => { + define_new_skill! { + @$trait $objects[$object] + fields { $( $fields )* } + struct { $( $struct )* $field_name $field_type, } // <- + new { + setup { $( $setup )* } + args { $( $args )* } + assigns { $( $assigns )* $field_name $field_default, } // <- + } + } + }; + + // Parse field with but skip for `new` function + ( + @$trait:ident $objects:ty[$object:ty] + fields { + $field_name:ident $field_type:ty |, // <- + $( $fields:tt )* + } + struct { $( $struct:tt )* } + $( $rest:tt )* + ) => { + define_new_skill! { + @$trait $objects[$object] + fields { $( $fields )* } + struct { $( $struct )* $field_name $field_type, } // <- + $( $rest )* + } + }; + + // Final output + ( + @$trait:ident $objects:ty[$object:ty] + fields {} + struct { + $( #[$meta:meta] )* + $vis:vis $name:ident + $( $field_name:ident $field_type:ty, )* + } + new { + setup { $( $setup:stmt )* } + args { $( $arg_name:ident $arg_type:ty, )* } + assigns { $( $assign_name:ident $( $assign_expr:expr )?, )* } + } + ) => { + $( #[$meta] )* + $vis struct $name { + $( $field_name: $field_type, )* + } + + impl $name { + $vis fn new( + $( $arg_name: $arg_type, )* + ) -> Self { + $( $setup )* + + Self { + $( $assign_name $( : $assign_expr )?, )* + } + } + } + + const _: () = { + #[expect(unused_imports, reason = "fine for macros")] + use crate::{ + util::traits::IEnumerable, + any::difficulty::{ + object::{IDifficultyObject, IDifficultyObjects, HasStartTime}, + skills_new::{ + skill::Skill, + strain_skill::NewStrainSkill, + variable_length_strain_skill::VariableLengthStrainSkill, + }, + }, + }; + + define_new_skill!( @impl $trait $name $objects[$object] ); + }; + }; + + // Implement `Skill` trait + ( @impl Skill $name:ident $objects:ty[$object:ty] ) => { + impl Skill for $name { + type DifficultyObject<'a> = $object; + type DifficultyObjects<'a> = $objects; + + fn process<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) { + let difficulty_value = self.process_internal(curr, objects); + self.skill_object_difficulties.push(difficulty_value); + } + + fn get_object_difficulties(&self) -> &[f64] { + &self.skill_object_difficulties + } + } + }; + + // Implement `NewStrainSkill` trait + ( @impl NewStrainSkill $name:ident $objects:ty[$object:ty] ) => { + define_new_skill!( @impl Skill $name $objects[$object] ); + + impl NewStrainSkill for $name { + fn process_internal<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64 { + let section_length = f64::from(Self::SECTION_LENGTH); + + // * The first object doesn't generate a strain, so we begin with an incremented section end + if curr.idx == 0 { + self.skill_current_section_end = + f64::ceil(curr.start_time / section_length) * section_length; + } + + while curr.start_time > self.skill_current_section_end { + self.save_current_peak(); + self.start_new_section_from( + self.skill_current_section_end, + curr, + objects + ); + self.skill_current_section_end += section_length; + } + + let strain = self.strain_value_at(curr, objects); + self.skill_current_section_peak + = f64::max(strain, self.skill_current_section_peak); + + strain + } + + #[expect(unused_variables, reason = "placeholder")] + fn strain_value_at<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64 { + todo!() + } + + fn count_top_weighted_strains(&self, difficulty_value: f64) -> f64 { + crate::any::difficulty::skills_new::count_top_weighted_strains( + &self.skill_object_difficulties, + difficulty_value, + Self::DECAY_WEIGHT, + ) + } + + fn save_current_peak(&mut self) { + self.skill_strain_peaks.push(self.skill_current_section_peak); + } + + fn start_new_section_from<'a>( + &mut self, + time: f64, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) { + self.skill_current_section_peak + = self.calculate_initial_strain(time, curr, objects); + } + + #[expect(unused_variables, reason = "placeholder")] + fn calculate_initial_strain<'a>( + &self, + time: f64, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64 { + todo!() + } + + fn into_current_strain_peaks(self) -> Vec { + Self::get_current_strain_peaks( + self.skill_strain_peaks, + self.skill_current_section_peak, + ) + } + + fn difficulty_value(current_strain_peaks: Vec) -> f64 { + crate::any::difficulty::skills_new::strain_skill::strain_skill_difficulty_value( + current_strain_peaks, + Self::DECAY_WEIGHT, + ) + } + + fn into_difficulty_value(self) -> f64 { + Self::difficulty_value( + Self::get_current_strain_peaks( + self.skill_strain_peaks, + self.skill_current_section_peak, + ) + ) + } + + fn cloned_difficulty_value(&self) -> f64 { + Self::difficulty_value( + Self::get_current_strain_peaks( + self.skill_strain_peaks.clone(), + self.skill_current_section_peak, + ) + ) + } + } + }; + + // Implement `VariableLengthStrainSkill` trait + ( @impl VariableLengthStrainSkill $name:ident $objects:ty[$object:ty] ) => { + define_new_skill!( @impl Skill $name $objects[$object] ); + + impl VariableLengthStrainSkill for $name { + fn process_internal<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64 { + // * If we're on the first object, set up the first section to end `MaxSectionLength` after it. + if curr.idx == 0 { + self.skill_current_section_begin = curr.start_time; + self.skill_current_section_end + = self.skill_current_section_begin + Self::MAX_SECTION_LENGTH; + + // * No work is required for first object after calculating difficulty + self.skill_current_section_peak + = self.strain_value_at(curr, objects); + + return self.skill_current_section_peak; + } + + self.backfill_peaks(curr, objects); + + let current_strain = self.strain_value_at(curr, objects); + + // * If the current strain is larger than the current peak, begin a new peak + // * Otherwise, add the current strain to the queue + if current_strain > self.skill_current_section_peak { + // * Clear the queue since none of the strains inside of it will be contributing to the difficulty. + self.skill_queued_strains.clear(); + + // * End the current section with the new peak + self.save_current_peak(curr.start_time - self.skill_current_section_begin); + + // * Set up the new section to start at the current object with the current strain + self.skill_current_section_begin = curr.start_time; + self.skill_current_section_end = self.skill_current_section_begin + Self::MAX_SECTION_LENGTH; + self.skill_current_section_peak = current_strain; + } else { + // * Empty the queue of smaller elements as they won't be relevant to difficulty + while !self.skill_queued_strains.is_empty() { + if self.skill_queued_strains.last().filter(|(strain_value, _)| strain_value < ¤t_strain).is_some() { + self.skill_queued_strains.pop(); + } + } + self.skill_queued_strains.push((current_strain, curr.start_time)); + } + + current_strain + } + + #[expect(unused_variables, reason = "placeholder")] + fn strain_value_at<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64 { + todo!() + } + + fn backfill_peaks<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) { + // * If the current object starts after the current section ends + // * then we want to start a new section without any harsh drop-off. + // * If we have previous strains that influence the current difficulty we will prioritise those first. + // * Otherwise, start with the current object's initial strain. + while curr.start_time > self.skill_current_section_end { + // * Save the current peak, marking the end of the section. + self.save_current_peak(self.skill_current_section_end - self.skill_current_section_begin); + self.skill_current_section_begin = self.skill_current_section_end; + + // * If we have any strains queued, then we will use those until the object falls into the new section. + if !self.skill_queued_strains.is_empty() { + let (strain, start_time) = self.skill_queued_strains.remove(0); + + // * We want the section to end `MaxSectionLength` after the strain we're using as an influence. + // * This effectively means the queued strain will exist in its own section if the gap between the queued strain and current object is large enough. + // * This is required to make sure there's no harsh difficulty difference between 2 sections if there was a large gap. + self.skill_current_section_end = start_time + Self::MAX_SECTION_LENGTH; + self.start_new_section_from(self.skill_current_section_begin, curr, objects); + + // * If the current object's peak was higher, we don't want to override it with a lower strain. + // * Only use the queued strain if it contributes more difficulty. + self.skill_current_section_peak = self.skill_current_section_peak.max(strain); + + // * If the queue is empty then we should start the section from the current object instead. + // * The queue can be empty if we're starting off of the back of a new peak, or if we drained through all the queue + // * and the current object is still later than the section end. + } else { + // * We don't have any prior strains to take as a reference, so end the new section `MaxSectionLength` after it starts. + self.skill_current_section_end = self.skill_current_section_begin + Self::MAX_SECTION_LENGTH; + self.start_new_section_from(self.skill_current_section_begin, curr, objects); + } + } + } + + fn save_current_peak(&mut self, section_length: f64) { + if let Some(final_peak) = self.skill_final_peak { + self.skill_strain_peaks.retain(|p| *p != final_peak); + self.skill_final_peak = None; + } + + let peak = crate::any::difficulty::skills_new::variable_length_strain_skill::StrainPeak::new(self.skill_current_section_peak, section_length); + + self.skill_strain_peaks.cs_add_in_place(peak); + self.skill_total_length += section_length; + + // * Remove from the back of our strain peaks if there's any which are too deep to contribute to difficulty. + // * `maxStoredLength` dictates for us how many sections will preserve at least 99.999% of the difficulty value. + while self.skill_total_length > Self::MAX_STORED_LENGTH * Self::MAX_SECTION_LENGTH { + let Some(strain_peak) = self.skill_strain_peaks.pop() else { + break; + }; + self.skill_total_length -= strain_peak.section_length; + } + } + + fn start_new_section_from<'a>( + &mut self, + time: f64, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) { + // * 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.skill_current_section_peak = self.calculate_initial_strain(time, curr, objects); + } + + #[expect(unused_variables, reason = "placeholder")] + fn calculate_initial_strain<'a>( + &self, + time: f64, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64 { + todo!() + } + + fn into_current_strain_peaks(self) -> Vec { + Self::get_current_strain_peaks( + self.skill_strain_peaks, + self.skill_final_peak, + self.skill_current_section_peak, + self.skill_current_section_begin, + self.skill_current_section_end + ) + } + + fn count_top_weighted_strains(&self, difficulty_value: f64) -> f64 { + crate::any::difficulty::skills_new::count_top_weighted_strains( + &self.skill_object_difficulties, + difficulty_value, + Self::DECAY_WEIGHT, + ) + } + } + }; +} \ No newline at end of file diff --git a/src/util/mod.rs b/src/util/mod.rs index 33c6ebb0..397d4fb9 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -9,6 +9,9 @@ pub mod sort; pub mod sync; pub mod traits; +#[macro_use] +mod macros_new; + #[macro_use] mod macros; diff --git a/src/util/traits.rs b/src/util/traits.rs index 1205addc..174344d9 100644 --- a/src/util/traits.rs +++ b/src/util/traits.rs @@ -7,6 +7,10 @@ pub trait IEnumerable: Sized { fn cs_sum(&self) -> S where S: for<'a> Sum<&'a T>; + + fn cs_add_in_place(&mut self, item: T) -> usize + where + T: Ord; } /// Mimics the C# `IOrderedEnumerable` interface. @@ -33,6 +37,18 @@ impl IEnumerable for Vec { { self.iter().sum() } + + /// Adds the given item to the list according to standard sorting rules. Do not use on unsorted lists. + /// + /// + fn cs_add_in_place(&mut self, item: T) -> usize + where + T: Ord, + { + let index = self.binary_search(&item).unwrap_or_else(|i| i); + self.insert(index, item); + index + } } impl IOrderedEnumerable for Vec { From 28109d02a8e5742104367aa6225e77d68aa47a55 Mon Sep 17 00:00:00 2001 From: myssto Date: Fri, 31 Jul 2026 05:38:44 -0500 Subject: [PATCH 09/27] fix: various errors and formatting --- .../difficulty/skills_new/harmonic_skill.rs | 18 ++- src/any/difficulty/skills_new/mod.rs | 18 +-- .../variable_length_strain_skill.rs | 11 +- src/osu/difficulty/evaluators/speed/speed.rs | 10 +- src/osu/difficulty/mod.rs | 10 +- src/osu/difficulty/skills/aim.rs | 120 ++++++++++-------- src/osu/difficulty/skills/mod.rs | 6 +- src/osu/strains.rs | 22 +++- 8 files changed, 129 insertions(+), 86 deletions(-) diff --git a/src/any/difficulty/skills_new/harmonic_skill.rs b/src/any/difficulty/skills_new/harmonic_skill.rs index af3192d5..d96ecf98 100644 --- a/src/any/difficulty/skills_new/harmonic_skill.rs +++ b/src/any/difficulty/skills_new/harmonic_skill.rs @@ -1,6 +1,9 @@ -use crate::{any::difficulty::skills_new::skill::Skill, util::traits::{IEnumerable, IOrderedEnumerable}}; +use crate::{ + any::difficulty::skills_new::skill::Skill, + util::traits::{IEnumerable, IOrderedEnumerable}, +}; -pub trait NewHarmonicSkill: Skill { +pub trait HarmonicSkill: Skill { const HARMONIC_SCALE: f64 = 1.0; const DECAY_EXPONENT: f64 = 0.9; @@ -46,9 +49,16 @@ pub fn harmonic_skill_difficulty_value( // * Objects with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // * These objects will not contribute to the difficulty. - for obj in transformed_object_difficulties.to_vec().cs_order_descending().cs_where(|v| *v > 0.0) { + for obj in transformed_object_difficulties + .to_vec() + .cs_order_descending() + .cs_where(|v| *v > 0.0) + { // * Use a harmonic sum that considers each object of the map according to a predefined weight. - let weight = (1.0 + (harmonic_scale / f64::from(1 + index))) / (f64::from(index).powf(decay_exponent) + 1.0 + (harmonic_scale / f64::from(1 + index))); + let weight = (1.0 + (harmonic_scale / f64::from(1 + index))) + / (f64::from(index).powf(decay_exponent) + + 1.0 + + (harmonic_scale / f64::from(1 + index))); *object_weight_sum += weight; diff --git a/src/any/difficulty/skills_new/mod.rs b/src/any/difficulty/skills_new/mod.rs index 29b62086..cf11ee2d 100644 --- a/src/any/difficulty/skills_new/mod.rs +++ b/src/any/difficulty/skills_new/mod.rs @@ -1,13 +1,9 @@ -use crate::util::{ - difficulty::logistic, - float_ext::FloatExt, - hint::unlikely, -}; +use crate::util::{difficulty::logistic, float_ext::FloatExt, hint::unlikely}; pub mod harmonic_skill; pub mod skill; -pub mod strain_skill; pub mod strain_decay_skill; +pub mod strain_skill; pub mod variable_length_strain_skill; pub fn count_top_weighted_object_difficulties( @@ -22,12 +18,12 @@ pub fn count_top_weighted_object_difficulties( // * What would the top difficulty be if all object difficulties were identical let consistent_top_obj = difficulty_value / object_weight_sum; - if consistent_top_obj == 0.0 { + if FloatExt::eq(consistent_top_obj, 0.0) { 0.0 } else { - object_difficulties - .iter() - .fold(0.0, |acc, od| acc + logistic(od / consistent_top_obj, 0.88, 10.0, Some(1.1))) + object_difficulties.iter().fold(0.0, |acc, od| { + acc + logistic(od / consistent_top_obj, 0.88, 10.0, Some(1.1)) + }) } } @@ -56,4 +52,4 @@ pub fn count_top_weighted_strains( pub fn strain_decay_base(ms: f64, strain_decay_base: f64) -> f64 { f64::powf(strain_decay_base, ms / 1000.0) -} \ No newline at end of file +} diff --git a/src/any/difficulty/skills_new/variable_length_strain_skill.rs b/src/any/difficulty/skills_new/variable_length_strain_skill.rs index 4327bd53..41a91baa 100644 --- a/src/any/difficulty/skills_new/variable_length_strain_skill.rs +++ b/src/any/difficulty/skills_new/variable_length_strain_skill.rs @@ -1,7 +1,4 @@ -use crate::{ - any::difficulty::skills_new::skill::Skill, - util::traits::IEnumerable, -}; +use crate::{any::difficulty::skills_new::skill::Skill, util::traits::IEnumerable}; pub trait VariableLengthStrainSkill: Skill { const DECAY_WEIGHT: f64 = 0.9; @@ -54,7 +51,7 @@ pub trait VariableLengthStrainSkill: Skill { if final_peak.is_none() { let final_peak = StrainPeak::new( current_section_peak, - current_section_end - current_section_begin + current_section_end - current_section_begin, ); strain_peaks.cs_add_in_place(final_peak); } @@ -75,7 +72,7 @@ impl StrainPeak { pub const fn new(value: f64, section_length: f64) -> Self { Self { value, - section_length: section_length.round() + section_length: section_length.round(), } } } @@ -99,4 +96,4 @@ impl PartialOrd for StrainPeak { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } -} \ No newline at end of file +} diff --git a/src/osu/difficulty/evaluators/speed/speed.rs b/src/osu/difficulty/evaluators/speed/speed.rs index 6c4c976b..dabc24e5 100644 --- a/src/osu/difficulty/evaluators/speed/speed.rs +++ b/src/osu/difficulty/evaluators/speed/speed.rs @@ -22,7 +22,9 @@ impl SpeedEvaluator { let osu_curr_obj = curr; let mut strain_time = osu_curr_obj.adjusted_delta_time; - let double_tap_feasibility = 1.0 - osu_curr_obj.calc_double_tap_feasibility(osu_curr_obj.next(0, diff_objects), hit_window); + let double_tap_feasibility = 1.0 + - osu_curr_obj + .calc_double_tap_feasibility(osu_curr_obj.next(0, diff_objects), hit_window); // * 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. @@ -30,7 +32,9 @@ impl SpeedEvaluator { let speed_bonus = if milliseconds_to_bpm(strain_time, None) > Self::MIN_SPEED_BONUS { // * Add additional scaling bonus for streams/bursts higher than 200bpm - 0.75 * ((bpm_to_milliseconds(Self::MIN_SPEED_BONUS, None) - strain_time) / Self::SPEED_BALANCING_FACTOR).powf(2.0) + 0.75 * ((bpm_to_milliseconds(Self::MIN_SPEED_BONUS, None) - strain_time) + / Self::SPEED_BALANCING_FACTOR) + .powf(2.0) } else { // * speedBonus will be 0.0 for BPM < 200 0.0 @@ -48,4 +52,4 @@ impl SpeedEvaluator { fn high_bpm_bonus(ms: f64) -> f64 { 1.0 / (1.0 - f64::powf(0.3, ms / 1000.0)) } -} \ No newline at end of file +} diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index cde4d2c7..e2b3cc94 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -164,12 +164,12 @@ impl DifficultyValues { let take_diff_objects = cmp::min(map.hit_objects.len(), take).saturating_sub(1); let mut skills = OsuSkills::new( - mods, - &scaling_factor, - great_hit_window, - time_preempt, + mods, + &scaling_factor, + great_hit_window, + time_preempt, take_diff_objects, - attrs.od() + attrs.od(), ); for hit_object in diff_objects.iter().take(take_diff_objects) { diff --git a/src/osu/difficulty/skills/aim.rs b/src/osu/difficulty/skills/aim.rs index 824ea33d..818dd384 100644 --- a/src/osu/difficulty/skills/aim.rs +++ b/src/osu/difficulty/skills/aim.rs @@ -6,18 +6,11 @@ use crate::{ object::{HasStartTime, IDifficultyObject}, skills_new::{ strain_decay_base, - variable_length_strain_skill::{ - VariableLengthStrainSkill, - StrainPeak, - }, + variable_length_strain_skill::{StrainPeak, VariableLengthStrainSkill}, }, }, osu::difficulty::{ - evaluators::{ - AgilityEvaluator, - FlowAimEvaluator, - SnapAimEvaluator, - }, + evaluators::{AgilityEvaluator, FlowAimEvaluator, SnapAimEvaluator}, object::OsuDifficultyObject, }, util::{ @@ -85,7 +78,7 @@ impl Aim { } let decay = Self::strain_decay(curr.adjusted_delta_time); - + self.current_strain *= decay; self.current_strain += self.calculate_adjusted_difficulty(curr, objects) * (1.0 - decay); @@ -96,12 +89,22 @@ impl Aim { self.current_strain } - fn calculate_adjusted_difficulty(&self, curr: &OsuDifficultyObject<'_>, objects: &[OsuDifficultyObject<'_>]) -> f64 { - let snap_difficulty = SnapAimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) * Self::SKILL_MULTIPLIER_SNAP; - let agility_difficulty = AgilityEvaluator::evaluate_diff_of(curr, objects) * Self::SKILL_MULTIPLIER_AGILITY; - let flow_difficulty = FlowAimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) * Self::SKILL_MULTIPLIER_FLOW; - - let mut total_difficulty = self.calculate_total_value(snap_difficulty, agility_difficulty, flow_difficulty); + fn calculate_adjusted_difficulty( + &self, + curr: &OsuDifficultyObject<'_>, + objects: &[OsuDifficultyObject<'_>], + ) -> f64 { + let snap_difficulty = + SnapAimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) + * Self::SKILL_MULTIPLIER_SNAP; + let agility_difficulty = + AgilityEvaluator::evaluate_diff_of(curr, objects) * Self::SKILL_MULTIPLIER_AGILITY; + let flow_difficulty = + FlowAimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) + * Self::SKILL_MULTIPLIER_FLOW; + + let mut total_difficulty = + self.calculate_total_value(snap_difficulty, agility_difficulty, flow_difficulty); if let Some(attraction_strength) = self.mods.attraction_strength() { total_difficulty *= 1.0 - attraction_strength; @@ -112,22 +115,34 @@ impl Aim { total_difficulty } - fn calculate_total_value(&self, snap_difficulty: f64, agility_difficulty: f64, flow_difficulty: f64) -> f64 { + fn calculate_total_value( + &self, + snap_difficulty: f64, + agility_difficulty: f64, + flow_difficulty: f64, + ) -> f64 { let mut snap_difficulty_new = snap_difficulty; let mut flow_difficulty_new = flow_difficulty; // * We compare flow to combined snap and agility because snap by itself doesn't have enough difficulty to be above flow on streams // * Agility on the other hand is supposed to measure the rate of cursor velocity changes while snapping // * So snapping every circle on a stream requires an enormous amount of agility at which point it's easier to flow - let mut combined_snap_difficulty = norm(Self::COMBINED_SNAP_NORM_EXPONENT, [snap_difficulty_new, agility_difficulty]); + let mut combined_snap_difficulty = norm( + Self::COMBINED_SNAP_NORM_EXPONENT, + [snap_difficulty_new, agility_difficulty], + ); - let p_snap = Self::calculate_snap_flow_probability(flow_difficulty / combined_snap_difficulty); + let p_snap = + Self::calculate_snap_flow_probability(flow_difficulty / combined_snap_difficulty); let p_flow = 1.0 - p_snap; if self.mods.td() { // * we don't adjust agility here since agility represents TD difficulty in a decent enough way snap_difficulty_new = snap_difficulty_new.powf(0.89); - combined_snap_difficulty = norm(Self::COMBINED_SNAP_NORM_EXPONENT, [snap_difficulty_new, agility_difficulty]); + combined_snap_difficulty = norm( + Self::COMBINED_SNAP_NORM_EXPONENT, + [snap_difficulty_new, agility_difficulty], + ); } if self.mods.rx() { @@ -201,41 +216,37 @@ impl Aim { pub fn difficulty_value(current_strain_peaks: Vec) -> f64 { aim_difficulty_value( - Self::get_reduced_strain_peaks(current_strain_peaks), - Self::MAX_SECTION_LENGTH, - Self::DECAY_WEIGHT + Self::get_reduced_strain_peaks(current_strain_peaks), + Self::MAX_SECTION_LENGTH, + Self::DECAY_WEIGHT, ) } pub fn into_difficulty_value(self) -> f64 { aim_difficulty_value( - Self::get_reduced_strain_peaks( - Self::get_current_strain_peaks( - self.skill_strain_peaks, - self.skill_final_peak, - self.skill_current_section_peak, - self.skill_current_section_begin, - self.skill_current_section_end - ) - ), - Self::MAX_SECTION_LENGTH, - Self::DECAY_WEIGHT + Self::get_reduced_strain_peaks(Self::get_current_strain_peaks( + self.skill_strain_peaks, + self.skill_final_peak, + self.skill_current_section_peak, + self.skill_current_section_begin, + self.skill_current_section_end, + )), + Self::MAX_SECTION_LENGTH, + Self::DECAY_WEIGHT, ) } pub fn cloned_difficulty_value(&self) -> f64 { aim_difficulty_value( - Self::get_reduced_strain_peaks( - Self::get_current_strain_peaks( - self.skill_strain_peaks.clone(), - self.skill_final_peak, - self.skill_current_section_peak, - self.skill_current_section_begin, - self.skill_current_section_end - ) - ), - Self::MAX_SECTION_LENGTH, - Self::DECAY_WEIGHT + Self::get_reduced_strain_peaks(Self::get_current_strain_peaks( + self.skill_strain_peaks.clone(), + self.skill_final_peak, + self.skill_current_section_peak, + self.skill_current_section_begin, + self.skill_current_section_end, + )), + Self::MAX_SECTION_LENGTH, + Self::DECAY_WEIGHT, ) } @@ -255,15 +266,20 @@ impl Aim { // * Strains are split into 20ms chunks to try to mitigate inconsistencies caused by reducing strains while strains.len() > skip_count && time < REDUCED_SECTION_TIME { let strain = strains[skip_count]; - + let mut added_time = 0.0; while added_time < strain.section_length { - let scale = lerp(1.0, 10.0, ((time + added_time) / REDUCED_SECTION_TIME).clamp(0.0, 1.0)).log10(); + let scale = lerp( + 1.0, + 10.0, + ((time + added_time) / REDUCED_SECTION_TIME).clamp(0.0, 1.0), + ) + .log10(); // * intentionally add at end and sort afterwards, should be cheaper. strains.push(StrainPeak::new( strain.value * lerp(REDUCED_STRAIN_BASELINE, 1.0, scale), - CHUNK_SIZE.min(strain.section_length - added_time) + CHUNK_SIZE.min(strain.section_length - added_time), )); added_time += CHUNK_SIZE; @@ -280,7 +296,11 @@ impl Aim { } } -fn aim_difficulty_value(reduced_strain_peaks: Vec, max_section_length: f64, decay_weight: f64) -> f64 { +fn aim_difficulty_value( + reduced_strain_peaks: Vec, + max_section_length: f64, + decay_weight: f64, +) -> f64 { let mut difficulty = 0.0; let mut time = 0.0; @@ -312,4 +332,4 @@ fn aim_difficulty_value(reduced_strain_peaks: Vec, max_section_lengt } difficulty / (1.0 - decay_weight) -} \ No newline at end of file +} diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index e3a771b2..7b9f1f46 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -44,8 +44,8 @@ impl OsuSkills { 400.0 * (time_preempt / OsuObject::PREEMPT_MIN).min(1.0) }; - let aim = Aim::new(true); - let aim_no_sliders = Aim::new(false); + let aim = Aim::new(&mods.clone(), true, overall_difficulty); + let aim_no_sliders = Aim::new(&mods.clone(), false, overall_difficulty); let speed = Speed::new(hit_window, mods.ap()); let flashlight = Flashlight::new( mods, @@ -53,7 +53,7 @@ impl OsuSkills { overall_difficulty, scaling_factor.radius, time_preempt, - time_fade_in + time_fade_in, ); Self { diff --git a/src/osu/strains.rs b/src/osu/strains.rs index 62ab6d65..881646dc 100644 --- a/src/osu/strains.rs +++ b/src/osu/strains.rs @@ -1,5 +1,13 @@ use crate::{ - Beatmap, Difficulty, any::difficulty::{skills::StrainSkill, skills_new::strain_skill::NewStrainSkill}, model::mode::ConvertError, osu::convert::prepare_map, + Beatmap, Difficulty, + any::difficulty::{ + skills::StrainSkill, + skills_new::{ + strain_skill::NewStrainSkill, variable_length_strain_skill::VariableLengthStrainSkill, + }, + }, + model::mode::ConvertError, + osu::convert::prepare_map, }; use super::difficulty::{DifficultyValues, skills::OsuSkills}; @@ -40,8 +48,16 @@ pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result>(), + aim_no_sliders: aim_no_sliders + .into_current_strain_peaks() + .iter() + .map(|sp| sp.value) + .collect::>(), speed: speed.into_current_strain_peaks(), flashlight: flashlight.into_current_strain_peaks(), }) From e19c790033c86d36380dda1b1145d5cead4e1827 Mon Sep 17 00:00:00 2001 From: myssto Date: Fri, 31 Jul 2026 08:15:50 -0500 Subject: [PATCH 10/27] refactor: finish refactoring osu skills --- .../difficulty/skills_new/harmonic_skill.rs | 14 +- src/any/difficulty/skills_new/mod.rs | 21 ++- src/osu/difficulty/skills/aim.rs | 43 ++---- src/osu/difficulty/skills/mod.rs | 9 +- src/osu/difficulty/skills/reading.rs | 113 +++++++++++++++ src/osu/difficulty/skills/speed.rs | 129 +++++++++--------- src/osu/difficulty/skills/strain.rs | 61 +-------- src/util/macros_new.rs | 84 +++++++++++- 8 files changed, 303 insertions(+), 171 deletions(-) create mode 100644 src/osu/difficulty/skills/reading.rs diff --git a/src/any/difficulty/skills_new/harmonic_skill.rs b/src/any/difficulty/skills_new/harmonic_skill.rs index d96ecf98..6b14fff9 100644 --- a/src/any/difficulty/skills_new/harmonic_skill.rs +++ b/src/any/difficulty/skills_new/harmonic_skill.rs @@ -21,11 +21,21 @@ pub trait HarmonicSkill: Skill { self.object_difficulty_of(curr, objects) } - fn get_transformed_difficulties(difficulties: &[f64]) -> &[f64] { + #[expect(unused_mut, reason = "staying in-sync with lazer")] + fn get_transformed_difficulties(&self, mut difficulties: Vec) -> Vec { difficulties } - fn count_top_weighted_object_difficulties(difficulty_value: f64) -> f64; + fn difficulty_value( + transformed_object_difficulties: Vec, + object_weight_sum: &mut f64, + ) -> f64; + + fn into_difficulty_value(self) -> f64; + + fn cloned_difficulty_value(&mut self) -> f64; + + fn count_top_weighted_object_difficulties(&self, difficulty_value: f64) -> f64; fn difficulty_to_performance(difficulty: f64) -> f64 { 4.0 * difficulty.powf(3.0) diff --git a/src/any/difficulty/skills_new/mod.rs b/src/any/difficulty/skills_new/mod.rs index cf11ee2d..62660ac2 100644 --- a/src/any/difficulty/skills_new/mod.rs +++ b/src/any/difficulty/skills_new/mod.rs @@ -10,6 +10,9 @@ pub fn count_top_weighted_object_difficulties( difficulty_value: f64, object_difficulties: &[f64], object_weight_sum: f64, + midpoint_offset: f64, + multiplier: f64, + max_value: Option, ) -> f64 { if object_difficulties.is_empty() || FloatExt::eq(object_weight_sum, 0.0) { return 0.0; @@ -19,12 +22,20 @@ pub fn count_top_weighted_object_difficulties( let consistent_top_obj = difficulty_value / object_weight_sum; if FloatExt::eq(consistent_top_obj, 0.0) { - 0.0 - } else { - object_difficulties.iter().fold(0.0, |acc, od| { - acc + logistic(od / consistent_top_obj, 0.88, 10.0, Some(1.1)) - }) + return 0.0; } + + object_difficulties + .iter() + .map(|s| { + logistic( + s / consistent_top_obj, + midpoint_offset, + multiplier, + max_value, + ) + }) + .sum() } pub fn count_top_weighted_strains( diff --git a/src/osu/difficulty/skills/aim.rs b/src/osu/difficulty/skills/aim.rs index 818dd384..e6d947b8 100644 --- a/src/osu/difficulty/skills/aim.rs +++ b/src/osu/difficulty/skills/aim.rs @@ -12,6 +12,7 @@ use crate::{ osu::difficulty::{ evaluators::{AgilityEvaluator, FlowAimEvaluator, SnapAimEvaluator}, object::OsuDifficultyObject, + skills::strain::count_top_weighted_sliders, }, util::{ difficulty::{lerp, logistic, logistic_exp, norm}, @@ -24,20 +25,10 @@ define_new_skill! { pub struct Aim: VariableLengthStrainSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] { current_strain: f64 = 0.0, slider_strains: Vec = Vec::with_capacity(64), - include_sliders: bool, mods: GameMods, + include_sliders: bool, overall_difficulty: f64, } - - pub fn new(mods: &GameMods, include_sliders: bool, overall_difficulty: f64) -> Self { - Self { - current_strain: 0.0, - slider_strains: Vec::with_capacity(64), - mods: mods.clone(), - include_sliders: include_sliders, - overall_difficulty: overall_difficulty, - } - } } impl Aim { @@ -203,15 +194,7 @@ impl Aim { // * What would the top strain be if all strain values were identical let consistent_top_strain = difficulty_value / 10.0; - if FloatExt::eq(consistent_top_strain, 0.0) { - return 0.0; - } - - // * Use a weighted sum of all strains. Constants are arbitrary and give nice values - self.slider_strains - .iter() - .map(|s| logistic(*s / consistent_top_strain, 0.88, 10.0, Some(1.1))) - .sum() + count_top_weighted_sliders(&self.slider_strains, consistent_top_strain) } pub fn difficulty_value(current_strain_peaks: Vec) -> f64 { @@ -223,31 +206,27 @@ impl Aim { } pub fn into_difficulty_value(self) -> f64 { - aim_difficulty_value( - Self::get_reduced_strain_peaks(Self::get_current_strain_peaks( + Self::difficulty_value(Self::get_reduced_strain_peaks( + Self::get_current_strain_peaks( self.skill_strain_peaks, self.skill_final_peak, self.skill_current_section_peak, self.skill_current_section_begin, self.skill_current_section_end, - )), - Self::MAX_SECTION_LENGTH, - Self::DECAY_WEIGHT, - ) + ), + )) } pub fn cloned_difficulty_value(&self) -> f64 { - aim_difficulty_value( - Self::get_reduced_strain_peaks(Self::get_current_strain_peaks( + Self::difficulty_value(Self::get_reduced_strain_peaks( + Self::get_current_strain_peaks( self.skill_strain_peaks.clone(), self.skill_final_peak, self.skill_current_section_peak, self.skill_current_section_begin, self.skill_current_section_end, - )), - Self::MAX_SECTION_LENGTH, - Self::DECAY_WEIGHT, - ) + ), + )) } fn get_reduced_strain_peaks(current_strain_peaks: Vec) -> Vec { diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index 7b9f1f46..296c58af 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -1,5 +1,5 @@ use crate::any::difficulty::skills_new::skill::Skill; -use crate::{any::difficulty::skills::StrainSkill, model::mods::GameMods, osu::object::OsuObject}; +use crate::{model::mods::GameMods, osu::object::OsuObject}; use self::{aim::Aim, flashlight::Flashlight, speed::Speed}; @@ -9,6 +9,7 @@ use super::{ pub mod aim; pub mod flashlight; +pub mod reading; pub mod speed; pub mod strain; @@ -44,9 +45,9 @@ impl OsuSkills { 400.0 * (time_preempt / OsuObject::PREEMPT_MIN).min(1.0) }; - let aim = Aim::new(&mods.clone(), true, overall_difficulty); - let aim_no_sliders = Aim::new(&mods.clone(), false, overall_difficulty); - let speed = Speed::new(hit_window, mods.ap()); + let aim = Aim::new(mods.clone(), true, overall_difficulty); + let aim_no_sliders = Aim::new(mods.clone(), false, overall_difficulty); + let speed = Speed::new(mods.clone(), hit_window); let flashlight = Flashlight::new( mods, total_objects as i32, diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs new file mode 100644 index 00000000..9307a89d --- /dev/null +++ b/src/osu/difficulty/skills/reading.rs @@ -0,0 +1,113 @@ +use crate::{ + GameMods, + any::difficulty::skills_new::{count_top_weighted_object_difficulties, strain_decay_base}, + osu::difficulty::{evaluators::ReadingEvaluator, object::OsuDifficultyObject}, + util::difficulty::lerp, +}; + +define_new_skill! { + pub struct Reading: HarmonicSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] { + current_strain: f64 = 0.0, + reduced_note_count: usize = 0, + reduced_duration: Option = None, + mods: GameMods, + reading_evaluator: ReadingEvaluator, + hit_window: f64, + overall_difficulty: f64, + } +} + +impl Reading { + pub const SKILL_MULTIPLIER: f64 = 2.5; + pub const REDUCED_DIFFICULTY_DURATION: i32 = 60 * 1000; + pub const REDUCED_DIFFICULTY_BASE_LINE: f64 = 0.0; + + fn strain_decay(ms: f64) -> f64 { + strain_decay_base(ms, 0.8) + } + + fn object_difficulty_of<'a>( + &mut self, + curr: &'a OsuDifficultyObject<'a>, + objects: &'a [OsuDifficultyObject<'a>], + ) -> f64 { + let decay = Self::strain_decay(curr.adjusted_delta_time); + + self.current_strain *= decay; + self.current_strain += self.calculate_adjusted_difficulty(curr, objects) + * (1.0 - decay) + * Self::SKILL_MULTIPLIER; + + // * This currently operates under the assumption that `ObjectDifficultyOf` is called once per object, and in order. + // * Under that assumption, we can trust that `current.StartTime` refers to the start time of the first object in the case that `reducedDuration` is yet to be set. + let reduced_duration = self + .reduced_duration + .get_or_insert(curr.start_time + f64::from(Self::REDUCED_DIFFICULTY_DURATION)); + + // * This relies on the same assumption, as calling in order means that we can safely increase the note count until we reach the first object after the reduced duration. + if curr.start_time <= *reduced_duration { + self.reduced_note_count += 1; + } + + self.current_strain + } + + fn calculate_adjusted_difficulty<'a>( + &mut self, + curr: &'a OsuDifficultyObject<'a>, + objects: &'a [OsuDifficultyObject<'a>], + ) -> f64 { + let mut difficulty = self + .reading_evaluator + .evaluate_diff_of(curr, objects, self.mods.hd()); + + if self.mods.td() { + difficulty *= difficulty.powf(0.89); + } + + if let Some(magnetised_strength) = self.mods.attraction_strength() { + difficulty *= 1.0 - magnetised_strength; + } + + if self.mods.rx() { + difficulty *= 0.4; + } + + if self.mods.ap() { + difficulty *= 0.1; + } + + difficulty *= self.overall_difficulty.max(0.0).powf(2.2) / 1125.0; + + difficulty + } + + fn get_transformed_difficulties(&self, mut difficulties: Vec) -> Vec { + difficulties.retain(|v| *v > 0.0); + + let count = difficulties.len().min(self.reduced_note_count); + for (i, difficulty) in difficulties.iter_mut().take(count).enumerate() { + let scale = lerp( + 1.0, + 10.0, + (i as f64 / self.reduced_note_count as f64).clamp(0.0, 1.0), + ) + .log10(); + + *difficulty *= lerp(Self::REDUCED_DIFFICULTY_BASE_LINE, 1.0, scale); + } + + difficulties + } + + fn count_top_weighted_object_difficulties(&self, difficulty_value: f64) -> f64 { + count_top_weighted_object_difficulties( + difficulty_value, + &self.skill_object_difficulties, + self.skill_object_weight_sum, + 1.15, + 5.0, + Some(1.1), + ) + } +} diff --git a/src/osu/difficulty/skills/speed.rs b/src/osu/difficulty/skills/speed.rs index 43655222..e8161887 100644 --- a/src/osu/difficulty/skills/speed.rs +++ b/src/osu/difficulty/skills/speed.rs @@ -1,61 +1,50 @@ use crate::{ - any::difficulty::{ - object::{HasStartTime, IDifficultyObject}, - skills::{StrainSkill, strain_decay}, - }, + GameMods, + any::difficulty::skills_new::strain_decay_base, osu::difficulty::{ evaluators::{RhythmEvaluator, SpeedEvaluator}, object::OsuDifficultyObject, + skills::strain::count_top_weighted_sliders, }, + util::{difficulty::logistic, float_ext::FloatExt}, }; -use super::strain::OsuStrainSkill; - -define_skill! { +define_new_skill! { #[derive(Clone)] - pub struct Speed: StrainSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] { + pub struct Speed: HarmonicSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] { + slider_strains: Vec = Vec::with_capacity(64), current_strain: f64 = 0.0, - current_rhythm: f64 = 0.0, + mods: GameMods, hit_window: f64, - has_autopilot_mod: bool, - slider_strains: Vec = Vec::with_capacity(64), } } impl Speed { - const SKILL_MULTIPLIER: f64 = 1.47; - const STRAIN_DECAY_BASE: f64 = 0.3; - const REDUCED_SECTION_COUNT: usize = 5; - - fn calculate_initial_strain( - &mut self, - time: f64, - curr: &OsuDifficultyObject<'_>, - objects: &[OsuDifficultyObject<'_>], - ) -> f64 { - let prev_start_time = curr - .previous(0, objects) - .map_or(0.0, HasStartTime::start_time); + const SKILL_MULTIPLIER: f64 = 1.16; - (self.current_strain * self.current_rhythm) - * strain_decay(time - prev_start_time, Self::STRAIN_DECAY_BASE) + fn strain_decay(ms: f64) -> f64 { + strain_decay_base(ms, 0.3) } - fn strain_value_at( + fn object_difficulty_of<'a>( &mut self, - curr: &OsuDifficultyObject<'_>, - objects: &[OsuDifficultyObject<'_>], + curr: &'a OsuDifficultyObject<'a>, + objects: &'a [OsuDifficultyObject<'a>], ) -> f64 { - self.current_strain *= strain_decay(curr.adjusted_delta_time, Self::STRAIN_DECAY_BASE); - self.current_strain += SpeedEvaluator::evaluate_diff_of( - curr, - objects, - self.hit_window, - self.has_autopilot_mod, - ) * Self::SKILL_MULTIPLIER; - self.current_rhythm = RhythmEvaluator::evaluate_diff_of(curr, objects, self.hit_window); + if self.mods.rx() { + return 0.0; + } + + let decay = Self::strain_decay(curr.adjusted_delta_time); + + self.current_strain *= decay; + self.current_strain += self.calculate_adjusted_difficulty(curr, objects) + * (1.0 - decay) + * Self::SKILL_MULTIPLIER; + + let curr_rhythm = RhythmEvaluator::evaluate_diff_of(curr, objects, self.hit_window); - let total_strain = self.current_strain * self.current_rhythm; + let total_strain = self.current_strain * curr_rhythm; if curr.base.is_slider() { self.slider_strains.push(total_strain); @@ -64,35 +53,49 @@ impl Speed { total_strain } - pub fn relevant_note_count(&self) -> f64 { - self.strain_skill_object_strains + fn calculate_adjusted_difficulty<'a>( + &mut self, + curr: &'a OsuDifficultyObject<'a>, + objects: &'a [OsuDifficultyObject<'a>], + ) -> f64 { + let mut difficulty = SpeedEvaluator::evaluate_diff_of(curr, objects, self.hit_window); + + if self.mods.ap() { + difficulty *= 0.5; + } + + difficulty + } + + pub fn relevant_object_count(&self) -> f64 { + if self.skill_object_difficulties.is_empty() { + return 0.0; + } + + let max_strain = self + .skill_object_difficulties .iter() .copied() - .max_by(f64::total_cmp) - .filter(|&n| n > 0.0) - .map_or(0.0, |max_strain| { - self.strain_skill_object_strains - .iter() - .fold(0.0, |sum, strain| { - sum + (1.0 + f64::exp(-(strain / max_strain * 12.0 - 6.0))).recip() - }) - }) - } + .fold(0.0, f64::max); - pub fn slider_strains(&self) -> &[f64] { - &self.slider_strains + if FloatExt::eq(max_strain, 0.0) { + return 0.0; + } + + self.skill_object_difficulties + .iter() + .map(|s| logistic(s / max_strain, 0.5, 12.0, None)) + .sum() } - // From `OsuStrainSkill`; native rather than trait function so that it has - // priority over `StrainSkill::difficulty_value` - fn difficulty_value(current_strain_peaks: Vec) -> f64 { - super::strain::difficulty_value( - current_strain_peaks, - Self::REDUCED_SECTION_COUNT, - Self::REDUCED_STRAIN_BASELINE, - Self::DECAY_WEIGHT, - ) + pub fn count_top_weighted_sliders(&self, difficulty_value: f64) -> f64 { + if self.slider_strains.is_empty() || FloatExt::eq(self.skill_object_weight_sum, 0.0) { + return 0.0; + } + + // * What would the top note be if all note values were identical + let consistent_top_object = difficulty_value / self.skill_object_weight_sum; + + count_top_weighted_sliders(&self.slider_strains, consistent_top_object) } } - -impl OsuStrainSkill for Speed {} diff --git a/src/osu/difficulty/skills/strain.rs b/src/osu/difficulty/skills/strain.rs index 265944d5..d2f7134d 100644 --- a/src/osu/difficulty/skills/strain.rs +++ b/src/osu/difficulty/skills/strain.rs @@ -1,55 +1,6 @@ -use crate::util::{ - difficulty::logistic, - float_ext::FloatExt, - traits::{IEnumerable, IOrderedEnumerable}, -}; - -pub trait OsuStrainSkill { - const REDUCED_SECTION_COUNT: usize = 10; - const REDUCED_STRAIN_BASELINE: f64 = 0.75; - - fn difficulty_to_performance(difficulty: f64) -> f64 { - difficulty_to_performance(difficulty) - } -} - -pub fn difficulty_value( - current_strain_peaks: Vec, - reduced_section_count: usize, - reduced_strain_baseline: f64, - decay_weight: f64, -) -> 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 peaks = current_strain_peaks.cs_where(|&p| p > 0.0); - - let mut strains = peaks.cs_order_descending(); - - for (i, strain) in strains.iter_mut().take(reduced_section_count).enumerate() { - let clamped = f64::from((i as f32 / reduced_section_count as f32).clamp(0.0, 1.0)); - let scale = f64::log10(lerp(1.0, 10.0, clamped)); - *strain *= lerp(reduced_strain_baseline, 1.0, scale); - } - - for strain in strains.cs_order_descending() { - difficulty += strain * weight; - weight *= decay_weight; - } - - difficulty -} - -pub fn count_top_weighted_sliders(slider_strains: &[f64], difficulty_value: f64) -> f64 { - if slider_strains.is_empty() { - return 0.0; - } - - // * What would the top strain be if all strain values were identical - let consistent_top_strain = difficulty_value / 10.0; +use crate::util::{difficulty::logistic, float_ext::FloatExt}; +pub fn count_top_weighted_sliders(slider_strains: &[f64], consistent_top_strain: f64) -> f64 { if FloatExt::eq(consistent_top_strain, 0.0) { return 0.0; } @@ -59,11 +10,3 @@ pub fn count_top_weighted_sliders(slider_strains: &[f64], difficulty_value: f64) .map(|s| logistic(*s / consistent_top_strain, 0.88, 10.0, Some(1.1))) .sum() } - -pub fn difficulty_to_performance(difficulty: f64) -> f64 { - f64::powf(5.0 * f64::max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100_000.0 -} - -const fn lerp(start: f64, end: f64, amount: f64) -> f64 { - start + (end - start) * amount -} diff --git a/src/util/macros_new.rs b/src/util/macros_new.rs index cbd0c71e..55f4fbb5 100644 --- a/src/util/macros_new.rs +++ b/src/util/macros_new.rs @@ -98,6 +98,23 @@ macro_rules! define_new_skill { } }; + // Extend `Skill`'s fields + ( + @$trait:ident $objects:ty[$object:ty] + extend_fields Skill // <- + fields { $( $fields:tt )* } + $( $rest:tt )* + ) => { + define_new_skill! { + @$trait $objects[$object] + fields { + $( $fields )* + skill_object_difficulties Vec = Vec::with_capacity(256), // <- + } + $( $rest )* + } + }; + // Extend `VariableLengthStrainSkill`'s fields ( @$trait:ident $objects:ty[$object:ty] @@ -142,18 +159,19 @@ macro_rules! define_new_skill { } }; - // Extend `Skill`'s fields + // Extend `HarmonicSkill`'s fields ( @$trait:ident $objects:ty[$object:ty] - extend_fields Skill // <- + extend_fields HarmonicSkill // <- fields { $( $fields:tt )* } $( $rest:tt )* ) => { define_new_skill! { @$trait $objects[$object] + extend_fields Skill fields { $( $fields )* - skill_object_difficulties Vec = Vec::with_capacity(256), // <- + skill_object_weight_sum f64 = 0.0, // <- } $( $rest )* } @@ -268,9 +286,10 @@ macro_rules! define_new_skill { any::difficulty::{ object::{IDifficultyObject, IDifficultyObjects, HasStartTime}, skills_new::{ - skill::Skill, + skill::Skill, strain_skill::NewStrainSkill, variable_length_strain_skill::VariableLengthStrainSkill, + harmonic_skill::HarmonicSkill, }, }, }; @@ -518,7 +537,7 @@ macro_rules! define_new_skill { } let peak = crate::any::difficulty::skills_new::variable_length_strain_skill::StrainPeak::new(self.skill_current_section_peak, section_length); - + self.skill_strain_peaks.cs_add_in_place(peak); self.skill_total_length += section_length; @@ -572,4 +591,57 @@ macro_rules! define_new_skill { } } }; -} \ No newline at end of file + + // Implement `HarmonicSkill` trait + ( @impl HarmonicSkill $name:ident $objects:ty[$object:ty] ) => { + define_new_skill!( @impl Skill $name $objects[$object] ); + + impl HarmonicSkill for $name { + #[expect(unused_variables, reason = "placeholder")] + fn object_difficulty_of<'a>( + &mut self, + curr: &Self::DifficultyObject<'a>, + objects: &Self::DifficultyObjects<'a>, + ) -> f64 { + todo!() + } + + fn difficulty_value( + transformed_object_difficulties: Vec, + object_weight_sum: &mut f64, + ) -> f64 { + crate::any::difficulty::skills_new::harmonic_skill::harmonic_skill_difficulty_value( + &transformed_object_difficulties, + object_weight_sum, + Self::HARMONIC_SCALE, + Self::DECAY_EXPONENT, + ) + } + + fn into_difficulty_value(mut self) -> f64 { + Self::difficulty_value( + self.get_transformed_difficulties(self.get_object_difficulties().to_vec()), + &mut self.skill_object_weight_sum, + ) + } + + fn cloned_difficulty_value(&mut self) -> f64 { + Self::difficulty_value( + self.get_transformed_difficulties(self.skill_object_difficulties.clone()), + &mut self.skill_object_weight_sum, + ) + } + + fn count_top_weighted_object_difficulties(&self, difficulty_value: f64) -> f64 { + crate::any::difficulty::skills_new::count_top_weighted_object_difficulties( + difficulty_value, + &self.skill_object_difficulties, + self.skill_object_weight_sum, + 0.88, + 10.0, + Some(1.1) + ) + } + } + }; +} From 1653b5a823fd6703f432f476e65efc18e1b499e0 Mon Sep 17 00:00:00 2001 From: myssto Date: Fri, 31 Jul 2026 10:18:14 -0500 Subject: [PATCH 11/27] feat: update osu difficulty attributes --- src/osu/attributes.rs | 6 + src/osu/difficulty/evaluators/aim/agility.rs | 7 +- src/osu/difficulty/evaluators/aim/flow_aim.rs | 73 +++++--- src/osu/difficulty/evaluators/aim/mod.rs | 2 +- src/osu/difficulty/evaluators/aim/snap_aim.rs | 165 ++++++++++++------ src/osu/difficulty/evaluators/flashlight.rs | 9 +- src/osu/difficulty/evaluators/mod.rs | 15 +- src/osu/difficulty/evaluators/reading.rs | 3 - src/osu/difficulty/evaluators/speed/mod.rs | 2 +- src/osu/difficulty/evaluators/speed/rhythm.rs | 61 ++++--- src/osu/difficulty/gradual.rs | 13 +- src/osu/difficulty/mod.rs | 142 ++++++++------- src/osu/difficulty/skills/aim.rs | 4 + src/osu/difficulty/skills/flashlight.rs | 34 ++-- src/osu/difficulty/skills/mod.rs | 14 +- src/osu/difficulty/skills/reading.rs | 16 +- src/osu/performance/calculator.rs | 5 +- src/osu/performance/mod.rs | 5 +- 18 files changed, 350 insertions(+), 226 deletions(-) diff --git a/src/osu/attributes.rs b/src/osu/attributes.rs index 34c735b0..f61e49ce 100644 --- a/src/osu/attributes.rs +++ b/src/osu/attributes.rs @@ -11,6 +11,8 @@ pub struct OsuDifficultyAttributes { pub speed: f64, /// The difficulty of the flashlight skill. pub flashlight: f64, + /// The difficulty of the reading skill. + pub reading: f64, /// The ratio of the aim strain with and without considering sliders pub slider_factor: f64, /// Describes how much of aim's difficult strain count is contributed to by sliders @@ -19,6 +21,8 @@ pub struct OsuDifficultyAttributes { pub speed_top_weighted_slider_factor: f64, /// The number of clickable objects weighted by difficulty. pub speed_note_count: f64, + /// The number of clickable objects weighted by difficulty. + pub reading_note_count: f64, /// Weighted sum of aim strains. pub aim_difficult_strain_count: f64, /// Weighted sum of speed strains. @@ -97,6 +101,8 @@ pub struct OsuPerformanceAttributes { pub pp_flashlight: f64, /// The speed portion of the final pp. pub pp_speed: f64, + /// The reading portion of the final pp. + pub pp_reading: f64, /// Misses including an approximated amount of slider breaks pub effective_miss_count: f64, /// Approximated unstable-rate diff --git a/src/osu/difficulty/evaluators/aim/agility.rs b/src/osu/difficulty/evaluators/aim/agility.rs index d59ce505..e9390b09 100644 --- a/src/osu/difficulty/evaluators/aim/agility.rs +++ b/src/osu/difficulty/evaluators/aim/agility.rs @@ -1,6 +1,5 @@ use crate::{ - any::difficulty::object::IDifficultyObject, - osu::difficulty::object::OsuDifficultyObject + any::difficulty::object::IDifficultyObject, osu::difficulty::object::OsuDifficultyObject, }; pub struct AgilityEvaluator; @@ -11,7 +10,7 @@ impl AgilityEvaluator { pub fn evaluate_diff_of<'a>( curr: &'a OsuDifficultyObject<'a>, - diff_objects: &'a [OsuDifficultyObject<'a>] + diff_objects: &'a [OsuDifficultyObject<'a>], ) -> f64 { if curr.base.is_spinner() { return 0.0; @@ -37,4 +36,4 @@ impl AgilityEvaluator { fn high_bpm_bonus(ms: f64) -> f64 { 1.0 / (1.0 - f64::powf(0.2, ms / 1000.0)) } -} \ No newline at end of file +} diff --git a/src/osu/difficulty/evaluators/aim/flow_aim.rs b/src/osu/difficulty/evaluators/aim/flow_aim.rs index fb47f07d..23518a0f 100644 --- a/src/osu/difficulty/evaluators/aim/flow_aim.rs +++ b/src/osu/difficulty/evaluators/aim/flow_aim.rs @@ -1,14 +1,12 @@ use rosu_map::util::Pos; use crate::{ - any::difficulty::object::IDifficultyObject, osu::difficulty::{ - evaluators::aim::snap_aim::SnapAimEvaluator, - object::OsuDifficultyObject - }, + any::difficulty::object::IDifficultyObject, + osu::difficulty::{evaluators::aim::snap_aim::SnapAimEvaluator, object::OsuDifficultyObject}, util::{ - difficulty::{smootherstep, smoothstep}, - float_ext::FloatExt - } + difficulty::{smootherstep, smoothstep}, + float_ext::FloatExt, + }, }; pub struct FlowAimEvaluator; @@ -30,8 +28,16 @@ impl FlowAimEvaluator { return 0.0; }; - let curr_dist = if with_slider_travel_dist { osu_curr_obj.lazy_jump_dist } else { osu_curr_obj.jump_dist }; - let prev_dist = if with_slider_travel_dist { osu_last_obj.lazy_jump_dist } else { osu_last_obj.jump_dist }; + let curr_dist = if with_slider_travel_dist { + osu_curr_obj.lazy_jump_dist + } else { + osu_curr_obj.jump_dist + }; + let prev_dist = if with_slider_travel_dist { + osu_last_obj.lazy_jump_dist + } else { + osu_last_obj.jump_dist + }; let mut curr_vel = curr_dist / osu_curr_obj.adjusted_delta_time; @@ -50,13 +56,20 @@ impl FlowAimEvaluator { flow_diff *= osu_curr_obj.small_circle_bonus.sqrt(); // * Rhythm changes are harder to flow. - flow_diff *= 1.0 + f64::min( - 0.25, - f64::powf( - (osu_curr_obj.adjusted_delta_time.max(osu_last_obj.adjusted_delta_time) - osu_curr_obj.adjusted_delta_time.min(osu_last_obj.adjusted_delta_time)) / 50.0, - 4.0 - ) - ); + flow_diff *= 1.0 + + f64::min( + 0.25, + f64::powf( + (osu_curr_obj + .adjusted_delta_time + .max(osu_last_obj.adjusted_delta_time) + - osu_curr_obj + .adjusted_delta_time + .min(osu_last_obj.adjusted_delta_time)) + / 50.0, + 4.0, + ), + ); if let (Some(curr_angle), Some(last_angle)) = (osu_curr_obj.angle, osu_last_obj.angle) { let angle_diff = (curr_angle - last_angle).abs(); @@ -75,8 +88,8 @@ impl FlowAimEvaluator { if let Some(osu_last_last_obj) = curr.previous(1, diff_objects) { overlapped_notes_weight = 1.0 - Self::calc_overlap_factor(osu_curr_obj, osu_last_obj) - * Self::calc_overlap_factor(osu_curr_obj, osu_last_last_obj) - * Self::calc_overlap_factor(osu_last_obj, osu_last_last_obj); + * Self::calc_overlap_factor(osu_curr_obj, osu_last_last_obj) + * Self::calc_overlap_factor(osu_last_obj, osu_last_last_obj); } if let Some(curr_angle) = osu_curr_obj.angle { @@ -95,13 +108,16 @@ impl FlowAimEvaluator { let dist_ratio = smoothstep( (prev_vel - curr_vel).abs() / prev_vel.max(curr_vel), 0.0, - 1.0 + 1.0, ); // * Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. let overlap_vel_buff = f64::min( - f64::from(OsuDifficultyObject::NORMALIZED_DIAMETER) * 1.25 / osu_curr_obj.adjusted_delta_time.min(osu_last_obj.adjusted_delta_time), - (prev_vel - curr_vel).abs() + f64::from(OsuDifficultyObject::NORMALIZED_DIAMETER) * 1.25 + / osu_curr_obj + .adjusted_delta_time + .min(osu_last_obj.adjusted_delta_time), + (prev_vel - curr_vel).abs(), ); flow_diff += overlap_vel_buff @@ -115,24 +131,29 @@ impl FlowAimEvaluator { flow_diff += osu_curr_obj.travel_dist / osu_curr_obj.travel_time; } - // * Final velocity is being raised to a power because flow difficulty scales harder with + // * Final velocity is being raised to a power because flow difficulty scales harder with // * both high distance and time, and we want to account for that. flow_diff = flow_diff.powf(1.45); // * Reduce difficulty for low spacing since spacing below radius is always to be flowed - flow_diff * smootherstep(curr_dist, 0.0, f64::from(OsuDifficultyObject::NORMALIZED_RADIUS)) + flow_diff + * smootherstep( + curr_dist, + 0.0, + f64::from(OsuDifficultyObject::NORMALIZED_RADIUS), + ) } fn calc_overlap_factor<'a>( first: &'a OsuDifficultyObject<'a>, - second: &'a OsuDifficultyObject<'a> + second: &'a OsuDifficultyObject<'a>, ) -> f64 { let dist = Pos::distance(&first.base.stacked_pos(), second.base.stacked_pos()); f64::clamp( 1.0 - (f64::from(dist) - first.radius).max(0.0).powf(2.0), 0.0, - 1.0 + 1.0, ) } -} \ No newline at end of file +} diff --git a/src/osu/difficulty/evaluators/aim/mod.rs b/src/osu/difficulty/evaluators/aim/mod.rs index 3264b91c..17307ee1 100644 --- a/src/osu/difficulty/evaluators/aim/mod.rs +++ b/src/osu/difficulty/evaluators/aim/mod.rs @@ -1,3 +1,3 @@ pub mod agility; pub mod flow_aim; -pub mod snap_aim; \ No newline at end of file +pub mod snap_aim; diff --git a/src/osu/difficulty/evaluators/aim/snap_aim.rs b/src/osu/difficulty/evaluators/aim/snap_aim.rs index 827786de..4a668952 100644 --- a/src/osu/difficulty/evaluators/aim/snap_aim.rs +++ b/src/osu/difficulty/evaluators/aim/snap_aim.rs @@ -1,10 +1,10 @@ use crate::{ - any::difficulty::object::IDifficultyObject, - osu::difficulty::object::OsuDifficultyObject, + any::difficulty::object::IDifficultyObject, + osu::difficulty::object::OsuDifficultyObject, util::{ - difficulty::{milliseconds_to_bpm, reverse_lerp, smootherstep, smoothstep}, - float_ext::FloatExt - } + difficulty::{milliseconds_to_bpm, reverse_lerp, smootherstep, smoothstep}, + float_ext::FloatExt, + }, }; pub struct SnapAimEvaluator; @@ -44,7 +44,11 @@ impl SnapAimEvaluator { // * Calculate the velocity to the current hitobject, which starts // * with a base distance / time assuming the last object is a hitcircle. - let curr_dist = if with_slider_travel_dist { osu_curr_obj.lazy_jump_dist } else { osu_curr_obj.jump_dist }; + let curr_dist = if with_slider_travel_dist { + osu_curr_obj.lazy_jump_dist + } else { + osu_curr_obj.jump_dist + }; let mut curr_vel = osu_curr_obj.lazy_jump_dist / osu_curr_obj.adjusted_delta_time; // * But if the last object is a slider, then we extend the travel @@ -54,7 +58,11 @@ impl SnapAimEvaluator { curr_vel = curr_vel.max(slider_distance / osu_curr_obj.adjusted_delta_time); } - let prev_dist = if with_slider_travel_dist { osu_last_obj.lazy_jump_dist } else { osu_last_obj.jump_dist }; + let prev_dist = if with_slider_travel_dist { + osu_last_obj.lazy_jump_dist + } else { + osu_last_obj.jump_dist + }; let prev_vel = prev_dist / osu_last_obj.adjusted_delta_time; // * Start difficulty with regular velocity. @@ -69,25 +77,32 @@ impl SnapAimEvaluator { let mut acute_angle_bonus = 0.0; // * If rhythms are the same. - if osu_curr_obj.adjusted_delta_time.max(osu_last_obj.adjusted_delta_time) < - 1.25 * osu_curr_obj.adjusted_delta_time.min(osu_last_obj.adjusted_delta_time) { + if osu_curr_obj + .adjusted_delta_time + .max(osu_last_obj.adjusted_delta_time) + < 1.25 + * osu_curr_obj + .adjusted_delta_time + .min(osu_last_obj.adjusted_delta_time) + { acute_angle_bonus = Self::calc_angle_acuteness(curr_angle); - + // * Penalize angle repetition. It is important to do it _before_ // * multiplying by anything because we compare raw acuteness here. - acute_angle_bonus *= 0.08 - + 0.92 - * (1.0 - f64::min( - acute_angle_bonus, - f64::powf(Self::calc_angle_acuteness(last_angle), 3.0) - )); + acute_angle_bonus *= 0.08 + + 0.92 + * (1.0 + - f64::min( + acute_angle_bonus, + f64::powf(Self::calc_angle_acuteness(last_angle), 3.0), + )); // * Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter. - acute_angle_bonus *= vel_influence + acute_angle_bonus *= vel_influence * smootherstep( milliseconds_to_bpm(osu_curr_obj.adjusted_delta_time, Some(2)), - 300.0, - 400.0 + 300.0, + 400.0, ) * smootherstep(curr_dist, 0.0, f64::from(DIAMETER * 2)); } @@ -96,24 +111,33 @@ impl SnapAimEvaluator { // * Penalize angle repetition. It is important to do it _before_ // * multiplying by anything because we compare raw wideness here. - wide_angle_bonus *= 0.25 - + 0.75 - * (1.0 - f64::min( - wide_angle_bonus, - f64::powf(Self::calc_angle_wideness(last_angle), 3.0) - )); - + wide_angle_bonus *= 0.25 + + 0.75 + * (1.0 + - f64::min( + wide_angle_bonus, + f64::powf(Self::calc_angle_wideness(last_angle), 3.0), + )); + // * Rescaling velocity for the wide angle bonus - let mut wide_angle_curr_vel = curr_dist / - osu_curr_obj.adjusted_delta_time.powf(Self::WIDE_ANGLE_TIME_SCALE); - let wide_angle_prev_vel = prev_dist / - osu_last_obj.adjusted_delta_time.powf(Self::WIDE_ANGLE_TIME_SCALE); + let mut wide_angle_curr_vel = curr_dist + / osu_curr_obj + .adjusted_delta_time + .powf(Self::WIDE_ANGLE_TIME_SCALE); + let wide_angle_prev_vel = prev_dist + / osu_last_obj + .adjusted_delta_time + .powf(Self::WIDE_ANGLE_TIME_SCALE); if osu_last_obj.base.is_slider() && with_slider_travel_dist { let slider_dist = osu_last_obj.lazy_travel_dist + osu_curr_obj.lazy_jump_dist; wide_angle_curr_vel = f64::max( wide_angle_curr_vel, - slider_dist / f64::powf(osu_curr_obj.adjusted_delta_time, Self::WIDE_ANGLE_TIME_SCALE) + slider_dist + / f64::powf( + osu_curr_obj.adjusted_delta_time, + Self::WIDE_ANGLE_TIME_SCALE, + ), ); } @@ -123,7 +147,8 @@ impl SnapAimEvaluator { // * If objects just go back and forth through a middle point - don't give as much wide bonus. // * Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, // * so any object's angle's center point is always the previous object. - let dist = (osu_last_2_obj.base.stacked_pos() - osu_last_obj.base.stacked_pos()).length(); + let dist = + (osu_last_2_obj.base.stacked_pos() - osu_last_obj.base.stacked_pos()).length(); if dist < 1.0 { wide_angle_bonus *= 1.0 - 0.55 * (1.0 - f64::from(dist)); @@ -133,7 +158,7 @@ impl SnapAimEvaluator { // * Add in acute angle bonus or wide angle bonus, whichever is larger. snap_diff += f64::max( acute_angle_bonus * Self::ACUTE_ANGLE_MULTIPLIER, - wide_angle_bonus * Self::WIDE_ANGLE_MULTIPLIER + wide_angle_bonus * Self::WIDE_ANGLE_MULTIPLIER, ); // * Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle @@ -142,16 +167,16 @@ impl SnapAimEvaluator { * smootherstep(curr_dist, f64::from(RADIUS), f64::from(DIAMETER)) * f64::powf( reverse_lerp(curr_dist, f64::from(DIAMETER * 3), f64::from(DIAMETER)), - 1.8 + 1.8, ) * smootherstep(curr_angle, f64::to_radians(110.0), f64::to_radians(60.0)) * smootherstep(prev_dist, f64::from(RADIUS), f64::from(DIAMETER)) * f64::powf( reverse_lerp(prev_dist, f64::from(DIAMETER * 3), f64::from(DIAMETER)), - 1.8 + 1.8, ) * smootherstep(last_angle, f64::to_radians(110.0), f64::to_radians(60.0)); - + snap_diff += wiggle_bonus * Self::WIGGLE_MULTIPLIER; } @@ -165,21 +190,27 @@ impl SnapAimEvaluator { let dist_ratio = smoothstep( (prev_vel - curr_vel).abs() / prev_vel.max(curr_vel), 0.0, - 1.0 + 1.0, ); // * Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. - let overlap_vel_buff = (f64::from(DIAMETER) * 1.25 / osu_curr_obj.adjusted_delta_time - .min(osu_last_obj.adjusted_delta_time) - ).min((prev_vel - curr_vel).abs()); + let overlap_vel_buff = (f64::from(DIAMETER) * 1.25 + / osu_curr_obj + .adjusted_delta_time + .min(osu_last_obj.adjusted_delta_time)) + .min((prev_vel - curr_vel).abs()); let mut vel_change_bonus = overlap_vel_buff * dist_ratio; // * Penalize for rhythm changes. vel_change_bonus *= f64::powf( - osu_curr_obj.adjusted_delta_time.min(osu_last_obj.adjusted_delta_time) - / osu_curr_obj.adjusted_delta_time.max(osu_last_obj.adjusted_delta_time), - 2.0 + osu_curr_obj + .adjusted_delta_time + .min(osu_last_obj.adjusted_delta_time) + / osu_curr_obj + .adjusted_delta_time + .max(osu_last_obj.adjusted_delta_time), + 2.0, ); snap_diff += vel_change_bonus * Self::VELOCITY_CHANGE_MULTIPLIER; @@ -188,9 +219,11 @@ impl SnapAimEvaluator { // * Reward sliders based on velocity. if osu_curr_obj.base.is_slider() && with_slider_travel_dist { let slider_bonus = osu_curr_obj.travel_dist / osu_curr_obj.travel_time; - snap_diff += if slider_bonus.lt(&1.0) - { slider_bonus } - else { slider_bonus.powf(0.75) * Self::SLIDER_MULTIPLIER }; + snap_diff += if slider_bonus.lt(&1.0) { + slider_bonus + } else { + slider_bonus.powf(0.75) * Self::SLIDER_MULTIPLIER + }; } // * Apply high circle size bonus @@ -208,7 +241,7 @@ impl SnapAimEvaluator { fn vector_angle_repetition<'a>( curr: &'a OsuDifficultyObject<'a>, prev: &'a OsuDifficultyObject<'a>, - diff_objects: &'a [OsuDifficultyObject<'a>] + diff_objects: &'a [OsuDifficultyObject<'a>], ) -> f64 { let (Some(curr_angle), Some(prev_angle)) = (curr.angle, prev.angle) else { return 1.0; @@ -217,17 +250,20 @@ impl SnapAimEvaluator { let mut const_angle_count: f64 = 0.0; for i in 0..Self::REPETITION_NOTE_LIMIT { let Some(prev_obj) = curr.previous(i, diff_objects) else { - break + break; }; // * Only consider vectors in the same jump section, // * stopping to change rhythm ruins momentum - if curr.adjusted_delta_time.max(prev_obj.adjusted_delta_time) > - 1.1 * curr.adjusted_delta_time.min(prev_obj.adjusted_delta_time) { - break + if curr.adjusted_delta_time.max(prev_obj.adjusted_delta_time) + > 1.1 * curr.adjusted_delta_time.min(prev_obj.adjusted_delta_time) + { + break; } - if let (Some(curr_vec_angle), Some(prev_vec_angle)) = (curr.normalized_vector_angle, prev.normalized_vector_angle) { + if let (Some(curr_vec_angle), Some(prev_vec_angle)) = + (curr.normalized_vector_angle, prev.normalized_vector_angle) + { let angle_diff = curr_vec_angle - prev_vec_angle; // * Refer to this desmos for tuning, constants need to be precise // * so that values stay within the range of 0 and 1. @@ -237,11 +273,24 @@ impl SnapAimEvaluator { } let vec_repetition = (0.5 / const_angle_count).min(1.0).powf(2.0); - let stack_factor = smootherstep(curr.lazy_jump_dist, 0.0, f64::from(OsuDifficultyObject::NORMALIZED_DIAMETER)); - let angle_diff_adjusted = (2.0 * f64::to_radians(45.0).min((curr_angle - prev_angle).abs() * stack_factor)).cos(); - let base_nerf = 1.0 - Self::REPETITION_MAX_NERF * Self::calc_angle_acuteness(prev_angle) * angle_diff_adjusted; - - (base_nerf + (1.0 - base_nerf) * vec_repetition * Self::REPETITION_MAX_VECTOR_INFLUENCE * stack_factor).powf(2.0) + let stack_factor = smootherstep( + curr.lazy_jump_dist, + 0.0, + f64::from(OsuDifficultyObject::NORMALIZED_DIAMETER), + ); + let angle_diff_adjusted = + (2.0 * f64::to_radians(45.0).min((curr_angle - prev_angle).abs() * stack_factor)).cos(); + let base_nerf = 1.0 + - Self::REPETITION_MAX_NERF + * Self::calc_angle_acuteness(prev_angle) + * angle_diff_adjusted; + + (base_nerf + + (1.0 - base_nerf) + * vec_repetition + * Self::REPETITION_MAX_VECTOR_INFLUENCE + * stack_factor) + .powf(2.0) } const fn calc_angle_wideness(angle: f64) -> f64 { @@ -251,4 +300,4 @@ impl SnapAimEvaluator { pub const fn calc_angle_acuteness(angle: f64) -> f64 { smoothstep(angle, f64::to_radians(140.0), f64::to_radians(40.0)) } -} \ No newline at end of file +} diff --git a/src/osu/difficulty/evaluators/flashlight.rs b/src/osu/difficulty/evaluators/flashlight.rs index 5d80b98f..5fe6315c 100644 --- a/src/osu/difficulty/evaluators/flashlight.rs +++ b/src/osu/difficulty/evaluators/flashlight.rs @@ -1,8 +1,8 @@ use std::cmp; use crate::{ - GameMods, - any::difficulty::object::IDifficultyObject, + GameMods, + any::difficulty::object::IDifficultyObject, osu::{difficulty::object::OsuDifficultyObject, object::OsuObjectKind}, }; @@ -84,8 +84,9 @@ impl FlashlightEvaluator { self.time_fade_in, )); - flashlight_difficulty += stack_nerf * opacity_bonus * self.scaling_factor * jump_dist - / cumulative_strain_time; + flashlight_difficulty += + stack_nerf * opacity_bonus * self.scaling_factor * jump_dist + / cumulative_strain_time; if let Some((curr_obj_angle, osu_curr_angle)) = curr_obj.angle.zip(osu_curr.angle) { // * Objects further back in time should count less for the nerf. diff --git a/src/osu/difficulty/evaluators/mod.rs b/src/osu/difficulty/evaluators/mod.rs index ad1812b5..cbb49cdf 100644 --- a/src/osu/difficulty/evaluators/mod.rs +++ b/src/osu/difficulty/evaluators/mod.rs @@ -1,18 +1,11 @@ pub use self::{ - aim::{ - agility::AgilityEvaluator, - flow_aim::FlowAimEvaluator, - snap_aim::SnapAimEvaluator, - }, - speed::{ - rhythm::RhythmEvaluator, - speed::SpeedEvaluator, - }, + aim::{agility::AgilityEvaluator, flow_aim::FlowAimEvaluator, snap_aim::SnapAimEvaluator}, flashlight::FlashlightEvaluator, reading::ReadingEvaluator, + speed::{rhythm::RhythmEvaluator, speed::SpeedEvaluator}, }; mod aim; -mod speed; mod flashlight; -mod reading; \ No newline at end of file +mod reading; +mod speed; diff --git a/src/osu/difficulty/evaluators/reading.rs b/src/osu/difficulty/evaluators/reading.rs index 97749c3e..2d17391f 100644 --- a/src/osu/difficulty/evaluators/reading.rs +++ b/src/osu/difficulty/evaluators/reading.rs @@ -120,8 +120,6 @@ impl ReadingEvaluator { fn calc_hidden_difficulty<'a>( &self, curr_obj: &'a OsuDifficultyObject<'a>, - // TODO: pass this OR see comment on l#109 - // curr_obj_preempt: f64, diff_objects: &'a [OsuDifficultyObject<'a>], past_obj_difficulty_influence: f64, curr_visible_obj_density: f64, @@ -142,7 +140,6 @@ impl ReadingEvaluator { if let Some(prev_obj) = curr_obj.previous(0, diff_objects) { if curr_obj.lazy_jump_dist.eq(0.0) && curr_obj.opacity_at(prev_obj.start_time, true, self.time_preempt, self.time_fade_in).eq(0.0) - // TODO: Double check using this preempt over OsuDifficultyHitObject.Preempt && prev_obj.start_time > curr_obj.start_time - self.time_preempt { // * Perfect stacks are harder the less time between notes diff --git a/src/osu/difficulty/evaluators/speed/mod.rs b/src/osu/difficulty/evaluators/speed/mod.rs index 0e7957d1..d262abb4 100644 --- a/src/osu/difficulty/evaluators/speed/mod.rs +++ b/src/osu/difficulty/evaluators/speed/mod.rs @@ -1,2 +1,2 @@ pub mod rhythm; -pub mod speed; \ No newline at end of file +pub mod speed; diff --git a/src/osu/difficulty/evaluators/speed/rhythm.rs b/src/osu/difficulty/evaluators/speed/rhythm.rs index bd5c79c1..f17b98b3 100644 --- a/src/osu/difficulty/evaluators/speed/rhythm.rs +++ b/src/osu/difficulty/evaluators/speed/rhythm.rs @@ -1,6 +1,6 @@ use crate::{ - any::difficulty::object::IDifficultyObject, - osu::difficulty::object::OsuDifficultyObject, + any::difficulty::object::IDifficultyObject, + osu::difficulty::object::OsuDifficultyObject, util::difficulty::{logistic, reverse_lerp, smoothstep_bell_curve}, }; @@ -58,11 +58,13 @@ impl RhythmEvaluator { }; if curr_obj.base.is_spinner() { - continue + continue; } // * Scales note 0 to 1 from history to now - let time_decay = (f64::from(Self::HISTORY_TIME_MAX) - (curr.start_time - curr_obj.start_time)) / f64::from(Self::HISTORY_TIME_MAX); + let time_decay = (f64::from(Self::HISTORY_TIME_MAX) + - (curr.start_time - curr_obj.start_time)) + / f64::from(Self::HISTORY_TIME_MAX); let note_decay = ((historical_note_count - i) / historical_note_count) as f64; let curr_historical_decay = note_decay.min(time_decay); @@ -79,28 +81,35 @@ impl RhythmEvaluator { // * Calculate how much current delta difference deserves a rhythm bonus. // * this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200). - let delta_difference_ratio = prev_delta.max(curr_delta) / prev_delta.min(curr_delta); + let delta_difference_ratio = + prev_delta.max(curr_delta) / prev_delta.min(curr_delta); // * reduce ratio bonus if delta difference is too big let difference_multiplier = (2.0 - delta_difference / 8.0).clamp(0.0, 1.0); - let window_penalty = ((delta_difference - delta_difference_eps) / delta_difference_eps).clamp(0.0, 1.0); + let window_penalty = ((delta_difference - delta_difference_eps) + / delta_difference_eps) + .clamp(0.0, 1.0); - let mut effective_difficulty = Self::get_effective_diff(delta_difference_ratio) * window_penalty * difference_multiplier; + let mut effective_difficulty = Self::get_effective_diff(delta_difference_ratio) + * window_penalty + * difference_multiplier; // * If previous object is a slider it might be easier to tap since you don't have to do a whole tapping motion // * while a full deltatime might end up some weird ratio the "unpress->tap" motion might be simple // * for example a slider-circle-circle pattern should be evaluated as a regular triple and not as a single->double if prev_obj.base.is_slider() { let slider_lazy_end_delta = curr_obj.min_jump_time; - let slider_lazy_delta_diff_ratio = slider_lazy_end_delta.max(curr_delta) / slider_lazy_end_delta.min(curr_delta); + let slider_lazy_delta_diff_ratio = slider_lazy_end_delta.max(curr_delta) + / slider_lazy_end_delta.min(curr_delta); let slider_real_end_delta = curr_obj.last_obj_end_delta_time; - let slider_real_delta_diff_ratio = slider_real_end_delta.max(curr_delta) / slider_real_end_delta.min(curr_delta); + let slider_real_delta_diff_ratio = slider_real_end_delta.max(curr_delta) + / slider_real_end_delta.min(curr_delta); let slider_effective_difficulty = f64::min( Self::get_effective_diff(slider_lazy_delta_diff_ratio), - Self::get_effective_diff(slider_real_delta_diff_ratio) + Self::get_effective_diff(slider_real_delta_diff_ratio), ); effective_difficulty = slider_effective_difficulty.min(effective_difficulty); } @@ -122,7 +131,10 @@ impl RhythmEvaluator { } // * previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. - if prev_prev_obj.delta_time.max(Self::DELTA_MIN_VALUE) > prev_delta + delta_difference_eps && prev_delta > curr_delta + delta_difference_eps { + if prev_prev_obj.delta_time.max(Self::DELTA_MIN_VALUE) + > prev_delta + delta_difference_eps + && prev_delta > curr_delta + delta_difference_eps + { effective_difficulty *= 0.125; } @@ -141,8 +153,8 @@ impl RhythmEvaluator { for existing_island in islands .iter_mut() - .filter(|i| i.almost_eq(&island, delta_difference_eps)) { - + .filter(|i| i.almost_eq(&island, delta_difference_eps)) + { // * only increase island occurrences if they're going one after another if prev_island.almost_eq(&island, delta_difference_eps) { existing_island.occurrences += 1; @@ -152,11 +164,11 @@ impl RhythmEvaluator { let power = logistic(f64::from(island.delta), 58.33, 0.24, Some(2.75)); effective_difficulty *= f64::min( 3.0 / f64::from(existing_island.occurrences), - (1.0 / f64::from(existing_island.occurrences)).powf(power) + (1.0 / f64::from(existing_island.occurrences)).powf(power), ); found = true; - break + break; } if !found && island.delta_count > 0 { @@ -164,10 +176,12 @@ impl RhythmEvaluator { } // * scale down the difficulty if the object is double-tappable - effective_difficulty *= 1.0 - prev_obj.calc_double_tap_feasibility(Some(curr_obj), hit_window) * 0.75; + effective_difficulty *= 1.0 + - prev_obj.calc_double_tap_feasibility(Some(curr_obj), hit_window) * 0.75; if island.delta_count > 1 { - rhythm_complexity_sum += (effective_difficulty * start_difficulty).sqrt() * curr_historical_decay; + rhythm_complexity_sum += (effective_difficulty * start_difficulty).sqrt() + * curr_historical_decay; } else { // * constant difficulty for single-note islands rhythm_complexity_sum += 0.7 * curr_historical_decay; @@ -183,7 +197,6 @@ impl RhythmEvaluator { prev_island = island; island = RhythmIsland::new(curr_delta as i32); - } else if prev_delta > curr_delta + delta_difference_eps { // * we're speeding up // * Begin counting island until we change speed again. @@ -218,7 +231,8 @@ impl RhythmEvaluator { fn get_effective_diff(delta_diff_ratio: f64) -> f64 { // * Take only the fractional part of the value since we're only interested in punishing multiples let delta_diff_fraction = delta_diff_ratio - delta_diff_ratio.trunc(); - 1.0 + Self::RHYTHM_RATIO_DIFF_MULTIPLIER * f64::min(0.5, smoothstep_bell_curve(delta_diff_fraction)) + 1.0 + Self::RHYTHM_RATIO_DIFF_MULTIPLIER + * f64::min(0.5, smoothstep_bell_curve(delta_diff_fraction)) } } @@ -226,7 +240,7 @@ impl RhythmEvaluator { struct RhythmIsland { delta: i32, delta_count: i32, - occurrences: i32 + occurrences: i32, } impl RhythmIsland { @@ -234,7 +248,7 @@ impl RhythmIsland { Self { delta: delta.max(OsuDifficultyObject::MIN_DELTA_TIME as i32), delta_count: 1, - occurrences: 1 + occurrences: 1, } } @@ -257,6 +271,7 @@ impl RhythmIsland { } fn almost_eq(&self, other: &Self, epsilon: f64) -> bool { - f64::from((self.delta - other.delta).abs()) < epsilon && self.delta_count == other.delta_count + f64::from((self.delta - other.delta).abs()) < epsilon + && self.delta_count == other.delta_count } -} \ No newline at end of file +} diff --git a/src/osu/difficulty/gradual.rs b/src/osu/difficulty/gradual.rs index beceec2f..e050a602 100644 --- a/src/osu/difficulty/gradual.rs +++ b/src/osu/difficulty/gradual.rs @@ -5,7 +5,7 @@ use rosu_map::section::general::GameMode; use crate::{ Beatmap, Difficulty, - any::{CalculateError, difficulty::skills::StrainSkill}, + any::CalculateError, model::mode::ConvertError, osu::{ convert::convert_objects, @@ -148,7 +148,14 @@ fn new(difficulty: Difficulty, map: &Beatmap) -> OsuGradualDifficulty { let great_hit_window = map_attrs.hit_windows().od_great.unwrap_or(0.0); - let skills = OsuSkills::new(mods, &scaling_factor, great_hit_window, time_preempt, diff_objects.len(), attrs.od()); + let skills = OsuSkills::new( + mods, + &scaling_factor, + great_hit_window, + time_preempt, + diff_objects.len(), + attrs.od(), + ); let diff_objects = extend_lifetime(diff_objects.into_boxed_slice()); let score_simulator = GradualLegacyScoreSimulator::new(map, map_attrs); @@ -215,7 +222,7 @@ impl Iterator for OsuGradualDifficulty { let mut attrs = self.attrs.clone(); - DifficultyValues::eval(&mut attrs, self.difficulty.get_mods(), &self.skills); + DifficultyValues::eval(&mut attrs, self.difficulty.get_mods(), &mut self.skills); Some(attrs) } diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index e2b3cc94..e59ddbe4 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -1,26 +1,32 @@ use std::{cmp, pin::Pin}; use rosu_map::section::general::GameMode; -use skills::{aim::Aim, flashlight::Flashlight, speed::Speed, strain::OsuStrainSkill}; +use skills::{aim::Aim, flashlight::Flashlight, speed::Speed}; use crate::{ Beatmap, any::{ CalculateError, - difficulty::{Difficulty, skills::StrainSkill}, + difficulty::{ + Difficulty, + skills_new::{ + harmonic_skill::HarmonicSkill, + variable_length_strain_skill::VariableLengthStrainSkill, + }, + }, }, model::{beatmap::BeatmapAttributes, mode::ConvertError, mods::GameMods}, osu::{ convert::{convert_objects, prepare_map}, difficulty::{ - object::OsuDifficultyObject, rating::OsuRatingCalculator, - scaling_factor::ScalingFactor, skills::strain::count_top_weighted_sliders, + object::OsuDifficultyObject, scaling_factor::ScalingFactor, skills::reading::Reading, }, legacy_score_simulator::OsuLegacyScoreSimulator, object::OsuObject, - performance::PERFORMANCE_BASE_MULTIPLIER, + performance::{PERFORMANCE_BASE_MULTIPLIER, PERFORMANCE_NORM_EXPONENT}, utils::legacy_score::NestedScorePerObject, }, + util::difficulty::norm, }; use self::skills::OsuSkills; @@ -63,14 +69,14 @@ fn calculate_difficulty(difficulty: &Difficulty, map: &Beatmap) -> OsuDifficulty let DifficultyValues { osu_objects, - skills, + mut skills, mut attrs, } = DifficultyValues::calculate(difficulty, map); let mods = difficulty.get_mods(); let passed_objects = difficulty.get_passed_objects(); - DifficultyValues::eval(&mut attrs, mods, &skills); + DifficultyValues::eval(&mut attrs, mods, &mut skills); let mut simulator = OsuLegacyScoreSimulator::new(&osu_objects, map, passed_objects); @@ -184,27 +190,30 @@ impl DifficultyValues { } /// Process the difficulty values and store the results in `attrs`. - pub fn eval(attrs: &mut OsuDifficultyAttributes, mods: &GameMods, skills: &OsuSkills) { + pub fn eval(attrs: &mut OsuDifficultyAttributes, mods: &GameMods, skills: &mut OsuSkills) { let OsuSkills { aim, aim_no_sliders, speed, flashlight, + reading, } = skills; let aim_difficulty_value = aim.cloned_difficulty_value(); + let aim_no_sliders_difficulty_value = aim_no_sliders.cloned_difficulty_value(); + let speed_difficulty_value = speed.cloned_difficulty_value(); + let reading_difficulty_value = reading.cloned_difficulty_value(); let aim_difficult_strain_count = aim.count_top_weighted_strains(aim_difficulty_value); + let speed_difficult_strain_count = + speed.count_top_weighted_object_difficulties(speed_difficulty_value); + let reading_difficult_note_count = + reading.count_top_weighted_object_difficulties(reading_difficulty_value); - let difficult_sliders = aim.get_difficult_sliders(); - - let aim_no_sliders_difficulty_value = aim_no_sliders.cloned_difficulty_value(); - - let aim_no_sliders_top_weighted_slider_count = count_top_weighted_sliders( - aim_no_sliders.slider_strains(), - aim_no_sliders_difficulty_value, - ); + let speed_notes = speed.relevant_object_count(); + let aim_no_sliders_top_weighted_slider_count = + aim_no_sliders.count_top_weighted_sliders(aim_no_sliders_difficulty_value); let aim_no_sliders_difficult_strain_count = aim_no_sliders.count_top_weighted_strains(aim_no_sliders_difficulty_value); @@ -212,53 +221,48 @@ impl DifficultyValues { / (aim_no_sliders_difficult_strain_count - aim_no_sliders_top_weighted_slider_count) .max(1.0); - let slider_factor = if aim_difficulty_value > 0.0 { - OsuRatingCalculator::calculate_difficulty_rating(aim_no_sliders_difficulty_value) - / OsuRatingCalculator::calculate_difficulty_rating(aim_difficulty_value) - } else { - 1.0 - }; - - let speed_difficulty_value = speed.cloned_difficulty_value(); let speed_top_weighted_slider_count = - count_top_weighted_sliders(speed.slider_strains(), speed_difficulty_value); - - let speed_difficult_strain_count = speed.count_top_weighted_strains(speed_difficulty_value); - + speed.count_top_weighted_sliders(speed_difficulty_value); let speed_top_weighted_slider_factor = speed_top_weighted_slider_count / (speed_difficult_strain_count - speed_top_weighted_slider_count).max(1.0); - let mechanical_difficulty_rating = - calculate_mechanical_difficulty_rating(aim_difficulty_value, speed_difficulty_value); + let difficult_sliders = aim.get_difficult_sliders(); - let osu_rating_calculator = OsuRatingCalculator::new( - mods, - attrs.n_objects(), - attrs.ar, - attrs.od(), - mechanical_difficulty_rating, - slider_factor, - ); + let aim_rating = calculate_aim_difficulty_rating(aim_difficulty_value); + let aim_no_sliders_rating = + calculate_aim_difficulty_rating(aim_no_sliders_difficulty_value); - let aim_rating = osu_rating_calculator.compute_aim_rating(aim_difficulty_value); - let speed_rating = osu_rating_calculator.compute_speed_rating(speed_difficulty_value); + let slider_factor = if aim_difficulty_value > 0.0 { + aim_no_sliders_rating / aim_rating + } else { + 1.0 + }; + + let speed_rating = calculate_difficulty_rating(speed_difficulty_value); + let reading_rating = calculate_difficulty_rating(reading_difficulty_value); let flashlight_rating = if mods.fl() { let flashlight_difficulty_value = flashlight.cloned_difficulty_value(); - - osu_rating_calculator.compute_flashlight_rating(flashlight_difficulty_value) + calculate_difficulty_rating(flashlight_difficulty_value) } else { 0.0 }; let base_aim_performance = Aim::difficulty_to_performance(aim_rating); let base_speed_performance = Speed::difficulty_to_performance(speed_rating); + let base_reading_performance = Reading::difficulty_to_performance(reading_rating); let base_flashlight_performance = Flashlight::difficulty_to_performance(flashlight_rating); - - 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 base_cognition_performance = + sum_cognition_difficulty(base_reading_performance, base_flashlight_performance); + + let base_performance = norm( + PERFORMANCE_NORM_EXPONENT, + [ + base_aim_performance, + base_speed_performance, + base_cognition_performance, + ], + ); let star_rating = calculate_star_rating(base_performance); @@ -266,13 +270,15 @@ impl DifficultyValues { attrs.aim_difficult_slider_count = difficult_sliders; attrs.speed = speed_rating; attrs.flashlight = flashlight_rating; + attrs.reading = reading_rating; attrs.slider_factor = slider_factor; attrs.aim_top_weighted_slider_factor = aim_top_weighted_slider_factor; attrs.speed_top_weighted_slider_factor = speed_top_weighted_slider_factor; attrs.aim_difficult_strain_count = aim_difficult_strain_count; attrs.speed_difficult_strain_count = speed_difficult_strain_count; attrs.stars = star_rating; - attrs.speed_note_count = speed.relevant_note_count(); + attrs.speed_note_count = speed_notes; + attrs.reading_note_count = reading_difficult_note_count; } pub fn create_difficulty_objects<'a>( @@ -323,28 +329,30 @@ impl DifficultyValues { } } -fn calculate_mechanical_difficulty_rating( - aim_difficulty_value: f64, - speed_difficulty_value: f64, -) -> f64 { - let aim_value = Aim::difficulty_to_performance( - OsuRatingCalculator::calculate_difficulty_rating(aim_difficulty_value), - ); - let speed_value = Speed::difficulty_to_performance( - OsuRatingCalculator::calculate_difficulty_rating(speed_difficulty_value), - ); +fn sum_cognition_difficulty(reading: f64, flashlight: f64) -> f64 { + if reading <= 0.0 { + return flashlight; + } - let total_value = (aim_value.powf(1.1) + speed_value.powf(1.1)).powf(1.0 / 1.1); + if flashlight <= 0.0 { + return reading; + } - calculate_star_rating(total_value) + // * Nerf flashlight value in cognition sum when reading is greater than flashlight + norm( + PERFORMANCE_NORM_EXPONENT, + [reading, flashlight, (flashlight / reading).clamp(0.25, 1.0)], + ) } -fn calculate_star_rating(base_performance: f64) -> f64 { - if base_performance <= 0.00001 { - return 0.0; - } +fn calculate_aim_difficulty_rating(difficulty_value: f64) -> f64 { + difficulty_value.powf(0.63) * 0.02275 +} + +fn calculate_difficulty_rating(difficulty_value: f64) -> f64 { + difficulty_value.sqrt() * 0.0675 +} - PERFORMANCE_BASE_MULTIPLIER.cbrt() - * STAR_RATING_MULTIPLIER - * ((100_000.0 / 2.0_f64.powf(1.0 / 1.1) * base_performance).cbrt() + 4.0) +fn calculate_star_rating(base_performance: f64) -> f64 { + f64::cbrt(base_performance * PERFORMANCE_BASE_MULTIPLIER) } diff --git a/src/osu/difficulty/skills/aim.rs b/src/osu/difficulty/skills/aim.rs index e6d947b8..27b9eb42 100644 --- a/src/osu/difficulty/skills/aim.rs +++ b/src/osu/difficulty/skills/aim.rs @@ -273,6 +273,10 @@ impl Aim { reduced } + + pub fn difficulty_to_performance(difficulty: f64) -> f64 { + 4.0 * difficulty.powf(3.0) + } } fn aim_difficulty_value( diff --git a/src/osu/difficulty/skills/flashlight.rs b/src/osu/difficulty/skills/flashlight.rs index f4457819..f8dc4804 100644 --- a/src/osu/difficulty/skills/flashlight.rs +++ b/src/osu/difficulty/skills/flashlight.rs @@ -2,15 +2,9 @@ use crate::{ GameMods, any::difficulty::{ object::{HasStartTime, IDifficultyObject}, - skills_new::{ - strain_decay_base, - strain_skill::NewStrainSkill, - }, - }, - osu::difficulty::{ - evaluators::FlashlightEvaluator, - object::OsuDifficultyObject + skills_new::{strain_decay_base, strain_skill::NewStrainSkill}, }, + osu::difficulty::{evaluators::FlashlightEvaluator, object::OsuDifficultyObject}, util::difficulty::reverse_lerp, }; @@ -38,7 +32,7 @@ define_new_skill! { impl Flashlight { const SKILL_MULTIPLIER: f64 = 0.058; - + fn strain_decay(ms: f64) -> f64 { strain_decay_base(ms, 0.15) } @@ -66,13 +60,14 @@ impl Flashlight { } self.current_strain *= Self::strain_decay(curr.delta_time); - self.current_strain += self.calculate_adjusted_difficulty(curr, objects) * Self::SKILL_MULTIPLIER; - + self.current_strain += + self.calculate_adjusted_difficulty(curr, objects) * Self::SKILL_MULTIPLIER; + self.current_strain } fn calculate_adjusted_difficulty( - &self, + &self, curr: &OsuDifficultyObject<'_>, objects: &[OsuDifficultyObject<'_>], ) -> f64 { @@ -114,7 +109,7 @@ impl Flashlight { self.skill_strain_peaks, self.skill_current_section_peak, ), - self.total_objects + self.total_objects, ) } @@ -123,21 +118,24 @@ impl Flashlight { Self::get_current_strain_peaks( self.skill_strain_peaks.clone(), self.skill_current_section_peak, - ), - self.total_objects + ), + self.total_objects, ) } + + pub fn difficulty_to_performance(difficulty: f64) -> f64 { + 25.0 * difficulty.powf(2.0) + } } fn flashlight_difficulty_value(current_strain_peaks: Vec, total_objects: i32) -> f64 { let sum: f64 = current_strain_peaks.iter().sum(); sum * 0.7 - + 0.1 - * (f64::from(total_objects) / 200.0).min(1.0) + + 0.1 * (f64::from(total_objects) / 200.0).min(1.0) + (if total_objects > 200 { 0.2 * f64::from((total_objects - 200).min(1) / 200) } else { 0.0 }) -} \ No newline at end of file +} diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index 296c58af..e94831c3 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -1,7 +1,7 @@ use crate::any::difficulty::skills_new::skill::Skill; use crate::{model::mods::GameMods, osu::object::OsuObject}; -use self::{aim::Aim, flashlight::Flashlight, speed::Speed}; +use self::{aim::Aim, flashlight::Flashlight, reading::Reading, speed::Speed}; use super::{ HD_FADE_IN_DURATION_MULTIPLIER, object::OsuDifficultyObject, scaling_factor::ScalingFactor, @@ -11,13 +11,14 @@ pub mod aim; pub mod flashlight; pub mod reading; pub mod speed; -pub mod strain; +mod strain; pub struct OsuSkills { pub aim: Aim, pub aim_no_sliders: Aim, pub speed: Speed, pub flashlight: Flashlight, + pub reading: Reading, } impl OsuSkills { @@ -56,12 +57,20 @@ impl OsuSkills { time_preempt, time_fade_in, ); + let reading = Reading::new( + mods, + time_preempt, + time_fade_in, + hit_window, + overall_difficulty, + ); Self { aim, aim_no_sliders, speed, flashlight, + reading, } } @@ -70,5 +79,6 @@ impl OsuSkills { self.aim_no_sliders.process(curr, objects); self.speed.process(curr, objects); self.flashlight.process(curr, objects); + self.reading.process(curr, objects); } } diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs index 9307a89d..4a6ec45c 100644 --- a/src/osu/difficulty/skills/reading.rs +++ b/src/osu/difficulty/skills/reading.rs @@ -11,10 +11,22 @@ define_new_skill! { reduced_note_count: usize = 0, reduced_duration: Option = None, mods: GameMods, - reading_evaluator: ReadingEvaluator, + evaluator: ReadingEvaluator, hit_window: f64, overall_difficulty: f64, } + + pub fn new(mods: &GameMods, time_preempt: f64, time_fade_in: f64, hit_window: f64, overall_difficulty: f64) -> Self { + Self { + current_strain: 0.0, + reduced_note_count: 0, + reduced_duration: None, + mods: mods.clone(), + evaluator: ReadingEvaluator { time_preempt, time_fade_in }, + hit_window: hit_window, + overall_difficulty: overall_difficulty, + } + } } impl Reading { @@ -58,7 +70,7 @@ impl Reading { objects: &'a [OsuDifficultyObject<'a>], ) -> f64 { let mut difficulty = self - .reading_evaluator + .evaluator .evaluate_diff_of(curr, objects, self.mods.hd()); if self.mods.td() { diff --git a/src/osu/performance/calculator.rs b/src/osu/performance/calculator.rs index e0d05946..8dab739a 100644 --- a/src/osu/performance/calculator.rs +++ b/src/osu/performance/calculator.rs @@ -6,7 +6,7 @@ use crate::{ OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState, difficulty::{ rating::OsuRatingCalculator, - skills::{aim::Aim, flashlight::Flashlight, speed::Speed, strain::OsuStrainSkill}, + skills::{aim::Aim, flashlight::Flashlight, speed::Speed}, }, legacy_score_miss_calc::OsuLegacyScoreMissCalculator, }, @@ -17,7 +17,8 @@ use crate::{ }; // * This is being adjusted to keep the final pp value scaled around what it used to be when changing things. -pub const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14; +pub const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.12; +pub const PERFORMANCE_NORM_EXPONENT: f64 = 1.1; pub(super) struct OsuPerformanceCalculator<'mods> { attrs: OsuDifficultyAttributes, diff --git a/src/osu/performance/mod.rs b/src/osu/performance/mod.rs index 6888c652..3b750f1a 100644 --- a/src/osu/performance/mod.rs +++ b/src/osu/performance/mod.rs @@ -4,7 +4,10 @@ use rosu_map::section::general::GameMode; use self::calculator::OsuPerformanceCalculator; -pub use self::{calculator::PERFORMANCE_BASE_MULTIPLIER, inspect::InspectOsuPerformance}; +pub use self::{ + calculator::{PERFORMANCE_BASE_MULTIPLIER, PERFORMANCE_NORM_EXPONENT}, + inspect::InspectOsuPerformance, +}; use crate::{ Beatmap, From 01ae6ab8f50796d65f0e486b3daa4de359b6eda1 Mon Sep 17 00:00:00 2001 From: myssto Date: Fri, 31 Jul 2026 11:35:43 -0500 Subject: [PATCH 12/27] feat: update osu performance calculator --- src/osu/difficulty/mod.rs | 2 +- src/osu/performance/calculator.rs | 175 ++++++++++++++++-------------- src/osu/strains.rs | 8 +- 3 files changed, 95 insertions(+), 90 deletions(-) diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index e59ddbe4..dcf1d6b2 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -329,7 +329,7 @@ impl DifficultyValues { } } -fn sum_cognition_difficulty(reading: f64, flashlight: f64) -> f64 { +pub fn sum_cognition_difficulty(reading: f64, flashlight: f64) -> f64 { if reading <= 0.0 { return flashlight; } diff --git a/src/osu/performance/calculator.rs b/src/osu/performance/calculator.rs index 8dab739a..4c00699b 100644 --- a/src/osu/performance/calculator.rs +++ b/src/osu/performance/calculator.rs @@ -2,16 +2,17 @@ use std::{cmp, f64::consts::PI}; use crate::{ GameMods, + any::difficulty::skills_new::harmonic_skill::HarmonicSkill, osu::{ OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState, difficulty::{ - rating::OsuRatingCalculator, - skills::{aim::Aim, flashlight::Flashlight, speed::Speed}, + skills::{aim::Aim, flashlight::Flashlight, reading::Reading, speed::Speed}, + sum_cognition_difficulty, }, legacy_score_miss_calc::OsuLegacyScoreMissCalculator, }, util::{ - difficulty::{erf, erf_inv, logistic, reverse_lerp, smoothstep}, + difficulty::{erf, erf_inv, logistic, norm, reverse_lerp, smoothstep}, float_ext::FloatExt, }, }; @@ -79,6 +80,7 @@ impl OsuPerformanceCalculator<'_> { effective_miss_count = effective_miss_count.max(f64::from(state.hitresults.misses)); effective_miss_count = effective_miss_count.min(f64::from(state.hitresults.total_hits())); + effective_miss_count = effective_miss_count.max(0.0); let total_hits = f64::from(total_hits); @@ -128,22 +130,24 @@ impl OsuPerformanceCalculator<'_> { &mut speed_estimated_slider_breaks, ); let acc_value = self.compute_accuracy_value(); + + let reading_value = self.compute_reading_value(effective_miss_count); let flashlight_value = self.compute_flashlight_value(effective_miss_count); + let cognition_value = sum_cognition_difficulty(reading_value, flashlight_value); - let pp = (aim_value.powf(1.1) - + speed_value.powf(1.1) - + acc_value.powf(1.1) - + flashlight_value.powf(1.1)) - .powf(1.0 / 1.1) - * multiplier; + let pp = norm( + PERFORMANCE_NORM_EXPONENT, + [aim_value, speed_value, acc_value, cognition_value], + ) * multiplier; OsuPerformanceAttributes { difficulty: self.attrs, + pp, pp_acc: acc_value, pp_aim: aim_value, pp_flashlight: flashlight_value, pp_speed: speed_value, - pp, + pp_reading: reading_value, effective_miss_count, speed_deviation, combo_based_estimated_miss_count, @@ -204,17 +208,12 @@ impl OsuPerformanceCalculator<'_> { let total_hits = self.total_hits(); let len_bonus = 0.95 - + 0.4 * (total_hits / 2000.0).min(1.0) + + 0.35 * (total_hits / 2000.0).min(1.0) + f64::from(u8::from(total_hits > 2000.0)) * (total_hits / 2000.0).log10() * 0.5; aim_value *= len_bonus; if effective_miss_count > 0.0 { - *aim_estimated_slider_breaks = self.calculate_estimated_slider_breaks( - self.attrs.aim_top_weighted_slider_factor, - effective_miss_count, - ); - let relevant_miss_count = (effective_miss_count + *aim_estimated_slider_breaks) .min(self.total_imperfect_hits() + f64::from(self.n_large_tick_miss())); @@ -232,13 +231,7 @@ impl OsuPerformanceCalculator<'_> { * self.acc.powf(16.0)) * (1.0 - 0.003 * self.attrs.hp * self.attrs.hp); } else if self.mods.tc() { - aim_value *= 1.0 - + OsuRatingCalculator::calculate_visibility_bonus( - self.mods, - self.attrs.ar, - Some(self.attrs.slider_factor), - None, - ); + aim_value *= 1.0 + self.calculate_traceable_bonus(self.attrs.slider_factor); } aim_value *= self.acc; @@ -258,20 +251,7 @@ impl OsuPerformanceCalculator<'_> { let mut speed_value = Speed::difficulty_to_performance(self.attrs.speed); - let total_hits = self.total_hits(); - - let len_bonus = 0.95 - + 0.4 * (total_hits / 2000.0).min(1.0) - + f64::from(u8::from(total_hits > 2000.0)) * (total_hits / 2000.0).log10() * 0.5; - - speed_value *= len_bonus; - if effective_miss_count > 0.0 { - *speed_estimated_slider_breaks = self.calculate_estimated_slider_breaks( - self.attrs.speed_top_weighted_slider_factor, - effective_miss_count, - ); - let relevant_miss_count = (effective_miss_count + *speed_estimated_slider_breaks) .min(self.total_imperfect_hits() + f64::from(self.n_large_tick_miss())); @@ -281,46 +261,24 @@ impl OsuPerformanceCalculator<'_> { ); } - // * TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen. if self.mods.bl() { // * 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.tc() { - speed_value *= 1.0 - + OsuRatingCalculator::calculate_visibility_bonus( - self.mods, - self.attrs.ar, - None, - None, - ); } let speed_high_deviation_mult = self.calculate_speed_high_deviation_nerf(speed_deviation); speed_value *= speed_high_deviation_mult; - // * Calculate accuracy assuming the worst case scenario - let relevant_total_diff = f64::max(0.0, total_hits - self.attrs.speed_note_count); - let hitresults = &self.state.hitresults; - let relevant_n300 = (f64::from(hitresults.n300) - relevant_total_diff).max(0.0); - let relevant_n100 = (f64::from(hitresults.n100) - - (relevant_total_diff - f64::from(hitresults.n300)).max(0.0)) - .max(0.0); - let relevant_n50 = (f64::from(hitresults.n50) - - (relevant_total_diff - f64::from(hitresults.n300 + hitresults.n100)).max(0.0)) - .max(0.0); - - let relevant_acc = if self.attrs.speed_note_count.eq(0.0) { - 0.0 - } else { - (relevant_n300 * 6.0 + relevant_n100 * 2.0 + relevant_n50) - / (self.attrs.speed_note_count * 6.0) - }; + // * An effective hit window is created based on the speed SR. The higher the speed difficulty, the shorter the hit window. + // * For example, a speed SR of 4.0 leads to an effective hit window of 20ms, which is OD 10. + let effective_hit_window = 20.0 * (4.0 / self.attrs.speed).powf(0.35); - let od = self.attrs.od(); + // * Find the proportion of 300s on speed notes assuming the hit window was the effective hit window. + let effective_acc = erf(effective_hit_window / speed_deviation); - // * Scale the speed value with accuracy and OD. - speed_value *= f64::powf((self.acc + relevant_acc) / 2.0, (14.5 - od) / 2.0); + // * Scale speed value by normalized accuracy. + speed_value *= effective_acc.powf(2.0); speed_value } @@ -366,23 +324,21 @@ impl OsuPerformanceCalculator<'_> { 1.52163_f64.powf(self.attrs.od()) * better_acc_percentage.powf(24.0) * 2.83; // * Bonus for many hitcircles - it's harder to keep good accuracy up for longer. - acc_value *= (f64::from(amount_hit_objects_with_acc) / 1000.0) - .powf(0.3) - .min(1.15); + acc_value *= if amount_hit_objects_with_acc < 1000 { + (f64::from(amount_hit_objects_with_acc) / 1000.0).powf(0.3) + } else { + (f64::from(amount_hit_objects_with_acc) / 1000.0).powf(0.1) + }; // * Increasing the accuracy value by object count for Blinds isn't // * ideal, so the minimum buff is given. if self.mods.bl() { acc_value *= 1.14; - } else if self.mods.hd() || self.mods.tc() { + } else if self.mods.tc() { // * Decrease bonus for AR > 10 acc_value *= 1.0 + 0.08 * reverse_lerp(self.attrs.ar, 11.5, 10.0); } - if self.mods.fl() { - acc_value *= 1.02; - } - acc_value } @@ -410,6 +366,20 @@ impl OsuPerformanceCalculator<'_> { flashlight_value } + fn compute_reading_value(&self, effective_miss_count: f64) -> f64 { + let mut reading_value = Reading::difficulty_to_performance(self.attrs.reading); + + if effective_miss_count > 0.0 { + reading_value *= + Self::calculate_miss_penalty(effective_miss_count, self.attrs.reading_note_count); + } + + // * Scale the reading value with accuracy _harshly_. + reading_value *= self.acc.powf(3.0); + + reading_value + } + fn calculate_combo_based_estimated_miss_count(&self) -> f64 { let Self { state, @@ -425,10 +395,16 @@ impl OsuPerformanceCalculator<'_> { let mut miss_count = f64::from(state.hitresults.misses); if *using_classic_slider_acc { + // * If sliders in the map are hard - it's likely for player to drop sliderends + // * If map has easy sliders - it's more likely for player to sliderbreak + let likely_missed_sliderend_portion = + 0.04 + 0.06 * attrs.aim_top_weighted_slider_factor.min(1.0).powf(2.0); + // * Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it - // * In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map - let full_combo_threshold = - f64::from(attrs.max_combo) - 0.1 * f64::from(attrs.n_sliders); + // * In classic scores we can't know the amount of dropped sliders so we estimate it + let full_combo_threshold = f64::from(attrs.max_combo) + - (4.0 + likely_missed_sliderend_portion * f64::from(attrs.n_sliders)) + .min(f64::from(attrs.n_sliders)); if f64::from(state.max_combo) < full_combo_threshold { miss_count = full_combo_threshold / f64::from(state.max_combo).max(1.0); @@ -479,22 +455,28 @@ impl OsuPerformanceCalculator<'_> { .. } = self; - if !using_classic_slider_acc || state.hitresults.n100 == 0 { + let non_miss_mistakes = state.hitresults.n100 + state.hitresults.n50; + + if !using_classic_slider_acc || non_miss_mistakes == 0 { return 0.0; } let missed_combo_percent = 1.0 - f64::from(state.max_combo) / f64::from(attrs.max_combo); - let mut estimated_slider_breaks = (effective_miss_count * top_weighted_slider_factor) - .min(f64::from(state.hitresults.n100)); + let mut estimated_slider_breaks = + f64::from(non_miss_mistakes).min(effective_miss_count * top_weighted_slider_factor); - // * Scores with more Oks are more likely to have slider breaks. - let ok_adjustment = ((f64::from(state.hitresults.n100) - estimated_slider_breaks) + 0.5) - / f64::from(state.hitresults.n100); + // * Scores with more Oks and Mehs are more likely to have slider breaks. + // * We add an arbitrary value to both sides of the division to make it more stable on extreme ends. + let non_miss_mistake_adjustment = (f64::from(non_miss_mistakes) - estimated_slider_breaks + + 4.5) + / f64::from(non_miss_mistakes + 4); // * There is a low probability of extra slider breaks on effective miss counts close to 1, as score based calculations are good at indicating if only a single break occurred. estimated_slider_breaks *= smoothstep(effective_miss_count, 1.0, 2.0); - estimated_slider_breaks * ok_adjustment * logistic(missed_combo_percent, 0.33, 15.0, None) + estimated_slider_breaks + * non_miss_mistake_adjustment + * logistic(missed_combo_percent, 0.33, 15.0, None) } fn calculate_speed_deviation(&self) -> Option { @@ -616,11 +598,36 @@ impl OsuPerformanceCalculator<'_> { adjusted_speed_value / speed_value } + fn calculate_traceable_bonus(&self, slider_factor: f64) -> f64 { + // * We want to reward slider aim less, more so at lower AR + let high_ar_slider_visibility_factor = 0.5 + (slider_factor.powf(6.0) / 2.0); + let low_ar_slider_visibility_factor = slider_factor.powf(6.0); + + // * Start from normal curve, rewarding lower AR up to AR7 + let mut traceable_bonus = 0.0275; + traceable_bonus += + 0.025 * (12.0 - self.attrs.ar.max(7.0)) * high_ar_slider_visibility_factor; + + // * For AR up to 0 - reduce reward for very low ARs when object is visible + if self.attrs.ar < 7.0 { + traceable_bonus += + 0.025 * (7.0 - self.attrs.ar.max(0.0)) * low_ar_slider_visibility_factor; + } + + // * Starting from AR0 - cap values so they won't grow to infinity + if self.attrs.ar < 0.0 { + traceable_bonus += + 0.025 * (1.0 - f64::powf(1.5, self.attrs.ar)) * low_ar_slider_visibility_factor; + } + + traceable_bonus + } + // * Miss penalty assumes that a player will miss on the hardest parts of a map, // * so we use the amount of relatively difficult sections to adjust miss penalty // * to make it more punishing on maps with lower amount of hard sections. fn calculate_miss_penalty(miss_count: f64, diff_strain_count: f64) -> f64 { - 0.96 / ((miss_count / (4.0 * diff_strain_count.ln().powf(0.94))) + 1.0) + 0.93 / (miss_count / (4.0 * diff_strain_count.max(1.0).ln()) + 1.0) } fn get_combo_scaling_factor(&self) -> f64 { diff --git a/src/osu/strains.rs b/src/osu/strains.rs index 881646dc..4707ecc8 100644 --- a/src/osu/strains.rs +++ b/src/osu/strains.rs @@ -1,10 +1,7 @@ use crate::{ Beatmap, Difficulty, - any::difficulty::{ - skills::StrainSkill, - skills_new::{ - strain_skill::NewStrainSkill, variable_length_strain_skill::VariableLengthStrainSkill, - }, + any::difficulty::skills_new::{ + strain_skill::NewStrainSkill, variable_length_strain_skill::VariableLengthStrainSkill, }, model::mode::ConvertError, osu::convert::prepare_map, @@ -43,6 +40,7 @@ pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result Date: Fri, 31 Jul 2026 11:47:53 -0500 Subject: [PATCH 13/27] fix: update OsuStrains with new skills --- src/any/difficulty/skills_new/harmonic_skill.rs | 2 ++ src/osu/strains.rs | 8 ++++++-- src/util/macros_new.rs | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/any/difficulty/skills_new/harmonic_skill.rs b/src/any/difficulty/skills_new/harmonic_skill.rs index 6b14fff9..d01462d7 100644 --- a/src/any/difficulty/skills_new/harmonic_skill.rs +++ b/src/any/difficulty/skills_new/harmonic_skill.rs @@ -26,6 +26,8 @@ pub trait HarmonicSkill: Skill { difficulties } + fn into_transformed_difficulties(self) -> Vec; + fn difficulty_value( transformed_object_difficulties: Vec, object_weight_sum: &mut f64, diff --git a/src/osu/strains.rs b/src/osu/strains.rs index 4707ecc8..d743d122 100644 --- a/src/osu/strains.rs +++ b/src/osu/strains.rs @@ -1,7 +1,8 @@ use crate::{ Beatmap, Difficulty, any::difficulty::skills_new::{ - strain_skill::NewStrainSkill, variable_length_strain_skill::VariableLengthStrainSkill, + harmonic_skill::HarmonicSkill, strain_skill::NewStrainSkill, + variable_length_strain_skill::VariableLengthStrainSkill, }, model::mode::ConvertError, osu::convert::prepare_map, @@ -22,6 +23,8 @@ pub struct OsuStrains { pub speed: Vec, /// Strain peaks of the flashlight skill. pub flashlight: Vec, + /// Strain peaks of the reading skill. + pub reading: Vec, } impl OsuStrains { @@ -56,7 +59,8 @@ pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result>(), - speed: speed.into_current_strain_peaks(), + speed: speed.into_transformed_difficulties(), flashlight: flashlight.into_current_strain_peaks(), + reading: reading.into_transformed_difficulties(), }) } diff --git a/src/util/macros_new.rs b/src/util/macros_new.rs index 55f4fbb5..63ccb260 100644 --- a/src/util/macros_new.rs +++ b/src/util/macros_new.rs @@ -606,6 +606,10 @@ macro_rules! define_new_skill { todo!() } + fn into_transformed_difficulties(self) -> Vec { + self.get_transformed_difficulties(self.get_object_difficulties().to_vec()) + } + fn difficulty_value( transformed_object_difficulties: Vec, object_weight_sum: &mut f64, From ffd9966fd6e6098229e736575bbee3bf097ac2c6 Mon Sep 17 00:00:00 2001 From: myssto Date: Fri, 31 Jul 2026 13:04:05 -0500 Subject: [PATCH 14/27] chore: formatting and fixing warnings --- .../difficulty/skills_new/harmonic_skill.rs | 13 +- src/any/difficulty/skills_new/strain_skill.rs | 6 +- .../variable_length_strain_skill.rs | 4 +- src/osu/difficulty/evaluators/reading.rs | 18 +- src/osu/difficulty/evaluators/speed/mod.rs | 1 + src/osu/difficulty/evaluators/speed/rhythm.rs | 19 +- src/osu/difficulty/gradual.rs | 4 +- src/osu/difficulty/mod.rs | 3 - src/osu/difficulty/rating.rs | 265 ------------------ src/osu/difficulty/skills/aim.rs | 25 +- src/osu/difficulty/skills/flashlight.rs | 13 +- src/osu/difficulty/skills/mod.rs | 8 +- src/osu/difficulty/skills/reading.rs | 10 +- src/osu/difficulty/skills/speed.rs | 1 + src/osu/performance/calculator.rs | 17 +- src/osu/performance/gradual.rs | 8 +- .../hitresult_generator/closest.rs | 24 +- 17 files changed, 94 insertions(+), 345 deletions(-) delete mode 100644 src/osu/difficulty/rating.rs diff --git a/src/any/difficulty/skills_new/harmonic_skill.rs b/src/any/difficulty/skills_new/harmonic_skill.rs index d01462d7..a7771eeb 100644 --- a/src/any/difficulty/skills_new/harmonic_skill.rs +++ b/src/any/difficulty/skills_new/harmonic_skill.rs @@ -33,6 +33,7 @@ pub trait HarmonicSkill: Skill { object_weight_sum: &mut f64, ) -> f64; + #[expect(dead_code, reason = "staying in-sync with existing skills")] fn into_difficulty_value(self) -> f64; fn cloned_difficulty_value(&mut self) -> f64; @@ -57,25 +58,23 @@ pub fn harmonic_skill_difficulty_value( } let mut difficulty = 0.0; - let mut index = 0; // * Objects with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // * These objects will not contribute to the difficulty. - for obj in transformed_object_difficulties + for (index, obj) in transformed_object_difficulties .to_vec() .cs_order_descending() .cs_where(|v| *v > 0.0) + .iter() + .enumerate() { // * Use a harmonic sum that considers each object of the map according to a predefined weight. - let weight = (1.0 + (harmonic_scale / f64::from(1 + index))) - / (f64::from(index).powf(decay_exponent) - + 1.0 - + (harmonic_scale / f64::from(1 + index))); + let weight = (1.0 + (harmonic_scale / (1 + index) as f64)) + / ((index as f64).powf(decay_exponent) + 1.0 + (harmonic_scale / (1 + index) as f64)); *object_weight_sum += weight; difficulty += obj * weight; - index += 1; } difficulty diff --git a/src/any/difficulty/skills_new/strain_skill.rs b/src/any/difficulty/skills_new/strain_skill.rs index 8d34d372..1d746ea7 100644 --- a/src/any/difficulty/skills_new/strain_skill.rs +++ b/src/any/difficulty/skills_new/strain_skill.rs @@ -1,5 +1,5 @@ use crate::{ - any::difficulty::skills_new::skill::Skill, + any::difficulty::skills_new::skill::Skill, util::traits::{IEnumerable, IOrderedEnumerable}, }; @@ -13,6 +13,7 @@ pub trait NewStrainSkill: Skill { objects: &Self::DifficultyObjects<'a>, ) -> f64; + #[expect(dead_code, reason = "used by process_internal")] fn strain_value_at<'a>( &mut self, curr: &Self::DifficultyObject<'a>, @@ -30,6 +31,7 @@ pub trait NewStrainSkill: Skill { objects: &Self::DifficultyObjects<'a>, ); + #[expect(dead_code, reason = "used by start_new_section_from")] fn calculate_initial_strain<'a>( &self, time: f64, @@ -68,4 +70,4 @@ pub fn strain_skill_difficulty_value(current_strain_peaks: Vec, decay_weigh } difficulty -} \ No newline at end of file +} diff --git a/src/any/difficulty/skills_new/variable_length_strain_skill.rs b/src/any/difficulty/skills_new/variable_length_strain_skill.rs index 41a91baa..777a4871 100644 --- a/src/any/difficulty/skills_new/variable_length_strain_skill.rs +++ b/src/any/difficulty/skills_new/variable_length_strain_skill.rs @@ -11,6 +11,7 @@ pub trait VariableLengthStrainSkill: Skill { objects: &Self::DifficultyObjects<'a>, ) -> f64; + #[expect(dead_code, reason = "used by process_internal")] fn strain_value_at<'a>( &mut self, curr: &Self::DifficultyObject<'a>, @@ -32,6 +33,7 @@ pub trait VariableLengthStrainSkill: Skill { objects: &Self::DifficultyObjects<'a>, ); + #[expect(dead_code, reason = "used by start_new_section_from")] fn calculate_initial_strain<'a>( &self, time: f64, @@ -72,7 +74,7 @@ impl StrainPeak { pub const fn new(value: f64, section_length: f64) -> Self { Self { value, - section_length: section_length.round(), + section_length: section_length.round_ties_even(), } } } diff --git a/src/osu/difficulty/evaluators/reading.rs b/src/osu/difficulty/evaluators/reading.rs index 2d17391f..2bc18fd0 100644 --- a/src/osu/difficulty/evaluators/reading.rs +++ b/src/osu/difficulty/evaluators/reading.rs @@ -29,6 +29,13 @@ impl ReadingEvaluator { const MINIMUM_ANGLE_RELEVANCY_TIME: f64 = 2000.0; // * 2 seconds const MAXIMUM_ANGLE_RELEVANCY_TIME: f64 = 200.0; + pub const fn new(time_preempt: f64, time_fade_in: f64) -> Self { + Self { + time_preempt, + time_fade_in, + } + } + pub fn evaluate_diff_of<'a>( &self, curr: &'a OsuDifficultyObject<'a>, @@ -137,15 +144,14 @@ impl ReadingEvaluator { // * Apply a soft cap to general HD reading to account for partial memorization hidden_difficulty = hidden_difficulty.powf(0.4) * Self::HIDDEN_MULTIPLIER; - if let Some(prev_obj) = curr_obj.previous(0, diff_objects) { - if curr_obj.lazy_jump_dist.eq(0.0) - && curr_obj.opacity_at(prev_obj.start_time, true, self.time_preempt, self.time_fade_in).eq(0.0) - && prev_obj.start_time > curr_obj.start_time - self.time_preempt { + if let Some(prev_obj) = curr_obj.previous(0, diff_objects) + && FloatExt::eq(curr_obj.lazy_jump_dist, 0.0) + && FloatExt::eq(curr_obj.opacity_at(prev_obj.start_time, true, self.time_preempt, self.time_fade_in), 0.0) + && prev_obj.start_time > curr_obj.start_time - self.time_preempt { // * Perfect stacks are harder the less time between notes hidden_difficulty += Self::HIDDEN_MULTIPLIER * 2500.0 / curr_obj.adjusted_delta_time.powf(1.5); } - } hidden_difficulty } @@ -220,7 +226,7 @@ impl ReadingEvaluator { let mut angle_diff_alternating = f64::consts::PI; if let (Some(loop_obj_prev0_angle), Some(loop_obj_prev1_angle), Some(loop_obj_prev2_angle)) - = (loop_obj_prev0.angle, loop_obj_prev1.map_or(None, |o| o.angle), loop_obj_prev2.map_or(None, |o| o.angle)) { + = (loop_obj_prev0.angle, loop_obj_prev1.and_then(|o| o.angle), loop_obj_prev2.and_then(|o| o.angle)) { angle_diff_alternating = (loop_obj_prev1_angle - loop_obj_angle).abs(); angle_diff_alternating += (loop_obj_prev2_angle - loop_obj_prev1_angle).abs(); diff --git a/src/osu/difficulty/evaluators/speed/mod.rs b/src/osu/difficulty/evaluators/speed/mod.rs index d262abb4..08588fc1 100644 --- a/src/osu/difficulty/evaluators/speed/mod.rs +++ b/src/osu/difficulty/evaluators/speed/mod.rs @@ -1,2 +1,3 @@ pub mod rhythm; +#[expect(clippy::module_inception, reason = "what else should I name it?")] pub mod speed; diff --git a/src/osu/difficulty/evaluators/speed/rhythm.rs b/src/osu/difficulty/evaluators/speed/rhythm.rs index f17b98b3..ac8612fc 100644 --- a/src/osu/difficulty/evaluators/speed/rhythm.rs +++ b/src/osu/difficulty/evaluators/speed/rhythm.rs @@ -9,11 +9,11 @@ pub struct RhythmEvaluator; impl RhythmEvaluator { const HISTORY_TIME_MAX: u32 = 5 * 1000; // 5 seconds const HISTORY_OBJECTS_MAX: usize = 32; - const RHYTHM_OVERALL_MULTIPLIER: f64 = 1.0; - const RHYTHM_RATIO_MULTIPLIER: f64 = 15.0; + const RHYTHM_OVERALL_MULTIPLIER: f64 = 0.95; const RHYTHM_RATIO_DIFF_MULTIPLIER: f64 = 26.0; const DELTA_MIN_VALUE: f64 = 1e-7; + #[expect(clippy::too_many_lines, reason = "staying in-sync with lazer")] pub fn evaluate_diff_of<'a>( curr: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>], @@ -33,16 +33,16 @@ impl RhythmEvaluator { // * Store the difficulty of the current start of an island to buff for tighter rhythms. let mut start_difficulty = 0.0; let mut first_delta_switch = false; - let historical_note_count = curr.idx.max(Self::HISTORY_OBJECTS_MAX); + let historical_note_count = std::cmp::max(curr.idx, Self::HISTORY_OBJECTS_MAX); let mut rhythm_start = 0; while curr .previous(rhythm_start, diff_objects) - .filter(|prev| { + .as_ref() + .is_some_and(|prev| { rhythm_start + 2 < historical_note_count && curr.start_time - prev.start_time < f64::from(Self::HISTORY_TIME_MAX) }) - .is_some() { rhythm_start += 1; } @@ -151,9 +151,9 @@ impl RhythmEvaluator { let mut found = false; - for existing_island in islands + if let Some(existing_island) = islands .iter_mut() - .filter(|i| i.almost_eq(&island, delta_difference_eps)) + .find(|i| i.almost_eq(&island, delta_difference_eps)) { // * only increase island occurrences if they're going one after another if prev_island.almost_eq(&island, delta_difference_eps) { @@ -168,7 +168,6 @@ impl RhythmEvaluator { ); found = true; - break; } if !found && island.delta_count > 0 { @@ -246,7 +245,7 @@ struct RhythmIsland { impl RhythmIsland { pub fn new(delta: i32) -> Self { Self { - delta: delta.max(OsuDifficultyObject::MIN_DELTA_TIME as i32), + delta: std::cmp::max(delta, OsuDifficultyObject::MIN_DELTA_TIME as i32), delta_count: 1, occurrences: 1, } @@ -254,7 +253,7 @@ impl RhythmIsland { fn add_delta(&mut self, delta: i32) { if delta == i32::MAX { - self.delta = delta.max(OsuDifficultyObject::MIN_DELTA_TIME as i32); + self.delta = std::cmp::max(delta, OsuDifficultyObject::MIN_DELTA_TIME as i32); } self.delta_count += 1; diff --git a/src/osu/difficulty/gradual.rs b/src/osu/difficulty/gradual.rs index e050a602..8bf59517 100644 --- a/src/osu/difficulty/gradual.rs +++ b/src/osu/difficulty/gradual.rs @@ -337,8 +337,8 @@ mod tests { for i in 1.. { let Some(next_gradual) = gradual.next() else { assert_eq!(i, hit_objects_len + 1); - assert!(gradual_2nd.last().is_some() || hit_objects_len % 2 == 0); - assert!(gradual_3rd.last().is_some() || hit_objects_len % 3 == 0); + assert!(gradual_2nd.last().is_some() || hit_objects_len.is_multiple_of(2)); + assert!(gradual_3rd.last().is_some() || hit_objects_len.is_multiple_of(3)); break; }; diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index dcf1d6b2..87227f7a 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -36,12 +36,9 @@ use super::attributes::OsuDifficultyAttributes; mod evaluators; pub mod gradual; mod object; -pub mod rating; pub mod scaling_factor; pub mod skills; -const STAR_RATING_MULTIPLIER: f64 = 0.0265; - const HD_FADE_IN_DURATION_MULTIPLIER: f64 = 0.4; const HD_FADE_OUT_DURATION_MULTIPLIER: f64 = 0.3; diff --git a/src/osu/difficulty/rating.rs b/src/osu/difficulty/rating.rs deleted file mode 100644 index da9bdd4a..00000000 --- a/src/osu/difficulty/rating.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::convert::identity; - -use crate::{ - GameMods, - util::{difficulty::reverse_lerp, float_ext::FloatExt}, -}; - -pub struct OsuRatingCalculator<'mods> { - mods: &'mods GameMods, - total_hits: u32, - approach_rate: f64, - overall_difficulty: f64, - mechanical_difficulty_rating: f64, - slider_factor: f64, -} - -const DIFFICULTY_MULTIPLIER: f64 = 0.0675; - -impl<'mods> OsuRatingCalculator<'mods> { - pub const fn new( - mods: &'mods GameMods, - total_hits: u32, - approach_rate: f64, - overall_difficulty: f64, - mechanical_difficulty_rating: f64, - slider_factor: f64, - ) -> Self { - Self { - mods, - total_hits, - approach_rate, - overall_difficulty, - mechanical_difficulty_rating, - slider_factor, - } - } -} - -impl OsuRatingCalculator<'_> { - pub fn compute_aim_rating(&self, aim_difficulty_value: f64) -> f64 { - if self.mods.ap() { - return 0.0; - } - - let mut aim_rating = Self::calculate_difficulty_rating(aim_difficulty_value); - - if self.mods.td() { - aim_rating = aim_rating.powf(0.8); - } - - if self.mods.rx() { - aim_rating *= 0.9; - } - - if let Some(magnetised_strength) = self.mods.attraction_strength() { - aim_rating *= 1.0 - magnetised_strength; - } - - let mut rating_multiplier = 1.0; - - let ar_length_bonus = 0.95 - + 0.4 * (f64::from(self.total_hits) / 2000.0).min(1.0) - + f64::from(u8::from(self.total_hits > 2000)) - * (f64::from(self.total_hits) / 2000.0).log10() - * 0.5; - - let ar_factor = if self.mods.rx() { - 0.0 - } else if self.approach_rate > 10.33 { - 0.3 * (self.approach_rate - 10.33) - } else if self.approach_rate < 8.0 { - 0.05 * (8.0 - self.approach_rate) - } else { - 0.0 - }; - - // * Buff for longer maps with high AR. - rating_multiplier += ar_factor * ar_length_bonus; - - if self.mods.hd() { - let visibility_factor = Self::calculate_aim_visibility_factor( - self.mechanical_difficulty_rating, - self.approach_rate, - ); - - rating_multiplier += Self::calculate_visibility_bonus( - self.mods, - self.approach_rate, - Some(visibility_factor), - Some(self.slider_factor), - ); - } - - // * It is important to consider accuracy difficulty when scaling with accuracy. - rating_multiplier *= 0.98 + self.overall_difficulty.max(0.0).powf(2.0) / 2500.0; - - aim_rating * rating_multiplier.cbrt() - } - - pub fn compute_speed_rating(&self, speed_difficulty_value: f64) -> f64 { - if self.mods.rx() { - return 0.0; - } - - let mut speed_rating = Self::calculate_difficulty_rating(speed_difficulty_value); - - if self.mods.ap() { - speed_rating *= 0.5; - } - - if let Some(magnetised_strength) = self.mods.attraction_strength() { - // * reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x - speed_rating *= 1.0 - magnetised_strength * 0.3; - } - - let mut rating_multiplier = 1.0; - - let ar_length_bonus = 0.95 - + 0.4 * (f64::from(self.total_hits) / 2000.0).min(1.0) - + f64::from(u8::from(self.total_hits > 2000)) - * (f64::from(self.total_hits) / 2000.0).log10() - * 0.5; - - let ar_factor = if self.mods.ap() { - 0.0 - } else if self.approach_rate > 10.33 { - 0.3 * (self.approach_rate - 10.33) - } else { - 0.0 - }; - - // * Buff for longer maps with high AR. - rating_multiplier += ar_factor * ar_length_bonus; - - if self.mods.hd() { - let visibility_factor = Self::calculate_speed_visibility_factor( - self.mechanical_difficulty_rating, - self.approach_rate, - ); - - rating_multiplier += Self::calculate_visibility_bonus( - self.mods, - self.approach_rate, - Some(visibility_factor), - None, - ); - } - - rating_multiplier *= 0.95 + self.overall_difficulty.max(0.0).powf(2.0) / 750.0; - - speed_rating * rating_multiplier.cbrt() - } - - pub fn compute_flashlight_rating(&self, flashlight_difficulty_value: f64) -> f64 { - if !self.mods.fl() { - return 0.0; - } - - let mut flashlight_rating = Self::calculate_difficulty_rating(flashlight_difficulty_value); - - if self.mods.td() { - flashlight_rating = flashlight_rating.powf(0.8); - } - - if self.mods.rx() { - flashlight_rating *= 0.7; - } else if self.mods.ap() { - flashlight_rating *= 0.4; - } - - if let Some(magnetised_strength) = self.mods.attraction_strength() { - flashlight_rating *= 1.0 - magnetised_strength; - } - - if let Some(deflate_initial_scale) = self.mods.deflate_start_scale() { - flashlight_rating *= reverse_lerp(deflate_initial_scale, 11.0, 1.0).clamp(0.1, 1.0); - } - - let mut rating_multiplier = 1.0; - - // * Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. - rating_multiplier *= 0.7 - + 0.1 * (f64::from(self.total_hits) / 200.0).min(1.0) - + f64::from(u8::from(self.total_hits > 200)) - * 0.2 - * (f64::from(self.total_hits.saturating_sub(200)) / 200.0).min(1.0); - - // * It is important to consider accuracy difficulty when scaling with accuracy. - rating_multiplier *= 0.98 + self.overall_difficulty.max(0.0).powf(2.0) / 2500.0; - - flashlight_rating * rating_multiplier.sqrt() - } - - pub fn calculate_visibility_bonus( - mods: &GameMods, - approach_rate: f64, - visibility_factor: Option, - slider_factor: Option, - ) -> f64 { - // * NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side. - let is_always_partially_visible = - mods.hd_only_fade_approach_circles().is_some_and(identity) || mods.tc(); - - // * Start from normal curve, rewarding lower AR up to AR7 - let mut reading_bonus = 0.04 * (12.0 - approach_rate.max(7.0)); - - reading_bonus *= visibility_factor.unwrap_or(1.0); - - // * We want to reward slideraim on low AR less - let slider_visibility_factor = slider_factor.unwrap_or(1.0).powf(3.0); - - // * For AR up to 0 - reduce reward for very low ARs when object is visible - if approach_rate < 7.0 { - let factor = if is_always_partially_visible { - 0.03 - } else { - 0.045 - }; - - reading_bonus += factor * (7.0 - approach_rate.max(0.0)) * slider_visibility_factor; - } - - // * Starting from AR0 - cap values so they won't grow to infinity - if approach_rate < 0.0 { - let factor = if is_always_partially_visible { - 0.075 - } else { - 0.1 - }; - - reading_bonus += - factor * (1.0 - 1.5_f64.powf(approach_rate)) * slider_visibility_factor; - } - - reading_bonus - } - - pub fn calculate_difficulty_rating(difficulty_value: f64) -> f64 { - difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER - } - - fn calculate_aim_visibility_factor( - mechanical_difficulty_rating: f64, - approach_rate: f64, - ) -> f64 { - const AR_FACTOR_END_POINT: f64 = 11.5; - - let mechanical_difficulty_factor = reverse_lerp(mechanical_difficulty_rating, 5.0, 10.0); - let ar_factor_starting_point = FloatExt::lerp(9.0, 10.33, mechanical_difficulty_factor); - - reverse_lerp(approach_rate, AR_FACTOR_END_POINT, ar_factor_starting_point) - } - - fn calculate_speed_visibility_factor( - mechanical_difficulty_rating: f64, - approach_rate: f64, - ) -> f64 { - const AR_FACTOR_END_POINT: f64 = 11.5; - - let mechanical_difficulty_factor = reverse_lerp(mechanical_difficulty_rating, 5.0, 10.0); - let ar_factor_starting_point = FloatExt::lerp(10.0, 10.33, mechanical_difficulty_factor); - - reverse_lerp(approach_rate, AR_FACTOR_END_POINT, ar_factor_starting_point) - } -} diff --git a/src/osu/difficulty/skills/aim.rs b/src/osu/difficulty/skills/aim.rs index 27b9eb42..6260112b 100644 --- a/src/osu/difficulty/skills/aim.rs +++ b/src/osu/difficulty/skills/aim.rs @@ -39,18 +39,15 @@ impl Aim { const SKILL_MULTIPLIER_TOTAL: f64 = 1.12; const COMBINED_SNAP_NORM_EXPONENT: f64 = 1.2; - const SKILL_MULTIPLIER: f64 = 26.0; - const STRAIN_DECAY_BASE: f64 = 0.2; - fn strain_decay(ms: f64) -> f64 { strain_decay_base(ms, 0.15) } - fn calculate_initial_strain( + fn calculate_initial_strain<'a>( &mut self, time: f64, - curr: &OsuDifficultyObject<'_>, - objects: &[OsuDifficultyObject<'_>], + curr: &OsuDifficultyObject<'a>, + objects: &[OsuDifficultyObject<'a>], ) -> f64 { let prev_start_time = curr .previous(0, objects) @@ -59,10 +56,10 @@ impl Aim { self.current_strain * Self::strain_decay(time - prev_start_time) } - fn strain_value_at( + fn strain_value_at<'a>( &mut self, - curr: &OsuDifficultyObject<'_>, - objects: &[OsuDifficultyObject<'_>], + curr: &OsuDifficultyObject<'a>, + objects: &[OsuDifficultyObject<'a>], ) -> f64 { if self.mods.ap() { return 0.0; @@ -80,10 +77,10 @@ impl Aim { self.current_strain } - fn calculate_adjusted_difficulty( + fn calculate_adjusted_difficulty<'a>( &self, - curr: &OsuDifficultyObject<'_>, - objects: &[OsuDifficultyObject<'_>], + curr: &OsuDifficultyObject<'a>, + objects: &[OsuDifficultyObject<'a>], ) -> f64 { let snap_difficulty = SnapAimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) @@ -142,9 +139,8 @@ impl Aim { } let total_difficulty = combined_snap_difficulty * p_snap * flow_difficulty_new * p_flow; - let total_strain = total_difficulty * Self::SKILL_MULTIPLIER_TOTAL; - total_strain + total_difficulty * Self::SKILL_MULTIPLIER_TOTAL } fn calculate_snap_flow_probability(ratio: f64) -> f64 { @@ -205,6 +201,7 @@ impl Aim { ) } + #[expect(dead_code, reason = "overwrites macro impl")] pub fn into_difficulty_value(self) -> f64 { Self::difficulty_value(Self::get_reduced_strain_peaks( Self::get_current_strain_peaks( diff --git a/src/osu/difficulty/skills/flashlight.rs b/src/osu/difficulty/skills/flashlight.rs index f8dc4804..90df9a8a 100644 --- a/src/osu/difficulty/skills/flashlight.rs +++ b/src/osu/difficulty/skills/flashlight.rs @@ -40,8 +40,8 @@ impl Flashlight { fn calculate_initial_strain<'a>( &self, time: f64, - curr: &OsuDifficultyObject<'_>, - objects: &[OsuDifficultyObject<'_>], + curr: &OsuDifficultyObject<'a>, + objects: &[OsuDifficultyObject<'a>], ) -> f64 { let prev_start_time = curr .previous(0, objects) @@ -52,8 +52,8 @@ impl Flashlight { fn strain_value_at<'a>( &mut self, - curr: &OsuDifficultyObject<'_>, - objects: &[OsuDifficultyObject<'_>], + curr: &OsuDifficultyObject<'a>, + objects: &[OsuDifficultyObject<'a>], ) -> f64 { if !self.mods.fl() { return 0.0; @@ -103,6 +103,7 @@ impl Flashlight { // and requires `self.total_objects`. Since the static method isn't ever used as far as I can tell, it can remain default for now. // As long as `into_difficulty_value` and `cloned_difficulty_value` are correct it should not affect elsewhere. + #[expect(dead_code, reason = "overwrites macro impl")] fn into_difficulty_value(self) -> f64 { flashlight_difficulty_value( Self::get_current_strain_peaks( @@ -129,12 +130,12 @@ impl Flashlight { } fn flashlight_difficulty_value(current_strain_peaks: Vec, total_objects: i32) -> f64 { - let sum: f64 = current_strain_peaks.iter().sum(); + let sum: f64 = current_strain_peaks.into_iter().sum(); sum * 0.7 + 0.1 * (f64::from(total_objects) / 200.0).min(1.0) + (if total_objects > 200 { - 0.2 * f64::from((total_objects - 200).min(1) / 200) + 0.2 * f64::from(std::cmp::min(total_objects - 200, 1) / 200) } else { 0.0 }) diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index e94831c3..7e31592f 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -57,13 +57,7 @@ impl OsuSkills { time_preempt, time_fade_in, ); - let reading = Reading::new( - mods, - time_preempt, - time_fade_in, - hit_window, - overall_difficulty, - ); + let reading = Reading::new(mods, time_preempt, time_fade_in, overall_difficulty); Self { aim, diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs index 4a6ec45c..4258eaaa 100644 --- a/src/osu/difficulty/skills/reading.rs +++ b/src/osu/difficulty/skills/reading.rs @@ -12,18 +12,16 @@ define_new_skill! { reduced_duration: Option = None, mods: GameMods, evaluator: ReadingEvaluator, - hit_window: f64, overall_difficulty: f64, } - pub fn new(mods: &GameMods, time_preempt: f64, time_fade_in: f64, hit_window: f64, overall_difficulty: f64) -> Self { + pub fn new(mods: &GameMods, time_preempt: f64, time_fade_in: f64, overall_difficulty: f64) -> Self { Self { current_strain: 0.0, reduced_note_count: 0, reduced_duration: None, mods: mods.clone(), - evaluator: ReadingEvaluator { time_preempt, time_fade_in }, - hit_window: hit_window, + evaluator: ReadingEvaluator::new(time_preempt, time_fade_in), overall_difficulty: overall_difficulty, } } @@ -38,6 +36,7 @@ impl Reading { strain_decay_base(ms, 0.8) } + #[expect(dead_code, reason = "used by process_internal")] fn object_difficulty_of<'a>( &mut self, curr: &'a OsuDifficultyObject<'a>, @@ -97,7 +96,7 @@ impl Reading { fn get_transformed_difficulties(&self, mut difficulties: Vec) -> Vec { difficulties.retain(|v| *v > 0.0); - let count = difficulties.len().min(self.reduced_note_count); + let count = std::cmp::min(difficulties.len(), self.reduced_note_count); for (i, difficulty) in difficulties.iter_mut().take(count).enumerate() { let scale = lerp( 1.0, @@ -112,6 +111,7 @@ impl Reading { difficulties } + #[expect(dead_code, reason = "overwrites macro impl")] fn count_top_weighted_object_difficulties(&self, difficulty_value: f64) -> f64 { count_top_weighted_object_difficulties( difficulty_value, diff --git a/src/osu/difficulty/skills/speed.rs b/src/osu/difficulty/skills/speed.rs index e8161887..01919807 100644 --- a/src/osu/difficulty/skills/speed.rs +++ b/src/osu/difficulty/skills/speed.rs @@ -26,6 +26,7 @@ impl Speed { strain_decay_base(ms, 0.3) } + #[expect(dead_code, reason = "used by process_internal")] fn object_difficulty_of<'a>( &mut self, curr: &'a OsuDifficultyObject<'a>, diff --git a/src/osu/performance/calculator.rs b/src/osu/performance/calculator.rs index 4c00699b..2351a2d5 100644 --- a/src/osu/performance/calculator.rs +++ b/src/osu/performance/calculator.rs @@ -82,6 +82,20 @@ impl OsuPerformanceCalculator<'_> { effective_miss_count = effective_miss_count.min(f64::from(state.hitresults.total_hits())); effective_miss_count = effective_miss_count.max(0.0); + let mut aim_estimated_slider_breaks = 0.0; + let mut speed_estimated_slider_breaks = 0.0; + + if effective_miss_count > 0.0 { + aim_estimated_slider_breaks = self.calculate_estimated_slider_breaks( + self.attrs.aim_top_weighted_slider_factor, + effective_miss_count, + ); + speed_estimated_slider_breaks = self.calculate_estimated_slider_breaks( + self.attrs.speed_top_weighted_slider_factor, + effective_miss_count, + ); + } + let total_hits = f64::from(total_hits); let mut multiplier = PERFORMANCE_BASE_MULTIPLIER; @@ -119,9 +133,6 @@ impl OsuPerformanceCalculator<'_> { let speed_deviation = self.calculate_speed_deviation(); - let mut aim_estimated_slider_breaks = 0.0; - let mut speed_estimated_slider_breaks = 0.0; - let aim_value = self.compute_aim_value(effective_miss_count, &mut aim_estimated_slider_breaks); let speed_value = self.compute_speed_value( diff --git a/src/osu/performance/gradual.rs b/src/osu/performance/gradual.rs index 5e990d7a..31f2581f 100644 --- a/src/osu/performance/gradual.rs +++ b/src/osu/performance/gradual.rs @@ -164,8 +164,12 @@ mod tests { let Some(next_gradual) = gradual.next(state.clone()) else { assert_eq!(i, hit_objects_len + 1); - assert!(gradual_2nd.last(state.clone()).is_some() || hit_objects_len % 2 == 0); - assert!(gradual_3rd.last(state.clone()).is_some() || hit_objects_len % 3 == 0); + assert!( + gradual_2nd.last(state.clone()).is_some() || hit_objects_len.is_multiple_of(2) + ); + assert!( + gradual_3rd.last(state.clone()).is_some() || hit_objects_len.is_multiple_of(3) + ); break; }; diff --git a/src/osu/performance/hitresult_generator/closest.rs b/src/osu/performance/hitresult_generator/closest.rs index 624229b0..ece534d3 100644 --- a/src/osu/performance/hitresult_generator/closest.rs +++ b/src/osu/performance/hitresult_generator/closest.rs @@ -284,22 +284,22 @@ mod tests { let n50 = remain - n300 - n100; // Skip if any provided constraints are violated - if let Some(expected_n300) = inspect.n300 { - if n300 != expected_n300 { - continue; - } + if let Some(expected_n300) = inspect.n300 + && n300 != expected_n300 + { + continue; } - if let Some(expected_n100) = inspect.n100 { - if n100 != expected_n100 { - continue; - } + if let Some(expected_n100) = inspect.n100 + && n100 != expected_n100 + { + continue; } - if let Some(expected_n50) = inspect.n50 { - if n50 != expected_n50 { - continue; - } + if let Some(expected_n50) = inspect.n50 + && n50 != expected_n50 + { + continue; } let candidate = OsuHitResults { From 0e65cecdde9fad41129803bb9d7ee63ca613fd4a Mon Sep 17 00:00:00 2001 From: myssto Date: Fri, 31 Jul 2026 20:46:51 -0500 Subject: [PATCH 15/27] fix: starting debug --- resources/801165.osu | 1768 +++++++++++++++++ src/osu/difficulty/evaluators/aim/snap_aim.rs | 2 +- src/osu/difficulty/object.rs | 63 +- src/osu/difficulty/skills/reading.rs | 2 +- src/osu/difficulty/skills/speed.rs | 1 - src/osu/object.rs | 6 +- src/util/macros_new.rs | 9 +- tests/difficulty.rs | 362 ++-- 8 files changed, 2009 insertions(+), 204 deletions(-) create mode 100644 resources/801165.osu diff --git a/resources/801165.osu b/resources/801165.osu new file mode 100644 index 00000000..7357bb1d --- /dev/null +++ b/resources/801165.osu @@ -0,0 +1,1768 @@ +osu file format v14 + +[General] +AudioLeadIn: 0 +PreviewTime: 231110 +Countdown: 0 +SampleSet: Normal +StackLeniency: 0.2 +Mode: 0 +LetterboxInBreaks: 0 +WidescreenStoryboard: 1 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:8 +ApproachRate:9.3 +SliderMultiplier:2 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +2,14204,16450 +2,117537,127450 +2,188649,195006 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +5338,444.444444444444,4,2,0,50,1,0 +6338,-100,4,2,7,60,0,0 +7115,-100,4,2,7,50,0,0 +13671,-100,4,2,7,60,0,0 +17560,-100,4,2,8,60,0,0 +17782,-100,4,2,7,60,0,0 +18226,-100,4,2,8,60,0,0 +21114,-83.3333333333333,4,2,8,60,0,0 +21337,-100,4,2,8,60,0,0 +22893,-83.3333333333333,4,2,8,60,0,0 +23448,-100,4,2,8,60,0,0 +25337,-66.6666666666667,4,2,8,60,0,0 +25670,-100,4,2,8,60,0,0 +31559,-55.5555555555556,4,2,8,60,0,0 +44448,-100,4,2,8,50,0,0 +44893,-100,4,2,8,55,0,0 +45337,-100,4,2,7,60,0,0 +45670,-200,4,2,8,60,0,0 +46226,-66.6666666666667,4,2,8,65,0,0 +58670,-66.6666666666667,4,2,8,55,0,0 +59115,-66.6666666666667,4,2,8,60,0,0 +59559,-66.6666666666667,4,2,8,65,0,0 +60226,-100,4,2,8,60,0,0 +60893,-100,4,2,8,55,0,0 +68004,-66.6666666666667,4,2,8,65,0,0 +73337,-100,4,2,8,55,0,0 +73781,-100,4,2,8,55,0,0 +73893,-100,4,2,7,55,0,0 +74004,-100,4,2,8,55,0,0 +74115,-100,4,2,7,55,0,0 +74226,-100,4,2,8,55,0,0 +74337,-100,4,2,7,55,0,0 +74448,-100,4,2,8,55,0,0 +82893,-76.9230769230769,4,2,8,50,0,0 +85115,-76.9230769230769,4,2,0,50,0,0 +85337,-100,4,2,8,60,0,0 +85893,-100,4,2,7,60,0,0 +86226,-100,4,2,8,60,0,0 +88893,-58.8235294117647,4,1,8,70,0,1 +102226,-58.8235294117647,4,2,8,70,0,1 +103115,-58.8235294117647,4,1,8,70,0,1 +115337,-50,4,1,8,70,0,1 +115670,-50,4,2,7,70,0,1 +116004,-76.9230769230769,4,2,8,70,0,1 +116559,-76.9230769230769,4,2,7,60,0,1 +117337,-58.8235294117647,4,2,8,70,0,0 +128004,-100,4,2,8,65,0,0 +129781,-100,4,2,8,55,0,0 +130226,-100,4,2,8,60,0,0 +130670,-100,4,2,8,65,0,0 +131337,-100,4,2,8,60,0,0 +132004,-100,4,2,8,55,0,0 +145004,-100,4,2,7,55,0,0 +145115,-100,4,2,8,55,0,0 +145226,-100,4,2,7,55,0,0 +145337,-100,4,2,8,55,0,0 +145448,-100,4,2,7,55,0,0 +145559,-100,4,2,8,55,0,0 +156226,-100,4,2,0,50,0,0 +156448,-100,4,2,8,60,0,0 +157004,-100,4,2,7,60,0,0 +157337,-100,4,2,8,60,0,0 +160004,-58.8235294117647,4,1,8,70,0,1 +186781,-58.8235294117647,4,2,7,70,0,1 +187115,-58.8235294117647,4,1,8,70,0,1 +187670,-58.8235294117647,4,1,7,70,0,1 +188449,-58.8235294117647,4,2,8,70,0,0 +195559,-100,4,2,7,40,0,0 +195670,-100,4,2,8,40,0,0 +202670,-66.6666666666667,4,2,8,70,0,0 +203115,-100,4,2,8,60,0,0 +215559,-100,4,2,7,60,0,0 +216893,-83.3333333333333,4,2,8,60,0,0 +228115,-83.3333333333333,4,2,7,60,0,0 +228226,-83.3333333333333,4,2,8,60,0,0 +228337,-83.3333333333333,4,2,7,60,0,0 +228448,-83.3333333333333,4,2,8,60,0,0 +228781,-83.3333333333333,4,2,7,60,0,0 +228893,-83.3333333333333,4,2,8,60,0,0 +229893,-83.3333333333333,4,2,7,60,0,0 +230004,-83.3333333333333,4,2,8,60,0,0 +231115,-166.666666666667,4,2,8,60,0,0 +238226,-166.666666666667,4,2,7,60,0,0 +238893,-166.666666666667,4,2,8,60,0,0 +245004,-166.666666666667,4,2,7,60,0,0 +245115,-166.666666666667,4,2,8,60,0,0 +245337,-100,4,2,8,60,0,0 +257893,-100,4,2,7,60,0,0 +258226,-100,4,2,8,60,0,0 +258337,-100,4,2,7,60,0,0 +258448,-100,4,2,8,60,0,0 +258559,-100,4,2,7,60,0,0 +258670,-100,4,2,8,60,0,0 +259560,-58.8235294117647,4,1,8,70,0,1 +273448,-58.8235294117647,4,2,7,70,0,1 +273559,-58.8235294117647,4,1,8,70,0,1 +286337,-58.8235294117647,4,2,7,70,0,1 +286670,-58.8235294117647,4,2,8,70,0,1 +286781,-58.8235294117647,4,2,7,70,0,1 +286893,-58.8235294117647,4,2,8,70,0,1 +287226,-58.8235294117647,4,1,7,70,0,1 +288004,-66.6666666666667,4,2,8,65,0,0 +295559,-50,4,2,8,65,0,0 +295893,-66.6666666666667,4,2,8,65,0,0 +298448,-66.6666666666667,4,2,0,65,0,0 +298559,-66.6666666666667,4,2,8,65,0,0 +312670,-66.6666666666667,4,2,0,65,0,0 +312781,-66.6666666666667,4,2,8,65,0,0 +313448,-66.6666666666667,4,2,7,65,0,0 +313781,-66.6666666666667,4,2,8,65,0,0 +315337,-66.6666666666667,4,2,7,65,0,0 +315559,-66.6666666666667,4,2,8,65,0,0 +316448,-100,4,2,8,60,0,0 +330559,-100,4,2,7,60,0,0 + + +[Colours] +Combo1 : 139,159,214 +Combo2 : 128,128,255 +Combo3 : 227,227,227 + +[HitObjects] +378,163,6337,6,4,P|427:159|468:188,2,100,8|8|8,3:2|3:2|3:2,0:0:0:0: +320,312,7115,2,0,L|332:360,1,50,6|2,0:0|0:0,0:0:0:0: +164,208,7560,6,0,L|132:208,1,25,10|0,0:0|0:0,0:0:0:0: +139,208,7671,2,0,L|92:208,1,25,2|2,0:0|0:0,0:0:0:0: +114,208,7782,2,0,L|84:208,1,25,2|2,0:0|0:0,0:0:0:0: +89,208,7893,2,0,L|60:208,1,25,10|0,0:0|0:0,0:0:0:0: +32,184,8004,6,0,L|48:112,1,50,2|10,0:0|0:0,0:0:0:0: +144,128,8226,2,0,L|160:56,1,50,8|0,3:2|1:0,0:0:0:0: +266,68,8449,5,2,0:0:0:0: +306,40,8560,1,2,1:2:0:0: +356,39,8671,1,8,3:2:0:0: +397,66,8782,1,0,0:0:0:0: +415,111,8893,6,0,L|447:168,1,50,2|10,3:2|0:0,0:0:0:0: +429,263,9115,2,0,L|463:321,1,50,2|0,0:0|0:0,0:0:0:0: +328,272,9337,6,0,L|312:244,1,25,2|2,0:0|0:0,0:0:0:0: +315,250,9449,2,0,L|291:209,1,25,2|2,0:0|0:0,0:0:0:0: +303,228,9560,2,0,L|288:202,1,25,2|2,0:0|0:0,0:0:0:0: +290,207,9671,2,0,L|275:181,1,25,2|2,0:0|0:0,0:0:0:0: +248,152,9782,6,0,L|248:88,1,50,2|10,0:0|0:0,0:0:0:0: +154,136,10004,2,0,L|154:80,1,50,8|0,3:2|1:0,0:0:0:0: +64,168,10226,5,2,0:0:0:0: +19,187,10337,1,2,1:2:0:0: +0,232,10449,1,8,3:2:0:0: +14,278,10560,1,0,0:0:0:0: +56,303,10671,5,2,3:2:0:0: +101,282,10782,1,10,0:0:0:0: +151,282,10893,1,2,0:0:0:0: +196,302,11004,1,0,0:0:0:0: +240,316,11115,6,0,L|272:316,1,25,10|0,0:0|0:0,0:0:0:0: +265,316,11226,2,0,L|312:316,1,25,2|2,0:0|0:0,0:0:0:0: +290,316,11337,2,0,L|320:316,1,25,2|2,0:0|0:0,0:0:0:0: +315,316,11449,2,0,L|344:316,1,25,10|0,0:0|0:0,0:0:0:0: +384,340,11560,6,0,L|416:292,1,50,2|10,0:0|0:0,0:0:0:0: +376,196,11782,2,0,L|344:244,1,50,10|0,3:0|1:0,0:0:0:0: +416,308,12004,6,0,L|429:251,1,50,2|2,0:0|1:2,0:0:0:0: +359,175,12226,2,0,L|345:231,1,50,10|0,3:0|0:0,0:0:0:0: +440,260,12449,6,0,L|433:202,1,50,2|10,3:2|0:0,0:0:0:0: +340,154,12671,2,0,L|346:211,1,50,2|0,0:3|0:0,0:0:0:0: +432,124,12893,6,0,L|420:94,1,25,10|0,0:0|0:0,0:0:0:0: +422,100,13004,2,0,L|404:56,1,25,2|2,0:0|0:0,0:0:0:0: +412,76,13115,2,0,L|400:48,1,25,2|2,0:0|0:0,0:0:0:0: +403,53,13226,2,0,L|392:26,1,25,10|0,0:0|0:0,0:0:0:0: +352,4,13337,5,2,0:0:0:0: +352,4,13449,1,10,0:0:0:0: +352,4,13560,1,8,3:2:0:0: +352,4,13671,6,0,L|324:96,1,50,0|0,1:0|0:0,0:0:0:0: +257,89,13893,2,0,L|229:-3,1,50,0|0,1:0|0:0,0:0:0:0: +66,72,17004,5,8,3:2:0:0: +128,199,17226,1,8,3:2:0:0: +207,82,17449,1,8,3:2:0:0: +283,160,17560,6,0,L|345:154,1,50,8|0,0:0|2:0,0:0:0:0: +487,113,17782,6,0,L|437:107,3,50,4|0|0|0,0:2|0:0|0:0|0:0,0:0:0:0: +403,76,18226,1,8,0:0:0:0: +365,107,18337,1,0,0:0:0:0: +356,156,18449,1,0,0:0:0:0: +379,199,18560,1,0,0:0:0:0: +423,222,18671,6,0,L|473:228,1,50,8|0,0:0|0:0,0:0:0:0: +332,345,18893,1,0,0:0:0:0: +332,345,19004,2,0,L|322:279,1,50,0|8,0:0|0:0,0:0:0:0: +211,233,19226,1,0,0:0:0:0: +211,233,19337,2,0,L|201:299,1,50 +91,298,19560,6,0,L|28:291,1,50,8|0,0:0|0:0,0:0:0:0: +166,184,19782,2,0,L|178:253,1,50 +220,356,20004,1,8,0:0:0:0: +263,331,20115,1,0,0:0:0:0: +312,322,20226,1,0,0:0:0:0: +361,329,20337,2,0,L|394:368,1,50,0|8,0:0|0:0,0:0:0:0: +457,201,20560,5,0,0:0:0:0: +457,201,20671,1,0,0:0:0:0: +457,201,20782,2,0,L|460:132,2,50,2|8|0,0:0|0:0|0:0,0:0:0:0: +344,244,21115,2,0,L|335:178,1,59.9999981689454,2|2,0:0|0:0,0:0:0:0: +214,70,21337,5,8,3:2:0:0: +263,76,21449,1,0,3:0:0:0: +313,82,21560,1,0,3:0:0:0: +362,88,21671,2,0,L|263:113,1,100,0|0,3:0|3:0,0:0:0:0: +164,64,22004,1,0,3:0:0:0: +164,64,22115,2,0,L|92:55,1,50,0|8,3:0|0:0,0:0:0:0: +21,221,22337,5,0,3:0:0:0: +69,229,22449,1,0,3:0:0:0: +114,208,22560,1,0,3:0:0:0: +139,166,22671,1,10,3:2:0:0: +145,226,22782,1,2,3:2:0:0: +150,286,22893,2,0,P|210:292|268:248,1,119.999996337891,2|8,3:2|0:0,0:0:0:0: +384,243,23226,5,0,0:0:0:0: +384,243,23337,1,0,0:0:0:0: +384,243,23449,2,0,L|389:193,2,50,0|8|0,0:0|0:0|0:0,0:0:0:0: +334,331,23782,1,0,0:0:0:0: +285,319,23893,1,0,0:0:0:0: +236,325,24004,1,10,0:2:0:0: +191,346,24115,1,2,0:0:0:0: +155,381,24226,6,0,L|42:371,1,100,2|10,0:0|0:2,0:0:0:0: +148,254,24560,1,2,0:0:0:0: +148,254,24671,2,0,L|261:243,1,100,2|8,0:0|0:0,0:0:0:0: +90,134,25115,6,0,L|98:196,1,50 +30,218,25337,2,0,B|45:131|45:131|42:126|42:126|46:123|46:123|57:64,1,150.000005722046,2|2,1:0|0:0,0:0:0:0: +179,120,25671,6,0,P|234:119|291:93,1,100 +349,47,26004,1,0,0:0:0:0: +349,47,26115,2,0,P|325:96|342:165,1,100 +411,158,26449,1,0,0:0:0:0: +411,158,26560,2,0,P|435:109|418:40,1,100 +351,96,26893,1,0,0:0:0:0: +384,255,27115,5,8,0:0:0:0: +384,255,27226,1,0,0:0:0:0: +384,255,27337,1,0,0:0:0:0: +384,255,27449,2,0,L|447:250,2,50,0|8|0,0:0|0:0|0:0,0:0:0:0: +228,173,27782,6,0,L|176:209,1,50,2|2,0:0|0:0,0:0:0:0: +279,342,28004,1,8,0:0:0:0: +279,342,28115,1,0,0:0:0:0: +279,342,28226,2,0,L|280:281,1,50,2|2,0:0|0:0,0:0:0:0: +357,136,28449,5,8,3:2:0:0: +307,139,28560,1,0,3:0:0:0: +257,142,28671,1,0,3:0:0:0: +207,145,28782,2,0,L|151:150,2,50,0|8|0,3:0|0:0|3:0,0:0:0:0: +257,142,29115,5,0,3:0:0:0: +307,139,29226,2,0,L|409:161,1,100,0|0,3:0|3:0,0:0:0:0: +445,188,29560,1,0,3:0:0:0: +468,231,29671,1,0,3:0:0:0: +464,280,29782,1,10,3:2:0:0: +435,320,29893,1,2,3:2:0:0: +389,339,30004,2,0,L|329:327,1,50,10|0,3:2|0:0,0:0:0:0: +177,222,30226,6,0,L|146:193,1,25,8|0,0:0|3:0,3:0:0:0: +145,248,30337,2,0,L|102:231,1,25,8|0,0:0|3:0,0:0:0:0: +130,287,30449,2,0,L|90:290,1,25,8|0,0:0|3:0,0:0:0:0: +134,328,30560,2,0,B|174:349|174:349|239:343,1,100,8|0,1:2|3:0,0:0:0:0: +294,251,30893,6,0,L|290:192,1,50,0|0,1:0|3:0,0:0:0:0: +226,74,31115,2,0,L|222:133,1,50,4|0,0:1|3:0,0:0:0:0: +359,150,31337,1,4,0:1:0:0: +359,150,31782,6,0,P|408:124|464:148,1,89.9999972534181,0|0,1:0|1:0,0:0:0:0: +340,240,32004,6,0,L|288:36,1,179.999994506836,12|0,0:0|0:0,0:0:0:0: +176,132,32449,2,0,P|87:100|7:180,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +139,383,32893,2,0,P|83:316|104:232,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +205,291,33337,6,0,L|449:323,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +506,223,33782,2,0,L|262:191,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +182,205,34226,6,0,P|261:157|336:244,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +76,191,34671,2,0,P|28:112|115:37,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +182,205,35115,6,2,L|155:305,1,89.9999972534181,10|0,0:0|0:0,0:1:0:0: +257,361,35337,2,2,L|284:261,1,89.9999972534181,2|0,0:0|0:0,0:1:0:0: +334,174,35560,6,0,P|339:277|458:283,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +447,191,36004,2,0,L|505:21,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +334,174,36449,6,0,P|269:112|169:165,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +133,25,36893,2,0,L|165:279,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +22,281,37337,6,0,P|91:338|207:265,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +306,240,37782,2,0,P|239:184|155:203,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: +0,92,38226,2,0,L|266:54,1,179.999994506836,8|8,0:0|0:0,0:0:0:0: +363,43,38671,6,2,P|386:76|392:133,1,89.9999972534181,10|0,0:0|0:0,0:1:0:0: +306,240,38893,2,2,P|283:207|277:150,1,89.9999972534181,10|0,0:0|0:0,0:1:0:0: +421,293,39115,5,8,3:2:0:0: +368,278,39226,1,0,3:0:0:0: +313,273,39337,1,2,3:3:0:0: +259,284,39449,1,0,0:0:0:0: +214,316,39560,1,8,3:2:0:0: +166,343,39671,1,0,3:0:0:0: +112,332,39782,1,2,3:3:0:0: +76,289,39893,1,0,0:0:0:0: +68,234,40004,1,10,3:2:0:0: +94,185,40115,1,0,3:0:0:0: +136,149,40226,1,0,3:0:0:0: +190,147,40337,1,2,3:0:0:0: +241,167,40449,1,8,0:0:0:0: +261,217,40560,1,0,3:0:0:0: +240,267,40671,1,2,3:0:0:0: +188,285,40782,1,0,0:0:0:0: +135,268,40893,5,8,3:2:0:0: +114,216,41004,1,0,3:0:0:0: +137,166,41115,1,2,3:0:0:0: +190,147,41226,1,0,0:0:0:0: +241,167,41337,1,8,3:2:0:0: +295,170,41449,1,0,3:0:0:0: +348,157,41560,1,2,3:0:0:0: +390,121,41671,1,0,0:0:0:0: +394,66,41782,1,10,3:2:0:0: +364,18,41893,1,0,3:0:0:0: +316,0,42004,1,0,3:0:0:0: +262,11,42115,1,2,3:0:0:0: +214,26,42226,6,2,L|107:43,1,89.9999972534181,8|2,0:0|3:0,0:1:0:0: +2,149,42449,2,2,L|109:166,1,89.9999972534181,4|0,3:2|0:0,0:1:0:0: +336,200,42671,5,12,3:2:0:0: +288,172,42782,1,0,3:0:0:0: +232,173,42893,1,0,3:0:0:0: +189,207,43004,1,0,0:0:0:0: +160,255,43115,1,10,3:0:0:0: +168,308,43226,1,0,3:0:0:0: +196,355,43337,1,2,3:0:0:0: +249,366,43449,1,0,0:0:0:0: +295,337,43560,1,8,3:2:0:0: +303,283,43671,1,0,3:0:0:0: +277,233,43782,1,0,3:0:0:0: +224,216,43893,1,2,3:0:0:0: +172,228,44004,1,8,0:0:0:0: +124,248,44115,1,0,0:0:0:0: +72,232,44226,1,2,3:0:0:0: +32,196,44337,1,0,3:0:0:0: +28,144,44449,6,0,L|1:135,1,25,8|0,0:0|0:0,0:0:0:0: +62,96,44560,2,0,L|45:73,1,25 +119,77,44671,2,0,L|119:49,1,25,8|0,0:0|0:0,0:0:0:0: +175,96,44782,2,0,L|191:73,1,25 +210,144,44893,2,0,L|236:135,1,25,8|0,0:0|0:0,0:0:0:0: +228,200,45004,2,0,L|201:209,1,25,8|0,0:0|0:0,0:0:0:0: +262,248,45115,2,0,L|245:271,1,25,8|0,0:0|0:0,0:0:0:0: +319,267,45226,2,0,L|319:295,1,25,8|0,0:0|0:0,0:0:0:0: +375,248,45337,2,8,L|391:271,1,25 +410,200,45449,2,8,L|436:209,1,25 +410,141,45560,2,8,L|436:132,1,25 +375,93,45671,5,12,0:3:0:0: +375,93,46004,2,0,L|317:86,1,50,0|12,1:0|0:2,0:0:0:0: +317,167,46337,5,0,0:0:0:0: +317,167,46449,2,0,L|160:184,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: +53,101,46782,1,0,0:0:0:0: +108,98,46893,1,0,1:0:0:0: +152,130,47004,1,0,0:0:0:0: +167,183,47115,6,2,L|161:268,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: +49,308,47337,1,0,1:0:0:0: +49,308,47449,2,2,L|44:204,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: +205,140,47671,1,0,0:0:0:0: +205,140,47782,2,2,L|210:244,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: +310,353,48004,5,8,0:0:0:0: +346,347,48115,1,0,0:0:0:0: +383,341,48226,2,0,P|440:289|426:219,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: +317,167,48560,1,0,0:0:0:0: +307,112,48671,1,0,1:0:0:0: +333,64,48782,1,0,0:0:0:0: +384,43,48893,6,2,L|468:54,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: +506,161,49115,1,0,1:0:0:0: +506,161,49226,2,2,L|422:150,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: +268,121,49449,1,0,0:0:0:0: +268,121,49560,2,2,L|352:110,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: +263,263,49782,5,8,0:0:0:0: +228,247,49893,1,0,0:0:0:0: +193,232,50004,2,0,P|128:234|53:312,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: +121,164,50337,1,0,0:0:0:0: +120,109,50449,1,0,1:0:0:0: +91,62,50560,1,0,0:0:0:0: +42,37,50671,6,2,L|123:29,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: +242,84,50893,1,0,1:0:0:0: +242,84,51004,2,2,L|321:118,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: +341,245,51226,1,0,0:0:0:0: +341,245,51337,2,2,L|353:331,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: +163,192,51560,5,8,0:0:0:0: +198,181,51671,1,0,0:0:0:0: +233,170,51782,2,0,P|303:163|372:219,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: +458,297,52115,1,0,0:0:0:0: +460,240,52226,1,0,1:0:0:0: +463,184,52337,1,0,0:0:0:0: +466,128,52449,6,0,B|452:65|452:65|416:155,1,150.000005722046,10|2,0:2|1:2,0:0:0:0: +272,189,52782,2,0,L|197:180,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: +338,25,53004,1,2,0:0:0:0: +338,25,53115,2,0,L|345:109,1,75.0000028610231,10|0,1:2|0:0,0:1:0:0: +179,225,53337,5,8,0:0:0:0: +175,187,53449,1,0,0:0:0:0: +172,149,53560,2,0,L|320:166,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: +296,293,53893,1,0,0:0:0:0: +259,334,54004,1,0,1:0:0:0: +209,357,54115,1,0,0:0:0:0: +154,358,54226,6,2,L|79:349,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: +179,225,54449,1,0,1:0:0:0: +179,225,54560,2,2,L|253:233,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: +91,140,54782,1,0,0:0:0:0: +91,140,54893,2,2,L|16:148,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: +191,43,55115,5,8,0:0:0:0: +195,80,55226,1,0,0:0:0:0: +199,117,55337,2,0,B|179:225|179:225|156:170,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: +289,165,55671,1,0,0:0:0:0: +344,159,55782,1,0,1:0:0:0: +399,154,55893,1,0,0:0:0:0: +454,149,56004,6,2,L|461:235,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: +359,281,56226,1,0,1:0:0:0: +359,281,56337,2,2,L|365:366,1,75.0000028610231,2|8,0:2|0:0,0:1:0:0: +262,132,56560,1,0,0:0:0:0: +262,132,56671,2,2,L|268:217,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: +148,358,56893,5,12,0:2:0:0: +110,355,57004,1,0,0:0:0:0: +79,333,57115,2,0,P|134:285|209:317,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: +329,327,57449,1,0,0:0:0:0: +359,281,57560,1,0,1:0:0:0: +364,226,57671,1,0,0:0:0:0: +343,175,57782,6,2,L|246:184,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: +342,20,58004,1,0,1:0:0:0: +342,20,58115,2,2,L|387:85,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: +210,97,58337,1,0,0:0:0:0: +210,97,58449,2,2,L|243:24,1,75.0000028610231,10|0,1:2|0:0,0:1:0:0: +343,175,58671,6,0,L|305:177,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +209,209,58893,2,0,L|255:224,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +316,267,59115,2,0,L|283:301,3,37.5000014305115,8|0|8|0,0:0|0:0|0:0|0:0,0:0:0:0: +211,329,59337,2,0,L|194:274,3,37.5000014305115,8|0|8|0,0:0|0:0|0:0|0:0,0:0:0:0: +103,287,59560,6,0,L|67:275,1,37.5000014305115,0|0,1:0|3:0,0:0:0:0: +108,240,59671,2,0,L|72:228,1,37.5000014305115,0|0,1:0|3:0,0:0:0:0: +113,193,59782,2,0,L|77:181,1,37.5000014305115,0|0,1:0|3:0,0:0:0:0: +120,146,59893,1,12,0:3:0:0: +120,146,60226,6,0,L|131:40,1,100,0|12,1:0|0:2,0:0:0:0: +374,174,60893,5,8,0:0:0:0: +326,323,61115,1,0,3:0:0:0: +462,267,61337,2,0,L|518:267,1,50,8|0,0:0|0:0,0:0:0:0: +344,182,61560,5,0,0:0:0:0: +344,182,61671,2,0,L|297:213,1,50,0|8,3:0|0:0,0:0:0:0: +174,241,61893,1,0,0:0:0:0: +174,241,62004,2,0,L|221:272,1,50,0|0,3:0|0:0,0:0:0:0: +66,161,62226,5,8,0:0:0:0: +104,326,62449,1,0,3:0:0:0: +269,288,62671,1,8,0:0:0:0: +231,122,62893,2,0,L|215:50,1,50,0|0,3:0|0:0,0:0:0:0: +296,0,63115,2,0,L|312:72,1,50,8|0,0:0|0:0,0:0:0:0: +373,120,63337,5,0,0:0:0:0: +373,120,63449,2,0,P|413:112|477:152,1,100,0|0,3:0|3:0,0:0:0:0: +400,216,63782,2,0,L|288:232,1,100,0|8,0:0|0:0,0:0:0:0: +48,160,64226,5,0,3:0:0:0: +216,200,64449,1,8,0:0:0:0: +104,288,64671,1,0,3:0:0:0: +216,200,64893,1,8,0:0:0:0: +160,64,65115,5,0,0:0:0:0: +160,64,65226,2,0,L|160:136,1,50,0|8,3:0|0:0,0:0:0:0: +264,104,65449,1,0,0:0:0:0: +264,104,65560,2,0,L|264:168,1,50,0|0,3:0|0:0,0:0:0:0: +456,160,65782,5,8,0:0:0:0: +288,192,66004,1,0,3:0:0:0: +336,48,66226,1,8,0:0:0:0: +408,296,66449,1,0,3:0:0:0: +196,148,66671,6,0,P|188:215|221:271,1,100,8|8,0:0|0:0,0:0:0:0: +288,192,67004,6,0,L|368:216,1,50,0|8,3:0|0:0,0:0:0:0: +297,308,67226,1,0,3:0:0:0: +297,308,67337,2,0,L|217:332,1,50,12|0,0:0|0:0,0:0:0:0: +107,256,67560,6,0,P|150:234|199:244,1,100,14|0,0:0|0:0,0:0:0:0: +460,300,68004,5,8,0:0:0:0: +407,107,68226,1,0,3:0:0:0: +364,364,68449,1,8,0:0:0:0: +345,18,68671,6,0,P|417:2|505:66,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: +167,18,69115,2,0,P|95:2|7:66,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: +407,107,69560,6,0,P|456:161|445:269,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: +158,202,70004,1,0,0:0:0:0: +354,202,70226,1,8,0:0:0:0: +105,107,70449,2,0,P|55:161|66:269,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: +364,281,70893,6,0,P|418:330|526:319,1,150.000005722046,0|8,0:0|0:0,0:0:0:0: +424,64,71337,6,0,B|434:73|434:73|467:145|424:213|284:257|262:111|277:26|173:-19|99:9|48:91|87:209|211:192|211:192|334:175|373:294|323:376|248:404|162:332,1,1050.00004005432,2|8,3:2|0:0,0:0:0:0: +161,331,73337,6,0,L|201:331,1,25,8|0,0:0|3:0,0:0:0:0: +186,331,73449,2,2,L|233:331,1,25,0|0,3:0|3:0,0:0:0:0: +211,331,73560,2,2,L|257:331,1,25,8|0,0:0|3:0,0:0:0:0: +236,331,73671,2,2,L|273:331,1,25,0|0,3:0|3:0,0:0:0:0: +297,339,73782,6,0,L|377:347,1,50,8|8,0:0|0:0,0:0:0:0: +321,181,74004,2,0,L|313:269,1,50,8|8,0:0|3:2,0:0:0:0: +184,116,74226,2,0,L|176:28,1,50,0|8,0:0|3:2,0:0:0:0: +283,82,74449,6,0,P|332:75|377:94,1,100,8|12,0:0|0:0,0:0:0:0: +150,188,75115,5,8,0:0:0:0: +127,46,75337,1,0,0:0:0:0: +157,218,75560,2,0,P|211:199|262:204,1,100,8|0,0:0|0:0,0:0:0:0: +322,283,75893,2,0,B|370:288|370:288|436:262,1,100,0|0,3:0|3:0,0:0:0:0: +439,170,76226,2,0,P|392:159|356:191,1,100,0|8,3:0|0:0,0:0:0:0: +219,92,76671,5,0,3:0:0:0: +371,22,76893,1,8,0:0:0:0: +356,191,77115,1,0,3:0:0:0: +194,73,77337,2,0,P|144:75|94:82,1,100,8|0,0:0|0:0,0:0:0:0: +15,164,77671,2,0,P|63:173|98:210,1,100,0|0,3:0|3:0,0:0:0:0: +26,302,78004,2,0,L|21:253,1,50 +181,348,78226,6,0,B|221:330|221:330|276:335,1,100,8|0,0:0|3:0,0:0:0:0: +422,231,78671,1,8,0:0:0:0: +435,376,78893,1,0,3:0:0:0: +271,230,79115,2,0,L|281:123,1,100,8|0,0:0|0:0,0:0:0:0: +367,56,79449,2,0,P|393:101|389:159,1,100,0|0,3:0|3:0,0:0:0:0: +280,130,79782,2,0,L|226:137,1,50,0|0,3:0|0:0,0:0:0:0: +104,280,80004,5,8,0:0:0:0: +243,207,80226,1,0,3:0:0:0: +104,134,80449,1,8,0:0:0:0: +243,61,80671,1,0,3:0:0:0: +384,189,80893,6,0,L|330:203,1,50,8|0,0:0|0:0,0:0:0:0: +259,262,81115,2,0,L|211:250,1,50,8|0,0:0|0:0,0:0:0:0: +83,157,81337,1,8,1:2:0:0: +60,186,81449,1,0,3:0:0:0: +48,221,81560,1,8,1:2:0:0: +48,259,81671,1,0,3:0:0:0: +61,294,81782,6,0,P|116:293|162:329,1,100,10|0,1:2|0:0,0:0:0:0: +350,269,82226,1,8,0:0:0:0: +226,187,82449,1,0,3:0:0:0: +265,365,82671,1,8,0:0:0:0: +444,207,82893,2,0,P|449:142|420:85,1,129.999994049073,2|8,0:0|0:0,0:0:0:0: +211,48,83337,6,0,P|159:84|136:144,1,129.999994049073,2|8,0:0|0:0,0:0:0:0: +270,337,83782,2,0,B|297:279|256:269|288:205,1,129.999994049073,2|8,0:0|0:0,0:0:0:0: +427,124,84226,1,0,3:0:0:0: +427,124,84337,1,0,3:0:0:0: +427,124,84449,1,8,0:0:0:0: +136,144,84671,2,0,B|183:228|275:200|275:200|303:273|208:263,1,259.999988098145,2|4,0:0|0:1,0:0:0:0: +235,99,85337,5,12,0:2:0:0: +278,92,85449,1,0,0:0:0:0: +319,109,85560,1,2,0:0:0:0: +174,43,85782,1,10,0:2:0:0: +129,35,85893,1,0,1:0:0:0: +86,47,86004,1,0,3:0:0:0: +52,75,86115,1,0,1:0:0:0: +32,115,86226,6,0,L|37:155,3,25,10|0|0|0,0:2|3:0|3:0|3:0,0:0:0:0: +83,252,86449,2,0,L|87:212,3,25,0|0|2|0,1:0|3:0|3:2|3:0,0:0:0:0: +174,189,86671,6,0,L|235:185,2,50,8|0|8,0:0|3:0|0:0,0:0:0:0: +83,252,87004,1,0,0:0:0:0: +83,252,87115,6,0,L|92:311,1,50,12|2,1:2|3:2,0:0:0:0: +185,379,87337,2,0,L|205:328,1,50,10|0,1:2|3:0,0:0:0:0: +310,279,87560,2,0,L|362:285,1,50,8|8,1:2|1:2,0:0:0:0: +490,357,87782,2,0,L|493:386,2,25,8|0|8,1:2|3:0|1:2,0:0:0:0: +456,208,88004,5,0,1:0:0:0: +456,208,88060,1,0,3:0:0:0: +456,208,88115,2,0,L|472:128,1,50,8|0,1:2|0:0,0:0:0:0: +352,40,88337,2,0,L|368:88,1,50,8|2,1:2|0:0,0:0:0:0: +256,136,88560,1,8,1:2:0:0: +256,136,88671,2,0,L|240:80,1,50 +48,184,88893,6,0,P|120:168|216:224,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: +328,336,89226,1,0,0:0:0:0: +424,328,89337,1,8,0:2:0:0: +472,248,89449,1,0,0:0:0:0: +488,160,89560,6,0,P|440:56|352:48,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +256,104,89893,1,0,0:0:0:0: +168,128,90004,1,0,0:0:0:0: +80,120,90115,1,0,0:0:0:0: +0,88,90226,6,0,P|-8:136|8:216,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +96,336,90449,2,0,P|104:288|88:208,1,85.0000016212464 +173,43,90671,6,0,P|165:85|168:128,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +256,296,90893,2,0,P|264:248|248:168,1,85.0000016212464 +336,72,91115,6,0,L|560:56,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +432,120,91449,1,2,0:2:0:0: +392,200,91560,1,8,0:2:0:0: +432,288,91671,1,0,0:0:0:0: +496,360,91782,5,2,0:2:0:0: +256,296,92004,1,10,0:2:0:0: +16,360,92226,2,0,P|64:272|48:184,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: +120,40,92560,5,0,0:0:0:0: +168,120,92671,1,0,0:0:0:0: +248,168,92782,1,0,0:0:0:0: +328,120,92893,6,0,P|414:120|477:240,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +472,360,93226,1,0,0:0:0:0: +472,360,93337,2,0,P|396:319|396:183,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +328,120,93671,5,0,0:0:0:0: +328,120,93782,2,0,L|312:8,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +93,147,94004,6,0,P|174:128|248:168,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: +448,288,94337,1,0,0:0:0:0: +360,264,94449,1,2,0:2:0:0: +272,288,94560,1,0,0:0:0:0: +216,360,94671,6,0,P|152:304|168:224,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +216,104,95004,5,2,0:2:0:0: +216,104,95115,2,0,L|232:16,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +368,48,95337,2,0,P|408:32|472:48,1,85.0000016212464 +432,224,95560,2,0,P|416:184|432:120,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +320,280,95782,2,0,P|336:320|320:384,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +32,232,96004,6,0,P|112:192|224:240,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +320,280,96337,1,0,0:0:0:0: +408,312,96449,1,8,0:2:0:0: +496,280,96560,1,0,0:0:0:0: +496,280,96671,6,0,L|456:112,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +384,56,97004,1,0,0:0:0:0: +296,24,97115,2,0,P|240:16|160:64,1,85.0000016212464 +104,160,97337,2,0,P|160:168|240:120,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +456,192,97560,6,0,P|384:160|304:248,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +264,288,97893,1,0,0:0:0:0: +176,264,98004,2,0,L|8:280,1,85.0000016212464 +16,88,98226,6,0,P|64:136|168:56,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +328,56,98560,1,2,0:2:0:0: +416,32,98671,1,8,0:2:0:0: +480,104,98782,1,0,0:0:0:0: +456,192,98893,6,0,L|464:280,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +328,360,99115,2,0,L|320:272,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +208,112,99337,2,0,L|216:200,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +64,320,99560,5,8,0:2:0:0: +136,264,99671,1,0,0:0:0:0: +232,256,99782,1,0,0:0:0:0: +320,280,99893,1,0,0:0:0:0: +384,344,100004,6,0,P|408:288|416:248,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +360,72,100226,2,0,P|336:128|328:168,1,85.0000016212464 +240,312,100449,5,8,0:2:0:0: +152,344,100560,1,0,0:0:0:0: +64,320,100671,1,0,0:0:0:0: +8,248,100782,1,0,0:0:0:0: +16,152,100893,1,8,0:2:0:0: +88,88,101004,1,0,0:0:0:0: +184,80,101115,1,0,0:0:0:0: +272,120,101226,1,0,0:0:0:0: +356,176,101337,5,10,0:2:0:0: +456,164,101449,1,0,0:0:0:0: +392,88,101560,1,2,0:2:0:0: +348,184,101671,5,0,0:0:0:0: +448,172,101782,1,10,0:2:0:0: +384,96,101893,1,0,0:0:0:0: +340,192,102004,5,10,0:2:0:0: +440,180,102115,1,0,0:0:0:0: +376,104,102226,2,0,P|329:93|290:112,1,85.0000016212464,10|8,1:2|1:2,0:0:0:0: +344,192,102449,2,0,P|380:226|423:229,1,85.0000016212464,0|8,0:0|0:0,0:0:0:0: +169,239,102671,5,2,1:2:0:0: +80,210,102782,1,0,1:0:0:0: +80,117,102893,1,8,1:2:0:0: +169,88,103004,1,8,1:2:0:0: +224,164,103115,6,0,P|315:166|404:144,1,170.000003242493,12|0,1:2|0:0,0:0:0:0: +456,317,103449,1,0,0:0:0:0: +366,290,103560,1,8,1:2:0:0: +324,206,103671,1,2,0:2:0:0: +326,112,103782,2,0,L|344:10,2,85.0000016212464,0|0|10,0:0|0:0|0:2,0:0:0:0: +314,203,104115,5,0,0:0:0:0: +242,148,104226,2,0,L|184:87,1,85.0000016212464 +398,167,104449,2,0,L|479:122,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +365,276,104671,2,0,L|383:378,2,85.0000016212464,0|0|8,0:0|0:0|0:2,0:0:0:0: +183,86,105004,5,0,0:0:0:0: +99,88,105115,1,0,0:0:0:0: +31,137,105226,1,0,0:0:0:0: +3,216,105337,1,10,0:2:0:0: +24,297,105449,1,0,0:0:0:0: +87,352,105560,1,0,0:0:0:0: +152,298,105671,1,2,0:2:0:0: +233,273,105782,1,8,0:2:0:0: +317,283,105893,1,0,0:0:0:0: +391,324,106004,6,0,L|484:310,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +282,227,106226,2,0,L|189:212,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +46,357,106449,2,0,L|138:342,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +62,140,106671,1,8,0:2:0:0: +62,140,106782,1,0,0:0:0:0: +62,140,106893,1,0,0:0:0:0: +62,140,107004,1,0,0:0:0:0: +62,140,107115,6,0,L|245:116,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +418,227,107449,1,0,0:0:0:0: +330,251,107560,1,8,1:2:0:0: +251,206,107671,1,0,0:0:0:0: +230,117,107782,1,0,0:0:0:0: +277,35,107893,1,0,0:0:0:0: +347,130,108004,2,0,L|247:147,1,85.0000016212464,8|0,1:2|0:0,0:0:0:0: +46,62,108226,6,0,P|30:145|24:230,1,170.000003242493,2|8,0:2|1:2,0:0:0:0: +215,312,108560,1,0,0:0:0:0: +215,312,108671,2,0,P|191:272|152:252,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +62,140,108893,2,0,P|146:173|234:167,1,170.000003242493,10|0,1:2|0:0,0:0:0:0: +371,114,109226,2,0,L|376:22,2,85.0000016212464,2|8|0,0:2|1:2|0:0,0:0:0:0: +312,190,109560,5,0,0:0:0:0: +389,239,109671,1,0,0:0:0:0: +308,283,109782,1,8,1:2:0:0: +386,333,109893,1,0,0:0:0:0: +305,377,110004,2,0,P|267:367|226:331,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +77,199,110226,6,0,P|152:149|227:172,1,170.000003242493,8|0,1:2|0:0,0:0:0:0: +417,221,110560,1,0,0:0:0:0: +444,135,110671,1,8,1:2:0:0: +389,64,110782,1,0,0:0:0:0: +299,68,110893,2,0,P|210:71|156:9,1,170.000003242493,0|8,0:0|1:2,0:0:0:0: +62,140,111226,1,0,0:0:0:0: +62,140,111337,2,0,L|83:234,1,85.0000016212464 +262,296,111560,2,0,L|169:278,1,85.0000016212464,8|0,1:2|0:0,0:0:0:0: +349,153,111782,6,0,L|340:68,2,85.0000016212464,0|0|8,0:0|0:0|1:2,0:0:0:0: +236,329,112115,1,0,0:0:0:0: +292,267,112226,1,0,0:0:0:0: +375,253,112337,1,0,0:0:0:0: +449,291,112449,1,10,1:2:0:0: +378,336,112560,5,2,0:2:0:0: +375,253,112671,1,0,0:0:0:0: +372,170,112782,1,2,0:2:0:0: +369,87,112893,1,8,1:2:0:0: +442,125,113004,1,0,0:0:0:0: +348,178,113115,6,0,L|252:169,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +80,269,113337,2,0,P|120:276|175:256,1,85.0000016212464,10|0,1:2|0:0,0:0:0:0: +105,18,113560,2,0,P|86:55|87:103,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +260,255,113782,5,12,1:2:0:0: +261,212,113893,1,0,0:0:0:0: +262,170,114004,1,2,0:2:0:0: +414,61,114226,5,8,1:2:0:0: +340,101,114337,1,0,0:0:0:0: +256,109,114449,1,2,0:2:0:0: +176,81,114560,1,0,0:0:0:0: +105,18,114671,1,8,1:2:0:0: +84,118,114782,1,0,0:0:0:0: +63,218,114893,6,2,P|105:233|154:235,1,85.0000016212464,2|0,0:2|0:0,0:2:0:0: +369,328,115115,2,2,P|325:320|276:336,1,85.0000016212464,10|0,1:2|0:0,0:2:0:0: +256,109,115337,2,2,B|301:123|301:123|376:112,1,100,2|0,0:2|0:0,0:1:0:0: +488,273,115560,5,8,1:2:0:0: +488,273,115671,1,8,0:0:0:0: +488,273,115782,1,8,0:0:0:0: +429,83,116004,1,8,1:2:0:0: +429,83,116115,1,0,3:0:0:0: +429,83,116226,6,0,L|431:47,2,32.4999985122681,8|0|0,3:2|3:0|3:0,3:0:0:0: +381,198,116449,1,8,1:2:0:0: +381,198,116560,5,0,1:1:0:0: +187,140,116782,1,0,1:1:0:0: +102,323,117004,1,0,1:1:0:0: +58,34,117337,5,12,0:0:0:0: +380,124,128004,6,0,P|400:120|440:124,1,50,12|0,0:0|0:0,0:0:0:0: +396,256,128226,2,0,P|376:260|336:256,1,50,2|0,1:2|0:0,0:0:0:0: +104,200,128449,6,0,L|116:260,1,50,8|0,0:0|0:0,0:0:0:0: +172,104,128671,2,0,L|160:44,1,50,0|0,1:0|0:0,0:0:0:0: +272,184,128893,5,10,0:2:0:0: +280,208,129004,1,0,0:0:0:0: +280,232,129115,1,0,1:0:0:0: +272,256,129226,1,2,0:2:0:0: +264,280,129337,6,0,L|328:308,1,50,8|0,0:0|0:0,0:0:0:0: +464,260,129560,2,0,L|400:232,1,50,10|0,1:2|0:0,0:0:0:0: +317,113,129782,6,0,L|339:84,1,25,8|0,0:0|0:0,0:0:0:0: +286,96,129893,2,0,L|298:62,1,25 +252,92,130004,2,0,L|251:56,1,25,8|0,0:0|0:0,0:0:0:0: +218,99,130115,2,0,L|205:65,1,25 +189,117,130226,2,0,L|164:90,1,25,8|0,0:0|0:0,0:0:0:0: +167,145,130337,2,0,L|135:128,1,25,8|0,0:0|0:0,0:0:0:0: +156,178,130449,2,0,L|121:173,1,25,8|0,0:0|0:0,0:0:0:0: +158,212,130560,2,0,L|122:220,1,25,8|0,0:0|0:0,0:0:0:0: +171,244,130671,2,0,L|140:264,1,25,0|0,1:0|3:0,0:0:0:0: +194,270,130782,2,0,L|172:299,1,25,0|0,1:0|3:0,0:0:0:0: +225,287,130893,2,0,L|213:321,1,25,0|0,1:0|3:0,0:0:0:0: +259,291,131004,5,12,0:3:0:0: +259,291,131337,6,0,P|312:272|376:300,1,100,0|12,1:0|0:0,0:0:0:0: +456,68,132004,5,8,0:0:0:0: +340,184,132226,1,0,3:0:0:0: +284,20,132449,1,8,0:0:0:0: +156,160,132671,5,0,0:0:0:0: +156,160,132782,2,0,P|216:152|256:160,1,100,0|0,3:0|0:0,0:0:0:0: +92,235,133115,2,0,L|20:251,1,50,0|0,3:0|0:0,0:0:0:0: +281,303,133337,5,8,0:0:0:0: +471,213,133560,1,0,3:0:0:0: +281,303,133782,1,8,0:0:0:0: +363,84,134004,1,0,3:0:0:0: +241,162,134226,6,0,L|222:94,1,50,8|0,0:0|0:0,0:0:0:0: +164,204,134449,1,0,0:0:0:0: +164,204,134560,2,0,P|112:196|51:220,1,100,0|0,3:0|3:0,0:0:0:0: +281,303,134893,2,0,P|333:311|394:287,1,100,0|8,0:0|0:0,0:0:0:0: +500,184,135337,5,0,3:0:0:0: +465,11,135560,1,8,0:0:0:0: +294,44,135782,1,0,3:0:0:0: +327,217,136004,1,8,0:0:0:0: +408,112,136226,5,0,0:0:0:0: +408,112,136337,1,0,3:0:0:0: +364,316,136560,1,0,0:0:0:0: +364,316,136671,1,0,3:0:0:0: +168,292,136893,5,8,0:0:0:0: +48,212,137115,1,0,3:0:0:0: +188,16,137337,1,8,0:0:0:0: +176,156,137560,1,0,3:0:0:0: +344,292,137782,6,0,P|408:288|464:252,1,100,8|0,0:0|0:0,0:0:0:0: +388,176,138115,2,0,P|408:124|400:68,1,100,0|0,3:0|3:0,0:0:0:0: +168,192,138449,1,0,0:0:0:0: +168,192,138560,1,0,0:0:0:0: +168,192,138671,2,0,L|280:204,1,100,10|0,0:0|3:0,0:0:0:0: +464,168,139115,5,8,0:0:0:0: +344,292,139337,1,0,3:0:0:0: +332,24,139560,1,8,0:0:0:0: +288,368,139782,6,0,P|344:384|404:368,1,100,2|8,0:0|0:0,0:0:0:0: +224,16,140226,2,0,P|168:0|108:16,1,100,2|8,0:0|0:0,0:0:0:0: +80,224,140671,2,0,P|64:280|80:340,1,100,2|8,0:0|0:0,0:0:0:0: +400,152,141115,5,0,0:0:0:0: +300,268,141337,1,8,0:0:0:0: +460,80,141560,2,0,P|404:56|336:88,1,100,2|8,0:0|0:0,0:0:0:0: +332,224,142004,2,0,L|352:360,1,100,0|8,0:0|0:0,0:0:0:0: +184,88,142449,6,0,L|178:38,1,50 +204,136,142671,1,8,0:0:0:0: +212,184,142782,1,0,3:0:0:0: +196,232,142893,1,0,3:0:0:0: +152,256,143004,1,0,0:0:0:0: +104,236,143115,1,8,0:0:0:0: +56,252,143226,1,0,3:0:0:0: +32,296,143337,1,0,3:0:0:0: +52,340,143449,1,0,3:0:0:0: +92,372,143560,1,8,0:0:0:0: +140,352,143671,1,0,3:0:0:0: +188,336,143782,1,8,0:0:0:0: +236,348,143893,1,0,3:0:0:0: +280,368,144004,5,8,0:0:0:0: +448,260,144449,6,0,L|416:248,1,25,8|0,0:0|3:0,0:0:0:0: +424,251,144560,2,0,L|392:240,1,25,0|0,3:0|3:0,0:0:0:0: +400,243,144671,2,0,L|360:228,1,25,8|0,0:0|3:0,0:0:0:0: +377,234,144782,2,0,L|348:224,1,25,0|0,3:0|3:0,0:0:0:0: +316,200,144893,6,0,P|308:176|308:132,1,50,8|8,0:0|0:0,0:0:0:0: +404,40,145115,2,0,P|412:64|412:108,1,50,8|8,0:0|3:2,0:0:0:0: +252,264,145337,5,8,0:0:0:0: +252,264,145449,1,8,3:2:0:0: +252,264,145560,2,0,P|196:256|136:272,1,100,8|12,0:0|0:0,0:0:0:0: +34,77,146226,5,8,0:0:0:0: +54,230,146449,1,0,3:0:0:0: +28,27,146671,2,0,L|36:105,1,50,8|0,0:0|0:0,0:0:0:0: +54,230,146893,1,0,0:0:0:0: +54,230,147004,2,0,L|62:152,1,50,0|8,3:0|0:0,0:0:0:0: +234,135,147226,1,0,0:0:0:0: +234,135,147337,2,0,L|347:149,1,100,0|8,3:0|0:0,0:0:0:0: +156,190,147782,5,0,3:0:0:0: +333,147,148004,1,8,0:0:0:0: +133,120,148226,1,0,3:0:0:0: +358,65,148449,2,0,L|261:89,1,100,8|0,0:0|0:0,0:0:0:0: +333,147,148782,2,0,L|446:161,1,100,0|0,3:0|3:0,0:0:0:0: +321,275,149115,2,0,L|302:214,1,50,0|0,1:0|0:0,0:0:0:0: +462,165,149337,6,0,L|428:273,1,100,8|0,0:0|3:0,0:0:0:0: +393,83,149782,1,8,0:0:0:0: +431,260,150004,1,0,3:0:0:0: +221,288,150226,2,0,L|331:273,1,100,8|0,0:0|0:0,0:0:0:0: +190,296,150560,2,0,L|171:235,1,50,0|8,3:0|0:0,0:0:0:0: +334,226,150782,1,0,0:0:0:0: +334,226,150893,2,0,L|315:287,1,50,0|0,3:0|0:0,0:0:0:0: +238,135,151115,6,0,L|257:196,1,50,8|0,0:0|0:0,0:0:0:0: +190,296,151337,2,0,L|209:357,1,50,0|0,3:0|0:0,0:0:0:0: +118,72,151560,2,0,L|137:133,1,50,8|0,0:0|0:0,0:0:0:0: +24,221,151782,2,0,L|43:282,1,50,0|0,3:0|0:0,0:0:0:0: +242,367,152004,5,8,0:0:0:0: +276,362,152115,1,0,3:0:0:0: +311,357,152226,1,8,0:0:0:0: +346,353,152337,1,0,3:0:0:0: +386,338,152449,5,8,1:2:0:0: +337,327,152560,1,0,3:0:0:0: +288,317,152671,1,8,1:2:0:0: +239,306,152782,1,0,3:0:0:0: +190,296,152893,1,10,1:2:0:0: +190,296,153337,5,8,0:0:0:0: +241,77,153560,1,0,3:0:0:0: +200,262,153782,1,8,0:0:0:0: +447,200,154004,2,0,L|336:181,1,100,2|8,0:0|0:0,0:0:0:0: +97,119,154449,2,0,L|159:109,1,50,2|0,0:0|0:0,0:0:0:0: +348,183,154671,5,10,0:2:0:0: +151,210,154893,2,0,L|146:111,1,100,2|8,0:0|0:0,0:0:0:0: +353,83,155337,1,0,3:0:0:0: +350,132,155449,1,0,3:0:0:0: +347,182,155560,1,8,0:0:0:0: +31,78,155782,2,0,B|174:62|174:62|145:110,1,200,2|4,0:0|0:1,0:0:0:0: +113,280,156449,6,0,L|111:229,1,50,12|0,0:2|0:0,0:0:0:0: +145,110,156671,1,2,0:0:0:0: +108,312,156893,2,0,L|42:303,1,50,10|0,0:2|1:0,0:0:0:0: +211,208,157115,2,0,L|277:217,1,50,0|0,3:0|1:0,0:0:0:0: +414,288,157337,6,0,L|387:290,3,25,10|0|0|0,0:2|3:0|3:0|3:0,0:0:0:0: +290,340,157560,2,0,L|316:342,3,25,0|0|2|0,1:0|3:0|3:2|3:0,0:0:0:0: +414,288,157782,6,0,L|421:221,1,50,8|0,0:0|3:0,0:0:0:0: +315,130,158004,2,0,L|332:193,1,50,8|0,0:0|3:0,0:0:0:0: +492,100,158226,2,0,L|426:91,1,50,12|2,1:2|3:2,0:0:0:0: +214,158,158449,2,0,L|202:97,1,50,10|0,1:2|3:0,0:0:0:0: +342,13,158671,1,8,1:2:0:0: +342,13,158782,1,8,1:2:0:0: +342,13,158893,2,0,L|339:46,2,25,8|0|8,1:2|3:0|1:2,0:0:0:0: +332,156,159115,5,0,1:0:0:0: +332,156,159171,1,0,3:0:0:0: +332,156,159226,2,0,P|352:156|380:166,1,50,8|0,1:2|0:0,0:0:0:0: +456,260,159449,2,0,P|456:280|446:308,1,50,8|2,1:2|0:0,0:0:0:0: +340,368,159671,1,8,1:2:0:0: +340,368,159782,2,0,P|320:368|292:358,1,50 +44,272,160004,6,0,P|152:268|212:196,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: +324,28,160337,1,0,0:0:0:0: +264,100,160449,1,8,0:2:0:0: +312,180,160560,1,0,0:0:0:0: +404,164,160671,6,0,P|400:248|348:320,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +184,328,161004,1,0,0:0:0:0: +96,352,161115,1,0,0:0:0:0: +32,280,161226,1,0,0:0:0:0: +56,192,161337,6,0,L|48:104,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +184,24,161560,6,0,P|228:76|344:32,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +476,176,161893,1,0,0:0:0:0: +384,196,162004,1,0,0:0:0:0: +356,280,162115,1,0,0:0:0:0: +416,352,162226,6,0,L|240:360,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +64,312,162560,1,2,2:0:0:0: +128,244,162671,1,8,0:2:0:0: +152,156,162782,1,0,0:0:0:0: +96,80,162893,6,0,P|120:40|168:12,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +361,18,163115,2,0,P|395:43|416:80,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +256,224,163337,2,0,L|256:124,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +356,280,163560,5,8,0:2:0:0: +356,280,163671,1,0,0:0:0:0: +356,280,163782,1,0,0:0:0:0: +356,280,163893,1,0,0:0:0:0: +356,280,164004,2,0,P|268:268|212:348,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +124,344,164337,1,0,0:0:0:0: +92,256,164449,2,0,P|140:180|96:92,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +180,184,164782,5,0,0:0:0:0: +180,184,164893,2,0,L|284:176,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +500,88,165115,6,0,P|418:70|345:110,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: +320,236,165449,1,0,0:0:0:0: +320,236,165560,2,0,L|332:336,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +432,156,165782,6,0,P|408:56|320:16,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +216,64,166115,5,2,0:2:0:0: +216,64,166226,2,0,L|126:70,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +143,208,166449,2,0,P|122:246|132:311,1,85.0000016212464 +312,291,166671,2,0,P|273:270|208:280,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +379,185,166893,2,0,P|417:205|482:195,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +456,64,167115,6,0,P|344:44|284:84,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +112,148,167449,1,0,0:0:0:0: +160,228,167560,1,8,0:2:0:0: +248,256,167671,1,0,0:0:0:0: +336,228,167782,6,0,P|432:216|480:272,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +392,308,168115,5,0,0:0:0:0: +336,228,168226,2,0,L|332:124,1,85.0000016212464 +448,32,168449,2,0,L|452:136,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +392,308,168671,6,0,P|324:272|216:320,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +156,340,169004,1,0,0:0:0:0: +88,272,169115,2,0,L|124:176,1,85.0000016212464 +88,56,169337,6,0,P|188:100|284:44,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +416,28,169671,1,2,0:2:0:0: +360,104,169782,1,8,0:2:0:0: +352,196,169893,1,0,0:0:0:0: +396,280,170004,6,0,P|400:328|432:364,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +272,296,170226,2,0,P|268:248|236:212,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +68,288,170449,2,0,P|116:276|168:288,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +324,148,170671,6,0,P|256:168|244:56,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +0,244,171004,2,0,L|180:264,1,170.000003242493 +460,240,171337,6,0,P|464:280|456:356,1,85.0000016212464 +336,284,171560,2,0,P|332:244|340:168,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +388,40,171782,2,0,P|412:80|416:132,1,85.0000016212464 +204,80,172004,2,0,P|252:80|308:96,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +208,192,172226,2,0,P|160:192|104:176,1,85.0000016212464 +256,48,172449,6,0,P|312:56|352:68,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +400,192,172671,2,0,P|392:248|380:288,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +256,336,172893,2,0,P|200:328|160:316,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +112,192,173115,2,0,P|120:136|132:96,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +400,192,173337,6,0,P|392:248|380:288,1,85.0000016212464,10|8,0:2|0:2,0:0:0:0: +256,48,173560,2,0,P|312:56|352:68,1,85.0000016212464,0|8,0:0|0:2,0:0:0:0: +172,320,173782,1,2,0:2:0:0: +117,245,173893,1,0,0:0:0:0: +116,153,174004,1,8,0:2:0:0: +169,78,174115,1,8,0:2:0:0: +256,48,174226,6,0,P|334:91|329:184,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: +168,268,174560,1,0,0:0:0:0: +238,207,174671,1,8,0:2:0:0: +329,194,174782,1,0,0:0:0:0: +413,232,174893,2,0,L|426:331,2,85.0000016212464,0|0|8,0:0|0:0|0:2,0:0:0:0: +344,150,175226,1,0,0:0:0:0: +344,150,175337,2,0,L|261:134,1,85.0000016212464 +478,62,175560,2,0,L|466:164,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +322,328,175782,6,0,P|253:282|149:306,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +50,366,176115,1,0,0:0:0:0: +91,283,176226,1,0,0:0:0:0: +81,191,176337,1,0,0:0:0:0: +24,119,176449,2,0,P|93:165|197:141,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +327,79,176782,2,0,L|306:252,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: +125,323,177115,6,0,P|161:302|203:298,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +389,256,177337,2,0,P|347:259|297:243,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +180,72,177560,2,0,P|221:88|261:137,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +66,257,177782,1,8,0:2:0:0: +64,235,177893,1,0,0:0:0:0: +63,214,178004,1,0,0:0:0:0: +62,193,178115,1,0,0:0:0:0: +61,172,178226,6,0,P|131:162|136:254,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +4,96,178560,1,0,0:0:0:0: +81,61,178671,1,8,0:2:0:0: +164,73,178782,1,0,0:0:0:0: +227,128,178893,1,0,0:0:0:0: +251,209,179004,1,0,0:0:0:0: +228,289,179115,2,0,P|213:252|218:200,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +401,62,179337,6,0,P|337:115|250:106,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: +93,212,179671,1,0,0:0:0:0: +93,212,179782,2,0,P|143:218|184:248,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +334,366,180004,2,0,P|339:312|345:185,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +311,33,180337,2,0,P|265:16|223:22,2,85.0000016212464,2|8|0,2:0|0:2|0:0,0:0:0:0: +289,212,180671,5,0,0:0:0:0: +196,198,180782,1,0,0:0:0:0: +255,124,180893,1,8,0:2:0:0: +301,221,181004,5,0,0:0:0:0: +181,203,181115,1,8,0:2:0:0: +257,108,181226,1,0,0:0:0:0: +372,147,181337,6,0,P|407:214|372:301,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +163,238,181671,1,0,0:0:0:0: +74,225,181782,1,8,0:2:0:0: +18,294,181893,1,0,0:0:0:0: +50,378,182004,2,0,P|161:346|236:375,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +355,315,182337,1,0,0:0:0:0: +355,315,182449,2,0,P|346:273|355:211,1,85.0000016212464 +423,86,182671,2,0,P|432:128|423:190,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +214,113,182893,6,0,L|29:142,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +181,224,183226,1,0,0:0:0:0: +264,237,183337,1,0,0:0:0:0: +348,250,183449,1,0,0:0:0:0: +431,262,183560,2,0,P|446:302|448:344,2,85.0000016212464,10|2|0,0:2|0:2|0:0,0:0:0:0: +282,68,183893,2,0,P|211:119|219:199,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: +391,175,184226,6,0,L|307:161,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +132,298,184449,2,0,L|186:232,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +154,12,184671,2,0,L|183:91,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +395,82,184893,5,12,0:2:0:0: +473,129,185004,1,0,0:0:0:0: +391,175,185115,2,0,P|341:179|286:160,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +105,112,185337,5,8,0:2:0:0: +26,158,185449,1,0,0:0:0:0: +108,204,185560,2,0,P|157:207|212:188,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +314,35,185782,5,8,0:2:0:0: +230,71,185893,1,0,0:0:0:0: +188,153,186004,1,2,0:2:0:0: +206,243,186115,1,0,0:0:0:0: +277,300,186226,2,0,P|319:313|381:310,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +469,143,186449,2,2,P|423:143|383:163,1,85.0000016212464,2|0,0:2|0:0,0:1:0:0: +180,96,186671,5,8,0:2:0:0: +180,96,186782,1,8,0:0:0:0: +180,96,186893,1,8,0:0:0:0: +333,236,187115,2,0,L|346:127,1,85.0000016212464,8|0,0:2|3:0,0:0:0:0: +404,53,187337,2,0,L|398:10,2,42.5000008106232,8|0|0,3:2|3:0|3:0,0:0:0:0: +426,195,187560,1,8,0:2:0:0: +426,195,187671,5,0,0:0:0:0: +240,159,187893,1,0,0:0:0:0: +350,339,188115,1,0,0:0:0:0: +70,296,188449,5,12,0:0:0:0: +432,192,195560,5,4,0:0:0:0: +432,168,195671,1,0,3:0:0:0: +424,144,195782,1,0,3:0:0:0: +416,120,195893,1,0,3:0:0:0: +400,96,196004,1,0,3:0:0:0: +384,80,196115,1,0,3:0:0:0: +368,64,196226,1,0,3:0:0:0: +344,48,196337,1,0,3:0:0:0: +320,40,196449,1,0,3:0:0:0: +296,40,196560,1,0,3:0:0:0: +272,40,196671,1,0,3:0:0:0: +248,48,196782,1,0,3:0:0:0: +224,64,196893,1,0,3:0:0:0: +208,80,197004,1,0,3:0:0:0: +192,104,197115,1,2,3:2:0:0: +184,128,197226,1,0,3:0:0:0: +176,152,197337,5,0,3:0:0:0: +160,168,197449,1,0,3:0:0:0: +136,184,197560,1,0,3:0:0:0: +112,192,197671,1,0,3:0:0:0: +96,208,197782,1,0,3:0:0:0: +80,232,197893,1,2,3:2:0:0: +80,256,198004,1,0,3:0:0:0: +88,280,198115,1,0,3:0:0:0: +112,296,198226,1,2,3:2:0:0: +136,304,198337,1,0,3:0:0:0: +160,304,198449,1,0,3:0:0:0: +184,296,198560,1,2,3:2:0:0: +200,280,198671,1,0,3:0:0:0: +224,264,198782,1,0,3:0:0:0: +248,256,198893,1,2,3:2:0:0: +272,264,199004,1,2,3:2:0:0: +296,280,199115,5,0,3:0:0:0: +320,288,199226,1,0,3:0:0:0: +344,288,199337,1,2,3:2:0:0: +368,280,199449,1,0,3:0:0:0: +392,264,199560,1,0,3:0:0:0: +408,248,199671,1,0,3:0:0:0: +424,224,199782,1,0,3:0:0:0: +432,200,199893,1,0,3:0:0:0: +432,176,200004,1,0,3:0:0:0: +424,152,200115,1,0,3:0:0:0: +416,128,200226,1,2,3:2:0:0: +400,104,200337,1,0,3:0:0:0: +384,88,200449,1,0,3:0:0:0: +360,72,200560,1,0,3:0:0:0: +336,64,200671,1,2,3:2:0:0: +312,64,200782,1,0,3:0:0:0: +288,72,200893,6,2,L|280:32,1,25,0|0,3:0|3:0,0:1:0:0: +264,80,201004,2,2,L|256:40,1,25,0|0,3:0|3:0,0:1:0:0: +240,88,201115,2,0,L|232:48,1,25,0|0,3:0|3:0,0:0:0:0: +216,96,201226,2,0,L|208:56,1,25,0|0,3:0|3:0,0:0:0:0: +192,104,201337,2,2,L|184:64,1,25,8|0,3:2|3:0,0:1:0:0: +168,112,201449,2,0,L|160:72,1,25,0|0,3:0|3:0,0:0:0:0: +144,120,201560,2,0,L|136:80,1,25,8|0,3:2|3:0,0:0:0:0: +120,128,201671,2,2,L|115:103,1,25,0|0,3:0|3:0,0:1:0:0: +96,136,201782,6,0,L|77:100,1,25,8|0,3:2|3:0,0:0:0:0: +57,173,201893,2,0,L|23:151,1,25,8|0,3:2|3:0,0:0:0:0: +41,224,202004,2,2,L|1:222,1,25,0|0,3:0|3:0,0:1:0:0: +53,277,202115,2,0,L|17:295,1,25,8|0,3:2|3:0,0:0:0:0: +90,316,202226,2,0,L|68:349,1,25,8|0,3:2|3:0,0:0:0:0: +141,332,202337,2,2,L|139:371,1,25,0|0,3:0|3:0,0:1:0:0: +194,320,202449,2,0,L|212:355,1,25,8|0,3:2|3:0,0:0:0:0: +233,283,202560,2,0,L|266:304,1,25,8|0,3:2|3:0,0:0:0:0: +249,231,202671,6,0,L|432:216,1,150.000005722046,12|0,0:0|0:0,0:0:0:0: +496,104,203115,5,8,0:0:0:0: +400,328,203337,1,0,0:0:0:0: +400,328,203449,1,0,0:0:0:0: +400,328,203560,2,0,P|352:296|336:256,1,100,10|0,0:0|0:0,0:0:0:0: +296,104,203893,5,0,0:0:0:0: +296,104,204004,2,0,L|352:112,1,50,8|0,0:0|0:0,0:0:0:0: +160,168,204226,1,0,0:0:0:0: +160,168,204337,1,0,0:0:0:0: +160,168,204449,2,0,P|112:160|56:192,1,100,10|2,0:0|0:0,0:0:0:0: +136,320,204893,5,8,0:0:0:0: +304,224,205115,1,0,0:0:0:0: +328,224,205226,1,0,0:0:0:0: +352,224,205337,1,10,0:0:0:0: +464,144,205560,5,0,0:0:0:0: +464,144,205671,1,0,0:0:0:0: +464,144,205782,2,2,P|480:112|480:96,1,50,8|0,0:0|0:0,0:1:0:0: +400,320,206004,2,2,P|384:352|384:368,1,50,0|0,3:0|0:0,0:1:0:0: +304,224,206226,6,0,P|248:200|184:224,1,100,10|2,0:0|0:0,0:0:0:0: +24,296,206671,1,8,0:0:0:0: +160,104,206893,1,0,0:0:0:0: +248,304,207115,2,0,P|304:328|368:304,1,100,10|0,0:0|0:0,0:0:0:0: +340,321,207782,6,0,L|316:193,1,100,2|8,0:0|0:0,0:0:0:0: +376,72,208226,1,2,0:0:0:0: +144,152,208449,1,8,0:0:0:0: +256,336,208671,1,0,0:0:0:0: +272,40,208893,5,10,0:0:0:0: +112,304,209115,1,8,0:0:0:0: +440,224,209337,6,2,P|480:224|512:232,1,50,8|0,0:0|3:0,0:1:0:0: +440,320,209560,2,2,P|400:320|368:312,1,50,8|0,0:0|3:0,0:1:0:0: +248,232,209782,5,8,3:2:0:0: +216,216,209893,1,0,3:0:0:0: +184,208,210004,1,2,3:0:0:0: +152,208,210115,1,0,0:0:0:0: +121,223,210226,6,2,L|109:132,1,50,8|0,3:2|3:0,0:1:0:0: +53,84,210449,1,2,3:0:0:0: +53,84,210560,2,0,P|108:67|180:81,1,100,0|0,0:0|3:0,0:0:0:0: +339,150,210893,1,0,3:0:0:0: +339,150,211004,1,2,3:0:0:0: +339,150,211115,2,0,L|202:141,1,100,8|2,0:0|3:0,0:0:0:0: +406,107,211449,5,0,3:0:0:0: +406,107,211560,2,0,P|380:155|400:244,1,150,8|0,0:2|0:0,0:0:0:0: +461,315,212004,6,0,L|533:308,1,50,8|0,3:2|3:0,0:0:0:0: +308,351,212226,1,2,3:0:0:0: +308,351,212337,2,0,P|255:332|177:351,1,100,0|0,0:0|3:0,0:0:0:0: +64,352,212671,1,0,3:0:0:0: +64,352,212782,1,2,3:0:0:0: +64,352,212893,2,0,L|96:240,1,100,8|4,0:0|3:2,0:0:0:0: +56,96,213226,5,0,0:0:0:0: +56,96,213337,2,0,P|113:140|185:126,1,150,12|0,0:0|0:0,0:0:0:0: +232,104,213782,1,8,0:0:0:0: +280,80,213893,1,0,0:0:0:0: +328,72,214004,1,0,0:0:0:0: +376,80,214115,1,0,0:0:0:0: +416,104,214226,1,10,0:0:0:0: +448,144,214337,1,0,0:0:0:0: +456,192,214449,1,2,0:0:0:0: +448,240,214560,1,0,0:0:0:0: +416,280,214671,1,8,0:0:0:0: +376,304,214782,1,0,0:0:0:0: +328,312,214893,1,10,0:0:0:0: +280,304,215004,1,0,0:0:0:0: +240,280,215115,6,0,L|192:280,1,25,8|0,3:2|3:0,0:0:0:0: +215,280,215226,2,0,L|176:280,1,25,0|0,3:0|3:0,0:0:0:0: +190,280,215337,2,0,L|160:280,1,25,2|0,3:2|3:0,0:0:0:0: +165,280,215449,2,0,L|120:280,1,25,0|0,3:0|3:0,0:0:0:0: +112,280,215560,5,8,0:0:0:0: +32,224,215671,1,8,0:0:0:0: +120,176,215782,2,0,L|120:128,2,25,8|8|8,0:0|0:0|0:0,0:0:0:0: +240,64,216115,5,0,1:0:0:0: +344,224,216337,1,0,1:0:0:0: +448,32,216560,1,0,1:0:0:0: +145,84,216893,5,12,0:0:0:0: +198,332,217115,2,0,L|219:193,1,119.999996337891,2|8,0:0|0:0,0:0:0:0: +308,165,217449,1,0,0:0:0:0: +308,165,217560,1,0,0:0:0:0: +81,244,217782,1,8,0:0:0:0: +76,214,217893,1,0,0:0:0:0: +72,184,218004,1,2,0:0:0:0: +67,155,218115,1,0,0:0:0:0: +63,125,218226,1,8,0:0:0:0: +200,49,218449,6,0,P|267:85|382:74,1,179.999994506836,2|0,0:0|0:0,0:0:0:0: +422,36,218893,2,0,P|355:108|357:200,1,179.999994506836,2|0,0:0|0:0,0:0:0:0: +512,151,219337,2,0,L|452:142,1,59.9999981689454,2|0,0:0|0:0,0:0:0:0: +318,194,219560,6,2,L|263:202,5,29.9999990844727,8|0|0|0|0|0,0:0|0:0|0:0|0:0|0:0|0:0,0:1:0:0: +424,218,219893,1,0,0:0:0:0: +426,247,220004,1,8,0:0:0:0: +429,276,220115,1,0,0:0:0:0: +432,305,220226,6,0,B|377:333|377:333|325:317|325:317|272:340|201:317,1,239.999992675781,2|0,0:0|0:0,0:0:0:0: +97,166,220893,1,8,0:0:0:0: +269,215,221115,2,0,L|278:135,1,59.9999981689454,2|0,0:0|0:0,0:0:0:0: +159,41,221337,1,8,3:2:0:0: +163,78,221449,1,0,3:0:0:0: +167,115,221560,1,0,3:0:0:0: +171,152,221671,1,2,3:2:0:0: +176,189,221782,2,0,L|167:270,1,59.9999981689454,8|0,0:0|0:0,0:0:0:0: +24,352,222004,6,0,L|203:335,1,179.999994506836,2|0,0:0|0:0,0:0:0:0: +346,297,222449,2,0,L|166:280,1,179.999994506836,2|0,0:0|3:0,0:0:0:0: +68,237,222893,1,2,3:2:0:0: +103,233,223004,1,0,3:0:0:0: +139,230,223115,6,2,L|216:238,1,59.9999981689454,8|0,0:0|0:0,0:1:0:0: +377,324,223337,2,2,L|419:260,1,59.9999981689454,8|0,0:0|0:0,0:1:0:0: +278,145,223560,2,0,L|290:227,1,59.9999981689454,8|0,0:0|0:0,0:0:0:0: +452,83,223782,2,0,L|375:91,1,59.9999981689454,8|0,0:0|0:0,0:0:0:0: +186,134,224004,5,8,3:2:0:0: +146,131,224115,1,0,3:0:0:0: +109,116,224226,1,2,3:0:0:0: +77,92,224337,1,0,0:0:0:0: +15,29,224449,5,8,3:2:0:0: +11,78,224560,1,0,3:0:0:0: +8,128,224671,1,2,3:0:0:0: +4,178,224782,1,0,0:0:0:0: +89,198,224893,5,8,3:2:0:0: +96,237,225004,1,0,3:0:0:0: +103,276,225115,1,0,3:0:0:0: +111,315,225226,1,2,3:0:0:0: +206,281,225337,5,8,0:0:0:0: +245,287,225449,1,0,3:0:0:0: +285,293,225560,1,2,3:0:0:0: +324,299,225671,1,0,0:0:0:0: +408,249,225782,5,8,3:2:0:0: +373,213,225893,1,0,3:0:0:0: +326,200,226004,1,0,3:0:0:0: +278,213,226115,1,0,3:0:0:0: +206,281,226226,5,8,3:2:0:0: +182,237,226337,1,0,3:0:0:0: +182,188,226449,1,0,3:0:0:0: +207,144,226560,1,0,0:0:0:0: +306,164,226671,5,8,3:2:0:0: +354,156,226782,1,0,3:0:0:0: +392,124,226893,1,0,3:0:0:0: +407,77,227004,1,2,3:0:0:0: +301,55,227115,5,8,0:0:0:0: +303,109,227226,1,2,3:0:0:0: +306,164,227337,1,0,3:0:0:0: +308,219,227449,1,0,0:0:0:0: +360,304,227560,5,8,3:2:0:0: +301,297,227671,1,0,3:2:0:0: +246,321,227782,2,0,P|216:329|176:319,1,59.9999981689454,2|0,3:2|0:0,0:0:0:0: +49,207,228004,2,0,L|56:281,1,59.9999981689454,8|0,3:2|1:0,0:0:0:0: +208,185,228226,2,0,L|215:111,1,59.9999981689454,2|0,3:2|1:0,0:0:0:0: +44,29,228449,5,10,3:2:0:0: +40,58,228560,1,0,3:0:0:0: +36,88,228671,1,0,3:0:0:0: +32,117,228782,1,8,0:0:0:0: +123,105,228893,1,8,3:2:0:0: +212,92,229004,6,0,L|254:72,1,29.9999990844727,0|0,3:0|3:0,3:0:0:0: +260,125,229115,2,0,L|300:123,1,29.9999990844727,10|0,3:2|3:0,3:0:0:0: +282,180,229226,2,0,L|319:193,1,29.9999990844727,0|0,3:0|3:0,3:0:0:0: +269,237,229337,5,8,3:2:0:0: +219,237,229449,1,0,3:0:0:0: +174,257,229560,1,2,3:2:0:0: +142,295,229671,1,0,0:0:0:0: +245,353,229782,5,8,3:2:0:0: +293,340,229893,1,8,3:2:0:0: +342,349,230004,2,0,L|377:334,1,29.9999990844727,2|0,3:2|3:0,0:0:0:0: +383,376,230115,1,0,3:0:0:0: +467,284,230226,5,0,1:0:0:0: +464,234,230337,1,8,1:2:0:0: +461,184,230449,1,0,0:0:0:0: +458,134,230560,1,8,1:2:0:0: +310,42,230671,5,0,3:0:0:0: +300,90,230782,1,8,1:2:0:0: +322,133,230893,1,0,3:0:0:0: +367,154,231004,1,0,0:0:0:0: +458,134,231115,6,0,L|465:242,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +369,307,231782,2,0,P|310:301|248:334,1,119.999996337891 +137,289,232449,1,0,0:0:0:0: +137,289,232671,2,0,B|124:242|124:242|140:160,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: +159,135,233337,2,0,P|221:159|296:148,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: +375,81,234004,1,2,0:0:0:0: +389,206,234226,1,2,0:0:0:0: +273,155,234449,1,2,0:0:0:0: +143,320,235115,6,0,P|80:311|26:342,2,119.999996337891 +253,363,236226,2,0,B|274:307|240:278|261:239,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: +303,208,236893,2,0,L|164:197,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: +76,264,237560,1,0,0:0:0:0: +48,170,237782,1,2,0:0:0:0: +99,88,238004,1,10,0:0:0:0: +195,72,238226,6,0,P|245:87|301:87,1,89.9999972534181,4|0,0:0|0:0,0:0:0:0: +430,40,238893,2,0,P|441:102|450:172,1,119.999996337891 +443,280,239560,1,0,0:0:0:0: +340,214,239782,2,0,P|325:155|343:98,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: +430,40,240449,2,0,P|384:47|286:27,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: +195,72,241115,1,0,0:0:0:0: +181,191,241337,1,0,0:0:0:0: +291,143,241560,1,2,0:0:0:0: +195,72,241782,6,0,P|143:75|97:107,1,89.9999972534181,2|0,0:0|0:0,0:0:0:0: +90,271,242449,2,0,P|196:312|291:241,1,239.999992675781,2|2,0:0|0:0,0:0:0:0: +289,243,243449,2,0,P|328:264|373:264,1,89.9999972534181,2|0,0:0|3:0,0:0:0:0: +465,186,244004,1,0,3:0:0:0: +409,80,244226,5,0,3:0:0:0: +379,82,244337,1,0,3:0:0:0: +349,84,244449,1,10,0:0:0:0: +321,87,244560,2,0,L|271:91,1,29.9999990844727,8|0,1:2|0:0,0:0:0:0: +160,48,244782,5,8,1:2:0:0: +152,72,244893,1,10,0:0:0:0: +144,96,245004,1,8,0:0:0:0: +136,120,245115,1,8,0:0:0:0: +128,144,245226,1,8,0:0:0:0: +72,184,245337,6,0,L|212:196,1,100,12|0,0:0|0:0,0:0:0:0: +208,232,245671,2,0,P|256:241|299:217,1,100 +320,176,246004,2,0,L|312:76,1,100,0|8,0:0|0:0,0:0:0:0: +200,136,246449,5,0,0:0:0:0: +304,256,246671,1,8,0:0:0:0: +400,120,246893,2,0,L|536:112,1,100,2|8,0:0|0:0,0:0:0:0: +336,176,247337,5,0,0:0:0:0: +336,176,247449,1,0,0:0:0:0: +336,176,247560,2,0,P|288:152|216:184,1,100,10|0,0:0|0:0,0:0:0:0: +244,160,248004,1,8,0:0:0:0: +80,216,248226,5,2,0:0:0:0: +280,256,248449,1,10,0:0:0:0: +192,80,248671,2,0,L|192:200,1,100,2|8,0:0|0:0,0:0:0:0: +280,256,249115,1,0,0:0:0:0: +392,208,249337,6,0,P|408:256|368:328,1,100,8|0,0:0|0:0,0:0:0:0: +280,256,249782,2,0,L|144:264,1,100,8|0,0:0|0:0,0:0:0:0: +72,184,250226,1,8,0:0:0:0: +120,80,250449,2,0,P|161:99|185:179,1,100,2|8,0:0|0:0,0:0:0:0: +136,280,250893,5,0,0:0:0:0: +136,280,251115,2,0,L|352:272,1,200,10|8,0:0|0:0,0:0:0:0: +416,152,251782,5,0,0:0:0:0: +488,80,252004,1,10,0:0:0:0: +416,8,252226,1,2,0:0:0:0: +344,80,252449,6,0,L|232:88,1,100,10|0,0:0|0:0,0:0:0:0: +192,48,252782,2,0,P|200:72|200:168,1,100 +152,176,253115,2,0,L|288:192,1,100,0|8,0:0|0:0,0:0:0:0: +448,176,253560,5,0,0:0:0:0: +344,304,253782,1,8,0:0:0:0: +272,152,254004,2,0,L|272:16,1,100,2|8,0:0|0:0,0:0:0:0: +328,208,254449,5,0,0:0:0:0: +328,208,254671,2,0,P|264:200|208:216,1,100,10|0,0:0|0:0,0:0:0:0: +72,288,255115,1,8,0:0:0:0: +136,168,255337,6,0,L|112:104,1,50,2|0,0:0|0:0,0:0:0:0: +216,32,255560,2,0,L|232:88,1,50,10|0,0:0|0:0,0:0:0:0: +328,160,255782,2,0,L|347:114,1,50,2|0,0:0|0:0,0:0:0:0: +424,224,256004,6,0,L|451:233,1,25,10|0,2:2|2:0,2:0:0:0: +390,272,256115,2,0,L|407:295,1,25,0|0,2:0|2:0,2:0:0:0: +333,291,256226,2,0,L|333:319,1,25,2|0,2:2|2:0,2:0:0:0: +277,272,256337,2,0,L|261:295,1,25,0|0,2:0|2:0,2:0:0:0: +242,224,256449,2,0,L|216:233,1,25,10|0,2:2|2:0,2:0:0:0: +224,168,256560,2,0,L|251:159,1,25,0|0,2:0|2:0,2:0:0:0: +190,120,256671,2,0,L|207:97,1,25,2|0,2:2|2:0,2:0:0:0: +133,101,256782,2,0,L|133:73,1,25,0|0,2:0|2:0,2:0:0:0: +77,120,256893,2,8,L|61:97,1,25,8|0,2:2|2:0,2:0:0:0: +42,168,257004,2,8,L|16:159,1,25,8|0,2:2|2:0,2:0:0:0: +42,227,257115,2,0,L|15:236,1,25,4|0,2:2|2:0,2:0:0:0: +76,275,257226,2,0,L|59:298,1,25,0|0,2:0|2:0,2:0:0:0: +133,294,257337,2,0,L|133:322,1,25,8|0,2:2|2:0,2:0:0:0: +189,275,257449,2,8,L|205:298,1,25,0|0,2:0|2:0,2:0:0:0: +224,227,257560,2,8,L|250:236,1,25,12|0,2:2|2:0,2:0:0:0: +224,168,257671,2,8,L|250:159,1,25,0|0,2:0|2:0,2:0:0:0: +190,120,257782,5,8,0:0:0:0: +133,101,257893,1,8,0:0:0:0: +77,120,258004,1,8,0:0:0:0: +224,227,258226,1,8,0:0:0:0: +258,275,258337,1,8,0:0:0:0: +315,294,258449,1,8,0:0:0:0: +371,275,258560,1,8,0:0:0:0: +406,227,258671,1,8,0:0:0:0: +412,167,258782,1,8,0:0:0:0: +391,110,258893,6,0,L|326:101,2,50,0|12|8,0:0|0:3|0:0,0:0:0:0: +315,194,259226,1,0,1:0:0:0: +315,194,259337,2,0,L|250:203,1,50,8|8,0:0|0:0,0:0:0:0: +88,95,259560,6,0,P|158:52|237:76,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: +362,269,259893,1,0,0:0:0:0: +272,288,260004,1,8,0:2:0:0: +194,241,260115,1,0,0:0:0:0: +171,153,260226,2,0,L|178:58,2,85.0000016212464,0|0|8,0:0|0:0|0:2,0:0:0:0: +106,216,260560,1,0,0:0:0:0: +24,177,260671,2,0,L|29:261,1,85.0000016212464 +165,349,260893,2,0,L|170:264,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +394,84,261115,6,0,P|343:147|251:128,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +394,84,261449,1,0,0:0:0:0: +425,169,261560,1,0,0:0:0:0: +388,252,261671,1,0,0:0:0:0: +303,285,261782,2,0,P|268:267|206:267,2,85.0000016212464,10|2|0,0:2|0:2|0:0,0:0:0:0: +454,347,262115,2,0,L|463:165,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: +297,56,262449,6,0,P|294:98|309:137,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +491,104,262671,2,0,P|443:101|409:118,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +223,263,262893,2,0,L|215:157,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +68,53,263115,1,8,0:2:0:0: +89,55,263226,1,0,0:0:0:0: +110,57,263337,1,0,0:0:0:0: +131,60,263449,1,0,0:0:0:0: +152,62,263560,6,0,P|237:73|284:142,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +87,182,263893,1,0,0:0:0:0: +87,182,264004,2,0,P|39:237|58:348,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +269,306,264337,1,0,0:0:0:0: +269,306,264449,2,0,L|186:290,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +283,137,264671,6,0,B|406:160|406:160|360:178,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: +165,193,265004,1,0,0:0:0:0: +165,193,265115,2,0,B|150:152|150:152|155:110,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +299,286,265337,2,0,P|339:271|391:274,2,85.0000016212464,10|2|0,0:2|0:2|0:0,0:0:0:0: +428,374,265671,1,2,0:2:0:0: +487,305,265782,1,8,0:2:0:0: +476,215,265893,1,0,0:0:0:0: +396,164,266004,6,0,L|274:178,1,85.0000016212464 +193,245,266226,2,0,L|108:235,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +210,73,266449,2,0,P|253:58|306:64,1,85.0000016212464 +438,263,266671,6,0,B|387:245|392:181|392:181|370:212|323:212,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +153,138,267004,1,0,0:0:0:0: +66,161,267115,1,8,0:2:0:0: +34,245,267226,1,0,0:0:0:0: +84,319,267337,2,0,P|168:311|263:307,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +334,211,267671,1,0,0:0:0:0: +334,211,267782,2,0,P|325:170|336:122,1,85.0000016212464 +458,77,268004,2,0,P|467:118|456:166,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +321,354,268226,6,0,P|253:306|164:315,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: +34,201,268560,1,0,0:0:0:0: +34,202,268671,2,0,L|129:194,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +263,87,268893,2,0,P|177:90|129:157,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +326,305,269226,1,2,0:2:0:0: +334,211,269337,1,8,0:2:0:0: +343,118,269449,1,0,0:0:0:0: +352,25,269560,6,0,L|362:109,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +223,176,269782,2,0,L|212:260,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +446,266,270004,2,0,P|408:240|357:234,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +113,288,270226,6,0,P|65:224|76:127,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +165,55,270560,1,0,0:0:0:0: +232,116,270671,1,8,0:2:0:0: +323,130,270782,1,0,0:0:0:0: +407,90,270893,2,0,P|426:124|432:182,1,85.0000016212464 +317,319,271115,5,8,0:2:0:0: +317,225,271226,1,0,0:0:0:0: +406,196,271337,1,0,0:0:0:0: +460,272,271449,1,0,0:0:0:0: +406,348,271560,1,8,0:2:0:0: +317,319,271671,1,0,0:0:0:0: +224,303,271782,2,0,P|178:303|138:324,1,85.0000016212464 +43,164,272004,5,10,0:2:0:0: +127,124,272115,1,0,0:0:0:0: +117,217,272226,1,2,0:2:0:0: +43,164,272337,5,0,0:0:0:0: +77,48,272449,1,10,0:2:0:0: +157,138,272560,1,0,0:0:0:0: +43,164,272671,2,2,P|39:120|56:80,1,85.0000016212464,2|0,0:2|0:0,0:1:0:0: +279,26,272893,6,0,L|416:16,1,85.0000016212464,10|8,0:2|0:2,0:0:0:0: +504,120,273115,2,0,L|367:130,1,85.0000016212464,0|8,0:0|0:2,0:0:0:0: +176,120,273337,5,10,0:2:0:0: +216,208,273449,1,8,0:2:0:0: +184,296,273560,1,8,0:2:0:0: +112,352,273671,1,8,0:2:0:0: +16,344,273782,6,0,L|224:328,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: +464,288,274115,2,0,P|392:312|288:248,1,170.000003242493 +105,186,274449,5,0,0:0:0:0: +143,95,274560,1,0,0:0:0:0: +233,58,274671,1,8,0:2:0:0: +324,95,274782,1,0,3:0:0:0: +361,186,274893,1,0,0:0:0:0: +324,276,275004,1,0,0:0:0:0: +233,314,275115,6,0,L|128:320,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +0,216,275337,2,0,P|96:168|192:232,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: +324,276,275671,1,0,0:0:0:0: +392,336,275782,2,0,P|413:299|414:257,1,85.0000016212464 +320,155,276004,6,0,P|272:217|324:276,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +424,368,276337,2,0,L|456:200,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: +360,24,276671,6,0,P|352:64|408:112,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +232,112,276893,2,0,P|192:104|144:160,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +200,296,277115,2,0,P|192:256|248:208,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +360,144,277337,5,8,0:2:0:0: +360,144,277449,1,0,0:0:0:0: +360,144,277560,1,0,0:0:0:0: +360,144,277671,1,0,0:0:0:0: +360,144,277782,2,0,L|336:328,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +256,352,278115,1,0,0:0:0:0: +168,328,278226,2,0,P|241:264|351:328,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +184,352,278560,5,0,0:0:0:0: +184,352,278671,2,0,L|80:360,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +307,274,278893,2,0,P|400:304|480:264,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: +504,136,279226,5,0,0:0:0:0: +504,137,279337,2,0,L|415:124,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +232,64,279560,2,0,P|200:136|256:200,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +488,264,279893,5,2,0:2:0:0: +400,232,280004,1,8,0:2:0:0: +312,256,280115,1,0,0:0:0:0: +248,320,280226,6,0,P|280:352|336:344,1,85.0000016212464 +120,280,280449,2,0,L|24:272,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +216,192,280671,2,0,P|264:184|312:200,1,85.0000016212464 +160,104,280893,6,0,P|64:136|8:80,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: +201,22,281226,2,0,L|217:214,1,170.000003242493 +376,336,281560,5,0,0:0:0:0: +416,248,281671,1,0,0:0:0:0: +320,264,281782,1,8,0:2:0:0: +376,336,281893,1,0,0:0:0:0: +416,248,282004,6,0,P|421:207|416:128,1,85.0000016212464 +312,56,282226,2,0,P|306:96|312:176,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: +200,256,282449,5,0,0:0:0:0: +185,163,282560,1,0,0:0:0:0: +92,148,282671,1,8,0:2:0:0: +51,231,282782,1,0,0:0:0:0: +116,298,282893,6,0,L|232:304,1,85.0000016212464 +448,264,283115,2,0,P|368:232|248:304,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: +368,328,283449,2,0,L|392:96,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: +288,32,283782,6,0,L|304:160,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +185,163,284004,2,0,L|128:80,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: +51,231,284226,2,0,L|160:264,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: +360,336,284449,6,0,L|424:352,1,42.5000008106232,10|0,3:2|3:0,0:0:0:0: +384,304,284560,2,0,L|440:320,1,42.5000008106232,0|4,3:0|3:2,0:0:0:0: +408,272,284671,2,0,L|472:288,1,42.5000008106232,2|0,3:2|3:0,0:0:0:0: +432,240,284782,2,0,L|496:256,1,42.5000008106232,0|0,3:0|3:0,0:0:0:0: +376,160,284893,6,0,L|312:176,1,42.5000008106232,10|0,3:2|3:0,0:0:0:0: +352,128,285004,2,0,L|296:144,1,42.5000008106232,0|0,3:0|3:0,0:0:0:0: +328,96,285115,2,0,L|264:112,1,42.5000008106232,2|0,3:2|3:0,0:0:0:0: +304,64,285226,2,0,L|240:80,1,42.5000008106232,0|0,3:0|3:0,0:0:0:0: +160,64,285337,6,0,L|160:8,1,42.5000008106232,8|0,3:2|3:0,0:0:0:0: +112,88,285449,2,0,L|72:48,1,42.5000008106232,0|0,3:0|3:0,0:0:0:0: +84,136,285560,2,2,L|28:136,1,42.5000008106232,4|0,3:2|2:0,0:1:0:0: +84,192,285671,2,0,L|44:231,1,42.5000008106232,0|0,2:0|2:0,2:0:0:0: +176,292,285782,6,0,L|176:348,1,42.5000008106232,8|0,2:2|2:0,2:0:0:0: +216,264,285893,2,0,L|255:303,1,42.5000008106232,0|0,2:0|2:0,2:0:0:0: +240,224,286004,2,2,L|296:224,1,42.5000008106232,2|0,2:2|2:0,2:1:0:0: +252,176,286115,2,0,L|291:136,1,42.5000008106232,0|0,2:0|2:0,2:0:0:0: +164,104,286226,5,8,0:2:0:0: +84,136,286337,1,8,2:0:0:0: +52,216,286449,1,8,2:0:0:0: +216,264,286671,1,8,0:0:0:0: +248,184,286782,1,8,0:0:0:0: +328,152,286893,2,0,L|328:104,2,42.5000008106232,8|0|0,3:2|3:0|3:0,0:0:0:0: +400,184,287115,1,8,0:0:0:0: +440,264,287226,5,0,0:0:0:0: +440,280,287449,1,4,0:3:0:0: +440,296,287671,1,0,0:0:0:0: +160,176,288004,6,0,L|337:155,1,150.000005722046,4|2,0:0|0:0,0:0:0:0: +335,290,288337,1,0,0:0:0:0: +335,290,288449,2,0,L|260:281,1,75.0000028610231,8|0,3:2|3:0,0:0:0:0: +136,148,288671,2,0,L|115:231,1,75.0000028610231,2|0,3:0|0:0,0:0:0:0: +254,138,288893,5,10,0:0:0:0: +223,70,289004,1,0,0:0:0:0: +160,33,289115,1,0,3:0:0:0: +86,39,289226,2,0,P|53:60|30:98,1,75.0000028610231,0|8,3:0|0:0,0:0:0:0: +117,220,289449,1,0,0:0:0:0: +117,220,289560,2,0,L|128:309,2,75.0000028610231,2|0|8,3:0|3:0|3:2,0:0:0:0: +228,182,289893,5,0,3:0:0:0: +268,192,290004,1,2,3:0:0:0: +300,161,290115,1,0,0:0:0:0: +341,171,290226,2,0,L|419:152,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: +243,96,290449,2,0,L|321:77,1,75.0000028610231,2|0,3:2|0:0,0:0:0:0: +140,30,290671,6,0,L|56:51,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: +223,123,290893,1,0,3:0:0:0: +223,123,291004,2,0,L|139:144,1,75.0000028610231,2|8,0:2|3:2,0:0:0:0: +306,216,291226,1,0,3:0:0:0: +233,234,291337,2,0,L|149:255,1,75.0000028610231,2|2,3:2|3:2,0:0:0:0: +60,82,291560,5,8,0:2:0:0: +114,95,291671,1,0,3:0:0:0: +169,109,291782,1,2,3:0:0:0: +223,123,291893,1,0,0:0:0:0: +277,108,292004,2,0,P|311:94|348:92,1,75.0000028610231,8|0,0:2|0:0,0:0:0:0: +479,209,292226,2,0,P|495:244|499:299,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: +285,299,292449,6,0,L|279:377,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: +407,190,292671,1,0,3:0:0:0: +407,190,292782,2,0,L|413:268,1,75.0000028610231,2|8,0:2|3:2,0:0:0:0: +253,128,293004,1,0,3:0:0:0: +248,203,293115,2,0,B|246:234|246:234|273:164|352:157,1,150.000005722046,2|8,3:2|0:2,0:0:0:0: +482,90,293449,5,0,0:0:0:0: +487,164,293560,2,0,L|406:144,1,75.0000028610231,2|0,0:3|0:0,0:0:0:0: +248,203,293782,1,8,3:2:0:0: +196,180,293893,1,0,3:0:0:0: +140,180,294004,1,0,3:0:0:0: +89,202,294115,1,0,3:0:0:0: +51,243,294226,6,0,B|12:200|46:133|46:133|43:157|54:173,1,150.000005722046,10|2,0:2|1:2,0:0:0:0: +92,319,294560,2,0,P|129:312|166:310,1,75.0000028610231,4|8,0:3|0:0,0:0:0:0: +317,351,294782,1,2,0:0:0:0: +399,338,294893,2,0,P|434:311|453:285,1,75.0000028610231,2|0,1:2|0:0,0:0:0:0: +281,104,295115,5,8,0:0:0:0: +247,147,295226,1,0,3:0:0:0: +248,203,295337,2,0,P|290:210|334:204,1,75.0000028610231,2|0,3:0|0:0,0:0:0:0: +281,104,295560,6,0,P|199:131|256:202,1,200,2|2,1:0|0:0,0:0:0:0: +301,78,295893,6,0,L|199:61,1,75.0000028610231,0|8,0:0|0:0,0:0:0:0: +322,206,296115,1,0,3:0:0:0: +378,203,296226,1,2,3:0:0:0: +434,200,296337,1,0,3:0:0:0: +490,197,296449,2,0,P|492:159|482:124,1,75.0000028610231,8|0,0:0|0:0,0:0:0:0: +384,359,296671,2,0,P|418:345|446:320,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: +295,164,296893,5,8,3:2:0:0: +242,144,297004,1,0,3:2:0:0: +187,149,297115,1,0,3:2:0:0: +140,179,297226,1,0,0:0:0:0: +112,227,297337,2,0,L|104:142,1,75.0000028610231,8|0,0:0|0:0,0:0:0:0: +0,63,297560,2,0,P|34:76|71:80,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: +259,18,297782,6,0,P|221:22|187:35,2,75.0000028610231,10|0|0,0:0|3:0|0:0,0:0:0:0: +383,73,298115,2,0,P|404:103|418:138,2,75.0000028610231,2|8|0,0:0|3:2|0:0,0:0:0:0: +242,144,298449,2,0,L|248:238,1,75.0000028610231,4|2,0:1|0:0,0:0:0:0: +409,298,298671,6,0,P|336:294|258:343,1,150.000005722046,12|0,0:0|3:0,0:0:0:0: +47,287,299115,1,10,0:0:0:0: +11,243,299226,1,0,3:0:0:0: +0,189,299337,1,2,3:0:0:0: +13,135,299449,1,0,0:0:0:0: +49,92,299560,6,0,B|80:80|80:80|136:91,1,75.0000028610231,10|0,0:0|3:0,0:0:0:0: +244,186,299782,1,0,3:0:0:0: +244,186,299893,2,0,B|213:198|213:198|172:189,1,75.0000028610231,2|8,0:0|0:0,0:0:0:0: +363,223,300115,1,0,0:0:0:0: +363,223,300226,2,0,L|357:126,1,75.0000028610231,10|0,1:2|0:0,0:0:0:0: +449,56,300449,6,0,L|455:13,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +403,162,300671,2,0,L|406:208,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +340,71,300893,2,0,L|296:67,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +254,153,301115,2,0,L|210:157,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +120,88,301337,6,0,L|88:62,1,37.5000014305115,10|0,0:0|0:0,0:0:0:0: +66,100,301449,2,0,L|21:88,1,37.5000014305115,8|0,0:0|0:0,0:0:0:0: +30,141,301560,2,0,L|-10:146,1,37.5000014305115,10|0,0:0|0:0,0:0:0:0: +23,196,301671,2,0,L|-21:218,1,37.5000014305115,10|0,0:0|0:0,0:0:0:0: +50,245,301782,1,8,0:0:0:0: +100,226,301893,1,10,0:0:0:0: +146,250,302004,1,10,0:0:0:0: +159,300,302115,1,8,0:0:0:0: +136,344,302226,6,0,L|112:152,1,150.000005722046,12|0,0:0|0:0,0:0:0:0: +192,80,302560,1,0,0:0:0:0: +192,80,302671,2,0,P|248:136|208:232,1,150.000005722046,8|2,3:2|3:0,0:0:0:0: +146,250,303004,5,0,0:0:0:0: +72,304,303115,2,0,P|56:264|56:224,1,75.0000028610231,10|0,0:0|0:0,0:0:0:0: +106,129,303337,6,0,P|158:179|146:250,1,150.000005722046,0|8,3:0|0:0,0:0:0:0: +72,152,303671,1,0,0:0:0:0: +72,152,303782,2,0,L|64:48,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: +168,0,304004,1,8,3:2:0:0: +168,0,304115,2,0,L|176:104,1,75.0000028610231,0|2,3:0|3:0,0:0:0:0: +224,184,304337,5,0,0:0:0:0: +159,247,304449,2,0,P|228:273|310:179,1,150.000005722046,10|2,3:2|3:2,0:0:0:0: +428,142,304782,1,0,0:0:0:0: +428,142,304893,2,0,L|336:147,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: +263,73,305115,5,0,3:0:0:0: +213,56,305226,1,4,0:3:0:0: +161,64,305337,1,8,3:2:0:0: +122,99,305449,1,0,3:0:0:0: +104,148,305560,2,0,L|122:225,1,75.0000028610231,2|2,3:2|3:2,0:0:0:0: +166,316,305782,6,0,B|96:318|90:334|90:334|65:342|12:324,1,150.000005722046,8|2,0:0|3:0,0:0:0:0: +309,200,306115,1,0,0:0:0:0: +309,200,306226,2,0,P|336:253|307:351,1,150.000005722046,8|2,0:0|3:0,0:0:0:0: +243,278,306560,5,0,3:0:0:0: +309,200,306671,2,0,L|391:196,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: +500,99,306893,5,0,3:0:0:0: +447,92,307004,1,2,0:2:0:0: +397,75,307115,1,8,3:2:0:0: +344,69,307226,1,0,3:0:0:0: +296,47,307337,2,0,L|207:35,1,75.0000028610231,2|0,3:2|0:0,0:0:0:0: +108,8,307560,6,0,B|110:50|110:50|124:100|124:100|105:170,1,150.000005722046,8|2,0:0|3:0,0:0:0:0: +78,336,307893,1,0,0:0:0:0: +78,336,308004,2,0,P|31:298|55:174,1,150.000005722046,8|0,3:2|3:0,0:0:0:0: +194,174,308337,1,0,3:0:0:0: +194,174,308449,2,0,L|186:258,1,75.0000028610231,10|0,0:2|0:0,0:0:0:0: +288,113,308671,5,2,1:2:0:0: +323,74,308782,1,2,0:0:0:0: +373,59,308893,1,8,0:0:0:0: +424,70,309004,1,2,0:0:0:0: +476,67,309115,5,2,1:2:0:0: +462,117,309226,1,0,0:0:0:0: +448,167,309337,1,8,0:0:0:0: +434,217,309449,1,0,3:0:0:0: +460,262,309560,5,2,3:0:0:0: +408,253,309671,1,0,0:0:0:0: +359,274,309782,2,0,P|343:319|405:383,1,75.0000028610231,2|0,1:0|0:0,0:0:0:0: +305,191,310004,1,2,0:0:0:0: +305,191,310115,2,0,L|329:18,1,150.000005722046,0|0,0:0|3:0,0:0:0:0: +216,296,310449,5,2,3:0:0:0: +165,310,310560,1,0,3:0:0:0: +118,290,310671,2,0,P|80:264|128:141,1,150.000005722046,8|2,0:0|3:0,0:0:0:0: +230,113,311004,5,0,0:0:0:0: +230,113,311115,2,0,L|-10:130,1,225.000008583069,8|0,3:2|0:0,0:0:0:0: +95,62,311560,6,0,P|132:80|151:116,1,75.0000028610231,8|0,0:0|0:0,0:0:0:0: +169,270,311782,1,2,3:0:0:0: +169,270,311893,2,0,P|248:233|338:273,1,150.000005722046,0|0,3:0|3:0,0:0:0:0: +405,327,312226,6,0,L|489:330,1,75.0000028610231,0|4,3:0|0:3,0:0:0:0: +200,285,312449,2,0,P|129:302|52:269,1,150.000005722046,8|4,0:0|0:1,0:0:0:0: +13,105,312782,5,0,0:0:0:0: +95,62,312893,2,0,L|328:80,1,225.000008583069,12|2,0:0|0:0,0:0:0:0: +488,272,313337,6,0,P|480:208|432:168,1,75.0000028610231,8|0,0:0|1:0,0:0:0:0: +360,168,313560,2,0,P|368:232|416:272,1,75.0000028610231,0|0,3:2|1:0,0:0:0:0: +464,312,313782,6,0,L|440:176,1,75.0000028610231,8|0,3:2|3:0,0:0:0:0: +320,144,314004,2,0,L|344:280,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: +288,352,314226,5,8,0:0:0:0: +248,320,314337,1,0,0:0:0:0: +208,304,314449,2,0,L|144:304,1,37.5000014305115,0|0,3:0|3:0,0:0:0:0: +168,304,314560,2,0,L|112:312,1,37.5000014305115,0|0,3:0|3:0,0:0:0:0: +112,320,314671,5,8,3:2:0:0: +64,312,314782,1,0,3:0:0:0: +32,272,314893,1,8,3:2:0:0: +40,216,315004,1,0,0:0:0:0: +72,176,315115,5,8,0:0:0:0: +120,160,315226,1,0,0:0:0:0: +168,144,315337,2,0,L|216:128,1,37.5000014305115,8|8,0:0|0:0,0:0:0:0: +203,132,315449,2,0,L|264:112,1,37.5000014305115,8|8,0:0|0:0,0:0:0:0: +264,136,315560,5,8,0:0:0:0: +296,96,315671,1,8,0:0:0:0: +288,48,315782,1,8,0:0:0:0: +256,8,315893,5,8,0:0:0:0: +256,352,316115,1,4,0:1:0:0: +256,8,316337,1,8,0:0:0:0: +219,91,316449,6,0,P|270:95|322:71,1,100,12|0,0:0|0:0,0:0:0:0: +434,29,316893,1,8,0:0:0:0: +437,66,317004,1,0,0:0:0:0: +440,103,317115,1,0,0:0:0:0: +371,215,317337,2,0,L|321:205,3,50,10|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +298,247,317782,2,0,L|248:256,3,50,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +216,221,318226,5,8,0:0:0:0: +178,217,318337,1,0,0:0:0:0: +143,229,318449,1,2,0:0:0:0: +23,142,318671,1,8,0:0:0:0: +161,78,318893,1,2,0:0:0:0: +56,224,319115,1,10,0:0:0:0: +36,255,319226,1,0,0:0:0:0: +31,292,319337,1,0,0:0:0:0: +40,328,319449,1,4,0:3:0:0: +62,358,319560,2,0,L|124:345,1,50,8|0,0:0|0:0,0:0:0:0: +249,272,319782,6,0,L|373:292,1,100,0|8,0:0|0:0,0:0:0:0: +219,363,320226,1,2,0:0:0:0: +260,226,320449,1,8,0:0:0:0: +380,331,320671,1,2,0:0:0:0: +462,168,320893,1,10,0:0:0:0: +420,173,321004,1,0,0:0:0:0: +379,178,321115,2,0,L|320:193,1,50,2|0,0:0|0:0,0:0:0:0: +200,259,321337,2,0,L|259:274,1,50,10|0,0:0|0:0,0:0:0:0: +380,331,321560,6,0,P|395:283|381:223,1,100,2|8,0:0|0:0,0:0:0:0: +338,49,322004,1,0,0:0:0:0: +330,190,322226,1,8,0:0:0:0: +443,275,322449,1,0,0:0:0:0: +426,118,322671,1,8,0:0:0:0: +394,97,322782,1,0,0:0:0:0: +360,83,322893,1,0,0:0:0:0: +323,76,323004,1,4,0:3:0:0: +285,78,323115,2,0,P|261:78|223:92,1,50,8|0,0:0|0:0,0:0:0:0: +165,223,323337,2,0,P|181:204|196:166,1,50 +65,126,323560,5,8,3:2:0:0: +43,156,323671,1,0,3:0:0:0: +30,191,323782,1,2,3:0:0:0: +27,229,323893,1,0,0:0:0:0: +34,265,324004,1,8,3:2:0:0: +69,278,324115,1,0,3:0:0:0: +106,283,324226,1,2,3:0:0:0: +144,280,324337,1,0,0:0:0:0: +179,269,324449,5,8,0:0:0:0: +205,294,324560,1,0,0:0:0:0: +205,331,324671,1,0,3:0:0:0: +179,356,324782,1,2,3:0:0:0: +143,356,324893,1,10,0:0:0:0: +120,319,325004,1,0,3:0:0:0: +111,276,325115,1,2,3:0:0:0: +118,233,325226,1,0,0:0:0:0: +139,195,325337,5,8,3:2:0:0: +168,203,325449,1,0,3:0:0:0: +199,204,325560,1,2,3:0:0:0: +230,196,325671,1,0,0:0:0:0: +256,180,325782,1,8,3:2:0:0: +282,164,325893,1,0,3:0:0:0: +313,156,326004,1,2,3:0:0:0: +344,157,326115,1,0,0:0:0:0: +374,166,326226,5,10,3:2:0:0: +334,137,326337,1,0,3:2:0:0: +318,90,326449,1,0,3:2:0:0: +331,43,326560,1,6,3:3:0:0: +370,12,326671,1,8,0:0:0:0: +419,9,326782,1,2,3:0:0:0: +461,35,326893,1,0,3:0:0:0: +480,80,327004,1,0,3:0:0:0: +470,128,327115,5,12,0:0:0:0: +448,166,327226,1,0,3:0:0:0: +444,209,327337,1,0,3:0:0:0: +458,250,327449,1,0,0:0:0:0: +487,282,327560,1,10,3:0:0:0: +475,241,327671,1,0,3:0:0:0: +442,213,327782,1,2,3:0:0:0: +400,208,327893,1,0,0:0:0:0: +361,227,328004,5,8,3:2:0:0: +336,258,328115,1,0,3:0:0:0: +275,214,328226,1,0,3:0:0:0: +261,246,328337,1,2,3:0:0:0: +192,227,328449,1,8,0:0:0:0: +186,256,328560,1,0,3:0:0:0: +114,258,328671,1,2,3:0:0:0: +113,282,328782,1,0,0:0:0:0: +45,304,328893,6,0,L|17:322,3,25,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +25,203,329115,2,0,L|-7:207,3,25,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +69,115,329337,2,0,L|37:105,3,25,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +154,70,329560,2,0,L|129:47,3,25,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: +300,116,329782,6,0,L|336:103,1,25,8|0,3:2|3:0,0:0:0:0: +282,168,329893,2,0,L|305:198,1,25,8|0,3:2|0:0,0:0:0:0: +228,168,330004,2,0,L|206:199,1,25,8|0,3:2|3:2,0:0:0:0: +211,115,330115,2,0,L|174:104,1,25,12|0,3:0|3:0,0:0:0:0: +256,83,330226,5,8,0:0:0:0: +256,133,330337,1,8,0:0:0:0: +256,183,330449,1,8,0:0:0:0: +256,233,330560,1,4,0:0:0:0: diff --git a/src/osu/difficulty/evaluators/aim/snap_aim.rs b/src/osu/difficulty/evaluators/aim/snap_aim.rs index 4a668952..a9b9bcdb 100644 --- a/src/osu/difficulty/evaluators/aim/snap_aim.rs +++ b/src/osu/difficulty/evaluators/aim/snap_aim.rs @@ -32,7 +32,7 @@ impl SnapAimEvaluator { let Some(osu_last_obj) = curr .previous(0, diff_objects) - .filter(|last| !(curr.base.is_spinner() || last.base.is_spinner())) + .filter(|last| curr.idx > 1 && !(curr.base.is_spinner() || last.base.is_spinner())) else { return 0.0; }; diff --git a/src/osu/difficulty/object.rs b/src/osu/difficulty/object.rs index 1abf6531..87639040 100644 --- a/src/osu/difficulty/object.rs +++ b/src/osu/difficulty/object.rs @@ -3,7 +3,9 @@ use std::borrow::Cow; use rosu_map::util::Pos; use crate::{ - any::difficulty::object::{HasStartTime, IDifficultyObject}, osu::object::{OsuObject, OsuObjectKind}, util::difficulty::reverse_lerp, + any::difficulty::object::{HasEndTime, HasStartTime, IDifficultyObject}, + osu::object::{OsuObject, OsuObjectKind}, + util::difficulty::reverse_lerp, }; use super::{HD_FADE_OUT_DURATION_MULTIPLIER, scaling_factor::ScalingFactor}; @@ -12,11 +14,13 @@ pub struct OsuDifficultyObject<'a> { pub idx: usize, pub base: &'a OsuObject, pub radius: f64, + pub start_time: f64, + pub end_time: f64, pub delta_time: f64, - pub adjusted_delta_time: f64, pub last_obj_end_delta_time: f64, + pub jump_dist: f64, pub lazy_jump_dist: f64, pub min_jump_dist: f64, @@ -49,13 +53,15 @@ impl<'a> OsuDifficultyObject<'a> { idx: usize, scaling_factor: &ScalingFactor, ) -> Self { - let radius = f64::from(OsuObject::OBJECT_RADIUS) * scaling_factor.radius; + let radius = f64::from(OsuObject::OBJECT_RADIUS * scaling_factor.scale); let delta_time = (hit_object.start_time - last_object.start_time) / clock_rate; let start_time = hit_object.start_time / clock_rate; + let end_time = hit_object.end_time() / clock_rate; + // * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. let adjusted_delta_time = delta_time.max(Self::MIN_DELTA_TIME); let last_obj_end_delta_time = if let Some(last) = last_diff_obj { - (start_time - last.start_time + last.delta_time).max(Self::MIN_DELTA_TIME) + (start_time - last.end_time).max(Self::MIN_DELTA_TIME) } else { adjusted_delta_time }; @@ -67,6 +73,7 @@ impl<'a> OsuDifficultyObject<'a> { base: hit_object, radius, start_time, + end_time, delta_time, adjusted_delta_time, last_obj_end_delta_time, @@ -120,9 +127,7 @@ impl<'a> OsuDifficultyObject<'a> { } pub fn calc_double_tap_feasibility(&self, next: Option<&Self>, hit_window: f64) -> f64 { - let Some(next) = next else { - return 0.0 - }; + let Some(next) = next else { return 0.0 }; let hit_window = if self.base.is_spinner() { 0.0 @@ -139,10 +144,11 @@ impl<'a> OsuDifficultyObject<'a> { // * Can't doubletap if circles don't intersect let distance_factor = reverse_lerp( - self.lazy_jump_dist, - f64::from(Self::NORMALIZED_DIAMETER), - f64::from(Self::NORMALIZED_RADIUS) - ).powf(2.0); + self.lazy_jump_dist, + f64::from(Self::NORMALIZED_DIAMETER), + f64::from(Self::NORMALIZED_RADIUS), + ) + .powf(2.0); 1.0 - speed_ratio.powf(distance_factor * (1.0 - window_ratio)) } @@ -156,7 +162,9 @@ impl<'a> OsuDifficultyObject<'a> { scaling_factor: &ScalingFactor, ) { if let OsuObjectKind::Slider(ref slider) = self.base.kind { - self.travel_dist = self.lazy_travel_dist * (slider.repeat_count() as f64).powf(0.3).max(1.0); + // * Bonus for repeat sliders until a better per nested object strain system can be achieved. + self.travel_dist = + self.lazy_travel_dist * (slider.repeat_count() as f64).powf(0.3).max(1.0); self.travel_time = (self.lazy_travel_time / clock_rate).max(OsuDifficultyObject::MIN_DELTA_TIME); @@ -181,12 +189,14 @@ impl<'a> OsuDifficultyObject<'a> { ); self.min_jump_dist = self.lazy_jump_dist; + self.jump_dist = f64::from( + (last_object.stacked_pos() - self.base.stacked_pos()).length() * scaling_factor, + ); + let Some(last_diff_obj) = last_diff_obj else { return; }; - self.jump_dist = f64::from((last_diff_obj.base.stacked_pos() - self.base.stacked_pos()).length() * scaling_factor); - if let OsuObjectKind::Slider(ref last_slider) = last_object.kind { let last_travel_time = (last_diff_obj.lazy_travel_time / clock_rate) .max(OsuDifficultyObject::MIN_DELTA_TIME); @@ -210,7 +220,7 @@ impl<'a> OsuDifficultyObject<'a> { let Some(last_last_diff_obj) = last_last_diff_obj else { return; }; - + if !last_last_diff_obj.base.is_spinner() { if last_object.is_slider() && last_diff_obj.travel_dist > 0.0 { last_cursor_pos = last_object.stacked_pos(); @@ -282,9 +292,12 @@ impl<'a> OsuDifficultyObject<'a> { end_time_min %= 1.0; } + // * temporary lazy end position until a real result can be derived. let mut lazy_end_pos = pos + stack_offset + slider.path.position_at(end_time_min); let mut curr_cursor_pos = pos + stack_offset; + + // * lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. let scaling_factor = f64::from(OsuDifficultyObject::NORMALIZED_RADIUS) / radius; for (curr_movement_obj, i) in nested_objects.iter().zip(1..) { @@ -293,6 +306,10 @@ impl<'a> OsuDifficultyObject<'a> { let mut required_movement = f64::from(OsuDifficultyObject::ASSUMED_SLIDER_RADIUS); if i == nested_objects.len() { + // * The end of a slider has special aim rules due to the relaxed time constraint on position. + // * There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement. + // * For sliders that are circular, the lazy end position may actually be farther away than the sliders true end. + // * This code is designed to prevent buffing situations where lazy end is actually a less efficient movement. let lazy_movement = lazy_end_pos - curr_cursor_pos; if lazy_movement.length() < curr_movement.length() { @@ -319,11 +336,21 @@ impl<'a> OsuDifficultyObject<'a> { self.lazy_end_pos = Some(lazy_end_pos); } - fn calculate_slider_angle(&self, last_diff_obj: &OsuDifficultyObject, last_last_pos: Pos) -> f64 { + fn calculate_slider_angle( + &self, + last_diff_obj: &OsuDifficultyObject, + last_last_pos: Pos, + ) -> f64 { let last_pos = Self::get_end_cursor_pos(last_diff_obj); - let adjusted_last_last_pos = if let OsuObjectKind::Slider(ref last_slider) = last_diff_obj.base.kind && last_diff_obj.travel_dist > 0.0 { - if let Some(second_last_nested) = last_slider.nested_objects.get(last_slider.nested_objects.len().saturating_sub(2)) { + let adjusted_last_last_pos = if let OsuObjectKind::Slider(ref last_slider) = + last_diff_obj.base.kind + && last_diff_obj.travel_dist > 0.0 + { + if let Some(second_last_nested) = last_slider + .nested_objects + .get(last_slider.nested_objects.len().saturating_sub(2)) + { second_last_nested.pos + last_diff_obj.base.stack_offset } else { last_last_pos diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs index 4258eaaa..fe8d2dda 100644 --- a/src/osu/difficulty/skills/reading.rs +++ b/src/osu/difficulty/skills/reading.rs @@ -36,7 +36,7 @@ impl Reading { strain_decay_base(ms, 0.8) } - #[expect(dead_code, reason = "used by process_internal")] + // #[expect(dead_code, reason = "used by process_internal")] fn object_difficulty_of<'a>( &mut self, curr: &'a OsuDifficultyObject<'a>, diff --git a/src/osu/difficulty/skills/speed.rs b/src/osu/difficulty/skills/speed.rs index 01919807..e8161887 100644 --- a/src/osu/difficulty/skills/speed.rs +++ b/src/osu/difficulty/skills/speed.rs @@ -26,7 +26,6 @@ impl Speed { strain_decay_base(ms, 0.3) } - #[expect(dead_code, reason = "used by process_internal")] fn object_difficulty_of<'a>( &mut self, curr: &'a OsuDifficultyObject<'a>, diff --git a/src/osu/object.rs b/src/osu/object.rs index 0ed98ee8..ff8e4243 100644 --- a/src/osu/object.rs +++ b/src/osu/object.rs @@ -95,7 +95,7 @@ impl OsuObject { } } - pub fn end_time(&self) -> f64 { + pub const fn end_time(&self) -> f64 { match self.kind { OsuObjectKind::Circle => self.start_time, OsuObjectKind::Slider(ref slider) => slider.end_time, @@ -103,6 +103,10 @@ impl OsuObject { } } + pub const fn duration(&self) -> f64 { + self.end_time() - self.start_time + } + pub const fn stacked_pos(&self) -> Pos { // Performed manually for const-ness // self.pos + self.stack_offset diff --git a/src/util/macros_new.rs b/src/util/macros_new.rs index 63ccb260..38ccd66c 100644 --- a/src/util/macros_new.rs +++ b/src/util/macros_new.rs @@ -471,10 +471,8 @@ macro_rules! define_new_skill { self.skill_current_section_peak = current_strain; } else { // * Empty the queue of smaller elements as they won't be relevant to difficulty - while !self.skill_queued_strains.is_empty() { - if self.skill_queued_strains.last().filter(|(strain_value, _)| strain_value < ¤t_strain).is_some() { - self.skill_queued_strains.pop(); - } + while self.skill_queued_strains.last().filter(|(strain_value, _)| strain_value < ¤t_strain).is_some() { + self.skill_queued_strains.pop(); } self.skill_queued_strains.push((current_strain, curr.start_time)); } @@ -597,13 +595,12 @@ macro_rules! define_new_skill { define_new_skill!( @impl Skill $name $objects[$object] ); impl HarmonicSkill for $name { - #[expect(unused_variables, reason = "placeholder")] fn object_difficulty_of<'a>( &mut self, curr: &Self::DifficultyObject<'a>, objects: &Self::DifficultyObjects<'a>, ) -> f64 { - todo!() + self.object_difficulty_of(curr, objects) } fn into_transformed_difficulties(self) -> Vec { diff --git a/tests/difficulty.rs b/tests/difficulty.rs index 1911d764..4bff0a5b 100644 --- a/tests/difficulty.rs +++ b/tests/difficulty.rs @@ -37,10 +37,12 @@ macro_rules! test_cases { aim_difficult_slider_count: $aim_difficult_slider_count:literal, speed: $speed:literal, flashlight: $flashlight:literal, + reading: $reading:literal, slider_factor: $slider_factor:literal, aim_top_weighted_slider_factor: $aim_top_weighted_slider_factor:literal, speed_top_weighted_slider_factor: $speed_top_weighted_slider_factor:literal, speed_note_count: $speed_note_count:literal, + reading_note_count: $reading_note_count:literal, aim_difficult_strain_count: $aim_difficult_strain_count:literal, speed_difficult_strain_count: $speed_difficult_strain_count:literal, nested_score_per_object: $nested_score_per_object:literal, @@ -63,10 +65,12 @@ macro_rules! test_cases { aim_difficult_slider_count: $aim_difficult_slider_count, speed: $speed, flashlight: $flashlight, + reading: $reading, slider_factor: $slider_factor, aim_top_weighted_slider_factor: $aim_top_weighted_slider_factor, speed_top_weighted_slider_factor: $speed_top_weighted_slider_factor, speed_note_count: $speed_note_count, + reading_note_count: $reading_note_count, aim_difficult_strain_count: $aim_difficult_strain_count, speed_difficult_strain_count: $speed_difficult_strain_count, nested_score_per_object: $nested_score_per_object, @@ -150,191 +154,193 @@ macro_rules! test_cases { #[test] fn basic_osu() { - #[cfg(target_os = "windows")] + // #[cfg(target_os = "windows")] test_cases! { Osu: OSU { NM => { - aim: 3.021506412510076, - aim_difficult_slider_count: 180.33980678704012, - speed: 2.5263145770639976, - flashlight: 0.0, - slider_factor: 0.9847225384137204, - aim_top_weighted_slider_factor: 1.3996332540321264, - speed_top_weighted_slider_factor: 0.6014562852677632, - speed_note_count: 202.24319351543616, - aim_difficult_strain_count: 108.47555309841259, - speed_difficult_strain_count: 78.39830024782772, - nested_score_per_object: 34.991680532445926, - legacy_score_base_multiplier: 5.0, - maximum_legacy_combo_score: 15729840.0, - ar: 9.300000190734863, - great_hit_window: 26.5, - ok_hit_window: 68.5, - meh_hit_window: 110.5, - hp: 5.0, - n_circles: 307, - n_sliders: 293, - n_large_ticks: 15, - n_spinners: 1, - stars: 5.740766046562339, - max_combo: 909, - }; - HD => { - aim: 3.121489829231887, - aim_difficult_slider_count: 180.33980678704012, - speed: 2.614171127905441, + aim: 3.5141717095461806, + aim_difficult_slider_count: 253.68984309768354, + speed: 2.4079921566524338, flashlight: 0.0, - slider_factor: 0.9847225384137204, - aim_top_weighted_slider_factor: 1.3996332540321264, - speed_top_weighted_slider_factor: 0.6014562852677632, - speed_note_count: 202.24319351543616, - aim_difficult_strain_count: 108.47555309841259, - speed_difficult_strain_count: 78.39830024782772, - nested_score_per_object: 34.991680532445926, - legacy_score_base_multiplier: 5.0, - maximum_legacy_combo_score: 15729840.0, - ar: 9.300000190734863, + reading: 1.2807842408799812, + slider_factor: 0.97955486167852612, + aim_top_weighted_slider_factor: 0.6280004197964858, + speed_top_weighted_slider_factor: 0.53840750544558069, + speed_note_count: 796.60849945939026, + reading_note_count: 97.229859463848399, + aim_difficult_strain_count: 154.31162398617261, + speed_difficult_strain_count: 432.92911452386227, + nested_score_per_object: 26.395781637717121, + legacy_score_base_multiplier: 4.0, + maximum_legacy_combo_score: 91937232.0, + ar: 9.30000019, great_hit_window: 26.5, ok_hit_window: 68.5, meh_hit_window: 110.5, - hp: 5.0, - n_circles: 307, - n_sliders: 293, + hp: 6.0, + n_circles: 938, + n_sliders: 674, n_large_ticks: 15, - n_spinners: 1, - stars: 5.934133851244851, - max_combo: 909, - }; - HR => { - aim: 3.4309052630747257, - aim_difficult_slider_count: 187.20300643263465, - speed: 2.6813963801152716, - flashlight: 0.0, - slider_factor: 0.9748562752795166, - aim_top_weighted_slider_factor: 1.3634873114118244, - speed_top_weighted_slider_factor: 0.6668815233244475, - speed_note_count: 185.01178339020348, - aim_difficult_strain_count: 112.28112750203013, - speed_difficult_strain_count: 74.53251006179151, - nested_score_per_object: 34.991680532445926, - legacy_score_base_multiplier: 5.0, - maximum_legacy_combo_score: 15729840.0, - ar: 10.0, - great_hit_window: 19.5, - ok_hit_window: 59.5, - meh_hit_window: 99.5, - hp: 7.0, - n_circles: 307, - n_sliders: 293, - n_large_ticks: 15, - n_spinners: 1, - stars: 6.375104448752039, - max_combo: 909, - }; - DT => { - aim: 4.3662195513104525, - aim_difficult_slider_count: 195.41476682131653, - speed: 3.7793477426295814, - flashlight: 0.0, - slider_factor: 0.9787310737204966, - aim_top_weighted_slider_factor: 1.3819099517666353, - speed_top_weighted_slider_factor: 0.6923456235877925, - speed_note_count: 208.98215163620375, - aim_difficult_strain_count: 130.48279566301667, - speed_difficult_strain_count: 93.64469563382437, - nested_score_per_object: 34.991680532445926, - legacy_score_base_multiplier: 5.0, - maximum_legacy_combo_score: 15729840.0, - ar: 10.533333460489908, - great_hit_window: 17.666666666666668, - ok_hit_window: 45.666666666666664, - meh_hit_window: 73.66666666666667, - hp: 5.0, - n_circles: 307, - n_sliders: 293, - n_large_ticks: 15, - n_spinners: 1, - stars: 8.40182116136074, - max_combo: 909, - }; - FL => { - aim: 3.021506412510076, - aim_difficult_slider_count: 180.33980678704012, - speed: 2.5263145770639976, - flashlight: 2.3005989208967885, - slider_factor: 0.9847225384137204, - aim_top_weighted_slider_factor: 1.3996332540321264, - speed_top_weighted_slider_factor: 0.6014562852677632, - speed_note_count: 202.24319351543616, - aim_difficult_strain_count: 108.47555309841259, - speed_difficult_strain_count: 78.39830024782772, - nested_score_per_object: 34.991680532445926, - legacy_score_base_multiplier: 5.0, - maximum_legacy_combo_score: 15729840.0, - ar: 9.300000190734863, - great_hit_window: 26.5, - ok_hit_window: 68.5, - meh_hit_window: 110.5, - hp: 5.0, - n_circles: 307, - n_sliders: 293, - n_large_ticks: 15, - n_spinners: 1, - stars: 6.864308231959398, - max_combo: 909, - }; - HD EZ => { - aim: 2.9818506123002706, - aim_difficult_slider_count: 173.00759261125233, - speed: 2.511531850339625, - flashlight: 0.0, - slider_factor: 0.9931728395338801, - aim_top_weighted_slider_factor: 1.4758126955064637, - speed_top_weighted_slider_factor: 0.48777057881852615, - speed_note_count: 211.97339651166865, - aim_difficult_strain_count: 107.45480335487801, - speed_difficult_strain_count: 78.94432491731223, - nested_score_per_object: 34.991680532445926, - legacy_score_base_multiplier: 3.0, - maximum_legacy_combo_score: 15729840.0, - ar: 4.650000095367432, - great_hit_window: 52.5, - ok_hit_window: 103.5, - meh_hit_window: 154.5, - hp: 2.5, - n_circles: 307, - n_sliders: 293, - n_large_ticks: 15, - n_spinners: 1, - stars: 5.680319048111094, - max_combo: 909, - }; - HD FL => { - aim: 3.121489829231887, - aim_difficult_slider_count: 180.33980678704012, - speed: 2.614171127905441, - flashlight: 2.620335643475851, - slider_factor: 0.9847225384137204, - aim_top_weighted_slider_factor: 1.3996332540321264, - speed_top_weighted_slider_factor: 0.6014562852677632, - speed_note_count: 202.24319351543616, - aim_difficult_strain_count: 108.47555309841259, - speed_difficult_strain_count: 78.39830024782772, - nested_score_per_object: 34.991680532445926, - legacy_score_base_multiplier: 5.0, - maximum_legacy_combo_score: 15729840.0, - ar: 9.300000190734863, - great_hit_window: 26.5, - ok_hit_window: 68.5, - meh_hit_window: 110.5, - hp: 5.0, - n_circles: 307, - n_sliders: 293, - n_large_ticks: 15, - n_spinners: 1, - stars: 7.2736222258399374, - max_combo: 909, + n_spinners: 0, + stars: 6.305978712257887, + max_combo: 2359, }; + // HD => { + // aim: 3.121489829231887, + // aim_difficult_slider_count: 180.33980678704012, + // speed: 2.614171127905441, + // flashlight: 0.0, + // slider_factor: 0.9847225384137204, + // aim_top_weighted_slider_factor: 1.3996332540321264, + // speed_top_weighted_slider_factor: 0.6014562852677632, + // speed_note_count: 202.24319351543616, + // aim_difficult_strain_count: 108.47555309841259, + // speed_difficult_strain_count: 78.39830024782772, + // nested_score_per_object: 34.991680532445926, + // legacy_score_base_multiplier: 5.0, + // maximum_legacy_combo_score: 15729840.0, + // ar: 9.300000190734863, + // great_hit_window: 26.5, + // ok_hit_window: 68.5, + // meh_hit_window: 110.5, + // hp: 5.0, + // n_circles: 307, + // n_sliders: 293, + // n_large_ticks: 15, + // n_spinners: 1, + // stars: 5.934133851244851, + // max_combo: 909, + // }; + // HR => { + // aim: 3.4309052630747257, + // aim_difficult_slider_count: 187.20300643263465, + // speed: 2.6813963801152716, + // flashlight: 0.0, + // slider_factor: 0.9748562752795166, + // aim_top_weighted_slider_factor: 1.3634873114118244, + // speed_top_weighted_slider_factor: 0.6668815233244475, + // speed_note_count: 185.01178339020348, + // aim_difficult_strain_count: 112.28112750203013, + // speed_difficult_strain_count: 74.53251006179151, + // nested_score_per_object: 34.991680532445926, + // legacy_score_base_multiplier: 5.0, + // maximum_legacy_combo_score: 15729840.0, + // ar: 10.0, + // great_hit_window: 19.5, + // ok_hit_window: 59.5, + // meh_hit_window: 99.5, + // hp: 7.0, + // n_circles: 307, + // n_sliders: 293, + // n_large_ticks: 15, + // n_spinners: 1, + // stars: 6.375104448752039, + // max_combo: 909, + // }; + // DT => { + // aim: 4.3662195513104525, + // aim_difficult_slider_count: 195.41476682131653, + // speed: 3.7793477426295814, + // flashlight: 0.0, + // slider_factor: 0.9787310737204966, + // aim_top_weighted_slider_factor: 1.3819099517666353, + // speed_top_weighted_slider_factor: 0.6923456235877925, + // speed_note_count: 208.98215163620375, + // aim_difficult_strain_count: 130.48279566301667, + // speed_difficult_strain_count: 93.64469563382437, + // nested_score_per_object: 34.991680532445926, + // legacy_score_base_multiplier: 5.0, + // maximum_legacy_combo_score: 15729840.0, + // ar: 10.533333460489908, + // great_hit_window: 17.666666666666668, + // ok_hit_window: 45.666666666666664, + // meh_hit_window: 73.66666666666667, + // hp: 5.0, + // n_circles: 307, + // n_sliders: 293, + // n_large_ticks: 15, + // n_spinners: 1, + // stars: 8.40182116136074, + // max_combo: 909, + // }; + // FL => { + // aim: 3.021506412510076, + // aim_difficult_slider_count: 180.33980678704012, + // speed: 2.5263145770639976, + // flashlight: 2.3005989208967885, + // slider_factor: 0.9847225384137204, + // aim_top_weighted_slider_factor: 1.3996332540321264, + // speed_top_weighted_slider_factor: 0.6014562852677632, + // speed_note_count: 202.24319351543616, + // aim_difficult_strain_count: 108.47555309841259, + // speed_difficult_strain_count: 78.39830024782772, + // nested_score_per_object: 34.991680532445926, + // legacy_score_base_multiplier: 5.0, + // maximum_legacy_combo_score: 15729840.0, + // ar: 9.300000190734863, + // great_hit_window: 26.5, + // ok_hit_window: 68.5, + // meh_hit_window: 110.5, + // hp: 5.0, + // n_circles: 307, + // n_sliders: 293, + // n_large_ticks: 15, + // n_spinners: 1, + // stars: 6.864308231959398, + // max_combo: 909, + // }; + // HD EZ => { + // aim: 2.9818506123002706, + // aim_difficult_slider_count: 173.00759261125233, + // speed: 2.511531850339625, + // flashlight: 0.0, + // slider_factor: 0.9931728395338801, + // aim_top_weighted_slider_factor: 1.4758126955064637, + // speed_top_weighted_slider_factor: 0.48777057881852615, + // speed_note_count: 211.97339651166865, + // aim_difficult_strain_count: 107.45480335487801, + // speed_difficult_strain_count: 78.94432491731223, + // nested_score_per_object: 34.991680532445926, + // legacy_score_base_multiplier: 3.0, + // maximum_legacy_combo_score: 15729840.0, + // ar: 4.650000095367432, + // great_hit_window: 52.5, + // ok_hit_window: 103.5, + // meh_hit_window: 154.5, + // hp: 2.5, + // n_circles: 307, + // n_sliders: 293, + // n_large_ticks: 15, + // n_spinners: 1, + // stars: 5.680319048111094, + // max_combo: 909, + // }; + // HD FL => { + // aim: 3.121489829231887, + // aim_difficult_slider_count: 180.33980678704012, + // speed: 2.614171127905441, + // flashlight: 2.620335643475851, + // slider_factor: 0.9847225384137204, + // aim_top_weighted_slider_factor: 1.3996332540321264, + // speed_top_weighted_slider_factor: 0.6014562852677632, + // speed_note_count: 202.24319351543616, + // aim_difficult_strain_count: 108.47555309841259, + // speed_difficult_strain_count: 78.39830024782772, + // nested_score_per_object: 34.991680532445926, + // legacy_score_base_multiplier: 5.0, + // maximum_legacy_combo_score: 15729840.0, + // ar: 9.300000190734863, + // great_hit_window: 26.5, + // ok_hit_window: 68.5, + // meh_hit_window: 110.5, + // hp: 5.0, + // n_circles: 307, + // n_sliders: 293, + // n_large_ticks: 15, + // n_spinners: 1, + // stars: 7.2736222258399374, + // max_combo: 909, + // }; } }; } @@ -581,10 +587,12 @@ impl AssertEq for OsuDifficultyAttributes { aim_difficult_slider_count, speed, flashlight, + reading, slider_factor, aim_top_weighted_slider_factor, speed_top_weighted_slider_factor, speed_note_count, + reading_note_count, aim_difficult_strain_count, speed_difficult_strain_count, nested_score_per_object, @@ -610,6 +618,7 @@ impl AssertEq for OsuDifficultyAttributes { ); assert_eq_float(*speed, expected.speed); assert_eq_float(*flashlight, expected.flashlight); + assert_eq_float(*reading, expected.reading); assert_eq_float(*slider_factor, expected.slider_factor); assert_eq_float( *aim_top_weighted_slider_factor, @@ -620,6 +629,7 @@ impl AssertEq for OsuDifficultyAttributes { expected.speed_top_weighted_slider_factor, ); assert_eq_float(*speed_note_count, expected.speed_note_count); + assert_eq_float(*reading_note_count, expected.reading_note_count); assert_eq_float( *aim_difficult_strain_count, expected.aim_difficult_strain_count, From e6519a46d7d34106e20c4a3e3c73dd72949c32d7 Mon Sep 17 00:00:00 2001 From: myssto Date: Sun, 2 Aug 2026 22:33:10 -0500 Subject: [PATCH 16/27] fix: osu aim skill value parity --- .../variable_length_strain_skill.rs | 26 +++++--- src/osu/difficulty/evaluators/aim/flow_aim.rs | 16 +++-- src/osu/difficulty/evaluators/aim/snap_aim.rs | 19 +++--- src/osu/difficulty/gradual.rs | 1 - src/osu/difficulty/mod.rs | 1 - src/osu/difficulty/object.rs | 43 ++++++------- src/osu/difficulty/skills/aim.rs | 63 +++++++++---------- src/osu/difficulty/skills/mod.rs | 16 ++++- src/util/macros_new.rs | 14 +---- src/util/traits.rs | 16 ++--- tests/performance.rs | 4 +- 11 files changed, 112 insertions(+), 107 deletions(-) diff --git a/src/any/difficulty/skills_new/variable_length_strain_skill.rs b/src/any/difficulty/skills_new/variable_length_strain_skill.rs index 777a4871..06ed37c7 100644 --- a/src/any/difficulty/skills_new/variable_length_strain_skill.rs +++ b/src/any/difficulty/skills_new/variable_length_strain_skill.rs @@ -1,4 +1,7 @@ -use crate::{any::difficulty::skills_new::skill::Skill, util::traits::IEnumerable}; +use crate::{ + any::difficulty::skills_new::skill::Skill, + util::traits::{IEnumerable, IOrderedEnumerable}, +}; pub trait VariableLengthStrainSkill: Skill { const DECAY_WEIGHT: f64 = 0.9; @@ -45,16 +48,15 @@ pub trait VariableLengthStrainSkill: Skill { fn get_current_strain_peaks( mut strain_peaks: Vec, - final_peak: Option, current_section_peak: f64, current_section_begin: f64, current_section_end: f64, ) -> Vec { - if final_peak.is_none() { - let final_peak = StrainPeak::new( - current_section_peak, - current_section_end - current_section_begin, - ); + let final_peak = StrainPeak::new( + current_section_peak, + current_section_end - current_section_begin, + ); + if strain_peaks.last().is_some_and(|peak| *peak != final_peak) { strain_peaks.cs_add_in_place(final_peak); } @@ -81,7 +83,7 @@ impl StrainPeak { impl PartialEq for StrainPeak { fn eq(&self, other: &Self) -> bool { - self.value == other.value + self.value == other.value && self.section_length == other.section_length } } @@ -99,3 +101,11 @@ impl PartialOrd for StrainPeak { Some(self.cmp(other)) } } + +impl IOrderedEnumerable for Vec { + fn cs_order_descending(mut self) -> Self { + self.sort_by(StrainPeak::cmp); + + self + } +} diff --git a/src/osu/difficulty/evaluators/aim/flow_aim.rs b/src/osu/difficulty/evaluators/aim/flow_aim.rs index 23518a0f..ba59b255 100644 --- a/src/osu/difficulty/evaluators/aim/flow_aim.rs +++ b/src/osu/difficulty/evaluators/aim/flow_aim.rs @@ -18,12 +18,13 @@ impl FlowAimEvaluator { curr: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>], with_slider_travel_dist: bool, + obj_radius: f64, ) -> f64 { let osu_curr_obj = curr; let Some(osu_last_obj) = curr .previous(0, diff_objects) - .filter(|last| !(curr.base.is_spinner() || last.base.is_spinner())) + .filter(|last| curr.idx > 1 && !(curr.base.is_spinner() || last.base.is_spinner())) else { return 0.0; }; @@ -85,11 +86,13 @@ impl FlowAimEvaluator { // NOTE: Source does not null check osuLastLastObj // instead current.Index > 2 is checked. - if let Some(osu_last_last_obj) = curr.previous(1, diff_objects) { + if curr.idx > 2 + && let Some(osu_last_last_obj) = curr.previous(1, diff_objects) + { overlapped_notes_weight = 1.0 - - Self::calc_overlap_factor(osu_curr_obj, osu_last_obj) - * Self::calc_overlap_factor(osu_curr_obj, osu_last_last_obj) - * Self::calc_overlap_factor(osu_last_obj, osu_last_last_obj); + - Self::calc_overlap_factor(osu_curr_obj, osu_last_obj, obj_radius) + * Self::calc_overlap_factor(osu_curr_obj, osu_last_last_obj, obj_radius) + * Self::calc_overlap_factor(osu_last_obj, osu_last_last_obj, obj_radius); } if let Some(curr_angle) = osu_curr_obj.angle { @@ -147,11 +150,12 @@ impl FlowAimEvaluator { fn calc_overlap_factor<'a>( first: &'a OsuDifficultyObject<'a>, second: &'a OsuDifficultyObject<'a>, + obj_radius: f64, ) -> f64 { let dist = Pos::distance(&first.base.stacked_pos(), second.base.stacked_pos()); f64::clamp( - 1.0 - (f64::from(dist) - first.radius).max(0.0).powf(2.0), + 1.0 - ((f64::from(dist) - obj_radius).max(0.0) / obj_radius).powf(2.0), 0.0, 1.0, ) diff --git a/src/osu/difficulty/evaluators/aim/snap_aim.rs b/src/osu/difficulty/evaluators/aim/snap_aim.rs index a9b9bcdb..14aaa583 100644 --- a/src/osu/difficulty/evaluators/aim/snap_aim.rs +++ b/src/osu/difficulty/evaluators/aim/snap_aim.rs @@ -49,7 +49,7 @@ impl SnapAimEvaluator { } else { osu_curr_obj.jump_dist }; - let mut curr_vel = osu_curr_obj.lazy_jump_dist / osu_curr_obj.adjusted_delta_time; + let mut curr_vel = curr_dist / osu_curr_obj.adjusted_delta_time; // * But if the last object is a slider, then we extend the travel // * velocity through the slider into the current object. @@ -219,11 +219,11 @@ impl SnapAimEvaluator { // * Reward sliders based on velocity. if osu_curr_obj.base.is_slider() && with_slider_travel_dist { let slider_bonus = osu_curr_obj.travel_dist / osu_curr_obj.travel_time; - snap_diff += if slider_bonus.lt(&1.0) { + snap_diff += if slider_bonus < 1.0 { slider_bonus } else { - slider_bonus.powf(0.75) * Self::SLIDER_MULTIPLIER - }; + slider_bonus.powf(0.75) + } * Self::SLIDER_MULTIPLIER; } // * Apply high circle size bonus @@ -261,10 +261,11 @@ impl SnapAimEvaluator { break; } - if let (Some(curr_vec_angle), Some(prev_vec_angle)) = - (curr.normalized_vector_angle, prev.normalized_vector_angle) - { - let angle_diff = curr_vec_angle - prev_vec_angle; + if let (Some(curr_vec_angle), Some(prev_vec_angle)) = ( + curr.normalized_vector_angle, + prev_obj.normalized_vector_angle, + ) { + let angle_diff = (curr_vec_angle - prev_vec_angle).abs(); // * Refer to this desmos for tuning, constants need to be precise // * so that values stay within the range of 0 and 1. // * https://www.desmos.com/calculator/a8jesv5sv2 @@ -294,7 +295,7 @@ impl SnapAimEvaluator { } const fn calc_angle_wideness(angle: f64) -> f64 { - smoothstep(angle, f64::to_radians(140.0), f64::to_radians(40.0)) + smoothstep(angle, f64::to_radians(40.0), f64::to_radians(140.0)) } pub const fn calc_angle_acuteness(angle: f64) -> f64 { diff --git a/src/osu/difficulty/gradual.rs b/src/osu/difficulty/gradual.rs index 8bf59517..05f831a6 100644 --- a/src/osu/difficulty/gradual.rs +++ b/src/osu/difficulty/gradual.rs @@ -154,7 +154,6 @@ fn new(difficulty: Difficulty, map: &Beatmap) -> OsuGradualDifficulty { great_hit_window, time_preempt, diff_objects.len(), - attrs.od(), ); let diff_objects = extend_lifetime(diff_objects.into_boxed_slice()); diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index 87227f7a..1a059f73 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -172,7 +172,6 @@ impl DifficultyValues { great_hit_window, time_preempt, take_diff_objects, - attrs.od(), ); for hit_object in diff_objects.iter().take(take_diff_objects) { diff --git a/src/osu/difficulty/object.rs b/src/osu/difficulty/object.rs index 87639040..f108f35a 100644 --- a/src/osu/difficulty/object.rs +++ b/src/osu/difficulty/object.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use rosu_map::util::Pos; use crate::{ - any::difficulty::object::{HasEndTime, HasStartTime, IDifficultyObject}, + any::difficulty::object::{HasStartTime, IDifficultyObject}, osu::object::{OsuObject, OsuObjectKind}, util::difficulty::reverse_lerp, }; @@ -13,12 +13,16 @@ use super::{HD_FADE_OUT_DURATION_MULTIPLIER, scaling_factor::ScalingFactor}; pub struct OsuDifficultyObject<'a> { pub idx: usize, pub base: &'a OsuObject, - pub radius: f64, + /// Start time (clock rate adjusted) pub start_time: f64, + /// End time (clock rate adjusted) pub end_time: f64, + /// Amount of time elapsed between the last and current [`OsuObject`] (clock rate adjusted) pub delta_time: f64, + /// [`delta_time`] capped to [`MIN_DELTA_TIME`] pub adjusted_delta_time: f64, + /// Amount of time elapsed between last and current [`OsuDifficultyObject`] capped to [`MIN_DELTA_TIME`] pub last_obj_end_delta_time: f64, pub jump_dist: f64, @@ -30,6 +34,7 @@ pub struct OsuDifficultyObject<'a> { pub lazy_end_pos: Option, pub lazy_travel_dist: f64, pub lazy_travel_time: f64, + pub angle: Option, pub normalized_vector_angle: Option, @@ -53,7 +58,6 @@ impl<'a> OsuDifficultyObject<'a> { idx: usize, scaling_factor: &ScalingFactor, ) -> Self { - let radius = f64::from(OsuObject::OBJECT_RADIUS * scaling_factor.scale); let delta_time = (hit_object.start_time - last_object.start_time) / clock_rate; let start_time = hit_object.start_time / clock_rate; let end_time = hit_object.end_time() / clock_rate; @@ -71,7 +75,6 @@ impl<'a> OsuDifficultyObject<'a> { let mut this = Self { idx, base: hit_object, - radius, start_time, end_time, delta_time, @@ -184,14 +187,12 @@ impl<'a> OsuDifficultyObject<'a> { last_object.stacked_pos() }; - self.lazy_jump_dist = f64::from( - (self.base.stacked_pos() * scaling_factor - last_cursor_pos * scaling_factor).length(), - ); - self.min_jump_dist = self.lazy_jump_dist; - self.jump_dist = f64::from( (last_object.stacked_pos() - self.base.stacked_pos()).length() * scaling_factor, ); + self.lazy_jump_dist = + f64::from((self.base.stacked_pos() - last_cursor_pos).length() * scaling_factor); + self.min_jump_dist = self.lazy_jump_dist; let Some(last_diff_obj) = last_diff_obj else { return; @@ -217,11 +218,7 @@ impl<'a> OsuDifficultyObject<'a> { self.min_jump_dist = ((self.lazy_jump_dist - diff).min(min)).max(0.0); } - let Some(last_last_diff_obj) = last_last_diff_obj else { - return; - }; - - if !last_last_diff_obj.base.is_spinner() { + if let Some(last_last_diff_obj) = last_last_diff_obj.filter(|ob| !ob.base.is_spinner()) { if last_object.is_slider() && last_diff_obj.travel_dist > 0.0 { last_cursor_pos = last_object.stacked_pos(); } @@ -343,23 +340,23 @@ impl<'a> OsuDifficultyObject<'a> { ) -> f64 { let last_pos = Self::get_end_cursor_pos(last_diff_obj); - let adjusted_last_last_pos = if let OsuObjectKind::Slider(ref last_slider) = - last_diff_obj.base.kind + let last_last_pos = if let OsuObjectKind::Slider(ref last_slider) = last_diff_obj.base.kind && last_diff_obj.travel_dist > 0.0 { - if let Some(second_last_nested) = last_slider - .nested_objects - .get(last_slider.nested_objects.len().saturating_sub(2)) - { - second_last_nested.pos + last_diff_obj.base.stack_offset + let m = last_slider.nested_objects.len(); + + if m >= 2 { + last_slider.nested_objects[m - 2].pos + last_diff_obj.base.stack_offset } else { - last_last_pos + // C#'s NestedHitObjects[^2] would resolve to the head circle here — + // which isn't present in `nested_objects`, so fall back to the base object. + last_diff_obj.base.stacked_pos() } } else { last_last_pos }; - self.calculate_angle(last_pos, adjusted_last_last_pos) + self.calculate_angle(last_pos, last_last_pos) } fn calculate_angle(&self, last_pos: Pos, last_last_pos: Pos) -> f64 { diff --git a/src/osu/difficulty/skills/aim.rs b/src/osu/difficulty/skills/aim.rs index 6260112b..e156217f 100644 --- a/src/osu/difficulty/skills/aim.rs +++ b/src/osu/difficulty/skills/aim.rs @@ -17,7 +17,7 @@ use crate::{ util::{ difficulty::{lerp, logistic, logistic_exp, norm}, float_ext::FloatExt, - traits::IEnumerable, + traits::{IEnumerable, IOrderedEnumerable}, }, }; @@ -28,6 +28,7 @@ define_new_skill! { mods: GameMods, include_sliders: bool, overall_difficulty: f64, + obj_radius: f64, } } @@ -40,7 +41,7 @@ impl Aim { const COMBINED_SNAP_NORM_EXPONENT: f64 = 1.2; fn strain_decay(ms: f64) -> f64 { - strain_decay_base(ms, 0.15) + strain_decay_base(ms, 0.2) } fn calculate_initial_strain<'a>( @@ -87,9 +88,12 @@ impl Aim { * Self::SKILL_MULTIPLIER_SNAP; let agility_difficulty = AgilityEvaluator::evaluate_diff_of(curr, objects) * Self::SKILL_MULTIPLIER_AGILITY; - let flow_difficulty = - FlowAimEvaluator::evaluate_diff_of(curr, objects, self.include_sliders) - * Self::SKILL_MULTIPLIER_FLOW; + let flow_difficulty = FlowAimEvaluator::evaluate_diff_of( + curr, + objects, + self.include_sliders, + self.obj_radius, + ) * Self::SKILL_MULTIPLIER_FLOW; let mut total_difficulty = self.calculate_total_value(snap_difficulty, agility_difficulty, flow_difficulty); @@ -138,7 +142,7 @@ impl Aim { flow_difficulty_new *= 0.6; } - let total_difficulty = combined_snap_difficulty * p_snap * flow_difficulty_new * p_flow; + let total_difficulty = combined_snap_difficulty * p_snap + flow_difficulty_new * p_flow; total_difficulty * Self::SKILL_MULTIPLIER_TOTAL } @@ -193,37 +197,31 @@ impl Aim { count_top_weighted_sliders(&self.slider_strains, consistent_top_strain) } - pub fn difficulty_value(current_strain_peaks: Vec) -> f64 { + pub fn difficulty_value( + current_strain_peaks: Vec, + current_section_peak: f64, + current_section_begin: f64, + current_section_end: f64, + ) -> f64 { aim_difficulty_value( - Self::get_reduced_strain_peaks(current_strain_peaks), + Self::get_reduced_strain_peaks(Self::get_current_strain_peaks( + current_strain_peaks, + current_section_peak, + current_section_begin, + current_section_end, + )), Self::MAX_SECTION_LENGTH, Self::DECAY_WEIGHT, ) } - #[expect(dead_code, reason = "overwrites macro impl")] - pub fn into_difficulty_value(self) -> f64 { - Self::difficulty_value(Self::get_reduced_strain_peaks( - Self::get_current_strain_peaks( - self.skill_strain_peaks, - self.skill_final_peak, - self.skill_current_section_peak, - self.skill_current_section_begin, - self.skill_current_section_end, - ), - )) - } - pub fn cloned_difficulty_value(&self) -> f64 { - Self::difficulty_value(Self::get_reduced_strain_peaks( - Self::get_current_strain_peaks( - self.skill_strain_peaks.clone(), - self.skill_final_peak, - self.skill_current_section_peak, - self.skill_current_section_begin, - self.skill_current_section_end, - ), - )) + Self::difficulty_value( + self.skill_strain_peaks.clone(), + self.skill_current_section_peak, + self.skill_current_section_begin, + self.skill_current_section_end, + ) } fn get_reduced_strain_peaks(current_strain_peaks: Vec) -> Vec { @@ -265,10 +263,7 @@ impl Aim { skip_count += 1; } - let mut reduced = strains.split_off(skip_count); - reduced.sort_by(|a, b| b.cmp(a)); - - reduced + strains.split_off(skip_count).cs_order_descending() } pub fn difficulty_to_performance(difficulty: f64) -> f64 { diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index 7e31592f..9a6b08b0 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -28,7 +28,6 @@ impl OsuSkills { great_hit_window: f64, time_preempt: f64, total_objects: usize, - overall_difficulty: f64, ) -> Self { let hit_window = 2.0 * great_hit_window; @@ -45,9 +44,20 @@ impl OsuSkills { } else { 400.0 * (time_preempt / OsuObject::PREEMPT_MIN).min(1.0) }; + let overall_difficulty = (79.5 - hit_window / 2.0) / 6.0; - let aim = Aim::new(mods.clone(), true, overall_difficulty); - let aim_no_sliders = Aim::new(mods.clone(), false, overall_difficulty); + let aim = Aim::new( + mods.clone(), + true, + overall_difficulty, + scaling_factor.radius, + ); + let aim_no_sliders = Aim::new( + mods.clone(), + false, + overall_difficulty, + scaling_factor.radius, + ); let speed = Speed::new(mods.clone(), hit_window); let flashlight = Flashlight::new( mods, diff --git a/src/util/macros_new.rs b/src/util/macros_new.rs index 38ccd66c..1863fd29 100644 --- a/src/util/macros_new.rs +++ b/src/util/macros_new.rs @@ -132,7 +132,6 @@ macro_rules! define_new_skill { skill_current_section_begin f64 = 0.0, // <- skill_total_length f64 = 0.0, // <- skill_strain_peaks Vec = Vec::with_capacity(256), // <- - skill_final_peak Option = None, // <- skill_queued_strains Vec<(f64, f64)> = Vec::with_capacity(256), // <- } $( $rest )* @@ -529,11 +528,6 @@ macro_rules! define_new_skill { } fn save_current_peak(&mut self, section_length: f64) { - if let Some(final_peak) = self.skill_final_peak { - self.skill_strain_peaks.retain(|p| *p != final_peak); - self.skill_final_peak = None; - } - let peak = crate::any::difficulty::skills_new::variable_length_strain_skill::StrainPeak::new(self.skill_current_section_peak, section_length); self.skill_strain_peaks.cs_add_in_place(peak); @@ -571,13 +565,7 @@ macro_rules! define_new_skill { } fn into_current_strain_peaks(self) -> Vec { - Self::get_current_strain_peaks( - self.skill_strain_peaks, - self.skill_final_peak, - self.skill_current_section_peak, - self.skill_current_section_begin, - self.skill_current_section_end - ) + self.skill_strain_peaks } fn count_top_weighted_strains(&self, difficulty_value: f64) -> f64 { diff --git a/src/util/traits.rs b/src/util/traits.rs index 174344d9..9da27173 100644 --- a/src/util/traits.rs +++ b/src/util/traits.rs @@ -15,6 +15,13 @@ pub trait IEnumerable: Sized { /// Mimics the C# `IOrderedEnumerable` interface. pub trait IOrderedEnumerable: IEnumerable { + /// Sorts the elements of a sequence in descending order. + /// + /// This method performs a stable sort; that is, if the keys of two elements + /// are equal, the order of the elements is preserved. In contrast, an unstable sort does not + /// preserve the order of elements that have the same key. + /// + /// fn cs_order_descending(self) -> Self; } @@ -39,7 +46,7 @@ impl IEnumerable for Vec { } /// Adds the given item to the list according to standard sorting rules. Do not use on unsorted lists. - /// + /// /// fn cs_add_in_place(&mut self, item: T) -> usize where @@ -52,13 +59,6 @@ impl IEnumerable for Vec { } impl IOrderedEnumerable for Vec { - /// Sorts the elements of a sequence in descending order. - /// - /// This method performs a stable sort; that is, if the keys of two elements - /// are equal, the order of the elements is preserved. In contrast, an unstable sort does not - /// preserve the order of elements that have the same key. - /// - /// fn cs_order_descending(mut self) -> Self { self.sort_by(|a, b| b.total_cmp(a)); diff --git a/tests/performance.rs b/tests/performance.rs index 81e219cb..7a5bca18 100644 --- a/tests/performance.rs +++ b/tests/performance.rs @@ -4,7 +4,7 @@ use rosu_pp::{ Beatmap, catch::{CatchPerformance, CatchPerformanceAttributes}, mania::{ManiaPerformance, ManiaPerformanceAttributes}, - osu::{OsuPerformance, OsuPerformanceAttributes}, + osu::OsuPerformanceAttributes, taiko::{TaikoPerformance, TaikoPerformanceAttributes}, }; @@ -334,6 +334,7 @@ impl AssertEq for OsuPerformanceAttributes { pp_acc, pp_aim, pp_flashlight, + pp_reading, pp_speed, effective_miss_count, speed_deviation, @@ -347,6 +348,7 @@ impl AssertEq for OsuPerformanceAttributes { assert_eq_float(*pp_acc, expected.pp_acc); assert_eq_float(*pp_aim, expected.pp_aim); assert_eq_float(*pp_flashlight, expected.pp_flashlight); + assert_eq_float(*pp_reading, expected.pp_reading); assert_eq_float(*pp_speed, expected.pp_speed); assert_eq_float(*effective_miss_count, expected.effective_miss_count); assert_eq_option(*speed_deviation, expected.speed_deviation); From 21e649f871817a8a5337b72b8fb02c17f2ad7ee5 Mon Sep 17 00:00:00 2001 From: myssto Date: Sun, 2 Aug 2026 23:12:44 -0500 Subject: [PATCH 17/27] fix: osu speed skill value parity --- .../difficulty/skills_new/harmonic_skill.rs | 27 ++++++++++--------- src/osu/difficulty/evaluators/speed/rhythm.rs | 8 +++--- src/osu/difficulty/gradual.rs | 2 +- src/osu/difficulty/mod.rs | 25 ++++++++++------- src/osu/difficulty/skills/reading.rs | 10 ++++--- src/osu/difficulty/skills/speed.rs | 7 ++--- src/util/macros_new.rs | 22 +++++++-------- 7 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/any/difficulty/skills_new/harmonic_skill.rs b/src/any/difficulty/skills_new/harmonic_skill.rs index a7771eeb..e9730b6a 100644 --- a/src/any/difficulty/skills_new/harmonic_skill.rs +++ b/src/any/difficulty/skills_new/harmonic_skill.rs @@ -28,17 +28,20 @@ pub trait HarmonicSkill: Skill { fn into_transformed_difficulties(self) -> Vec; - fn difficulty_value( - transformed_object_difficulties: Vec, - object_weight_sum: &mut f64, - ) -> f64; + /// Returns `(difficulty_value, object_weight_sum)`. + fn difficulty_value(transformed_object_difficulties: Vec) -> (f64, f64); #[expect(dead_code, reason = "staying in-sync with existing skills")] fn into_difficulty_value(self) -> f64; - fn cloned_difficulty_value(&mut self) -> f64; + /// Returns `(difficulty_value, object_weight_sum)`. + fn cloned_difficulty_value(&self) -> (f64, f64); - fn count_top_weighted_object_difficulties(&self, difficulty_value: f64) -> f64; + fn count_top_weighted_object_difficulties( + &self, + difficulty_value: f64, + object_weight_sum: f64, + ) -> f64; fn difficulty_to_performance(difficulty: f64) -> f64 { 4.0 * difficulty.powf(3.0) @@ -47,17 +50,15 @@ pub trait HarmonicSkill: Skill { pub fn harmonic_skill_difficulty_value( transformed_object_difficulties: &[f64], - object_weight_sum: &mut f64, harmonic_scale: f64, decay_exponent: f64, -) -> f64 { - *object_weight_sum = 0.0; - +) -> (f64, f64) { if transformed_object_difficulties.is_empty() { - return 0.0; + return (0.0, 0.0); } let mut difficulty = 0.0; + let mut object_weight_sum = 0.0; // * Objects with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // * These objects will not contribute to the difficulty. @@ -72,10 +73,10 @@ pub fn harmonic_skill_difficulty_value( let weight = (1.0 + (harmonic_scale / (1 + index) as f64)) / ((index as f64).powf(decay_exponent) + 1.0 + (harmonic_scale / (1 + index) as f64)); - *object_weight_sum += weight; + object_weight_sum += weight; difficulty += obj * weight; } - difficulty + (difficulty, object_weight_sum) } diff --git a/src/osu/difficulty/evaluators/speed/rhythm.rs b/src/osu/difficulty/evaluators/speed/rhythm.rs index ac8612fc..d5a9a97e 100644 --- a/src/osu/difficulty/evaluators/speed/rhythm.rs +++ b/src/osu/difficulty/evaluators/speed/rhythm.rs @@ -33,7 +33,7 @@ impl RhythmEvaluator { // * Store the difficulty of the current start of an island to buff for tighter rhythms. let mut start_difficulty = 0.0; let mut first_delta_switch = false; - let historical_note_count = std::cmp::max(curr.idx, Self::HISTORY_OBJECTS_MAX); + let historical_note_count = std::cmp::min(curr.idx, Self::HISTORY_OBJECTS_MAX); let mut rhythm_start = 0; while curr @@ -65,7 +65,7 @@ impl RhythmEvaluator { let time_decay = (f64::from(Self::HISTORY_TIME_MAX) - (curr.start_time - curr_obj.start_time)) / f64::from(Self::HISTORY_TIME_MAX); - let note_decay = ((historical_note_count - i) / historical_note_count) as f64; + let note_decay = (historical_note_count - i) as f64 / historical_note_count as f64; let curr_historical_decay = note_decay.min(time_decay); @@ -85,7 +85,7 @@ impl RhythmEvaluator { prev_delta.max(curr_delta) / prev_delta.min(curr_delta); // * reduce ratio bonus if delta difference is too big - let difference_multiplier = (2.0 - delta_difference / 8.0).clamp(0.0, 1.0); + let difference_multiplier = (2.0 - delta_difference_ratio / 8.0).clamp(0.0, 1.0); let window_penalty = ((delta_difference - delta_difference_eps) / delta_difference_eps) @@ -252,7 +252,7 @@ impl RhythmIsland { } fn add_delta(&mut self, delta: i32) { - if delta == i32::MAX { + if self.delta == i32::MAX { self.delta = std::cmp::max(delta, OsuDifficultyObject::MIN_DELTA_TIME as i32); } diff --git a/src/osu/difficulty/gradual.rs b/src/osu/difficulty/gradual.rs index 05f831a6..861c0ec3 100644 --- a/src/osu/difficulty/gradual.rs +++ b/src/osu/difficulty/gradual.rs @@ -221,7 +221,7 @@ impl Iterator for OsuGradualDifficulty { let mut attrs = self.attrs.clone(); - DifficultyValues::eval(&mut attrs, self.difficulty.get_mods(), &mut self.skills); + DifficultyValues::eval(&mut attrs, self.difficulty.get_mods(), &self.skills); Some(attrs) } diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index 1a059f73..93f7d00b 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -66,14 +66,14 @@ fn calculate_difficulty(difficulty: &Difficulty, map: &Beatmap) -> OsuDifficulty let DifficultyValues { osu_objects, - mut skills, + skills, mut attrs, } = DifficultyValues::calculate(difficulty, map); let mods = difficulty.get_mods(); let passed_objects = difficulty.get_passed_objects(); - DifficultyValues::eval(&mut attrs, mods, &mut skills); + DifficultyValues::eval(&mut attrs, mods, &skills); let mut simulator = OsuLegacyScoreSimulator::new(&osu_objects, map, passed_objects); @@ -186,7 +186,7 @@ impl DifficultyValues { } /// Process the difficulty values and store the results in `attrs`. - pub fn eval(attrs: &mut OsuDifficultyAttributes, mods: &GameMods, skills: &mut OsuSkills) { + pub fn eval(attrs: &mut OsuDifficultyAttributes, mods: &GameMods, skills: &OsuSkills) { let OsuSkills { aim, aim_no_sliders, @@ -197,14 +197,19 @@ impl DifficultyValues { let aim_difficulty_value = aim.cloned_difficulty_value(); let aim_no_sliders_difficulty_value = aim_no_sliders.cloned_difficulty_value(); - let speed_difficulty_value = speed.cloned_difficulty_value(); - let reading_difficulty_value = reading.cloned_difficulty_value(); + let (speed_difficulty_value, speed_object_weight_sum) = speed.cloned_difficulty_value(); + let (reading_difficulty_value, reading_object_weight_sum) = + reading.cloned_difficulty_value(); let aim_difficult_strain_count = aim.count_top_weighted_strains(aim_difficulty_value); - let speed_difficult_strain_count = - speed.count_top_weighted_object_difficulties(speed_difficulty_value); - let reading_difficult_note_count = - reading.count_top_weighted_object_difficulties(reading_difficulty_value); + let speed_difficult_strain_count = speed.count_top_weighted_object_difficulties( + speed_difficulty_value, + speed_object_weight_sum, + ); + let reading_difficult_note_count = reading.count_top_weighted_object_difficulties( + reading_difficulty_value, + reading_object_weight_sum, + ); let speed_notes = speed.relevant_object_count(); @@ -218,7 +223,7 @@ impl DifficultyValues { .max(1.0); let speed_top_weighted_slider_count = - speed.count_top_weighted_sliders(speed_difficulty_value); + speed.count_top_weighted_sliders(speed_difficulty_value, speed_object_weight_sum); let speed_top_weighted_slider_factor = speed_top_weighted_slider_count / (speed_difficult_strain_count - speed_top_weighted_slider_count).max(1.0); diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs index fe8d2dda..f21a9095 100644 --- a/src/osu/difficulty/skills/reading.rs +++ b/src/osu/difficulty/skills/reading.rs @@ -42,7 +42,7 @@ impl Reading { curr: &'a OsuDifficultyObject<'a>, objects: &'a [OsuDifficultyObject<'a>], ) -> f64 { - let decay = Self::strain_decay(curr.adjusted_delta_time); + let decay = Self::strain_decay(curr.delta_time); self.current_strain *= decay; self.current_strain += self.calculate_adjusted_difficulty(curr, objects) @@ -112,11 +112,15 @@ impl Reading { } #[expect(dead_code, reason = "overwrites macro impl")] - fn count_top_weighted_object_difficulties(&self, difficulty_value: f64) -> f64 { + fn count_top_weighted_object_difficulties( + &self, + difficulty_value: f64, + object_weight_sum: f64, + ) -> f64 { count_top_weighted_object_difficulties( difficulty_value, &self.skill_object_difficulties, - self.skill_object_weight_sum, + object_weight_sum, 1.15, 5.0, Some(1.1), diff --git a/src/osu/difficulty/skills/speed.rs b/src/osu/difficulty/skills/speed.rs index e8161887..dadfa820 100644 --- a/src/osu/difficulty/skills/speed.rs +++ b/src/osu/difficulty/skills/speed.rs @@ -21,6 +21,7 @@ define_new_skill! { impl Speed { const SKILL_MULTIPLIER: f64 = 1.16; + const HARMONIC_SCALE: f64 = 20.0; fn strain_decay(ms: f64) -> f64 { strain_decay_base(ms, 0.3) @@ -88,13 +89,13 @@ impl Speed { .sum() } - pub fn count_top_weighted_sliders(&self, difficulty_value: f64) -> f64 { - if self.slider_strains.is_empty() || FloatExt::eq(self.skill_object_weight_sum, 0.0) { + pub fn count_top_weighted_sliders(&self, difficulty_value: f64, object_weight_sum: f64) -> f64 { + if self.slider_strains.is_empty() || FloatExt::eq(object_weight_sum, 0.0) { return 0.0; } // * What would the top note be if all note values were identical - let consistent_top_object = difficulty_value / self.skill_object_weight_sum; + let consistent_top_object = difficulty_value / object_weight_sum; count_top_weighted_sliders(&self.slider_strains, consistent_top_object) } diff --git a/src/util/macros_new.rs b/src/util/macros_new.rs index 1863fd29..68a2e48d 100644 --- a/src/util/macros_new.rs +++ b/src/util/macros_new.rs @@ -170,7 +170,6 @@ macro_rules! define_new_skill { extend_fields Skill fields { $( $fields )* - skill_object_weight_sum f64 = 0.0, // <- } $( $rest )* } @@ -595,37 +594,36 @@ macro_rules! define_new_skill { self.get_transformed_difficulties(self.get_object_difficulties().to_vec()) } - fn difficulty_value( - transformed_object_difficulties: Vec, - object_weight_sum: &mut f64, - ) -> f64 { + fn difficulty_value(transformed_object_difficulties: Vec) -> (f64, f64) { crate::any::difficulty::skills_new::harmonic_skill::harmonic_skill_difficulty_value( &transformed_object_difficulties, - object_weight_sum, Self::HARMONIC_SCALE, Self::DECAY_EXPONENT, ) } - fn into_difficulty_value(mut self) -> f64 { + fn into_difficulty_value(self) -> f64 { Self::difficulty_value( self.get_transformed_difficulties(self.get_object_difficulties().to_vec()), - &mut self.skill_object_weight_sum, ) + .0 } - fn cloned_difficulty_value(&mut self) -> f64 { + fn cloned_difficulty_value(&self) -> (f64, f64) { Self::difficulty_value( self.get_transformed_difficulties(self.skill_object_difficulties.clone()), - &mut self.skill_object_weight_sum, ) } - fn count_top_weighted_object_difficulties(&self, difficulty_value: f64) -> f64 { + fn count_top_weighted_object_difficulties( + &self, + difficulty_value: f64, + object_weight_sum: f64, + ) -> f64 { crate::any::difficulty::skills_new::count_top_weighted_object_difficulties( difficulty_value, &self.skill_object_difficulties, - self.skill_object_weight_sum, + object_weight_sum, 0.88, 10.0, Some(1.1) From 67fb9a996482a6f7ec4181fbb5cb1c6a6904e29d Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 3 Aug 2026 02:26:05 -0500 Subject: [PATCH 18/27] fix: osu reading skill value parity --- .../difficulty/skills_new/harmonic_skill.rs | 19 +- src/osu/difficulty/evaluators/reading.rs | 203 ++++++++++++------ src/osu/difficulty/mod.rs | 2 +- src/osu/difficulty/skills/reading.rs | 5 +- tests/difficulty.rs | 14 +- 5 files changed, 160 insertions(+), 83 deletions(-) diff --git a/src/any/difficulty/skills_new/harmonic_skill.rs b/src/any/difficulty/skills_new/harmonic_skill.rs index e9730b6a..a41a921c 100644 --- a/src/any/difficulty/skills_new/harmonic_skill.rs +++ b/src/any/difficulty/skills_new/harmonic_skill.rs @@ -53,22 +53,21 @@ pub fn harmonic_skill_difficulty_value( harmonic_scale: f64, decay_exponent: f64, ) -> (f64, f64) { + let mut difficulty = 0.0; + let mut object_weight_sum = 0.0; + if transformed_object_difficulties.is_empty() { - return (0.0, 0.0); + return (difficulty, object_weight_sum); } - let mut difficulty = 0.0; - let mut object_weight_sum = 0.0; + let new_iter = transformed_object_difficulties + .to_vec() + .cs_order_descending() + .cs_where(|v| *v > 0.0); // * Objects with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // * These objects will not contribute to the difficulty. - for (index, obj) in transformed_object_difficulties - .to_vec() - .cs_order_descending() - .cs_where(|v| *v > 0.0) - .iter() - .enumerate() - { + for (index, obj) in new_iter.iter().enumerate() { // * Use a harmonic sum that considers each object of the map according to a predefined weight. let weight = (1.0 + (harmonic_scale / (1 + index) as f64)) / ((index as f64).powf(decay_exponent) + 1.0 + (harmonic_scale / (1 + index) as f64)); diff --git a/src/osu/difficulty/evaluators/reading.rs b/src/osu/difficulty/evaluators/reading.rs index 2bc18fd0..31e2f90b 100644 --- a/src/osu/difficulty/evaluators/reading.rs +++ b/src/osu/difficulty/evaluators/reading.rs @@ -1,11 +1,11 @@ use core::f64; use crate::{ - any::difficulty::object::IDifficultyObject, - osu::difficulty::object::OsuDifficultyObject, + any::difficulty::object::IDifficultyObject, + osu::difficulty::object::OsuDifficultyObject, util::{ - difficulty::{norm, reverse_lerp, smootherstep}, - float_ext::FloatExt + difficulty::{norm, reverse_lerp, smootherstep}, + float_ext::FloatExt, }, }; @@ -16,7 +16,8 @@ pub struct ReadingEvaluator { impl ReadingEvaluator { const READING_WINDOW_SIZE: f64 = 3000.0; // * 3 seconds - const DISTANCE_INFLUENCE_THRESHOLD: f64 = (OsuDifficultyObject::NORMALIZED_DIAMETER as f64) * 1.5; + const DISTANCE_INFLUENCE_THRESHOLD: f64 = + (OsuDifficultyObject::NORMALIZED_DIAMETER as f64) * 1.5; const DENSITY_MULTIPLIER: f64 = 2.4; const DESNITY_DIFFICULTY_BASE: f64 = 2.5; @@ -41,7 +42,7 @@ impl ReadingEvaluator { curr: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>], hidden: bool, - ) -> f64 { + ) -> f64 { if curr.base.is_spinner() || curr.idx == 0 { return 0.0; } @@ -51,29 +52,47 @@ impl ReadingEvaluator { // * Only allow velocity to buff let velocity = f64::max(1.0, curr_obj.lazy_jump_dist / curr_obj.adjusted_delta_time); - - let curr_visible_obj_density = self.retrieve_current_visible_object_density(curr_obj, diff_objects); - let past_obj_difficulty_influence = self.get_past_obj_difficulty_influence(curr_obj, diff_objects); - let constant_angle_nerf_factor = Self::get_constant_angle_nerf_factor(curr_obj, diff_objects); + let curr_visible_obj_density = + self.retrieve_current_visible_object_density(curr_obj, diff_objects); + let past_obj_difficulty_influence = + self.get_past_obj_difficulty_influence(curr_obj, diff_objects); + + let constant_angle_nerf_factor = + Self::get_constant_angle_nerf_factor(curr_obj, diff_objects); let note_density_difficulty = Self::calc_density_difficulty( next_obj, velocity, constant_angle_nerf_factor, past_obj_difficulty_influence, - curr_visible_obj_density + curr_visible_obj_density, ); let hidden_difficulty = if hidden { - self.calc_hidden_difficulty(curr_obj, diff_objects, past_obj_difficulty_influence, curr_visible_obj_density, velocity, constant_angle_nerf_factor) + self.calc_hidden_difficulty( + curr_obj, + diff_objects, + past_obj_difficulty_influence, + curr_visible_obj_density, + velocity, + constant_angle_nerf_factor, + ) } else { 0.0 }; - let preempt_difficulty = Self::calc_preempt_difficulty(velocity, constant_angle_nerf_factor, self.time_preempt); + let preempt_difficulty = + Self::calc_preempt_difficulty(velocity, constant_angle_nerf_factor, self.time_preempt); - let mut reading_difficulty = norm(1.5, [preempt_difficulty, hidden_difficulty, note_density_difficulty]); + let mut reading_difficulty = norm( + 1.5, + [ + preempt_difficulty, + hidden_difficulty, + note_density_difficulty, + ], + ); // Having less time to process information is harder reading_difficulty *= Self::high_bpm_bonus(curr_obj.adjusted_delta_time); @@ -90,21 +109,26 @@ impl ReadingEvaluator { ) -> f64 { // * Consider future densities too because it can make the path the cursor takes less clear let mut fut_obj_difficulty_influence = curr_visible_obj_density.sqrt(); - + if let Some(next_obj) = next_obj { // * Reduce difficulty if movement to next object is small - fut_obj_difficulty_influence = smootherstep(next_obj.lazy_jump_dist, 15.0, Self::DISTANCE_INFLUENCE_THRESHOLD); + fut_obj_difficulty_influence *= smootherstep( + next_obj.lazy_jump_dist, + 15.0, + Self::DISTANCE_INFLUENCE_THRESHOLD, + ); } // * Value higher note densities exponentially - let mut note_density_difficulty = (past_obj_difficulty_influence + fut_obj_difficulty_influence) - .powf(1.7) - * 0.4 - * constant_angle_nerf_factor - * velocity; + let mut note_density_difficulty = + (past_obj_difficulty_influence + fut_obj_difficulty_influence).powf(1.7) + * 0.4 + * constant_angle_nerf_factor + * velocity; // * Award only denser than average maps. - note_density_difficulty = (note_density_difficulty - Self::DESNITY_DIFFICULTY_BASE).max(0.0); + note_density_difficulty = + (note_density_difficulty - Self::DESNITY_DIFFICULTY_BASE).max(0.0); // Apply a soft cap to general density reading to account for partial memorization note_density_difficulty.powf(0.45) * Self::DENSITY_MULTIPLIER @@ -117,7 +141,8 @@ impl ReadingEvaluator { ) -> f64 { // * Arbitrary curve for the base value preempt difficulty should have as approach rate increases. // * https://www.desmos.com/calculator/c175335a71 - ((Self::PREEMPT_STARTING_POINT - preempt + (preempt - Self::PREEMPT_STARTING_POINT).abs()) / 2.0) + ((Self::PREEMPT_STARTING_POINT - preempt + (preempt - Self::PREEMPT_STARTING_POINT).abs()) + / 2.0) .powf(2.5) / Self::PREEMPT_BALANCING_FACTOR * constant_angle_nerf_factor @@ -137,39 +162,63 @@ impl ReadingEvaluator { let preempt_factor = self.time_preempt.powf(2.2) * 0.01; // * Account for both past and current densities - let density_factor = (curr_visible_obj_density + past_obj_difficulty_influence).powf(3.3) * 3.0; + let density_factor = + (curr_visible_obj_density + past_obj_difficulty_influence).powf(3.3) * 3.0; - let mut hidden_difficulty = (preempt_factor + density_factor) * constant_angle_nerf_factor * velocity * 0.01; + let mut hidden_difficulty = + (preempt_factor + density_factor) * constant_angle_nerf_factor * velocity * 0.01; // * Apply a soft cap to general HD reading to account for partial memorization hidden_difficulty = hidden_difficulty.powf(0.4) * Self::HIDDEN_MULTIPLIER; if let Some(prev_obj) = curr_obj.previous(0, diff_objects) && FloatExt::eq(curr_obj.lazy_jump_dist, 0.0) - && FloatExt::eq(curr_obj.opacity_at(prev_obj.start_time, true, self.time_preempt, self.time_fade_in), 0.0) - && prev_obj.start_time > curr_obj.start_time - self.time_preempt { - - // * Perfect stacks are harder the less time between notes - hidden_difficulty += Self::HIDDEN_MULTIPLIER * 2500.0 / curr_obj.adjusted_delta_time.powf(1.5); - } + && FloatExt::eq( + curr_obj.opacity_at( + prev_obj.start_time, + true, + self.time_preempt, + self.time_fade_in, + ), + 0.0, + ) + && prev_obj.start_time > curr_obj.start_time - self.time_preempt + { + // * Perfect stacks are harder the less time between notes + hidden_difficulty += + Self::HIDDEN_MULTIPLIER * 2500.0 / curr_obj.adjusted_delta_time.powf(1.5); + } hidden_difficulty } - fn get_past_obj_difficulty_influence<'a>(&self, curr_obj: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>]) -> f64 { + fn get_past_obj_difficulty_influence<'a>( + &self, + curr_obj: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>], + ) -> f64 { diff_objects .iter() // Note: This achieves the same as retrievePastVisibleObjects .filter(|d| { d.idx < curr_obj.idx - && curr_obj.start_time - d.start_time > Self::READING_WINDOW_SIZE - && d.start_time < curr_obj.start_time - self.time_preempt + && curr_obj.start_time - d.start_time <= Self::READING_WINDOW_SIZE + && d.start_time >= curr_obj.start_time - self.time_preempt }) .fold(0.0, |past_obj_difficulty_influence, loop_obj| { - let mut loop_difficulty = loop_obj.opacity_at(loop_obj.start_time, false, self.time_preempt, self.time_fade_in); + let mut loop_difficulty = curr_obj.opacity_at( + loop_obj.start_time, + false, + self.time_preempt, + self.time_fade_in, + ); // * When aiming an object small distances mean previous objects may be cheesed, so it doesn't matter whether they were arranged confusingly. - loop_difficulty *= smootherstep(loop_obj.lazy_jump_dist, 15.0, Self::DISTANCE_INFLUENCE_THRESHOLD); + loop_difficulty *= smootherstep( + loop_obj.lazy_jump_dist, + 15.0, + Self::DISTANCE_INFLUENCE_THRESHOLD, + ); // * Account less for objects close to the max reading window let delta_time = curr_obj.start_time - loop_obj.start_time; @@ -181,19 +230,28 @@ impl ReadingEvaluator { } // * Returns the density of objects visible at the point in time the current object needs to be clicked capped by the reading window. - fn retrieve_current_visible_object_density<'a>(&self, curr_obj: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>]) -> f64 { + fn retrieve_current_visible_object_density<'a>( + &self, + curr_obj: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>], + ) -> f64 { let mut forwards_idx = 0; let mut visible_object_count = 0.0; - + while let Some(hit_obj) = curr_obj.next(forwards_idx, diff_objects).filter(|next| { - next.start_time - curr_obj.start_time > Self::READING_WINDOW_SIZE + next.start_time - curr_obj.start_time <= Self::READING_WINDOW_SIZE // * Object not visible at the time current object needs to be clicked. - || curr_obj.start_time < next.start_time - self.time_preempt + && curr_obj.start_time >= next.start_time - self.time_preempt }) { let delta_time = hit_obj.start_time - curr_obj.start_time; let time_nerf_factor = Self::get_time_nerf_factor(delta_time); - visible_object_count += hit_obj.opacity_at(curr_obj.start_time, false, self.time_preempt, self.time_fade_in) * time_nerf_factor; + visible_object_count += hit_obj.opacity_at( + curr_obj.start_time, + false, + self.time_preempt, + self.time_fade_in, + ) * time_nerf_factor; forwards_idx += 1; } @@ -204,7 +262,10 @@ impl ReadingEvaluator { // * Returns a factor of how often the current object's angle has been repeated in a certain time frame. // * It does this by checking the difference in angle between current and past objects and sums them based on a range of similarity. // * https://www.desmos.com/calculator/eb057a4822 - fn get_constant_angle_nerf_factor<'a>(curr_obj: &'a OsuDifficultyObject<'a>, diff_objects: &'a [OsuDifficultyObject<'a>]) -> f64 { + fn get_constant_angle_nerf_factor<'a>( + curr_obj: &'a OsuDifficultyObject<'a>, + diff_objects: &'a [OsuDifficultyObject<'a>], + ) -> f64 { let mut constant_angle_count = 0.0; let mut backwards_idx = 0; let mut curr_time_gap = 0.0; @@ -213,46 +274,64 @@ impl ReadingEvaluator { let mut loop_obj_prev1: Option<&OsuDifficultyObject<'a>> = None; let mut loop_obj_prev2: Option<&OsuDifficultyObject<'a>> = None; - while let Some(loop_obj) = curr_obj.previous(backwards_idx, diff_objects).filter(|_| curr_time_gap < Self::MINIMUM_ANGLE_RELEVANCY_TIME) { + while let Some(loop_obj) = curr_obj + .previous(backwards_idx, diff_objects) + .filter(|_| curr_time_gap < Self::MINIMUM_ANGLE_RELEVANCY_TIME) + { // * Account less for objects that are close to the time limit. - let long_interval_factor = reverse_lerp( - loop_obj.adjusted_delta_time, - Self::MAXIMUM_ANGLE_RELEVANCY_TIME, - Self::MINIMUM_ANGLE_RELEVANCY_TIME - ); + let long_interval_factor = 1.0 + - reverse_lerp( + loop_obj.adjusted_delta_time, + Self::MAXIMUM_ANGLE_RELEVANCY_TIME, + Self::MINIMUM_ANGLE_RELEVANCY_TIME, + ); if let (Some(loop_obj_angle), Some(curr_obj_angle)) = (loop_obj.angle, curr_obj.angle) { let angle_diff = (curr_obj_angle - loop_obj_angle).abs(); let mut angle_diff_alternating = f64::consts::PI; - if let (Some(loop_obj_prev0_angle), Some(loop_obj_prev1_angle), Some(loop_obj_prev2_angle)) - = (loop_obj_prev0.angle, loop_obj_prev1.and_then(|o| o.angle), loop_obj_prev2.and_then(|o| o.angle)) { + if let ( + Some(loop_obj_prev0_angle), + Some(loop_obj_prev1_angle), + Some(loop_obj_prev2_angle), + ) = ( + loop_obj_prev0.angle, + loop_obj_prev1.and_then(|o| o.angle), + loop_obj_prev2.and_then(|o| o.angle), + ) { angle_diff_alternating = (loop_obj_prev1_angle - loop_obj_angle).abs(); - angle_diff_alternating += (loop_obj_prev2_angle - loop_obj_prev1_angle).abs(); + angle_diff_alternating += (loop_obj_prev2_angle - loop_obj_prev0_angle).abs(); let mut weight = 1.0; // * Be sure that one of the angles is very sharp, when other is wide weight *= reverse_lerp( - loop_obj_angle.min(loop_obj_prev0_angle) * 100.0 / f64::consts::PI, + loop_obj_angle.min(loop_obj_prev0_angle) * 180.0 / f64::consts::PI, 20.0, - 5.0 + 5.0, ); weight *= reverse_lerp( - loop_obj_angle.min(loop_obj_prev0_angle) * 100.0 / f64::consts::PI, + loop_obj_angle.max(loop_obj_prev0_angle) * 180.0 / f64::consts::PI, 60.0, - 120.0 + 120.0, ); // * Lerp between max angle difference and rescaled alternating difference, with more harsh scaling compared to normal difference - angle_diff_alternating = f64::lerp(f64::consts::PI, 0.1 * angle_diff_alternating, weight); + angle_diff_alternating = + f64::lerp(f64::consts::PI, 0.1 * angle_diff_alternating, weight); } - let stack_factor = smootherstep(loop_obj.lazy_jump_dist, 0.0, f64::from(OsuDifficultyObject::NORMALIZED_RADIUS)); - - constant_angle_count += ( - 3.0 * (f64::to_radians(30.0).min((angle_diff).min(angle_diff_alternating) * stack_factor)) - ).cos() * long_interval_factor; + let stack_factor = smootherstep( + loop_obj.lazy_jump_dist, + 0.0, + f64::from(OsuDifficultyObject::NORMALIZED_RADIUS), + ); + + constant_angle_count += (3.0 + * (f64::to_radians(30.0) + .min((angle_diff).min(angle_diff_alternating) * stack_factor))) + .cos() + * long_interval_factor; } curr_time_gap = curr_obj.start_time - loop_obj.start_time; @@ -274,4 +353,4 @@ impl ReadingEvaluator { fn high_bpm_bonus(ms: f64) -> f64 { 1.0 / (1.0 - f64::powf(0.8, ms / 1000.0)) } -} \ No newline at end of file +} diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index 93f7d00b..5f08a57e 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -118,7 +118,7 @@ impl OsuDifficultySetup { ..Default::default() }; - let time_preempt = f64::from((hit_windows.ar.unwrap_or(0.0) * clock_rate) as f32); + let time_preempt = f64::from((hit_windows.ar.unwrap_or(0.0) * clock_rate).trunc() as f32); Self { scaling_factor, diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs index f21a9095..24d6a184 100644 --- a/src/osu/difficulty/skills/reading.rs +++ b/src/osu/difficulty/skills/reading.rs @@ -36,7 +36,6 @@ impl Reading { strain_decay_base(ms, 0.8) } - // #[expect(dead_code, reason = "used by process_internal")] fn object_difficulty_of<'a>( &mut self, curr: &'a OsuDifficultyObject<'a>, @@ -73,7 +72,7 @@ impl Reading { .evaluate_diff_of(curr, objects, self.mods.hd()); if self.mods.td() { - difficulty *= difficulty.powf(0.89); + difficulty = difficulty.powf(0.89); } if let Some(magnetised_strength) = self.mods.attraction_strength() { @@ -88,7 +87,7 @@ impl Reading { difficulty *= 0.1; } - difficulty *= self.overall_difficulty.max(0.0).powf(2.2) / 1125.0; + difficulty *= 0.825 + self.overall_difficulty.max(0.0).powf(2.2) / 1125.0; difficulty } diff --git a/tests/difficulty.rs b/tests/difficulty.rs index 4bff0a5b..b3c73313 100644 --- a/tests/difficulty.rs +++ b/tests/difficulty.rs @@ -160,17 +160,17 @@ fn basic_osu() { NM => { aim: 3.5141717095461806, aim_difficult_slider_count: 253.68984309768354, - speed: 2.4079921566524338, + speed: 2.407992156652433, flashlight: 0.0, reading: 1.2807842408799812, - slider_factor: 0.97955486167852612, + slider_factor: 0.9795548616785261, aim_top_weighted_slider_factor: 0.6280004197964858, - speed_top_weighted_slider_factor: 0.53840750544558069, - speed_note_count: 796.60849945939026, - reading_note_count: 97.229859463848399, - aim_difficult_strain_count: 154.31162398617261, + speed_top_weighted_slider_factor: 0.5384075054455806, + speed_note_count: 796.6084994593902, + reading_note_count: 97.22985946384839, + aim_difficult_strain_count: 154.3116239861726, speed_difficult_strain_count: 432.92911452386227, - nested_score_per_object: 26.395781637717121, + nested_score_per_object: 26.39578163771712, legacy_score_base_multiplier: 4.0, maximum_legacy_combo_score: 91937232.0, ar: 9.30000019, From dc6cc4308526dd01c41174ef53755e1c39a20f16 Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 3 Aug 2026 02:27:25 -0500 Subject: [PATCH 19/27] fix: remove debug change --- src/any/difficulty/skills_new/harmonic_skill.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/any/difficulty/skills_new/harmonic_skill.rs b/src/any/difficulty/skills_new/harmonic_skill.rs index a41a921c..6ad43bf0 100644 --- a/src/any/difficulty/skills_new/harmonic_skill.rs +++ b/src/any/difficulty/skills_new/harmonic_skill.rs @@ -60,14 +60,15 @@ pub fn harmonic_skill_difficulty_value( return (difficulty, object_weight_sum); } - let new_iter = transformed_object_difficulties - .to_vec() - .cs_order_descending() - .cs_where(|v| *v > 0.0); - // * Objects with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // * These objects will not contribute to the difficulty. - for (index, obj) in new_iter.iter().enumerate() { + for (index, obj) in transformed_object_difficulties + .to_vec() + .cs_order_descending() + .cs_where(|v| *v > 0.0) + .iter() + .enumerate() + { // * Use a harmonic sum that considers each object of the map according to a predefined weight. let weight = (1.0 + (harmonic_scale / (1 + index) as f64)) / ((index as f64).powf(decay_exponent) + 1.0 + (harmonic_scale / (1 + index) as f64)); From 3218ed67b70df56beaa5c9184308e291584ddc0b Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 3 Aug 2026 19:00:34 -0500 Subject: [PATCH 20/27] chore: osu nm test passing + cleanup --- src/osu/difficulty/object.rs | 2 +- src/osu/difficulty/skills/aim.rs | 3 +- src/osu/difficulty/skills/mod.rs | 14 ++++++++- src/osu/difficulty/skills/reading.rs | 3 +- src/osu/difficulty/skills/speed.rs | 3 +- src/osu/difficulty/skills/strain.rs | 12 -------- tests/common.rs | 7 +++++ tests/difficulty.rs | 43 ++++++++++++++-------------- 8 files changed, 45 insertions(+), 42 deletions(-) delete mode 100644 src/osu/difficulty/skills/strain.rs diff --git a/src/osu/difficulty/object.rs b/src/osu/difficulty/object.rs index f108f35a..4f87583e 100644 --- a/src/osu/difficulty/object.rs +++ b/src/osu/difficulty/object.rs @@ -248,7 +248,7 @@ impl<'a> OsuDifficultyObject<'a> { let pos = self.base.pos; let stack_offset = self.base.stack_offset; let start_time = self.base.start_time; - let duration = slider.end_time - start_time; + let duration = self.base.duration(); let mut nested_objects = Cow::Borrowed(slider.nested_objects.as_slice()); diff --git a/src/osu/difficulty/skills/aim.rs b/src/osu/difficulty/skills/aim.rs index e156217f..92bd82de 100644 --- a/src/osu/difficulty/skills/aim.rs +++ b/src/osu/difficulty/skills/aim.rs @@ -12,7 +12,6 @@ use crate::{ osu::difficulty::{ evaluators::{AgilityEvaluator, FlowAimEvaluator, SnapAimEvaluator}, object::OsuDifficultyObject, - skills::strain::count_top_weighted_sliders, }, util::{ difficulty::{lerp, logistic, logistic_exp, norm}, @@ -194,7 +193,7 @@ impl Aim { // * What would the top strain be if all strain values were identical let consistent_top_strain = difficulty_value / 10.0; - count_top_weighted_sliders(&self.slider_strains, consistent_top_strain) + super::count_top_weighted_sliders(&self.slider_strains, consistent_top_strain) } pub fn difficulty_value( diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index 9a6b08b0..bbc6b345 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -1,4 +1,6 @@ use crate::any::difficulty::skills_new::skill::Skill; +use crate::util::difficulty::logistic; +use crate::util::float_ext::FloatExt; use crate::{model::mods::GameMods, osu::object::OsuObject}; use self::{aim::Aim, flashlight::Flashlight, reading::Reading, speed::Speed}; @@ -11,7 +13,6 @@ pub mod aim; pub mod flashlight; pub mod reading; pub mod speed; -mod strain; pub struct OsuSkills { pub aim: Aim, @@ -86,3 +87,14 @@ impl OsuSkills { self.reading.process(curr, objects); } } + +fn count_top_weighted_sliders(slider_strains: &[f64], consistent_top_strain: f64) -> f64 { + if FloatExt::eq(consistent_top_strain, 0.0) { + return 0.0; + } + + slider_strains + .iter() + .map(|s| logistic(*s / consistent_top_strain, 0.88, 10.0, Some(1.1))) + .sum() +} diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs index 24d6a184..e25e88d0 100644 --- a/src/osu/difficulty/skills/reading.rs +++ b/src/osu/difficulty/skills/reading.rs @@ -110,8 +110,7 @@ impl Reading { difficulties } - #[expect(dead_code, reason = "overwrites macro impl")] - fn count_top_weighted_object_difficulties( + pub fn count_top_weighted_object_difficulties( &self, difficulty_value: f64, object_weight_sum: f64, diff --git a/src/osu/difficulty/skills/speed.rs b/src/osu/difficulty/skills/speed.rs index dadfa820..fb59516a 100644 --- a/src/osu/difficulty/skills/speed.rs +++ b/src/osu/difficulty/skills/speed.rs @@ -4,7 +4,6 @@ use crate::{ osu::difficulty::{ evaluators::{RhythmEvaluator, SpeedEvaluator}, object::OsuDifficultyObject, - skills::strain::count_top_weighted_sliders, }, util::{difficulty::logistic, float_ext::FloatExt}, }; @@ -97,6 +96,6 @@ impl Speed { // * What would the top note be if all note values were identical let consistent_top_object = difficulty_value / object_weight_sum; - count_top_weighted_sliders(&self.slider_strains, consistent_top_object) + super::count_top_weighted_sliders(&self.slider_strains, consistent_top_object) } } diff --git a/src/osu/difficulty/skills/strain.rs b/src/osu/difficulty/skills/strain.rs deleted file mode 100644 index d2f7134d..00000000 --- a/src/osu/difficulty/skills/strain.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::util::{difficulty::logistic, float_ext::FloatExt}; - -pub fn count_top_weighted_sliders(slider_strains: &[f64], consistent_top_strain: f64) -> f64 { - if FloatExt::eq(consistent_top_strain, 0.0) { - return 0.0; - } - - slider_strains - .iter() - .map(|s| logistic(*s / consistent_top_strain, 0.88, 10.0, Some(1.1))) - .sum() -} diff --git a/tests/common.rs b/tests/common.rs index 0666b3bc..10c632d6 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -67,8 +67,15 @@ macro_rules! impl_float { ( $( $ty:ty )* ) => { $( impl Float for $ty { + #[cfg(not(target_family = "unix"))] const EPSILON: Self = Self::EPSILON; + // Differing libm implementations can cause values generated on hosts that aren't + // windows based to fall barely outside of the default epsilon values. + // For the purposes of diffcalc, precision to 12 decimal places should suffice. + #[cfg(target_family = "unix")] + const EPSILON: Self = 1e-12; + fn abs(self) -> Self { self.abs() } diff --git a/tests/difficulty.rs b/tests/difficulty.rs index b3c73313..aac03b1b 100644 --- a/tests/difficulty.rs +++ b/tests/difficulty.rs @@ -154,36 +154,35 @@ macro_rules! test_cases { #[test] fn basic_osu() { - // #[cfg(target_os = "windows")] test_cases! { Osu: OSU { NM => { - aim: 3.5141717095461806, - aim_difficult_slider_count: 253.68984309768354, - speed: 2.407992156652433, + aim: 3.27863857424994, + aim_difficult_slider_count: 192.5269999738169, + speed: 2.4917265153109014, flashlight: 0.0, - reading: 1.2807842408799812, - slider_factor: 0.9795548616785261, - aim_top_weighted_slider_factor: 0.6280004197964858, - speed_top_weighted_slider_factor: 0.5384075054455806, - speed_note_count: 796.6084994593902, - reading_note_count: 97.22985946384839, - aim_difficult_strain_count: 154.3116239861726, - speed_difficult_strain_count: 432.92911452386227, - nested_score_per_object: 26.39578163771712, - legacy_score_base_multiplier: 4.0, - maximum_legacy_combo_score: 91937232.0, + reading: 0.8229208521405954, + slider_factor: 0.9630386892765709, + aim_top_weighted_slider_factor: 1.524202370856421, + speed_top_weighted_slider_factor: 0.536191641231213, + speed_note_count: 183.0639785973236, + reading_note_count: 34.92251595856365, + aim_difficult_strain_count: 124.69544446818438, + speed_difficult_strain_count: 81.74921671931915, + nested_score_per_object: 34.991680532445926, + legacy_score_base_multiplier: 5.0, + maximum_legacy_combo_score: 15729840.0, ar: 9.30000019, great_hit_window: 26.5, ok_hit_window: 68.5, meh_hit_window: 110.5, - hp: 6.0, - n_circles: 938, - n_sliders: 674, + hp: 5.0, + n_circles: 307, + n_sliders: 293, n_large_ticks: 15, - n_spinners: 0, - stars: 6.305978712257887, - max_combo: 2359, + n_spinners: 1, + stars: 6.004027372552197, + max_combo: 909, }; // HD => { // aim: 3.121489829231887, @@ -647,7 +646,7 @@ impl AssertEq for OsuDifficultyAttributes { *maximum_legacy_combo_score, expected.maximum_legacy_combo_score, ); - assert_eq_float(*ar, expected.ar); + assert_eq_float(*ar as f32, expected.ar as f32); assert_eq_float(*great_hit_window, expected.great_hit_window); assert_eq_float(*ok_hit_window, expected.ok_hit_window); assert_eq_float(*meh_hit_window, expected.meh_hit_window); From 7768822e203e817b70288402c7a1a88d35937850 Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 3 Aug 2026 20:40:46 -0500 Subject: [PATCH 21/27] fix: properly use base and clockrate adjusted object values --- src/osu/attributes.rs | 2 +- src/osu/difficulty/evaluators/reading.rs | 24 ++-- src/osu/difficulty/gradual.rs | 2 + src/osu/difficulty/mod.rs | 4 +- src/osu/difficulty/object.rs | 2 + src/osu/difficulty/skills/mod.rs | 10 +- src/osu/difficulty/skills/reading.rs | 4 +- src/osu/performance/calculator.rs | 6 +- src/util/difficulty.rs | 1 + tests/difficulty.rs | 175 ++++++++++++----------- 10 files changed, 131 insertions(+), 99 deletions(-) diff --git a/src/osu/attributes.rs b/src/osu/attributes.rs index f61e49ce..4b6babe8 100644 --- a/src/osu/attributes.rs +++ b/src/osu/attributes.rs @@ -22,7 +22,7 @@ pub struct OsuDifficultyAttributes { /// The number of clickable objects weighted by difficulty. pub speed_note_count: f64, /// The number of clickable objects weighted by difficulty. - pub reading_note_count: f64, + pub reading_difficult_note_count: f64, /// Weighted sum of aim strains. pub aim_difficult_strain_count: f64, /// Weighted sum of speed strains. diff --git a/src/osu/difficulty/evaluators/reading.rs b/src/osu/difficulty/evaluators/reading.rs index 31e2f90b..b606db14 100644 --- a/src/osu/difficulty/evaluators/reading.rs +++ b/src/osu/difficulty/evaluators/reading.rs @@ -10,7 +10,11 @@ use crate::{ }; pub struct ReadingEvaluator { + // Maps from OsuDifficultyHitObject.Preempt (clock rate adjusted OsuHitObject.TimePreempt) + preempt: f64, + // Maps from OsuHitObject.TimePreempt time_preempt: f64, + // Maps from OsuHitObject.TimeFadeIn time_fade_in: f64, } @@ -30,8 +34,9 @@ impl ReadingEvaluator { const MINIMUM_ANGLE_RELEVANCY_TIME: f64 = 2000.0; // * 2 seconds const MAXIMUM_ANGLE_RELEVANCY_TIME: f64 = 200.0; - pub const fn new(time_preempt: f64, time_fade_in: f64) -> Self { + pub const fn new(preempt: f64, time_preempt: f64, time_fade_in: f64) -> Self { Self { + preempt, time_preempt, time_fade_in, } @@ -83,7 +88,7 @@ impl ReadingEvaluator { }; let preempt_difficulty = - Self::calc_preempt_difficulty(velocity, constant_angle_nerf_factor, self.time_preempt); + Self::calc_preempt_difficulty(velocity, constant_angle_nerf_factor, self.preempt); let mut reading_difficulty = norm( 1.5, @@ -159,7 +164,7 @@ impl ReadingEvaluator { constant_angle_nerf_factor: f64, ) -> f64 { // * Higher preempt means that time spent invisible is higher too, we want to reward that - let preempt_factor = self.time_preempt.powf(2.2) * 0.01; + let preempt_factor = self.preempt.powf(2.2) * 0.01; // * Account for both past and current densities let density_factor = @@ -171,18 +176,19 @@ impl ReadingEvaluator { // * Apply a soft cap to general HD reading to account for partial memorization hidden_difficulty = hidden_difficulty.powf(0.4) * Self::HIDDEN_MULTIPLIER; + // * Buff perfect stacks only if current note is completely invisible at the time you click the previous note. if let Some(prev_obj) = curr_obj.previous(0, diff_objects) && FloatExt::eq(curr_obj.lazy_jump_dist, 0.0) && FloatExt::eq( curr_obj.opacity_at( - prev_obj.start_time, + prev_obj.base.start_time, true, self.time_preempt, self.time_fade_in, ), 0.0, ) - && prev_obj.start_time > curr_obj.start_time - self.time_preempt + && prev_obj.start_time > curr_obj.start_time - self.preempt { // * Perfect stacks are harder the less time between notes hidden_difficulty += @@ -203,11 +209,11 @@ impl ReadingEvaluator { .filter(|d| { d.idx < curr_obj.idx && curr_obj.start_time - d.start_time <= Self::READING_WINDOW_SIZE - && d.start_time >= curr_obj.start_time - self.time_preempt + && d.start_time >= curr_obj.start_time - self.preempt }) .fold(0.0, |past_obj_difficulty_influence, loop_obj| { let mut loop_difficulty = curr_obj.opacity_at( - loop_obj.start_time, + loop_obj.base.start_time, false, self.time_preempt, self.time_fade_in, @@ -241,13 +247,13 @@ impl ReadingEvaluator { while let Some(hit_obj) = curr_obj.next(forwards_idx, diff_objects).filter(|next| { next.start_time - curr_obj.start_time <= Self::READING_WINDOW_SIZE // * Object not visible at the time current object needs to be clicked. - && curr_obj.start_time >= next.start_time - self.time_preempt + && curr_obj.start_time >= next.start_time - self.preempt }) { let delta_time = hit_obj.start_time - curr_obj.start_time; let time_nerf_factor = Self::get_time_nerf_factor(delta_time); visible_object_count += hit_obj.opacity_at( - curr_obj.start_time, + curr_obj.base.start_time, false, self.time_preempt, self.time_fade_in, diff --git a/src/osu/difficulty/gradual.rs b/src/osu/difficulty/gradual.rs index 861c0ec3..5a4d30af 100644 --- a/src/osu/difficulty/gradual.rs +++ b/src/osu/difficulty/gradual.rs @@ -147,12 +147,14 @@ fn new(difficulty: Difficulty, map: &Beatmap) -> OsuGradualDifficulty { ); let great_hit_window = map_attrs.hit_windows().od_great.unwrap_or(0.0); + let clock_rate = difficulty.get_clock_rate(); let skills = OsuSkills::new( mods, &scaling_factor, great_hit_window, time_preempt, + clock_rate, diff_objects.len(), ); let diff_objects = extend_lifetime(diff_objects.into_boxed_slice()); diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index 5f08a57e..2742b769 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -162,6 +162,7 @@ impl DifficultyValues { Self::create_difficulty_objects(difficulty, &scaling_factor, osu_object_iter); let great_hit_window = map_attrs.hit_windows().od_great.unwrap_or(0.0); + let clock_rate = difficulty.get_clock_rate(); // The first hit object has no difficulty object let take_diff_objects = cmp::min(map.hit_objects.len(), take).saturating_sub(1); @@ -171,6 +172,7 @@ impl DifficultyValues { &scaling_factor, great_hit_window, time_preempt, + clock_rate, take_diff_objects, ); @@ -279,7 +281,7 @@ impl DifficultyValues { attrs.speed_difficult_strain_count = speed_difficult_strain_count; attrs.stars = star_rating; attrs.speed_note_count = speed_notes; - attrs.reading_note_count = reading_difficult_note_count; + attrs.reading_difficult_note_count = reading_difficult_note_count; } pub fn create_difficulty_objects<'a>( diff --git a/src/osu/difficulty/object.rs b/src/osu/difficulty/object.rs index 4f87583e..c9224dbd 100644 --- a/src/osu/difficulty/object.rs +++ b/src/osu/difficulty/object.rs @@ -115,6 +115,8 @@ impl<'a> OsuDifficultyObject<'a> { } let fade_in_start_time = self.base.start_time - time_preempt; + + // * Equal to `OsuHitObject.TimeFadeIn` minus any adjustments from the HD mod. let fade_in_duration = 400.0 * (time_preempt / OsuObject::PREEMPT_MIN).min(1.0); if hidden { diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index bbc6b345..dfeb3242 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -28,6 +28,7 @@ impl OsuSkills { scaling_factor: &ScalingFactor, great_hit_window: f64, time_preempt: f64, + clock_rate: f64, total_objects: usize, ) -> Self { let hit_window = 2.0 * great_hit_window; @@ -46,6 +47,7 @@ impl OsuSkills { 400.0 * (time_preempt / OsuObject::PREEMPT_MIN).min(1.0) }; let overall_difficulty = (79.5 - hit_window / 2.0) / 6.0; + let preempt = time_preempt / clock_rate; let aim = Aim::new( mods.clone(), @@ -68,7 +70,13 @@ impl OsuSkills { time_preempt, time_fade_in, ); - let reading = Reading::new(mods, time_preempt, time_fade_in, overall_difficulty); + let reading = Reading::new( + mods, + preempt, + time_preempt, + time_fade_in, + overall_difficulty, + ); Self { aim, diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs index e25e88d0..38afbd5a 100644 --- a/src/osu/difficulty/skills/reading.rs +++ b/src/osu/difficulty/skills/reading.rs @@ -15,13 +15,13 @@ define_new_skill! { overall_difficulty: f64, } - pub fn new(mods: &GameMods, time_preempt: f64, time_fade_in: f64, overall_difficulty: f64) -> Self { + pub fn new(mods: &GameMods, preempt: f64, time_preempt: f64, time_fade_in: f64, overall_difficulty: f64) -> Self { Self { current_strain: 0.0, reduced_note_count: 0, reduced_duration: None, mods: mods.clone(), - evaluator: ReadingEvaluator::new(time_preempt, time_fade_in), + evaluator: ReadingEvaluator::new(preempt, time_preempt, time_fade_in), overall_difficulty: overall_difficulty, } } diff --git a/src/osu/performance/calculator.rs b/src/osu/performance/calculator.rs index 2351a2d5..9aca530d 100644 --- a/src/osu/performance/calculator.rs +++ b/src/osu/performance/calculator.rs @@ -381,8 +381,10 @@ impl OsuPerformanceCalculator<'_> { let mut reading_value = Reading::difficulty_to_performance(self.attrs.reading); if effective_miss_count > 0.0 { - reading_value *= - Self::calculate_miss_penalty(effective_miss_count, self.attrs.reading_note_count); + reading_value *= Self::calculate_miss_penalty( + effective_miss_count, + self.attrs.reading_difficult_note_count, + ); } // * Scale the reading value with accuracy _harshly_. diff --git a/src/util/difficulty.rs b/src/util/difficulty.rs index 760a3c5c..07f1347d 100644 --- a/src/util/difficulty.rs +++ b/src/util/difficulty.rs @@ -58,6 +58,7 @@ pub const fn smootherstep(x: f64, start: f64, end: f64) -> f64 { x * x * x * (x * (6.0 * x - 15.0) + 10.0) } +// osu.Framework.Utils.Interpolation.Lerp pub const fn lerp(start: f64, end: f64, amount: f64) -> f64 { start + (end - start) * amount } diff --git a/tests/difficulty.rs b/tests/difficulty.rs index aac03b1b..e71ad5e5 100644 --- a/tests/difficulty.rs +++ b/tests/difficulty.rs @@ -42,7 +42,7 @@ macro_rules! test_cases { aim_top_weighted_slider_factor: $aim_top_weighted_slider_factor:literal, speed_top_weighted_slider_factor: $speed_top_weighted_slider_factor:literal, speed_note_count: $speed_note_count:literal, - reading_note_count: $reading_note_count:literal, + reading_difficult_note_count: $reading_difficult_note_count:literal, aim_difficult_strain_count: $aim_difficult_strain_count:literal, speed_difficult_strain_count: $speed_difficult_strain_count:literal, nested_score_per_object: $nested_score_per_object:literal, @@ -70,7 +70,7 @@ macro_rules! test_cases { aim_top_weighted_slider_factor: $aim_top_weighted_slider_factor, speed_top_weighted_slider_factor: $speed_top_weighted_slider_factor, speed_note_count: $speed_note_count, - reading_note_count: $reading_note_count, + reading_difficult_note_count: $reading_difficult_note_count, aim_difficult_strain_count: $aim_difficult_strain_count, speed_difficult_strain_count: $speed_difficult_strain_count, nested_score_per_object: $nested_score_per_object, @@ -166,7 +166,7 @@ fn basic_osu() { aim_top_weighted_slider_factor: 1.524202370856421, speed_top_weighted_slider_factor: 0.536191641231213, speed_note_count: 183.0639785973236, - reading_note_count: 34.92251595856365, + reading_difficult_note_count: 34.92251595856365, aim_difficult_strain_count: 124.69544446818438, speed_difficult_strain_count: 81.74921671931915, nested_score_per_object: 34.991680532445926, @@ -184,84 +184,90 @@ fn basic_osu() { stars: 6.004027372552197, max_combo: 909, }; - // HD => { - // aim: 3.121489829231887, - // aim_difficult_slider_count: 180.33980678704012, - // speed: 2.614171127905441, - // flashlight: 0.0, - // slider_factor: 0.9847225384137204, - // aim_top_weighted_slider_factor: 1.3996332540321264, - // speed_top_weighted_slider_factor: 0.6014562852677632, - // speed_note_count: 202.24319351543616, - // aim_difficult_strain_count: 108.47555309841259, - // speed_difficult_strain_count: 78.39830024782772, - // nested_score_per_object: 34.991680532445926, - // legacy_score_base_multiplier: 5.0, - // maximum_legacy_combo_score: 15729840.0, - // ar: 9.300000190734863, - // great_hit_window: 26.5, - // ok_hit_window: 68.5, - // meh_hit_window: 110.5, - // hp: 5.0, - // n_circles: 307, - // n_sliders: 293, - // n_large_ticks: 15, - // n_spinners: 1, - // stars: 5.934133851244851, - // max_combo: 909, - // }; - // HR => { - // aim: 3.4309052630747257, - // aim_difficult_slider_count: 187.20300643263465, - // speed: 2.6813963801152716, - // flashlight: 0.0, - // slider_factor: 0.9748562752795166, - // aim_top_weighted_slider_factor: 1.3634873114118244, - // speed_top_weighted_slider_factor: 0.6668815233244475, - // speed_note_count: 185.01178339020348, - // aim_difficult_strain_count: 112.28112750203013, - // speed_difficult_strain_count: 74.53251006179151, - // nested_score_per_object: 34.991680532445926, - // legacy_score_base_multiplier: 5.0, - // maximum_legacy_combo_score: 15729840.0, - // ar: 10.0, - // great_hit_window: 19.5, - // ok_hit_window: 59.5, - // meh_hit_window: 99.5, - // hp: 7.0, - // n_circles: 307, - // n_sliders: 293, - // n_large_ticks: 15, - // n_spinners: 1, - // stars: 6.375104448752039, - // max_combo: 909, - // }; - // DT => { - // aim: 4.3662195513104525, - // aim_difficult_slider_count: 195.41476682131653, - // speed: 3.7793477426295814, - // flashlight: 0.0, - // slider_factor: 0.9787310737204966, - // aim_top_weighted_slider_factor: 1.3819099517666353, - // speed_top_weighted_slider_factor: 0.6923456235877925, - // speed_note_count: 208.98215163620375, - // aim_difficult_strain_count: 130.48279566301667, - // speed_difficult_strain_count: 93.64469563382437, - // nested_score_per_object: 34.991680532445926, - // legacy_score_base_multiplier: 5.0, - // maximum_legacy_combo_score: 15729840.0, - // ar: 10.533333460489908, - // great_hit_window: 17.666666666666668, - // ok_hit_window: 45.666666666666664, - // meh_hit_window: 73.66666666666667, - // hp: 5.0, - // n_circles: 307, - // n_sliders: 293, - // n_large_ticks: 15, - // n_spinners: 1, - // stars: 8.40182116136074, - // max_combo: 909, - // }; + HD => { + aim: 3.27863857424994, + aim_difficult_slider_count: 192.5269999738169, + speed: 2.4917265153109014, + flashlight: 0.0, + reading: 2.1827264342501156, + slider_factor: 0.963038689276571, + aim_top_weighted_slider_factor: 1.524202370856421, + speed_top_weighted_slider_factor: 0.536191641231213, + speed_note_count: 183.0639785973236, + reading_difficult_note_count: 135.7746987103988, + aim_difficult_strain_count: 124.69544446818438, + speed_difficult_strain_count: 81.74921671931915, + nested_score_per_object: 34.991680532445926, + legacy_score_base_multiplier: 5.0, + maximum_legacy_combo_score: 15729840.0, + ar: 9.300000190734863, + great_hit_window: 26.5, + ok_hit_window: 68.5, + meh_hit_window: 110.5, + hp: 5.0, + n_circles: 307, + n_sliders: 293, + n_large_ticks: 15, + n_spinners: 1, + stars: 6.308336834596954, + max_combo: 909, + }; + HR => { + aim: 3.799232322249643, + aim_difficult_slider_count: 191.8309507640488, + speed: 2.4917265153109014, + flashlight: 0.0, + reading: 0.9144658746351508, + slider_factor: 0.9475983634088616, + aim_top_weighted_slider_factor: 1.510078933241033, + speed_top_weighted_slider_factor: 0.5361916412312123, + speed_note_count: 183.0639785973236, + reading_difficult_note_count: 38.427842331296304, + aim_difficult_strain_count: 119.62860170592188, + speed_difficult_strain_count: 81.74921671931915, + nested_score_per_object: 34.991680532445926, + legacy_score_base_multiplier: 5.0, + maximum_legacy_combo_score: 15729840.0, + ar: 10.0, + great_hit_window: 19.5, + ok_hit_window: 59.5, + meh_hit_window: 99.5, + hp: 7.0, + n_circles: 307, + n_sliders: 293, + n_large_ticks: 15, + n_spinners: 1, + stars: 6.713673504226164, + max_combo: 909, + }; + DT => { + aim: 4.693556954514378, + aim_difficult_slider_count: 207.97415619378023, + speed: 3.674242476685813, + flashlight: 0.0, + reading: 2.0228172499241897, + slider_factor: 0.9674908735064722, + aim_top_weighted_slider_factor: 1.476888448482664, + speed_top_weighted_slider_factor: 0.6387657108874909, + speed_note_count: 211.45478779956713, + reading_difficult_note_count: 190.90786995621218, + aim_difficult_strain_count: 144.31095827870723, + speed_difficult_strain_count: 86.60969041721593, + nested_score_per_object: 34.991680532445926, + legacy_score_base_multiplier: 5.0, + maximum_legacy_combo_score: 15729840.0, + ar: 10.533333460489908, + great_hit_window: 17.666666666666668, + ok_hit_window: 45.666666666666664, + meh_hit_window: 73.66666666666667, + hp: 5.0, + n_circles: 307, + n_sliders: 293, + n_large_ticks: 15, + n_spinners: 1, + stars: 8.762958976840826, + max_combo: 909, + }; // FL => { // aim: 3.021506412510076, // aim_difficult_slider_count: 180.33980678704012, @@ -591,7 +597,7 @@ impl AssertEq for OsuDifficultyAttributes { aim_top_weighted_slider_factor, speed_top_weighted_slider_factor, speed_note_count, - reading_note_count, + reading_difficult_note_count, aim_difficult_strain_count, speed_difficult_strain_count, nested_score_per_object, @@ -628,7 +634,10 @@ impl AssertEq for OsuDifficultyAttributes { expected.speed_top_weighted_slider_factor, ); assert_eq_float(*speed_note_count, expected.speed_note_count); - assert_eq_float(*reading_note_count, expected.reading_note_count); + assert_eq_float( + *reading_difficult_note_count, + expected.reading_difficult_note_count, + ); assert_eq_float( *aim_difficult_strain_count, expected.aim_difficult_strain_count, From b681d373888c473a843daa39f204c1570e279253 Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 3 Aug 2026 21:02:59 -0500 Subject: [PATCH 22/27] fix: flashlight difficulty value formula --- src/osu/difficulty/evaluators/flashlight.rs | 4 +- src/osu/difficulty/gradual.rs | 4 +- src/osu/difficulty/mod.rs | 8 +-- src/osu/difficulty/skills/flashlight.rs | 7 +-- tests/difficulty.rs | 54 +++++++++++---------- 5 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/osu/difficulty/evaluators/flashlight.rs b/src/osu/difficulty/evaluators/flashlight.rs index 5fe6315c..ac5b7dd6 100644 --- a/src/osu/difficulty/evaluators/flashlight.rs +++ b/src/osu/difficulty/evaluators/flashlight.rs @@ -88,7 +88,9 @@ impl FlashlightEvaluator { stack_nerf * opacity_bonus * self.scaling_factor * jump_dist / cumulative_strain_time; - if let Some((curr_obj_angle, osu_curr_angle)) = curr_obj.angle.zip(osu_curr.angle) { + if let (Some(curr_obj_angle), Some(osu_curr_angle)) = + (curr_obj.angle, osu_curr.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); diff --git a/src/osu/difficulty/gradual.rs b/src/osu/difficulty/gradual.rs index 5a4d30af..6d4f54c4 100644 --- a/src/osu/difficulty/gradual.rs +++ b/src/osu/difficulty/gradual.rs @@ -138,6 +138,8 @@ fn new(difficulty: Difficulty, map: &Beatmap) -> OsuGradualDifficulty { OsuGradualDifficulty::increment_combo(h, &mut attrs); } + let total_hit_objects = osu_objects.len(); + let mut osu_objects = OsuObjects::new(osu_objects); let diff_objects = DifficultyValues::create_difficulty_objects( @@ -155,7 +157,7 @@ fn new(difficulty: Difficulty, map: &Beatmap) -> OsuGradualDifficulty { great_hit_window, time_preempt, clock_rate, - diff_objects.len(), + total_hit_objects, ); let diff_objects = extend_lifetime(diff_objects.into_boxed_slice()); diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index 2742b769..f975c15f 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -164,8 +164,10 @@ impl DifficultyValues { let great_hit_window = map_attrs.hit_windows().od_great.unwrap_or(0.0); let clock_rate = difficulty.get_clock_rate(); + let take_hit_objects = cmp::min(map.hit_objects.len(), take); + // The first hit object has no difficulty object - let take_diff_objects = cmp::min(map.hit_objects.len(), take).saturating_sub(1); + let take_diff_objects = take_hit_objects.saturating_sub(1); let mut skills = OsuSkills::new( mods, @@ -173,7 +175,7 @@ impl DifficultyValues { great_hit_window, time_preempt, clock_rate, - take_diff_objects, + take_hit_objects, ); for hit_object in diff_objects.iter().take(take_diff_objects) { @@ -344,7 +346,7 @@ pub fn sum_cognition_difficulty(reading: f64, flashlight: f64) -> f64 { // * Nerf flashlight value in cognition sum when reading is greater than flashlight norm( PERFORMANCE_NORM_EXPONENT, - [reading, flashlight, (flashlight / reading).clamp(0.25, 1.0)], + [reading, flashlight * (flashlight / reading).clamp(0.25, 1.0)], ) } diff --git a/src/osu/difficulty/skills/flashlight.rs b/src/osu/difficulty/skills/flashlight.rs index 90df9a8a..af01bdf6 100644 --- a/src/osu/difficulty/skills/flashlight.rs +++ b/src/osu/difficulty/skills/flashlight.rs @@ -132,10 +132,11 @@ impl Flashlight { fn flashlight_difficulty_value(current_strain_peaks: Vec, total_objects: i32) -> f64 { let sum: f64 = current_strain_peaks.into_iter().sum(); - sum * 0.7 + // * Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. + sum * (0.7 + 0.1 * (f64::from(total_objects) / 200.0).min(1.0) - + (if total_objects > 200 { - 0.2 * f64::from(std::cmp::min(total_objects - 200, 1) / 200) + + if total_objects > 200 { + 0.2 * (f64::from(total_objects - 200) / 200.0).min(1.0) } else { 0.0 }) diff --git a/tests/difficulty.rs b/tests/difficulty.rs index e71ad5e5..02fc1017 100644 --- a/tests/difficulty.rs +++ b/tests/difficulty.rs @@ -268,32 +268,34 @@ fn basic_osu() { stars: 8.762958976840826, max_combo: 909, }; - // FL => { - // aim: 3.021506412510076, - // aim_difficult_slider_count: 180.33980678704012, - // speed: 2.5263145770639976, - // flashlight: 2.3005989208967885, - // slider_factor: 0.9847225384137204, - // aim_top_weighted_slider_factor: 1.3996332540321264, - // speed_top_weighted_slider_factor: 0.6014562852677632, - // speed_note_count: 202.24319351543616, - // aim_difficult_strain_count: 108.47555309841259, - // speed_difficult_strain_count: 78.39830024782772, - // nested_score_per_object: 34.991680532445926, - // legacy_score_base_multiplier: 5.0, - // maximum_legacy_combo_score: 15729840.0, - // ar: 9.300000190734863, - // great_hit_window: 26.5, - // ok_hit_window: 68.5, - // meh_hit_window: 110.5, - // hp: 5.0, - // n_circles: 307, - // n_sliders: 293, - // n_large_ticks: 15, - // n_spinners: 1, - // stars: 6.864308231959398, - // max_combo: 909, - // }; + FL => { + aim: 3.27863857424994, + aim_difficult_slider_count: 192.5269999738169, + speed: 2.4917265153109014, + flashlight: 2.345608737345168, + reading: 0.8229208521405954, + slider_factor: 0.963038689276571, + aim_top_weighted_slider_factor: 1.524202370856421, + speed_top_weighted_slider_factor: 0.536191641231213, + speed_note_count: 183.0639785973236, + reading_difficult_note_count: 34.92251595856365, + aim_difficult_strain_count: 124.69544446818438, + speed_difficult_strain_count: 81.74921671931915, + nested_score_per_object: 34.991680532445926, + legacy_score_base_multiplier: 5.0, + maximum_legacy_combo_score: 15729840.0, + ar: 9.300000190734863, + great_hit_window: 26.5, + ok_hit_window: 68.5, + meh_hit_window: 110.5, + hp: 5.0, + n_circles: 307, + n_sliders: 293, + n_large_ticks: 15, + n_spinners: 1, + stars: 7.036258413665859, + max_combo: 909, + }; // HD EZ => { // aim: 2.9818506123002706, // aim_difficult_slider_count: 173.00759261125233, From b2c2c76c3e9c71f7f32896348ece418430696eef Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 3 Aug 2026 21:26:00 -0500 Subject: [PATCH 23/27] fix: hd+fl bugs --- src/model/mods.rs | 10 ++ src/osu/difficulty/evaluators/flashlight.rs | 2 +- src/osu/difficulty/object.rs | 8 ++ src/osu/difficulty/skills/reading.rs | 2 +- tests/difficulty.rs | 108 ++++++++++---------- 5 files changed, 76 insertions(+), 54 deletions(-) diff --git a/src/model/mods.rs b/src/model/mods.rs index 32d0c450..0bc7b163 100644 --- a/src/model/mods.rs +++ b/src/model/mods.rs @@ -277,6 +277,16 @@ impl GameMods { }) .flatten() } + + /// Whether Hidden is active and configured to fade the whole object + /// (not just the approach circle). + /// + /// `OnlyFadeApproachCircles` is a lazer-only setting that defaults to + /// `false`, so legacy/intermode mods (which cannot carry that setting) + /// are treated the same as an explicit `false`. + pub(crate) fn hd_full_fade(&self) -> bool { + self.hd() && self.hd_only_fade_approach_circles() != Some(true) + } } macro_rules! impl_has_mod { diff --git a/src/osu/difficulty/evaluators/flashlight.rs b/src/osu/difficulty/evaluators/flashlight.rs index ac5b7dd6..1dab5fec 100644 --- a/src/osu/difficulty/evaluators/flashlight.rs +++ b/src/osu/difficulty/evaluators/flashlight.rs @@ -79,7 +79,7 @@ impl FlashlightEvaluator { * (1.0 - osu_curr.opacity_at( curr_hit_obj.start_time, - mods.hd_only_fade_approach_circles() == Some(false), + mods.hd_full_fade(), self.time_preempt, self.time_fade_in, )); diff --git a/src/osu/difficulty/object.rs b/src/osu/difficulty/object.rs index c9224dbd..82a2a670 100644 --- a/src/osu/difficulty/object.rs +++ b/src/osu/difficulty/object.rs @@ -120,6 +120,14 @@ impl<'a> OsuDifficultyObject<'a> { let fade_in_duration = 400.0 * (time_preempt / OsuObject::PREEMPT_MIN).min(1.0); if hidden { + // Sliders retain their default `TimeFadeIn` under HD. + // Only non-slider objects get the HD-adjusted fade in duration. + let time_fade_in = if self.base.is_slider() { + fade_in_duration + } else { + time_fade_in + }; + // * Taken from OsuModHidden. let fade_out_start_time = self.base.start_time - time_preempt + time_fade_in; let fade_out_duration = time_preempt * HD_FADE_OUT_DURATION_MULTIPLIER; diff --git a/src/osu/difficulty/skills/reading.rs b/src/osu/difficulty/skills/reading.rs index 38afbd5a..84190175 100644 --- a/src/osu/difficulty/skills/reading.rs +++ b/src/osu/difficulty/skills/reading.rs @@ -69,7 +69,7 @@ impl Reading { ) -> f64 { let mut difficulty = self .evaluator - .evaluate_diff_of(curr, objects, self.mods.hd()); + .evaluate_diff_of(curr, objects, self.mods.hd_full_fade()); if self.mods.td() { difficulty = difficulty.powf(0.89); diff --git a/tests/difficulty.rs b/tests/difficulty.rs index 02fc1017..dc480f09 100644 --- a/tests/difficulty.rs +++ b/tests/difficulty.rs @@ -296,58 +296,62 @@ fn basic_osu() { stars: 7.036258413665859, max_combo: 909, }; - // HD EZ => { - // aim: 2.9818506123002706, - // aim_difficult_slider_count: 173.00759261125233, - // speed: 2.511531850339625, - // flashlight: 0.0, - // slider_factor: 0.9931728395338801, - // aim_top_weighted_slider_factor: 1.4758126955064637, - // speed_top_weighted_slider_factor: 0.48777057881852615, - // speed_note_count: 211.97339651166865, - // aim_difficult_strain_count: 107.45480335487801, - // speed_difficult_strain_count: 78.94432491731223, - // nested_score_per_object: 34.991680532445926, - // legacy_score_base_multiplier: 3.0, - // maximum_legacy_combo_score: 15729840.0, - // ar: 4.650000095367432, - // great_hit_window: 52.5, - // ok_hit_window: 103.5, - // meh_hit_window: 154.5, - // hp: 2.5, - // n_circles: 307, - // n_sliders: 293, - // n_large_ticks: 15, - // n_spinners: 1, - // stars: 5.680319048111094, - // max_combo: 909, - // }; - // HD FL => { - // aim: 3.121489829231887, - // aim_difficult_slider_count: 180.33980678704012, - // speed: 2.614171127905441, - // flashlight: 2.620335643475851, - // slider_factor: 0.9847225384137204, - // aim_top_weighted_slider_factor: 1.3996332540321264, - // speed_top_weighted_slider_factor: 0.6014562852677632, - // speed_note_count: 202.24319351543616, - // aim_difficult_strain_count: 108.47555309841259, - // speed_difficult_strain_count: 78.39830024782772, - // nested_score_per_object: 34.991680532445926, - // legacy_score_base_multiplier: 5.0, - // maximum_legacy_combo_score: 15729840.0, - // ar: 9.300000190734863, - // great_hit_window: 26.5, - // ok_hit_window: 68.5, - // meh_hit_window: 110.5, - // hp: 5.0, - // n_circles: 307, - // n_sliders: 293, - // n_large_ticks: 15, - // n_spinners: 1, - // stars: 7.2736222258399374, - // max_combo: 909, - // }; + HD EZ => { + aim: 2.7625488821040367, + aim_difficult_slider_count: 196.89037873007746, + speed: 2.3995360680924946, + flashlight: 0.0, + reading: 3.5538685094268883, + slider_factor: 0.9796909646357417, + aim_top_weighted_slider_factor: 1.562739917685155, + speed_top_weighted_slider_factor: 0.5369556982593612, + speed_note_count: 192.2649456376246, + reading_difficult_note_count: 124.55280093376417, + aim_difficult_strain_count: 129.38126835796155, + speed_difficult_strain_count: 84.40439036292067, + nested_score_per_object: 34.991680532445926, + legacy_score_base_multiplier: 3.0, + maximum_legacy_combo_score: 15729840.0, + ar: 4.650000095367432, + great_hit_window: 52.5, + ok_hit_window: 103.5, + meh_hit_window: 154.5, + hp: 2.5, + n_circles: 307, + n_sliders: 293, + n_large_ticks: 15, + n_spinners: 1, + stars: 6.891768477382506, + max_combo: 909, + }; + HD FL => { + aim: 3.27863857424994, + aim_difficult_slider_count: 192.5269999738169, + speed: 2.4917265153109014, + flashlight: 2.6270894946667935, + reading: 2.1827264342501156, + slider_factor: 0.963038689276571, + aim_top_weighted_slider_factor: 1.524202370856421, + speed_top_weighted_slider_factor: 0.536191641231213, + speed_note_count: 183.0639785973236, + reading_difficult_note_count: 135.7746987103988, + aim_difficult_strain_count: 124.6954444681844, + speed_difficult_strain_count: 81.74921671931915, + nested_score_per_object: 34.991680532445926, + legacy_score_base_multiplier: 5.0, + maximum_legacy_combo_score: 15729840.0, + ar: 9.300000190734863, + great_hit_window: 26.5, + ok_hit_window: 68.5, + meh_hit_window: 110.5, + hp: 5.0, + n_circles: 307, + n_sliders: 293, + n_large_ticks: 15, + n_spinners: 1, + stars: 7.47402836368438, + max_combo: 909, + }; } }; } From b9c7bad6dfbdcfbea28ecf301dbf56b4982f6080 Mon Sep 17 00:00:00 2001 From: myssto Date: Mon, 3 Aug 2026 21:29:06 -0500 Subject: [PATCH 24/27] chore: remove extra beatmap file --- resources/801165.osu | 1768 ------------------------------------------ 1 file changed, 1768 deletions(-) delete mode 100644 resources/801165.osu diff --git a/resources/801165.osu b/resources/801165.osu deleted file mode 100644 index 7357bb1d..00000000 --- a/resources/801165.osu +++ /dev/null @@ -1,1768 +0,0 @@ -osu file format v14 - -[General] -AudioLeadIn: 0 -PreviewTime: 231110 -Countdown: 0 -SampleSet: Normal -StackLeniency: 0.2 -Mode: 0 -LetterboxInBreaks: 0 -WidescreenStoryboard: 1 - -[Difficulty] -HPDrainRate:6 -CircleSize:4 -OverallDifficulty:8 -ApproachRate:9.3 -SliderMultiplier:2 -SliderTickRate:1 - -[Events] -//Background and Video events -//Break Periods -2,14204,16450 -2,117537,127450 -2,188649,195006 -//Storyboard Layer 0 (Background) -//Storyboard Layer 1 (Fail) -//Storyboard Layer 2 (Pass) -//Storyboard Layer 3 (Foreground) -//Storyboard Layer 4 (Overlay) -//Storyboard Sound Samples - -[TimingPoints] -5338,444.444444444444,4,2,0,50,1,0 -6338,-100,4,2,7,60,0,0 -7115,-100,4,2,7,50,0,0 -13671,-100,4,2,7,60,0,0 -17560,-100,4,2,8,60,0,0 -17782,-100,4,2,7,60,0,0 -18226,-100,4,2,8,60,0,0 -21114,-83.3333333333333,4,2,8,60,0,0 -21337,-100,4,2,8,60,0,0 -22893,-83.3333333333333,4,2,8,60,0,0 -23448,-100,4,2,8,60,0,0 -25337,-66.6666666666667,4,2,8,60,0,0 -25670,-100,4,2,8,60,0,0 -31559,-55.5555555555556,4,2,8,60,0,0 -44448,-100,4,2,8,50,0,0 -44893,-100,4,2,8,55,0,0 -45337,-100,4,2,7,60,0,0 -45670,-200,4,2,8,60,0,0 -46226,-66.6666666666667,4,2,8,65,0,0 -58670,-66.6666666666667,4,2,8,55,0,0 -59115,-66.6666666666667,4,2,8,60,0,0 -59559,-66.6666666666667,4,2,8,65,0,0 -60226,-100,4,2,8,60,0,0 -60893,-100,4,2,8,55,0,0 -68004,-66.6666666666667,4,2,8,65,0,0 -73337,-100,4,2,8,55,0,0 -73781,-100,4,2,8,55,0,0 -73893,-100,4,2,7,55,0,0 -74004,-100,4,2,8,55,0,0 -74115,-100,4,2,7,55,0,0 -74226,-100,4,2,8,55,0,0 -74337,-100,4,2,7,55,0,0 -74448,-100,4,2,8,55,0,0 -82893,-76.9230769230769,4,2,8,50,0,0 -85115,-76.9230769230769,4,2,0,50,0,0 -85337,-100,4,2,8,60,0,0 -85893,-100,4,2,7,60,0,0 -86226,-100,4,2,8,60,0,0 -88893,-58.8235294117647,4,1,8,70,0,1 -102226,-58.8235294117647,4,2,8,70,0,1 -103115,-58.8235294117647,4,1,8,70,0,1 -115337,-50,4,1,8,70,0,1 -115670,-50,4,2,7,70,0,1 -116004,-76.9230769230769,4,2,8,70,0,1 -116559,-76.9230769230769,4,2,7,60,0,1 -117337,-58.8235294117647,4,2,8,70,0,0 -128004,-100,4,2,8,65,0,0 -129781,-100,4,2,8,55,0,0 -130226,-100,4,2,8,60,0,0 -130670,-100,4,2,8,65,0,0 -131337,-100,4,2,8,60,0,0 -132004,-100,4,2,8,55,0,0 -145004,-100,4,2,7,55,0,0 -145115,-100,4,2,8,55,0,0 -145226,-100,4,2,7,55,0,0 -145337,-100,4,2,8,55,0,0 -145448,-100,4,2,7,55,0,0 -145559,-100,4,2,8,55,0,0 -156226,-100,4,2,0,50,0,0 -156448,-100,4,2,8,60,0,0 -157004,-100,4,2,7,60,0,0 -157337,-100,4,2,8,60,0,0 -160004,-58.8235294117647,4,1,8,70,0,1 -186781,-58.8235294117647,4,2,7,70,0,1 -187115,-58.8235294117647,4,1,8,70,0,1 -187670,-58.8235294117647,4,1,7,70,0,1 -188449,-58.8235294117647,4,2,8,70,0,0 -195559,-100,4,2,7,40,0,0 -195670,-100,4,2,8,40,0,0 -202670,-66.6666666666667,4,2,8,70,0,0 -203115,-100,4,2,8,60,0,0 -215559,-100,4,2,7,60,0,0 -216893,-83.3333333333333,4,2,8,60,0,0 -228115,-83.3333333333333,4,2,7,60,0,0 -228226,-83.3333333333333,4,2,8,60,0,0 -228337,-83.3333333333333,4,2,7,60,0,0 -228448,-83.3333333333333,4,2,8,60,0,0 -228781,-83.3333333333333,4,2,7,60,0,0 -228893,-83.3333333333333,4,2,8,60,0,0 -229893,-83.3333333333333,4,2,7,60,0,0 -230004,-83.3333333333333,4,2,8,60,0,0 -231115,-166.666666666667,4,2,8,60,0,0 -238226,-166.666666666667,4,2,7,60,0,0 -238893,-166.666666666667,4,2,8,60,0,0 -245004,-166.666666666667,4,2,7,60,0,0 -245115,-166.666666666667,4,2,8,60,0,0 -245337,-100,4,2,8,60,0,0 -257893,-100,4,2,7,60,0,0 -258226,-100,4,2,8,60,0,0 -258337,-100,4,2,7,60,0,0 -258448,-100,4,2,8,60,0,0 -258559,-100,4,2,7,60,0,0 -258670,-100,4,2,8,60,0,0 -259560,-58.8235294117647,4,1,8,70,0,1 -273448,-58.8235294117647,4,2,7,70,0,1 -273559,-58.8235294117647,4,1,8,70,0,1 -286337,-58.8235294117647,4,2,7,70,0,1 -286670,-58.8235294117647,4,2,8,70,0,1 -286781,-58.8235294117647,4,2,7,70,0,1 -286893,-58.8235294117647,4,2,8,70,0,1 -287226,-58.8235294117647,4,1,7,70,0,1 -288004,-66.6666666666667,4,2,8,65,0,0 -295559,-50,4,2,8,65,0,0 -295893,-66.6666666666667,4,2,8,65,0,0 -298448,-66.6666666666667,4,2,0,65,0,0 -298559,-66.6666666666667,4,2,8,65,0,0 -312670,-66.6666666666667,4,2,0,65,0,0 -312781,-66.6666666666667,4,2,8,65,0,0 -313448,-66.6666666666667,4,2,7,65,0,0 -313781,-66.6666666666667,4,2,8,65,0,0 -315337,-66.6666666666667,4,2,7,65,0,0 -315559,-66.6666666666667,4,2,8,65,0,0 -316448,-100,4,2,8,60,0,0 -330559,-100,4,2,7,60,0,0 - - -[Colours] -Combo1 : 139,159,214 -Combo2 : 128,128,255 -Combo3 : 227,227,227 - -[HitObjects] -378,163,6337,6,4,P|427:159|468:188,2,100,8|8|8,3:2|3:2|3:2,0:0:0:0: -320,312,7115,2,0,L|332:360,1,50,6|2,0:0|0:0,0:0:0:0: -164,208,7560,6,0,L|132:208,1,25,10|0,0:0|0:0,0:0:0:0: -139,208,7671,2,0,L|92:208,1,25,2|2,0:0|0:0,0:0:0:0: -114,208,7782,2,0,L|84:208,1,25,2|2,0:0|0:0,0:0:0:0: -89,208,7893,2,0,L|60:208,1,25,10|0,0:0|0:0,0:0:0:0: -32,184,8004,6,0,L|48:112,1,50,2|10,0:0|0:0,0:0:0:0: -144,128,8226,2,0,L|160:56,1,50,8|0,3:2|1:0,0:0:0:0: -266,68,8449,5,2,0:0:0:0: -306,40,8560,1,2,1:2:0:0: -356,39,8671,1,8,3:2:0:0: -397,66,8782,1,0,0:0:0:0: -415,111,8893,6,0,L|447:168,1,50,2|10,3:2|0:0,0:0:0:0: -429,263,9115,2,0,L|463:321,1,50,2|0,0:0|0:0,0:0:0:0: -328,272,9337,6,0,L|312:244,1,25,2|2,0:0|0:0,0:0:0:0: -315,250,9449,2,0,L|291:209,1,25,2|2,0:0|0:0,0:0:0:0: -303,228,9560,2,0,L|288:202,1,25,2|2,0:0|0:0,0:0:0:0: -290,207,9671,2,0,L|275:181,1,25,2|2,0:0|0:0,0:0:0:0: -248,152,9782,6,0,L|248:88,1,50,2|10,0:0|0:0,0:0:0:0: -154,136,10004,2,0,L|154:80,1,50,8|0,3:2|1:0,0:0:0:0: -64,168,10226,5,2,0:0:0:0: -19,187,10337,1,2,1:2:0:0: -0,232,10449,1,8,3:2:0:0: -14,278,10560,1,0,0:0:0:0: -56,303,10671,5,2,3:2:0:0: -101,282,10782,1,10,0:0:0:0: -151,282,10893,1,2,0:0:0:0: -196,302,11004,1,0,0:0:0:0: -240,316,11115,6,0,L|272:316,1,25,10|0,0:0|0:0,0:0:0:0: -265,316,11226,2,0,L|312:316,1,25,2|2,0:0|0:0,0:0:0:0: -290,316,11337,2,0,L|320:316,1,25,2|2,0:0|0:0,0:0:0:0: -315,316,11449,2,0,L|344:316,1,25,10|0,0:0|0:0,0:0:0:0: -384,340,11560,6,0,L|416:292,1,50,2|10,0:0|0:0,0:0:0:0: -376,196,11782,2,0,L|344:244,1,50,10|0,3:0|1:0,0:0:0:0: -416,308,12004,6,0,L|429:251,1,50,2|2,0:0|1:2,0:0:0:0: -359,175,12226,2,0,L|345:231,1,50,10|0,3:0|0:0,0:0:0:0: -440,260,12449,6,0,L|433:202,1,50,2|10,3:2|0:0,0:0:0:0: -340,154,12671,2,0,L|346:211,1,50,2|0,0:3|0:0,0:0:0:0: -432,124,12893,6,0,L|420:94,1,25,10|0,0:0|0:0,0:0:0:0: -422,100,13004,2,0,L|404:56,1,25,2|2,0:0|0:0,0:0:0:0: -412,76,13115,2,0,L|400:48,1,25,2|2,0:0|0:0,0:0:0:0: -403,53,13226,2,0,L|392:26,1,25,10|0,0:0|0:0,0:0:0:0: -352,4,13337,5,2,0:0:0:0: -352,4,13449,1,10,0:0:0:0: -352,4,13560,1,8,3:2:0:0: -352,4,13671,6,0,L|324:96,1,50,0|0,1:0|0:0,0:0:0:0: -257,89,13893,2,0,L|229:-3,1,50,0|0,1:0|0:0,0:0:0:0: -66,72,17004,5,8,3:2:0:0: -128,199,17226,1,8,3:2:0:0: -207,82,17449,1,8,3:2:0:0: -283,160,17560,6,0,L|345:154,1,50,8|0,0:0|2:0,0:0:0:0: -487,113,17782,6,0,L|437:107,3,50,4|0|0|0,0:2|0:0|0:0|0:0,0:0:0:0: -403,76,18226,1,8,0:0:0:0: -365,107,18337,1,0,0:0:0:0: -356,156,18449,1,0,0:0:0:0: -379,199,18560,1,0,0:0:0:0: -423,222,18671,6,0,L|473:228,1,50,8|0,0:0|0:0,0:0:0:0: -332,345,18893,1,0,0:0:0:0: -332,345,19004,2,0,L|322:279,1,50,0|8,0:0|0:0,0:0:0:0: -211,233,19226,1,0,0:0:0:0: -211,233,19337,2,0,L|201:299,1,50 -91,298,19560,6,0,L|28:291,1,50,8|0,0:0|0:0,0:0:0:0: -166,184,19782,2,0,L|178:253,1,50 -220,356,20004,1,8,0:0:0:0: -263,331,20115,1,0,0:0:0:0: -312,322,20226,1,0,0:0:0:0: -361,329,20337,2,0,L|394:368,1,50,0|8,0:0|0:0,0:0:0:0: -457,201,20560,5,0,0:0:0:0: -457,201,20671,1,0,0:0:0:0: -457,201,20782,2,0,L|460:132,2,50,2|8|0,0:0|0:0|0:0,0:0:0:0: -344,244,21115,2,0,L|335:178,1,59.9999981689454,2|2,0:0|0:0,0:0:0:0: -214,70,21337,5,8,3:2:0:0: -263,76,21449,1,0,3:0:0:0: -313,82,21560,1,0,3:0:0:0: -362,88,21671,2,0,L|263:113,1,100,0|0,3:0|3:0,0:0:0:0: -164,64,22004,1,0,3:0:0:0: -164,64,22115,2,0,L|92:55,1,50,0|8,3:0|0:0,0:0:0:0: -21,221,22337,5,0,3:0:0:0: -69,229,22449,1,0,3:0:0:0: -114,208,22560,1,0,3:0:0:0: -139,166,22671,1,10,3:2:0:0: -145,226,22782,1,2,3:2:0:0: -150,286,22893,2,0,P|210:292|268:248,1,119.999996337891,2|8,3:2|0:0,0:0:0:0: -384,243,23226,5,0,0:0:0:0: -384,243,23337,1,0,0:0:0:0: -384,243,23449,2,0,L|389:193,2,50,0|8|0,0:0|0:0|0:0,0:0:0:0: -334,331,23782,1,0,0:0:0:0: -285,319,23893,1,0,0:0:0:0: -236,325,24004,1,10,0:2:0:0: -191,346,24115,1,2,0:0:0:0: -155,381,24226,6,0,L|42:371,1,100,2|10,0:0|0:2,0:0:0:0: -148,254,24560,1,2,0:0:0:0: -148,254,24671,2,0,L|261:243,1,100,2|8,0:0|0:0,0:0:0:0: -90,134,25115,6,0,L|98:196,1,50 -30,218,25337,2,0,B|45:131|45:131|42:126|42:126|46:123|46:123|57:64,1,150.000005722046,2|2,1:0|0:0,0:0:0:0: -179,120,25671,6,0,P|234:119|291:93,1,100 -349,47,26004,1,0,0:0:0:0: -349,47,26115,2,0,P|325:96|342:165,1,100 -411,158,26449,1,0,0:0:0:0: -411,158,26560,2,0,P|435:109|418:40,1,100 -351,96,26893,1,0,0:0:0:0: -384,255,27115,5,8,0:0:0:0: -384,255,27226,1,0,0:0:0:0: -384,255,27337,1,0,0:0:0:0: -384,255,27449,2,0,L|447:250,2,50,0|8|0,0:0|0:0|0:0,0:0:0:0: -228,173,27782,6,0,L|176:209,1,50,2|2,0:0|0:0,0:0:0:0: -279,342,28004,1,8,0:0:0:0: -279,342,28115,1,0,0:0:0:0: -279,342,28226,2,0,L|280:281,1,50,2|2,0:0|0:0,0:0:0:0: -357,136,28449,5,8,3:2:0:0: -307,139,28560,1,0,3:0:0:0: -257,142,28671,1,0,3:0:0:0: -207,145,28782,2,0,L|151:150,2,50,0|8|0,3:0|0:0|3:0,0:0:0:0: -257,142,29115,5,0,3:0:0:0: -307,139,29226,2,0,L|409:161,1,100,0|0,3:0|3:0,0:0:0:0: -445,188,29560,1,0,3:0:0:0: -468,231,29671,1,0,3:0:0:0: -464,280,29782,1,10,3:2:0:0: -435,320,29893,1,2,3:2:0:0: -389,339,30004,2,0,L|329:327,1,50,10|0,3:2|0:0,0:0:0:0: -177,222,30226,6,0,L|146:193,1,25,8|0,0:0|3:0,3:0:0:0: -145,248,30337,2,0,L|102:231,1,25,8|0,0:0|3:0,0:0:0:0: -130,287,30449,2,0,L|90:290,1,25,8|0,0:0|3:0,0:0:0:0: -134,328,30560,2,0,B|174:349|174:349|239:343,1,100,8|0,1:2|3:0,0:0:0:0: -294,251,30893,6,0,L|290:192,1,50,0|0,1:0|3:0,0:0:0:0: -226,74,31115,2,0,L|222:133,1,50,4|0,0:1|3:0,0:0:0:0: -359,150,31337,1,4,0:1:0:0: -359,150,31782,6,0,P|408:124|464:148,1,89.9999972534181,0|0,1:0|1:0,0:0:0:0: -340,240,32004,6,0,L|288:36,1,179.999994506836,12|0,0:0|0:0,0:0:0:0: -176,132,32449,2,0,P|87:100|7:180,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -139,383,32893,2,0,P|83:316|104:232,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -205,291,33337,6,0,L|449:323,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -506,223,33782,2,0,L|262:191,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -182,205,34226,6,0,P|261:157|336:244,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -76,191,34671,2,0,P|28:112|115:37,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -182,205,35115,6,2,L|155:305,1,89.9999972534181,10|0,0:0|0:0,0:1:0:0: -257,361,35337,2,2,L|284:261,1,89.9999972534181,2|0,0:0|0:0,0:1:0:0: -334,174,35560,6,0,P|339:277|458:283,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -447,191,36004,2,0,L|505:21,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -334,174,36449,6,0,P|269:112|169:165,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -133,25,36893,2,0,L|165:279,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -22,281,37337,6,0,P|91:338|207:265,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -306,240,37782,2,0,P|239:184|155:203,1,179.999994506836,8|0,0:0|0:0,0:0:0:0: -0,92,38226,2,0,L|266:54,1,179.999994506836,8|8,0:0|0:0,0:0:0:0: -363,43,38671,6,2,P|386:76|392:133,1,89.9999972534181,10|0,0:0|0:0,0:1:0:0: -306,240,38893,2,2,P|283:207|277:150,1,89.9999972534181,10|0,0:0|0:0,0:1:0:0: -421,293,39115,5,8,3:2:0:0: -368,278,39226,1,0,3:0:0:0: -313,273,39337,1,2,3:3:0:0: -259,284,39449,1,0,0:0:0:0: -214,316,39560,1,8,3:2:0:0: -166,343,39671,1,0,3:0:0:0: -112,332,39782,1,2,3:3:0:0: -76,289,39893,1,0,0:0:0:0: -68,234,40004,1,10,3:2:0:0: -94,185,40115,1,0,3:0:0:0: -136,149,40226,1,0,3:0:0:0: -190,147,40337,1,2,3:0:0:0: -241,167,40449,1,8,0:0:0:0: -261,217,40560,1,0,3:0:0:0: -240,267,40671,1,2,3:0:0:0: -188,285,40782,1,0,0:0:0:0: -135,268,40893,5,8,3:2:0:0: -114,216,41004,1,0,3:0:0:0: -137,166,41115,1,2,3:0:0:0: -190,147,41226,1,0,0:0:0:0: -241,167,41337,1,8,3:2:0:0: -295,170,41449,1,0,3:0:0:0: -348,157,41560,1,2,3:0:0:0: -390,121,41671,1,0,0:0:0:0: -394,66,41782,1,10,3:2:0:0: -364,18,41893,1,0,3:0:0:0: -316,0,42004,1,0,3:0:0:0: -262,11,42115,1,2,3:0:0:0: -214,26,42226,6,2,L|107:43,1,89.9999972534181,8|2,0:0|3:0,0:1:0:0: -2,149,42449,2,2,L|109:166,1,89.9999972534181,4|0,3:2|0:0,0:1:0:0: -336,200,42671,5,12,3:2:0:0: -288,172,42782,1,0,3:0:0:0: -232,173,42893,1,0,3:0:0:0: -189,207,43004,1,0,0:0:0:0: -160,255,43115,1,10,3:0:0:0: -168,308,43226,1,0,3:0:0:0: -196,355,43337,1,2,3:0:0:0: -249,366,43449,1,0,0:0:0:0: -295,337,43560,1,8,3:2:0:0: -303,283,43671,1,0,3:0:0:0: -277,233,43782,1,0,3:0:0:0: -224,216,43893,1,2,3:0:0:0: -172,228,44004,1,8,0:0:0:0: -124,248,44115,1,0,0:0:0:0: -72,232,44226,1,2,3:0:0:0: -32,196,44337,1,0,3:0:0:0: -28,144,44449,6,0,L|1:135,1,25,8|0,0:0|0:0,0:0:0:0: -62,96,44560,2,0,L|45:73,1,25 -119,77,44671,2,0,L|119:49,1,25,8|0,0:0|0:0,0:0:0:0: -175,96,44782,2,0,L|191:73,1,25 -210,144,44893,2,0,L|236:135,1,25,8|0,0:0|0:0,0:0:0:0: -228,200,45004,2,0,L|201:209,1,25,8|0,0:0|0:0,0:0:0:0: -262,248,45115,2,0,L|245:271,1,25,8|0,0:0|0:0,0:0:0:0: -319,267,45226,2,0,L|319:295,1,25,8|0,0:0|0:0,0:0:0:0: -375,248,45337,2,8,L|391:271,1,25 -410,200,45449,2,8,L|436:209,1,25 -410,141,45560,2,8,L|436:132,1,25 -375,93,45671,5,12,0:3:0:0: -375,93,46004,2,0,L|317:86,1,50,0|12,1:0|0:2,0:0:0:0: -317,167,46337,5,0,0:0:0:0: -317,167,46449,2,0,L|160:184,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: -53,101,46782,1,0,0:0:0:0: -108,98,46893,1,0,1:0:0:0: -152,130,47004,1,0,0:0:0:0: -167,183,47115,6,2,L|161:268,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: -49,308,47337,1,0,1:0:0:0: -49,308,47449,2,2,L|44:204,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: -205,140,47671,1,0,0:0:0:0: -205,140,47782,2,2,L|210:244,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: -310,353,48004,5,8,0:0:0:0: -346,347,48115,1,0,0:0:0:0: -383,341,48226,2,0,P|440:289|426:219,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: -317,167,48560,1,0,0:0:0:0: -307,112,48671,1,0,1:0:0:0: -333,64,48782,1,0,0:0:0:0: -384,43,48893,6,2,L|468:54,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: -506,161,49115,1,0,1:0:0:0: -506,161,49226,2,2,L|422:150,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: -268,121,49449,1,0,0:0:0:0: -268,121,49560,2,2,L|352:110,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: -263,263,49782,5,8,0:0:0:0: -228,247,49893,1,0,0:0:0:0: -193,232,50004,2,0,P|128:234|53:312,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: -121,164,50337,1,0,0:0:0:0: -120,109,50449,1,0,1:0:0:0: -91,62,50560,1,0,0:0:0:0: -42,37,50671,6,2,L|123:29,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: -242,84,50893,1,0,1:0:0:0: -242,84,51004,2,2,L|321:118,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: -341,245,51226,1,0,0:0:0:0: -341,245,51337,2,2,L|353:331,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: -163,192,51560,5,8,0:0:0:0: -198,181,51671,1,0,0:0:0:0: -233,170,51782,2,0,P|303:163|372:219,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: -458,297,52115,1,0,0:0:0:0: -460,240,52226,1,0,1:0:0:0: -463,184,52337,1,0,0:0:0:0: -466,128,52449,6,0,B|452:65|452:65|416:155,1,150.000005722046,10|2,0:2|1:2,0:0:0:0: -272,189,52782,2,0,L|197:180,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: -338,25,53004,1,2,0:0:0:0: -338,25,53115,2,0,L|345:109,1,75.0000028610231,10|0,1:2|0:0,0:1:0:0: -179,225,53337,5,8,0:0:0:0: -175,187,53449,1,0,0:0:0:0: -172,149,53560,2,0,L|320:166,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: -296,293,53893,1,0,0:0:0:0: -259,334,54004,1,0,1:0:0:0: -209,357,54115,1,0,0:0:0:0: -154,358,54226,6,2,L|79:349,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: -179,225,54449,1,0,1:0:0:0: -179,225,54560,2,2,L|253:233,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: -91,140,54782,1,0,0:0:0:0: -91,140,54893,2,2,L|16:148,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: -191,43,55115,5,8,0:0:0:0: -195,80,55226,1,0,0:0:0:0: -199,117,55337,2,0,B|179:225|179:225|156:170,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: -289,165,55671,1,0,0:0:0:0: -344,159,55782,1,0,1:0:0:0: -399,154,55893,1,0,0:0:0:0: -454,149,56004,6,2,L|461:235,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: -359,281,56226,1,0,1:0:0:0: -359,281,56337,2,2,L|365:366,1,75.0000028610231,2|8,0:2|0:0,0:1:0:0: -262,132,56560,1,0,0:0:0:0: -262,132,56671,2,2,L|268:217,1,75.0000028610231,2|0,1:2|0:0,0:1:0:0: -148,358,56893,5,12,0:2:0:0: -110,355,57004,1,0,0:0:0:0: -79,333,57115,2,0,P|134:285|209:317,1,150.000005722046,2|8,1:2|0:0,0:0:0:0: -329,327,57449,1,0,0:0:0:0: -359,281,57560,1,0,1:0:0:0: -364,226,57671,1,0,0:0:0:0: -343,175,57782,6,2,L|246:184,1,75.0000028610231,10|0,0:2|0:0,0:1:0:0: -342,20,58004,1,0,1:0:0:0: -342,20,58115,2,2,L|387:85,1,75.0000028610231,2|8,0:0|0:0,0:1:0:0: -210,97,58337,1,0,0:0:0:0: -210,97,58449,2,2,L|243:24,1,75.0000028610231,10|0,1:2|0:0,0:1:0:0: -343,175,58671,6,0,L|305:177,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -209,209,58893,2,0,L|255:224,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -316,267,59115,2,0,L|283:301,3,37.5000014305115,8|0|8|0,0:0|0:0|0:0|0:0,0:0:0:0: -211,329,59337,2,0,L|194:274,3,37.5000014305115,8|0|8|0,0:0|0:0|0:0|0:0,0:0:0:0: -103,287,59560,6,0,L|67:275,1,37.5000014305115,0|0,1:0|3:0,0:0:0:0: -108,240,59671,2,0,L|72:228,1,37.5000014305115,0|0,1:0|3:0,0:0:0:0: -113,193,59782,2,0,L|77:181,1,37.5000014305115,0|0,1:0|3:0,0:0:0:0: -120,146,59893,1,12,0:3:0:0: -120,146,60226,6,0,L|131:40,1,100,0|12,1:0|0:2,0:0:0:0: -374,174,60893,5,8,0:0:0:0: -326,323,61115,1,0,3:0:0:0: -462,267,61337,2,0,L|518:267,1,50,8|0,0:0|0:0,0:0:0:0: -344,182,61560,5,0,0:0:0:0: -344,182,61671,2,0,L|297:213,1,50,0|8,3:0|0:0,0:0:0:0: -174,241,61893,1,0,0:0:0:0: -174,241,62004,2,0,L|221:272,1,50,0|0,3:0|0:0,0:0:0:0: -66,161,62226,5,8,0:0:0:0: -104,326,62449,1,0,3:0:0:0: -269,288,62671,1,8,0:0:0:0: -231,122,62893,2,0,L|215:50,1,50,0|0,3:0|0:0,0:0:0:0: -296,0,63115,2,0,L|312:72,1,50,8|0,0:0|0:0,0:0:0:0: -373,120,63337,5,0,0:0:0:0: -373,120,63449,2,0,P|413:112|477:152,1,100,0|0,3:0|3:0,0:0:0:0: -400,216,63782,2,0,L|288:232,1,100,0|8,0:0|0:0,0:0:0:0: -48,160,64226,5,0,3:0:0:0: -216,200,64449,1,8,0:0:0:0: -104,288,64671,1,0,3:0:0:0: -216,200,64893,1,8,0:0:0:0: -160,64,65115,5,0,0:0:0:0: -160,64,65226,2,0,L|160:136,1,50,0|8,3:0|0:0,0:0:0:0: -264,104,65449,1,0,0:0:0:0: -264,104,65560,2,0,L|264:168,1,50,0|0,3:0|0:0,0:0:0:0: -456,160,65782,5,8,0:0:0:0: -288,192,66004,1,0,3:0:0:0: -336,48,66226,1,8,0:0:0:0: -408,296,66449,1,0,3:0:0:0: -196,148,66671,6,0,P|188:215|221:271,1,100,8|8,0:0|0:0,0:0:0:0: -288,192,67004,6,0,L|368:216,1,50,0|8,3:0|0:0,0:0:0:0: -297,308,67226,1,0,3:0:0:0: -297,308,67337,2,0,L|217:332,1,50,12|0,0:0|0:0,0:0:0:0: -107,256,67560,6,0,P|150:234|199:244,1,100,14|0,0:0|0:0,0:0:0:0: -460,300,68004,5,8,0:0:0:0: -407,107,68226,1,0,3:0:0:0: -364,364,68449,1,8,0:0:0:0: -345,18,68671,6,0,P|417:2|505:66,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: -167,18,69115,2,0,P|95:2|7:66,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: -407,107,69560,6,0,P|456:161|445:269,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: -158,202,70004,1,0,0:0:0:0: -354,202,70226,1,8,0:0:0:0: -105,107,70449,2,0,P|55:161|66:269,1,150.000005722046,2|8,0:0|0:0,0:0:0:0: -364,281,70893,6,0,P|418:330|526:319,1,150.000005722046,0|8,0:0|0:0,0:0:0:0: -424,64,71337,6,0,B|434:73|434:73|467:145|424:213|284:257|262:111|277:26|173:-19|99:9|48:91|87:209|211:192|211:192|334:175|373:294|323:376|248:404|162:332,1,1050.00004005432,2|8,3:2|0:0,0:0:0:0: -161,331,73337,6,0,L|201:331,1,25,8|0,0:0|3:0,0:0:0:0: -186,331,73449,2,2,L|233:331,1,25,0|0,3:0|3:0,0:0:0:0: -211,331,73560,2,2,L|257:331,1,25,8|0,0:0|3:0,0:0:0:0: -236,331,73671,2,2,L|273:331,1,25,0|0,3:0|3:0,0:0:0:0: -297,339,73782,6,0,L|377:347,1,50,8|8,0:0|0:0,0:0:0:0: -321,181,74004,2,0,L|313:269,1,50,8|8,0:0|3:2,0:0:0:0: -184,116,74226,2,0,L|176:28,1,50,0|8,0:0|3:2,0:0:0:0: -283,82,74449,6,0,P|332:75|377:94,1,100,8|12,0:0|0:0,0:0:0:0: -150,188,75115,5,8,0:0:0:0: -127,46,75337,1,0,0:0:0:0: -157,218,75560,2,0,P|211:199|262:204,1,100,8|0,0:0|0:0,0:0:0:0: -322,283,75893,2,0,B|370:288|370:288|436:262,1,100,0|0,3:0|3:0,0:0:0:0: -439,170,76226,2,0,P|392:159|356:191,1,100,0|8,3:0|0:0,0:0:0:0: -219,92,76671,5,0,3:0:0:0: -371,22,76893,1,8,0:0:0:0: -356,191,77115,1,0,3:0:0:0: -194,73,77337,2,0,P|144:75|94:82,1,100,8|0,0:0|0:0,0:0:0:0: -15,164,77671,2,0,P|63:173|98:210,1,100,0|0,3:0|3:0,0:0:0:0: -26,302,78004,2,0,L|21:253,1,50 -181,348,78226,6,0,B|221:330|221:330|276:335,1,100,8|0,0:0|3:0,0:0:0:0: -422,231,78671,1,8,0:0:0:0: -435,376,78893,1,0,3:0:0:0: -271,230,79115,2,0,L|281:123,1,100,8|0,0:0|0:0,0:0:0:0: -367,56,79449,2,0,P|393:101|389:159,1,100,0|0,3:0|3:0,0:0:0:0: -280,130,79782,2,0,L|226:137,1,50,0|0,3:0|0:0,0:0:0:0: -104,280,80004,5,8,0:0:0:0: -243,207,80226,1,0,3:0:0:0: -104,134,80449,1,8,0:0:0:0: -243,61,80671,1,0,3:0:0:0: -384,189,80893,6,0,L|330:203,1,50,8|0,0:0|0:0,0:0:0:0: -259,262,81115,2,0,L|211:250,1,50,8|0,0:0|0:0,0:0:0:0: -83,157,81337,1,8,1:2:0:0: -60,186,81449,1,0,3:0:0:0: -48,221,81560,1,8,1:2:0:0: -48,259,81671,1,0,3:0:0:0: -61,294,81782,6,0,P|116:293|162:329,1,100,10|0,1:2|0:0,0:0:0:0: -350,269,82226,1,8,0:0:0:0: -226,187,82449,1,0,3:0:0:0: -265,365,82671,1,8,0:0:0:0: -444,207,82893,2,0,P|449:142|420:85,1,129.999994049073,2|8,0:0|0:0,0:0:0:0: -211,48,83337,6,0,P|159:84|136:144,1,129.999994049073,2|8,0:0|0:0,0:0:0:0: -270,337,83782,2,0,B|297:279|256:269|288:205,1,129.999994049073,2|8,0:0|0:0,0:0:0:0: -427,124,84226,1,0,3:0:0:0: -427,124,84337,1,0,3:0:0:0: -427,124,84449,1,8,0:0:0:0: -136,144,84671,2,0,B|183:228|275:200|275:200|303:273|208:263,1,259.999988098145,2|4,0:0|0:1,0:0:0:0: -235,99,85337,5,12,0:2:0:0: -278,92,85449,1,0,0:0:0:0: -319,109,85560,1,2,0:0:0:0: -174,43,85782,1,10,0:2:0:0: -129,35,85893,1,0,1:0:0:0: -86,47,86004,1,0,3:0:0:0: -52,75,86115,1,0,1:0:0:0: -32,115,86226,6,0,L|37:155,3,25,10|0|0|0,0:2|3:0|3:0|3:0,0:0:0:0: -83,252,86449,2,0,L|87:212,3,25,0|0|2|0,1:0|3:0|3:2|3:0,0:0:0:0: -174,189,86671,6,0,L|235:185,2,50,8|0|8,0:0|3:0|0:0,0:0:0:0: -83,252,87004,1,0,0:0:0:0: -83,252,87115,6,0,L|92:311,1,50,12|2,1:2|3:2,0:0:0:0: -185,379,87337,2,0,L|205:328,1,50,10|0,1:2|3:0,0:0:0:0: -310,279,87560,2,0,L|362:285,1,50,8|8,1:2|1:2,0:0:0:0: -490,357,87782,2,0,L|493:386,2,25,8|0|8,1:2|3:0|1:2,0:0:0:0: -456,208,88004,5,0,1:0:0:0: -456,208,88060,1,0,3:0:0:0: -456,208,88115,2,0,L|472:128,1,50,8|0,1:2|0:0,0:0:0:0: -352,40,88337,2,0,L|368:88,1,50,8|2,1:2|0:0,0:0:0:0: -256,136,88560,1,8,1:2:0:0: -256,136,88671,2,0,L|240:80,1,50 -48,184,88893,6,0,P|120:168|216:224,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: -328,336,89226,1,0,0:0:0:0: -424,328,89337,1,8,0:2:0:0: -472,248,89449,1,0,0:0:0:0: -488,160,89560,6,0,P|440:56|352:48,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -256,104,89893,1,0,0:0:0:0: -168,128,90004,1,0,0:0:0:0: -80,120,90115,1,0,0:0:0:0: -0,88,90226,6,0,P|-8:136|8:216,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -96,336,90449,2,0,P|104:288|88:208,1,85.0000016212464 -173,43,90671,6,0,P|165:85|168:128,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -256,296,90893,2,0,P|264:248|248:168,1,85.0000016212464 -336,72,91115,6,0,L|560:56,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -432,120,91449,1,2,0:2:0:0: -392,200,91560,1,8,0:2:0:0: -432,288,91671,1,0,0:0:0:0: -496,360,91782,5,2,0:2:0:0: -256,296,92004,1,10,0:2:0:0: -16,360,92226,2,0,P|64:272|48:184,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: -120,40,92560,5,0,0:0:0:0: -168,120,92671,1,0,0:0:0:0: -248,168,92782,1,0,0:0:0:0: -328,120,92893,6,0,P|414:120|477:240,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -472,360,93226,1,0,0:0:0:0: -472,360,93337,2,0,P|396:319|396:183,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -328,120,93671,5,0,0:0:0:0: -328,120,93782,2,0,L|312:8,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -93,147,94004,6,0,P|174:128|248:168,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: -448,288,94337,1,0,0:0:0:0: -360,264,94449,1,2,0:2:0:0: -272,288,94560,1,0,0:0:0:0: -216,360,94671,6,0,P|152:304|168:224,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -216,104,95004,5,2,0:2:0:0: -216,104,95115,2,0,L|232:16,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -368,48,95337,2,0,P|408:32|472:48,1,85.0000016212464 -432,224,95560,2,0,P|416:184|432:120,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -320,280,95782,2,0,P|336:320|320:384,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -32,232,96004,6,0,P|112:192|224:240,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -320,280,96337,1,0,0:0:0:0: -408,312,96449,1,8,0:2:0:0: -496,280,96560,1,0,0:0:0:0: -496,280,96671,6,0,L|456:112,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -384,56,97004,1,0,0:0:0:0: -296,24,97115,2,0,P|240:16|160:64,1,85.0000016212464 -104,160,97337,2,0,P|160:168|240:120,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -456,192,97560,6,0,P|384:160|304:248,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -264,288,97893,1,0,0:0:0:0: -176,264,98004,2,0,L|8:280,1,85.0000016212464 -16,88,98226,6,0,P|64:136|168:56,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -328,56,98560,1,2,0:2:0:0: -416,32,98671,1,8,0:2:0:0: -480,104,98782,1,0,0:0:0:0: -456,192,98893,6,0,L|464:280,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -328,360,99115,2,0,L|320:272,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -208,112,99337,2,0,L|216:200,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -64,320,99560,5,8,0:2:0:0: -136,264,99671,1,0,0:0:0:0: -232,256,99782,1,0,0:0:0:0: -320,280,99893,1,0,0:0:0:0: -384,344,100004,6,0,P|408:288|416:248,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -360,72,100226,2,0,P|336:128|328:168,1,85.0000016212464 -240,312,100449,5,8,0:2:0:0: -152,344,100560,1,0,0:0:0:0: -64,320,100671,1,0,0:0:0:0: -8,248,100782,1,0,0:0:0:0: -16,152,100893,1,8,0:2:0:0: -88,88,101004,1,0,0:0:0:0: -184,80,101115,1,0,0:0:0:0: -272,120,101226,1,0,0:0:0:0: -356,176,101337,5,10,0:2:0:0: -456,164,101449,1,0,0:0:0:0: -392,88,101560,1,2,0:2:0:0: -348,184,101671,5,0,0:0:0:0: -448,172,101782,1,10,0:2:0:0: -384,96,101893,1,0,0:0:0:0: -340,192,102004,5,10,0:2:0:0: -440,180,102115,1,0,0:0:0:0: -376,104,102226,2,0,P|329:93|290:112,1,85.0000016212464,10|8,1:2|1:2,0:0:0:0: -344,192,102449,2,0,P|380:226|423:229,1,85.0000016212464,0|8,0:0|0:0,0:0:0:0: -169,239,102671,5,2,1:2:0:0: -80,210,102782,1,0,1:0:0:0: -80,117,102893,1,8,1:2:0:0: -169,88,103004,1,8,1:2:0:0: -224,164,103115,6,0,P|315:166|404:144,1,170.000003242493,12|0,1:2|0:0,0:0:0:0: -456,317,103449,1,0,0:0:0:0: -366,290,103560,1,8,1:2:0:0: -324,206,103671,1,2,0:2:0:0: -326,112,103782,2,0,L|344:10,2,85.0000016212464,0|0|10,0:0|0:0|0:2,0:0:0:0: -314,203,104115,5,0,0:0:0:0: -242,148,104226,2,0,L|184:87,1,85.0000016212464 -398,167,104449,2,0,L|479:122,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -365,276,104671,2,0,L|383:378,2,85.0000016212464,0|0|8,0:0|0:0|0:2,0:0:0:0: -183,86,105004,5,0,0:0:0:0: -99,88,105115,1,0,0:0:0:0: -31,137,105226,1,0,0:0:0:0: -3,216,105337,1,10,0:2:0:0: -24,297,105449,1,0,0:0:0:0: -87,352,105560,1,0,0:0:0:0: -152,298,105671,1,2,0:2:0:0: -233,273,105782,1,8,0:2:0:0: -317,283,105893,1,0,0:0:0:0: -391,324,106004,6,0,L|484:310,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -282,227,106226,2,0,L|189:212,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -46,357,106449,2,0,L|138:342,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -62,140,106671,1,8,0:2:0:0: -62,140,106782,1,0,0:0:0:0: -62,140,106893,1,0,0:0:0:0: -62,140,107004,1,0,0:0:0:0: -62,140,107115,6,0,L|245:116,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -418,227,107449,1,0,0:0:0:0: -330,251,107560,1,8,1:2:0:0: -251,206,107671,1,0,0:0:0:0: -230,117,107782,1,0,0:0:0:0: -277,35,107893,1,0,0:0:0:0: -347,130,108004,2,0,L|247:147,1,85.0000016212464,8|0,1:2|0:0,0:0:0:0: -46,62,108226,6,0,P|30:145|24:230,1,170.000003242493,2|8,0:2|1:2,0:0:0:0: -215,312,108560,1,0,0:0:0:0: -215,312,108671,2,0,P|191:272|152:252,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -62,140,108893,2,0,P|146:173|234:167,1,170.000003242493,10|0,1:2|0:0,0:0:0:0: -371,114,109226,2,0,L|376:22,2,85.0000016212464,2|8|0,0:2|1:2|0:0,0:0:0:0: -312,190,109560,5,0,0:0:0:0: -389,239,109671,1,0,0:0:0:0: -308,283,109782,1,8,1:2:0:0: -386,333,109893,1,0,0:0:0:0: -305,377,110004,2,0,P|267:367|226:331,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -77,199,110226,6,0,P|152:149|227:172,1,170.000003242493,8|0,1:2|0:0,0:0:0:0: -417,221,110560,1,0,0:0:0:0: -444,135,110671,1,8,1:2:0:0: -389,64,110782,1,0,0:0:0:0: -299,68,110893,2,0,P|210:71|156:9,1,170.000003242493,0|8,0:0|1:2,0:0:0:0: -62,140,111226,1,0,0:0:0:0: -62,140,111337,2,0,L|83:234,1,85.0000016212464 -262,296,111560,2,0,L|169:278,1,85.0000016212464,8|0,1:2|0:0,0:0:0:0: -349,153,111782,6,0,L|340:68,2,85.0000016212464,0|0|8,0:0|0:0|1:2,0:0:0:0: -236,329,112115,1,0,0:0:0:0: -292,267,112226,1,0,0:0:0:0: -375,253,112337,1,0,0:0:0:0: -449,291,112449,1,10,1:2:0:0: -378,336,112560,5,2,0:2:0:0: -375,253,112671,1,0,0:0:0:0: -372,170,112782,1,2,0:2:0:0: -369,87,112893,1,8,1:2:0:0: -442,125,113004,1,0,0:0:0:0: -348,178,113115,6,0,L|252:169,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -80,269,113337,2,0,P|120:276|175:256,1,85.0000016212464,10|0,1:2|0:0,0:0:0:0: -105,18,113560,2,0,P|86:55|87:103,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -260,255,113782,5,12,1:2:0:0: -261,212,113893,1,0,0:0:0:0: -262,170,114004,1,2,0:2:0:0: -414,61,114226,5,8,1:2:0:0: -340,101,114337,1,0,0:0:0:0: -256,109,114449,1,2,0:2:0:0: -176,81,114560,1,0,0:0:0:0: -105,18,114671,1,8,1:2:0:0: -84,118,114782,1,0,0:0:0:0: -63,218,114893,6,2,P|105:233|154:235,1,85.0000016212464,2|0,0:2|0:0,0:2:0:0: -369,328,115115,2,2,P|325:320|276:336,1,85.0000016212464,10|0,1:2|0:0,0:2:0:0: -256,109,115337,2,2,B|301:123|301:123|376:112,1,100,2|0,0:2|0:0,0:1:0:0: -488,273,115560,5,8,1:2:0:0: -488,273,115671,1,8,0:0:0:0: -488,273,115782,1,8,0:0:0:0: -429,83,116004,1,8,1:2:0:0: -429,83,116115,1,0,3:0:0:0: -429,83,116226,6,0,L|431:47,2,32.4999985122681,8|0|0,3:2|3:0|3:0,3:0:0:0: -381,198,116449,1,8,1:2:0:0: -381,198,116560,5,0,1:1:0:0: -187,140,116782,1,0,1:1:0:0: -102,323,117004,1,0,1:1:0:0: -58,34,117337,5,12,0:0:0:0: -380,124,128004,6,0,P|400:120|440:124,1,50,12|0,0:0|0:0,0:0:0:0: -396,256,128226,2,0,P|376:260|336:256,1,50,2|0,1:2|0:0,0:0:0:0: -104,200,128449,6,0,L|116:260,1,50,8|0,0:0|0:0,0:0:0:0: -172,104,128671,2,0,L|160:44,1,50,0|0,1:0|0:0,0:0:0:0: -272,184,128893,5,10,0:2:0:0: -280,208,129004,1,0,0:0:0:0: -280,232,129115,1,0,1:0:0:0: -272,256,129226,1,2,0:2:0:0: -264,280,129337,6,0,L|328:308,1,50,8|0,0:0|0:0,0:0:0:0: -464,260,129560,2,0,L|400:232,1,50,10|0,1:2|0:0,0:0:0:0: -317,113,129782,6,0,L|339:84,1,25,8|0,0:0|0:0,0:0:0:0: -286,96,129893,2,0,L|298:62,1,25 -252,92,130004,2,0,L|251:56,1,25,8|0,0:0|0:0,0:0:0:0: -218,99,130115,2,0,L|205:65,1,25 -189,117,130226,2,0,L|164:90,1,25,8|0,0:0|0:0,0:0:0:0: -167,145,130337,2,0,L|135:128,1,25,8|0,0:0|0:0,0:0:0:0: -156,178,130449,2,0,L|121:173,1,25,8|0,0:0|0:0,0:0:0:0: -158,212,130560,2,0,L|122:220,1,25,8|0,0:0|0:0,0:0:0:0: -171,244,130671,2,0,L|140:264,1,25,0|0,1:0|3:0,0:0:0:0: -194,270,130782,2,0,L|172:299,1,25,0|0,1:0|3:0,0:0:0:0: -225,287,130893,2,0,L|213:321,1,25,0|0,1:0|3:0,0:0:0:0: -259,291,131004,5,12,0:3:0:0: -259,291,131337,6,0,P|312:272|376:300,1,100,0|12,1:0|0:0,0:0:0:0: -456,68,132004,5,8,0:0:0:0: -340,184,132226,1,0,3:0:0:0: -284,20,132449,1,8,0:0:0:0: -156,160,132671,5,0,0:0:0:0: -156,160,132782,2,0,P|216:152|256:160,1,100,0|0,3:0|0:0,0:0:0:0: -92,235,133115,2,0,L|20:251,1,50,0|0,3:0|0:0,0:0:0:0: -281,303,133337,5,8,0:0:0:0: -471,213,133560,1,0,3:0:0:0: -281,303,133782,1,8,0:0:0:0: -363,84,134004,1,0,3:0:0:0: -241,162,134226,6,0,L|222:94,1,50,8|0,0:0|0:0,0:0:0:0: -164,204,134449,1,0,0:0:0:0: -164,204,134560,2,0,P|112:196|51:220,1,100,0|0,3:0|3:0,0:0:0:0: -281,303,134893,2,0,P|333:311|394:287,1,100,0|8,0:0|0:0,0:0:0:0: -500,184,135337,5,0,3:0:0:0: -465,11,135560,1,8,0:0:0:0: -294,44,135782,1,0,3:0:0:0: -327,217,136004,1,8,0:0:0:0: -408,112,136226,5,0,0:0:0:0: -408,112,136337,1,0,3:0:0:0: -364,316,136560,1,0,0:0:0:0: -364,316,136671,1,0,3:0:0:0: -168,292,136893,5,8,0:0:0:0: -48,212,137115,1,0,3:0:0:0: -188,16,137337,1,8,0:0:0:0: -176,156,137560,1,0,3:0:0:0: -344,292,137782,6,0,P|408:288|464:252,1,100,8|0,0:0|0:0,0:0:0:0: -388,176,138115,2,0,P|408:124|400:68,1,100,0|0,3:0|3:0,0:0:0:0: -168,192,138449,1,0,0:0:0:0: -168,192,138560,1,0,0:0:0:0: -168,192,138671,2,0,L|280:204,1,100,10|0,0:0|3:0,0:0:0:0: -464,168,139115,5,8,0:0:0:0: -344,292,139337,1,0,3:0:0:0: -332,24,139560,1,8,0:0:0:0: -288,368,139782,6,0,P|344:384|404:368,1,100,2|8,0:0|0:0,0:0:0:0: -224,16,140226,2,0,P|168:0|108:16,1,100,2|8,0:0|0:0,0:0:0:0: -80,224,140671,2,0,P|64:280|80:340,1,100,2|8,0:0|0:0,0:0:0:0: -400,152,141115,5,0,0:0:0:0: -300,268,141337,1,8,0:0:0:0: -460,80,141560,2,0,P|404:56|336:88,1,100,2|8,0:0|0:0,0:0:0:0: -332,224,142004,2,0,L|352:360,1,100,0|8,0:0|0:0,0:0:0:0: -184,88,142449,6,0,L|178:38,1,50 -204,136,142671,1,8,0:0:0:0: -212,184,142782,1,0,3:0:0:0: -196,232,142893,1,0,3:0:0:0: -152,256,143004,1,0,0:0:0:0: -104,236,143115,1,8,0:0:0:0: -56,252,143226,1,0,3:0:0:0: -32,296,143337,1,0,3:0:0:0: -52,340,143449,1,0,3:0:0:0: -92,372,143560,1,8,0:0:0:0: -140,352,143671,1,0,3:0:0:0: -188,336,143782,1,8,0:0:0:0: -236,348,143893,1,0,3:0:0:0: -280,368,144004,5,8,0:0:0:0: -448,260,144449,6,0,L|416:248,1,25,8|0,0:0|3:0,0:0:0:0: -424,251,144560,2,0,L|392:240,1,25,0|0,3:0|3:0,0:0:0:0: -400,243,144671,2,0,L|360:228,1,25,8|0,0:0|3:0,0:0:0:0: -377,234,144782,2,0,L|348:224,1,25,0|0,3:0|3:0,0:0:0:0: -316,200,144893,6,0,P|308:176|308:132,1,50,8|8,0:0|0:0,0:0:0:0: -404,40,145115,2,0,P|412:64|412:108,1,50,8|8,0:0|3:2,0:0:0:0: -252,264,145337,5,8,0:0:0:0: -252,264,145449,1,8,3:2:0:0: -252,264,145560,2,0,P|196:256|136:272,1,100,8|12,0:0|0:0,0:0:0:0: -34,77,146226,5,8,0:0:0:0: -54,230,146449,1,0,3:0:0:0: -28,27,146671,2,0,L|36:105,1,50,8|0,0:0|0:0,0:0:0:0: -54,230,146893,1,0,0:0:0:0: -54,230,147004,2,0,L|62:152,1,50,0|8,3:0|0:0,0:0:0:0: -234,135,147226,1,0,0:0:0:0: -234,135,147337,2,0,L|347:149,1,100,0|8,3:0|0:0,0:0:0:0: -156,190,147782,5,0,3:0:0:0: -333,147,148004,1,8,0:0:0:0: -133,120,148226,1,0,3:0:0:0: -358,65,148449,2,0,L|261:89,1,100,8|0,0:0|0:0,0:0:0:0: -333,147,148782,2,0,L|446:161,1,100,0|0,3:0|3:0,0:0:0:0: -321,275,149115,2,0,L|302:214,1,50,0|0,1:0|0:0,0:0:0:0: -462,165,149337,6,0,L|428:273,1,100,8|0,0:0|3:0,0:0:0:0: -393,83,149782,1,8,0:0:0:0: -431,260,150004,1,0,3:0:0:0: -221,288,150226,2,0,L|331:273,1,100,8|0,0:0|0:0,0:0:0:0: -190,296,150560,2,0,L|171:235,1,50,0|8,3:0|0:0,0:0:0:0: -334,226,150782,1,0,0:0:0:0: -334,226,150893,2,0,L|315:287,1,50,0|0,3:0|0:0,0:0:0:0: -238,135,151115,6,0,L|257:196,1,50,8|0,0:0|0:0,0:0:0:0: -190,296,151337,2,0,L|209:357,1,50,0|0,3:0|0:0,0:0:0:0: -118,72,151560,2,0,L|137:133,1,50,8|0,0:0|0:0,0:0:0:0: -24,221,151782,2,0,L|43:282,1,50,0|0,3:0|0:0,0:0:0:0: -242,367,152004,5,8,0:0:0:0: -276,362,152115,1,0,3:0:0:0: -311,357,152226,1,8,0:0:0:0: -346,353,152337,1,0,3:0:0:0: -386,338,152449,5,8,1:2:0:0: -337,327,152560,1,0,3:0:0:0: -288,317,152671,1,8,1:2:0:0: -239,306,152782,1,0,3:0:0:0: -190,296,152893,1,10,1:2:0:0: -190,296,153337,5,8,0:0:0:0: -241,77,153560,1,0,3:0:0:0: -200,262,153782,1,8,0:0:0:0: -447,200,154004,2,0,L|336:181,1,100,2|8,0:0|0:0,0:0:0:0: -97,119,154449,2,0,L|159:109,1,50,2|0,0:0|0:0,0:0:0:0: -348,183,154671,5,10,0:2:0:0: -151,210,154893,2,0,L|146:111,1,100,2|8,0:0|0:0,0:0:0:0: -353,83,155337,1,0,3:0:0:0: -350,132,155449,1,0,3:0:0:0: -347,182,155560,1,8,0:0:0:0: -31,78,155782,2,0,B|174:62|174:62|145:110,1,200,2|4,0:0|0:1,0:0:0:0: -113,280,156449,6,0,L|111:229,1,50,12|0,0:2|0:0,0:0:0:0: -145,110,156671,1,2,0:0:0:0: -108,312,156893,2,0,L|42:303,1,50,10|0,0:2|1:0,0:0:0:0: -211,208,157115,2,0,L|277:217,1,50,0|0,3:0|1:0,0:0:0:0: -414,288,157337,6,0,L|387:290,3,25,10|0|0|0,0:2|3:0|3:0|3:0,0:0:0:0: -290,340,157560,2,0,L|316:342,3,25,0|0|2|0,1:0|3:0|3:2|3:0,0:0:0:0: -414,288,157782,6,0,L|421:221,1,50,8|0,0:0|3:0,0:0:0:0: -315,130,158004,2,0,L|332:193,1,50,8|0,0:0|3:0,0:0:0:0: -492,100,158226,2,0,L|426:91,1,50,12|2,1:2|3:2,0:0:0:0: -214,158,158449,2,0,L|202:97,1,50,10|0,1:2|3:0,0:0:0:0: -342,13,158671,1,8,1:2:0:0: -342,13,158782,1,8,1:2:0:0: -342,13,158893,2,0,L|339:46,2,25,8|0|8,1:2|3:0|1:2,0:0:0:0: -332,156,159115,5,0,1:0:0:0: -332,156,159171,1,0,3:0:0:0: -332,156,159226,2,0,P|352:156|380:166,1,50,8|0,1:2|0:0,0:0:0:0: -456,260,159449,2,0,P|456:280|446:308,1,50,8|2,1:2|0:0,0:0:0:0: -340,368,159671,1,8,1:2:0:0: -340,368,159782,2,0,P|320:368|292:358,1,50 -44,272,160004,6,0,P|152:268|212:196,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: -324,28,160337,1,0,0:0:0:0: -264,100,160449,1,8,0:2:0:0: -312,180,160560,1,0,0:0:0:0: -404,164,160671,6,0,P|400:248|348:320,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -184,328,161004,1,0,0:0:0:0: -96,352,161115,1,0,0:0:0:0: -32,280,161226,1,0,0:0:0:0: -56,192,161337,6,0,L|48:104,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -184,24,161560,6,0,P|228:76|344:32,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -476,176,161893,1,0,0:0:0:0: -384,196,162004,1,0,0:0:0:0: -356,280,162115,1,0,0:0:0:0: -416,352,162226,6,0,L|240:360,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -64,312,162560,1,2,2:0:0:0: -128,244,162671,1,8,0:2:0:0: -152,156,162782,1,0,0:0:0:0: -96,80,162893,6,0,P|120:40|168:12,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -361,18,163115,2,0,P|395:43|416:80,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -256,224,163337,2,0,L|256:124,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -356,280,163560,5,8,0:2:0:0: -356,280,163671,1,0,0:0:0:0: -356,280,163782,1,0,0:0:0:0: -356,280,163893,1,0,0:0:0:0: -356,280,164004,2,0,P|268:268|212:348,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -124,344,164337,1,0,0:0:0:0: -92,256,164449,2,0,P|140:180|96:92,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -180,184,164782,5,0,0:0:0:0: -180,184,164893,2,0,L|284:176,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -500,88,165115,6,0,P|418:70|345:110,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: -320,236,165449,1,0,0:0:0:0: -320,236,165560,2,0,L|332:336,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -432,156,165782,6,0,P|408:56|320:16,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -216,64,166115,5,2,0:2:0:0: -216,64,166226,2,0,L|126:70,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -143,208,166449,2,0,P|122:246|132:311,1,85.0000016212464 -312,291,166671,2,0,P|273:270|208:280,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -379,185,166893,2,0,P|417:205|482:195,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -456,64,167115,6,0,P|344:44|284:84,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -112,148,167449,1,0,0:0:0:0: -160,228,167560,1,8,0:2:0:0: -248,256,167671,1,0,0:0:0:0: -336,228,167782,6,0,P|432:216|480:272,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -392,308,168115,5,0,0:0:0:0: -336,228,168226,2,0,L|332:124,1,85.0000016212464 -448,32,168449,2,0,L|452:136,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -392,308,168671,6,0,P|324:272|216:320,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -156,340,169004,1,0,0:0:0:0: -88,272,169115,2,0,L|124:176,1,85.0000016212464 -88,56,169337,6,0,P|188:100|284:44,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -416,28,169671,1,2,0:2:0:0: -360,104,169782,1,8,0:2:0:0: -352,196,169893,1,0,0:0:0:0: -396,280,170004,6,0,P|400:328|432:364,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -272,296,170226,2,0,P|268:248|236:212,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -68,288,170449,2,0,P|116:276|168:288,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -324,148,170671,6,0,P|256:168|244:56,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -0,244,171004,2,0,L|180:264,1,170.000003242493 -460,240,171337,6,0,P|464:280|456:356,1,85.0000016212464 -336,284,171560,2,0,P|332:244|340:168,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -388,40,171782,2,0,P|412:80|416:132,1,85.0000016212464 -204,80,172004,2,0,P|252:80|308:96,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -208,192,172226,2,0,P|160:192|104:176,1,85.0000016212464 -256,48,172449,6,0,P|312:56|352:68,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -400,192,172671,2,0,P|392:248|380:288,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -256,336,172893,2,0,P|200:328|160:316,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -112,192,173115,2,0,P|120:136|132:96,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -400,192,173337,6,0,P|392:248|380:288,1,85.0000016212464,10|8,0:2|0:2,0:0:0:0: -256,48,173560,2,0,P|312:56|352:68,1,85.0000016212464,0|8,0:0|0:2,0:0:0:0: -172,320,173782,1,2,0:2:0:0: -117,245,173893,1,0,0:0:0:0: -116,153,174004,1,8,0:2:0:0: -169,78,174115,1,8,0:2:0:0: -256,48,174226,6,0,P|334:91|329:184,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: -168,268,174560,1,0,0:0:0:0: -238,207,174671,1,8,0:2:0:0: -329,194,174782,1,0,0:0:0:0: -413,232,174893,2,0,L|426:331,2,85.0000016212464,0|0|8,0:0|0:0|0:2,0:0:0:0: -344,150,175226,1,0,0:0:0:0: -344,150,175337,2,0,L|261:134,1,85.0000016212464 -478,62,175560,2,0,L|466:164,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -322,328,175782,6,0,P|253:282|149:306,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -50,366,176115,1,0,0:0:0:0: -91,283,176226,1,0,0:0:0:0: -81,191,176337,1,0,0:0:0:0: -24,119,176449,2,0,P|93:165|197:141,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -327,79,176782,2,0,L|306:252,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: -125,323,177115,6,0,P|161:302|203:298,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -389,256,177337,2,0,P|347:259|297:243,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -180,72,177560,2,0,P|221:88|261:137,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -66,257,177782,1,8,0:2:0:0: -64,235,177893,1,0,0:0:0:0: -63,214,178004,1,0,0:0:0:0: -62,193,178115,1,0,0:0:0:0: -61,172,178226,6,0,P|131:162|136:254,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -4,96,178560,1,0,0:0:0:0: -81,61,178671,1,8,0:2:0:0: -164,73,178782,1,0,0:0:0:0: -227,128,178893,1,0,0:0:0:0: -251,209,179004,1,0,0:0:0:0: -228,289,179115,2,0,P|213:252|218:200,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -401,62,179337,6,0,P|337:115|250:106,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: -93,212,179671,1,0,0:0:0:0: -93,212,179782,2,0,P|143:218|184:248,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -334,366,180004,2,0,P|339:312|345:185,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -311,33,180337,2,0,P|265:16|223:22,2,85.0000016212464,2|8|0,2:0|0:2|0:0,0:0:0:0: -289,212,180671,5,0,0:0:0:0: -196,198,180782,1,0,0:0:0:0: -255,124,180893,1,8,0:2:0:0: -301,221,181004,5,0,0:0:0:0: -181,203,181115,1,8,0:2:0:0: -257,108,181226,1,0,0:0:0:0: -372,147,181337,6,0,P|407:214|372:301,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -163,238,181671,1,0,0:0:0:0: -74,225,181782,1,8,0:2:0:0: -18,294,181893,1,0,0:0:0:0: -50,378,182004,2,0,P|161:346|236:375,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -355,315,182337,1,0,0:0:0:0: -355,315,182449,2,0,P|346:273|355:211,1,85.0000016212464 -423,86,182671,2,0,P|432:128|423:190,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -214,113,182893,6,0,L|29:142,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -181,224,183226,1,0,0:0:0:0: -264,237,183337,1,0,0:0:0:0: -348,250,183449,1,0,0:0:0:0: -431,262,183560,2,0,P|446:302|448:344,2,85.0000016212464,10|2|0,0:2|0:2|0:0,0:0:0:0: -282,68,183893,2,0,P|211:119|219:199,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: -391,175,184226,6,0,L|307:161,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -132,298,184449,2,0,L|186:232,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -154,12,184671,2,0,L|183:91,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -395,82,184893,5,12,0:2:0:0: -473,129,185004,1,0,0:0:0:0: -391,175,185115,2,0,P|341:179|286:160,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -105,112,185337,5,8,0:2:0:0: -26,158,185449,1,0,0:0:0:0: -108,204,185560,2,0,P|157:207|212:188,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -314,35,185782,5,8,0:2:0:0: -230,71,185893,1,0,0:0:0:0: -188,153,186004,1,2,0:2:0:0: -206,243,186115,1,0,0:0:0:0: -277,300,186226,2,0,P|319:313|381:310,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -469,143,186449,2,2,P|423:143|383:163,1,85.0000016212464,2|0,0:2|0:0,0:1:0:0: -180,96,186671,5,8,0:2:0:0: -180,96,186782,1,8,0:0:0:0: -180,96,186893,1,8,0:0:0:0: -333,236,187115,2,0,L|346:127,1,85.0000016212464,8|0,0:2|3:0,0:0:0:0: -404,53,187337,2,0,L|398:10,2,42.5000008106232,8|0|0,3:2|3:0|3:0,0:0:0:0: -426,195,187560,1,8,0:2:0:0: -426,195,187671,5,0,0:0:0:0: -240,159,187893,1,0,0:0:0:0: -350,339,188115,1,0,0:0:0:0: -70,296,188449,5,12,0:0:0:0: -432,192,195560,5,4,0:0:0:0: -432,168,195671,1,0,3:0:0:0: -424,144,195782,1,0,3:0:0:0: -416,120,195893,1,0,3:0:0:0: -400,96,196004,1,0,3:0:0:0: -384,80,196115,1,0,3:0:0:0: -368,64,196226,1,0,3:0:0:0: -344,48,196337,1,0,3:0:0:0: -320,40,196449,1,0,3:0:0:0: -296,40,196560,1,0,3:0:0:0: -272,40,196671,1,0,3:0:0:0: -248,48,196782,1,0,3:0:0:0: -224,64,196893,1,0,3:0:0:0: -208,80,197004,1,0,3:0:0:0: -192,104,197115,1,2,3:2:0:0: -184,128,197226,1,0,3:0:0:0: -176,152,197337,5,0,3:0:0:0: -160,168,197449,1,0,3:0:0:0: -136,184,197560,1,0,3:0:0:0: -112,192,197671,1,0,3:0:0:0: -96,208,197782,1,0,3:0:0:0: -80,232,197893,1,2,3:2:0:0: -80,256,198004,1,0,3:0:0:0: -88,280,198115,1,0,3:0:0:0: -112,296,198226,1,2,3:2:0:0: -136,304,198337,1,0,3:0:0:0: -160,304,198449,1,0,3:0:0:0: -184,296,198560,1,2,3:2:0:0: -200,280,198671,1,0,3:0:0:0: -224,264,198782,1,0,3:0:0:0: -248,256,198893,1,2,3:2:0:0: -272,264,199004,1,2,3:2:0:0: -296,280,199115,5,0,3:0:0:0: -320,288,199226,1,0,3:0:0:0: -344,288,199337,1,2,3:2:0:0: -368,280,199449,1,0,3:0:0:0: -392,264,199560,1,0,3:0:0:0: -408,248,199671,1,0,3:0:0:0: -424,224,199782,1,0,3:0:0:0: -432,200,199893,1,0,3:0:0:0: -432,176,200004,1,0,3:0:0:0: -424,152,200115,1,0,3:0:0:0: -416,128,200226,1,2,3:2:0:0: -400,104,200337,1,0,3:0:0:0: -384,88,200449,1,0,3:0:0:0: -360,72,200560,1,0,3:0:0:0: -336,64,200671,1,2,3:2:0:0: -312,64,200782,1,0,3:0:0:0: -288,72,200893,6,2,L|280:32,1,25,0|0,3:0|3:0,0:1:0:0: -264,80,201004,2,2,L|256:40,1,25,0|0,3:0|3:0,0:1:0:0: -240,88,201115,2,0,L|232:48,1,25,0|0,3:0|3:0,0:0:0:0: -216,96,201226,2,0,L|208:56,1,25,0|0,3:0|3:0,0:0:0:0: -192,104,201337,2,2,L|184:64,1,25,8|0,3:2|3:0,0:1:0:0: -168,112,201449,2,0,L|160:72,1,25,0|0,3:0|3:0,0:0:0:0: -144,120,201560,2,0,L|136:80,1,25,8|0,3:2|3:0,0:0:0:0: -120,128,201671,2,2,L|115:103,1,25,0|0,3:0|3:0,0:1:0:0: -96,136,201782,6,0,L|77:100,1,25,8|0,3:2|3:0,0:0:0:0: -57,173,201893,2,0,L|23:151,1,25,8|0,3:2|3:0,0:0:0:0: -41,224,202004,2,2,L|1:222,1,25,0|0,3:0|3:0,0:1:0:0: -53,277,202115,2,0,L|17:295,1,25,8|0,3:2|3:0,0:0:0:0: -90,316,202226,2,0,L|68:349,1,25,8|0,3:2|3:0,0:0:0:0: -141,332,202337,2,2,L|139:371,1,25,0|0,3:0|3:0,0:1:0:0: -194,320,202449,2,0,L|212:355,1,25,8|0,3:2|3:0,0:0:0:0: -233,283,202560,2,0,L|266:304,1,25,8|0,3:2|3:0,0:0:0:0: -249,231,202671,6,0,L|432:216,1,150.000005722046,12|0,0:0|0:0,0:0:0:0: -496,104,203115,5,8,0:0:0:0: -400,328,203337,1,0,0:0:0:0: -400,328,203449,1,0,0:0:0:0: -400,328,203560,2,0,P|352:296|336:256,1,100,10|0,0:0|0:0,0:0:0:0: -296,104,203893,5,0,0:0:0:0: -296,104,204004,2,0,L|352:112,1,50,8|0,0:0|0:0,0:0:0:0: -160,168,204226,1,0,0:0:0:0: -160,168,204337,1,0,0:0:0:0: -160,168,204449,2,0,P|112:160|56:192,1,100,10|2,0:0|0:0,0:0:0:0: -136,320,204893,5,8,0:0:0:0: -304,224,205115,1,0,0:0:0:0: -328,224,205226,1,0,0:0:0:0: -352,224,205337,1,10,0:0:0:0: -464,144,205560,5,0,0:0:0:0: -464,144,205671,1,0,0:0:0:0: -464,144,205782,2,2,P|480:112|480:96,1,50,8|0,0:0|0:0,0:1:0:0: -400,320,206004,2,2,P|384:352|384:368,1,50,0|0,3:0|0:0,0:1:0:0: -304,224,206226,6,0,P|248:200|184:224,1,100,10|2,0:0|0:0,0:0:0:0: -24,296,206671,1,8,0:0:0:0: -160,104,206893,1,0,0:0:0:0: -248,304,207115,2,0,P|304:328|368:304,1,100,10|0,0:0|0:0,0:0:0:0: -340,321,207782,6,0,L|316:193,1,100,2|8,0:0|0:0,0:0:0:0: -376,72,208226,1,2,0:0:0:0: -144,152,208449,1,8,0:0:0:0: -256,336,208671,1,0,0:0:0:0: -272,40,208893,5,10,0:0:0:0: -112,304,209115,1,8,0:0:0:0: -440,224,209337,6,2,P|480:224|512:232,1,50,8|0,0:0|3:0,0:1:0:0: -440,320,209560,2,2,P|400:320|368:312,1,50,8|0,0:0|3:0,0:1:0:0: -248,232,209782,5,8,3:2:0:0: -216,216,209893,1,0,3:0:0:0: -184,208,210004,1,2,3:0:0:0: -152,208,210115,1,0,0:0:0:0: -121,223,210226,6,2,L|109:132,1,50,8|0,3:2|3:0,0:1:0:0: -53,84,210449,1,2,3:0:0:0: -53,84,210560,2,0,P|108:67|180:81,1,100,0|0,0:0|3:0,0:0:0:0: -339,150,210893,1,0,3:0:0:0: -339,150,211004,1,2,3:0:0:0: -339,150,211115,2,0,L|202:141,1,100,8|2,0:0|3:0,0:0:0:0: -406,107,211449,5,0,3:0:0:0: -406,107,211560,2,0,P|380:155|400:244,1,150,8|0,0:2|0:0,0:0:0:0: -461,315,212004,6,0,L|533:308,1,50,8|0,3:2|3:0,0:0:0:0: -308,351,212226,1,2,3:0:0:0: -308,351,212337,2,0,P|255:332|177:351,1,100,0|0,0:0|3:0,0:0:0:0: -64,352,212671,1,0,3:0:0:0: -64,352,212782,1,2,3:0:0:0: -64,352,212893,2,0,L|96:240,1,100,8|4,0:0|3:2,0:0:0:0: -56,96,213226,5,0,0:0:0:0: -56,96,213337,2,0,P|113:140|185:126,1,150,12|0,0:0|0:0,0:0:0:0: -232,104,213782,1,8,0:0:0:0: -280,80,213893,1,0,0:0:0:0: -328,72,214004,1,0,0:0:0:0: -376,80,214115,1,0,0:0:0:0: -416,104,214226,1,10,0:0:0:0: -448,144,214337,1,0,0:0:0:0: -456,192,214449,1,2,0:0:0:0: -448,240,214560,1,0,0:0:0:0: -416,280,214671,1,8,0:0:0:0: -376,304,214782,1,0,0:0:0:0: -328,312,214893,1,10,0:0:0:0: -280,304,215004,1,0,0:0:0:0: -240,280,215115,6,0,L|192:280,1,25,8|0,3:2|3:0,0:0:0:0: -215,280,215226,2,0,L|176:280,1,25,0|0,3:0|3:0,0:0:0:0: -190,280,215337,2,0,L|160:280,1,25,2|0,3:2|3:0,0:0:0:0: -165,280,215449,2,0,L|120:280,1,25,0|0,3:0|3:0,0:0:0:0: -112,280,215560,5,8,0:0:0:0: -32,224,215671,1,8,0:0:0:0: -120,176,215782,2,0,L|120:128,2,25,8|8|8,0:0|0:0|0:0,0:0:0:0: -240,64,216115,5,0,1:0:0:0: -344,224,216337,1,0,1:0:0:0: -448,32,216560,1,0,1:0:0:0: -145,84,216893,5,12,0:0:0:0: -198,332,217115,2,0,L|219:193,1,119.999996337891,2|8,0:0|0:0,0:0:0:0: -308,165,217449,1,0,0:0:0:0: -308,165,217560,1,0,0:0:0:0: -81,244,217782,1,8,0:0:0:0: -76,214,217893,1,0,0:0:0:0: -72,184,218004,1,2,0:0:0:0: -67,155,218115,1,0,0:0:0:0: -63,125,218226,1,8,0:0:0:0: -200,49,218449,6,0,P|267:85|382:74,1,179.999994506836,2|0,0:0|0:0,0:0:0:0: -422,36,218893,2,0,P|355:108|357:200,1,179.999994506836,2|0,0:0|0:0,0:0:0:0: -512,151,219337,2,0,L|452:142,1,59.9999981689454,2|0,0:0|0:0,0:0:0:0: -318,194,219560,6,2,L|263:202,5,29.9999990844727,8|0|0|0|0|0,0:0|0:0|0:0|0:0|0:0|0:0,0:1:0:0: -424,218,219893,1,0,0:0:0:0: -426,247,220004,1,8,0:0:0:0: -429,276,220115,1,0,0:0:0:0: -432,305,220226,6,0,B|377:333|377:333|325:317|325:317|272:340|201:317,1,239.999992675781,2|0,0:0|0:0,0:0:0:0: -97,166,220893,1,8,0:0:0:0: -269,215,221115,2,0,L|278:135,1,59.9999981689454,2|0,0:0|0:0,0:0:0:0: -159,41,221337,1,8,3:2:0:0: -163,78,221449,1,0,3:0:0:0: -167,115,221560,1,0,3:0:0:0: -171,152,221671,1,2,3:2:0:0: -176,189,221782,2,0,L|167:270,1,59.9999981689454,8|0,0:0|0:0,0:0:0:0: -24,352,222004,6,0,L|203:335,1,179.999994506836,2|0,0:0|0:0,0:0:0:0: -346,297,222449,2,0,L|166:280,1,179.999994506836,2|0,0:0|3:0,0:0:0:0: -68,237,222893,1,2,3:2:0:0: -103,233,223004,1,0,3:0:0:0: -139,230,223115,6,2,L|216:238,1,59.9999981689454,8|0,0:0|0:0,0:1:0:0: -377,324,223337,2,2,L|419:260,1,59.9999981689454,8|0,0:0|0:0,0:1:0:0: -278,145,223560,2,0,L|290:227,1,59.9999981689454,8|0,0:0|0:0,0:0:0:0: -452,83,223782,2,0,L|375:91,1,59.9999981689454,8|0,0:0|0:0,0:0:0:0: -186,134,224004,5,8,3:2:0:0: -146,131,224115,1,0,3:0:0:0: -109,116,224226,1,2,3:0:0:0: -77,92,224337,1,0,0:0:0:0: -15,29,224449,5,8,3:2:0:0: -11,78,224560,1,0,3:0:0:0: -8,128,224671,1,2,3:0:0:0: -4,178,224782,1,0,0:0:0:0: -89,198,224893,5,8,3:2:0:0: -96,237,225004,1,0,3:0:0:0: -103,276,225115,1,0,3:0:0:0: -111,315,225226,1,2,3:0:0:0: -206,281,225337,5,8,0:0:0:0: -245,287,225449,1,0,3:0:0:0: -285,293,225560,1,2,3:0:0:0: -324,299,225671,1,0,0:0:0:0: -408,249,225782,5,8,3:2:0:0: -373,213,225893,1,0,3:0:0:0: -326,200,226004,1,0,3:0:0:0: -278,213,226115,1,0,3:0:0:0: -206,281,226226,5,8,3:2:0:0: -182,237,226337,1,0,3:0:0:0: -182,188,226449,1,0,3:0:0:0: -207,144,226560,1,0,0:0:0:0: -306,164,226671,5,8,3:2:0:0: -354,156,226782,1,0,3:0:0:0: -392,124,226893,1,0,3:0:0:0: -407,77,227004,1,2,3:0:0:0: -301,55,227115,5,8,0:0:0:0: -303,109,227226,1,2,3:0:0:0: -306,164,227337,1,0,3:0:0:0: -308,219,227449,1,0,0:0:0:0: -360,304,227560,5,8,3:2:0:0: -301,297,227671,1,0,3:2:0:0: -246,321,227782,2,0,P|216:329|176:319,1,59.9999981689454,2|0,3:2|0:0,0:0:0:0: -49,207,228004,2,0,L|56:281,1,59.9999981689454,8|0,3:2|1:0,0:0:0:0: -208,185,228226,2,0,L|215:111,1,59.9999981689454,2|0,3:2|1:0,0:0:0:0: -44,29,228449,5,10,3:2:0:0: -40,58,228560,1,0,3:0:0:0: -36,88,228671,1,0,3:0:0:0: -32,117,228782,1,8,0:0:0:0: -123,105,228893,1,8,3:2:0:0: -212,92,229004,6,0,L|254:72,1,29.9999990844727,0|0,3:0|3:0,3:0:0:0: -260,125,229115,2,0,L|300:123,1,29.9999990844727,10|0,3:2|3:0,3:0:0:0: -282,180,229226,2,0,L|319:193,1,29.9999990844727,0|0,3:0|3:0,3:0:0:0: -269,237,229337,5,8,3:2:0:0: -219,237,229449,1,0,3:0:0:0: -174,257,229560,1,2,3:2:0:0: -142,295,229671,1,0,0:0:0:0: -245,353,229782,5,8,3:2:0:0: -293,340,229893,1,8,3:2:0:0: -342,349,230004,2,0,L|377:334,1,29.9999990844727,2|0,3:2|3:0,0:0:0:0: -383,376,230115,1,0,3:0:0:0: -467,284,230226,5,0,1:0:0:0: -464,234,230337,1,8,1:2:0:0: -461,184,230449,1,0,0:0:0:0: -458,134,230560,1,8,1:2:0:0: -310,42,230671,5,0,3:0:0:0: -300,90,230782,1,8,1:2:0:0: -322,133,230893,1,0,3:0:0:0: -367,154,231004,1,0,0:0:0:0: -458,134,231115,6,0,L|465:242,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: -369,307,231782,2,0,P|310:301|248:334,1,119.999996337891 -137,289,232449,1,0,0:0:0:0: -137,289,232671,2,0,B|124:242|124:242|140:160,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: -159,135,233337,2,0,P|221:159|296:148,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: -375,81,234004,1,2,0:0:0:0: -389,206,234226,1,2,0:0:0:0: -273,155,234449,1,2,0:0:0:0: -143,320,235115,6,0,P|80:311|26:342,2,119.999996337891 -253,363,236226,2,0,B|274:307|240:278|261:239,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: -303,208,236893,2,0,L|164:197,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: -76,264,237560,1,0,0:0:0:0: -48,170,237782,1,2,0:0:0:0: -99,88,238004,1,10,0:0:0:0: -195,72,238226,6,0,P|245:87|301:87,1,89.9999972534181,4|0,0:0|0:0,0:0:0:0: -430,40,238893,2,0,P|441:102|450:172,1,119.999996337891 -443,280,239560,1,0,0:0:0:0: -340,214,239782,2,0,P|325:155|343:98,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: -430,40,240449,2,0,P|384:47|286:27,1,119.999996337891,2|0,0:0|0:0,0:0:0:0: -195,72,241115,1,0,0:0:0:0: -181,191,241337,1,0,0:0:0:0: -291,143,241560,1,2,0:0:0:0: -195,72,241782,6,0,P|143:75|97:107,1,89.9999972534181,2|0,0:0|0:0,0:0:0:0: -90,271,242449,2,0,P|196:312|291:241,1,239.999992675781,2|2,0:0|0:0,0:0:0:0: -289,243,243449,2,0,P|328:264|373:264,1,89.9999972534181,2|0,0:0|3:0,0:0:0:0: -465,186,244004,1,0,3:0:0:0: -409,80,244226,5,0,3:0:0:0: -379,82,244337,1,0,3:0:0:0: -349,84,244449,1,10,0:0:0:0: -321,87,244560,2,0,L|271:91,1,29.9999990844727,8|0,1:2|0:0,0:0:0:0: -160,48,244782,5,8,1:2:0:0: -152,72,244893,1,10,0:0:0:0: -144,96,245004,1,8,0:0:0:0: -136,120,245115,1,8,0:0:0:0: -128,144,245226,1,8,0:0:0:0: -72,184,245337,6,0,L|212:196,1,100,12|0,0:0|0:0,0:0:0:0: -208,232,245671,2,0,P|256:241|299:217,1,100 -320,176,246004,2,0,L|312:76,1,100,0|8,0:0|0:0,0:0:0:0: -200,136,246449,5,0,0:0:0:0: -304,256,246671,1,8,0:0:0:0: -400,120,246893,2,0,L|536:112,1,100,2|8,0:0|0:0,0:0:0:0: -336,176,247337,5,0,0:0:0:0: -336,176,247449,1,0,0:0:0:0: -336,176,247560,2,0,P|288:152|216:184,1,100,10|0,0:0|0:0,0:0:0:0: -244,160,248004,1,8,0:0:0:0: -80,216,248226,5,2,0:0:0:0: -280,256,248449,1,10,0:0:0:0: -192,80,248671,2,0,L|192:200,1,100,2|8,0:0|0:0,0:0:0:0: -280,256,249115,1,0,0:0:0:0: -392,208,249337,6,0,P|408:256|368:328,1,100,8|0,0:0|0:0,0:0:0:0: -280,256,249782,2,0,L|144:264,1,100,8|0,0:0|0:0,0:0:0:0: -72,184,250226,1,8,0:0:0:0: -120,80,250449,2,0,P|161:99|185:179,1,100,2|8,0:0|0:0,0:0:0:0: -136,280,250893,5,0,0:0:0:0: -136,280,251115,2,0,L|352:272,1,200,10|8,0:0|0:0,0:0:0:0: -416,152,251782,5,0,0:0:0:0: -488,80,252004,1,10,0:0:0:0: -416,8,252226,1,2,0:0:0:0: -344,80,252449,6,0,L|232:88,1,100,10|0,0:0|0:0,0:0:0:0: -192,48,252782,2,0,P|200:72|200:168,1,100 -152,176,253115,2,0,L|288:192,1,100,0|8,0:0|0:0,0:0:0:0: -448,176,253560,5,0,0:0:0:0: -344,304,253782,1,8,0:0:0:0: -272,152,254004,2,0,L|272:16,1,100,2|8,0:0|0:0,0:0:0:0: -328,208,254449,5,0,0:0:0:0: -328,208,254671,2,0,P|264:200|208:216,1,100,10|0,0:0|0:0,0:0:0:0: -72,288,255115,1,8,0:0:0:0: -136,168,255337,6,0,L|112:104,1,50,2|0,0:0|0:0,0:0:0:0: -216,32,255560,2,0,L|232:88,1,50,10|0,0:0|0:0,0:0:0:0: -328,160,255782,2,0,L|347:114,1,50,2|0,0:0|0:0,0:0:0:0: -424,224,256004,6,0,L|451:233,1,25,10|0,2:2|2:0,2:0:0:0: -390,272,256115,2,0,L|407:295,1,25,0|0,2:0|2:0,2:0:0:0: -333,291,256226,2,0,L|333:319,1,25,2|0,2:2|2:0,2:0:0:0: -277,272,256337,2,0,L|261:295,1,25,0|0,2:0|2:0,2:0:0:0: -242,224,256449,2,0,L|216:233,1,25,10|0,2:2|2:0,2:0:0:0: -224,168,256560,2,0,L|251:159,1,25,0|0,2:0|2:0,2:0:0:0: -190,120,256671,2,0,L|207:97,1,25,2|0,2:2|2:0,2:0:0:0: -133,101,256782,2,0,L|133:73,1,25,0|0,2:0|2:0,2:0:0:0: -77,120,256893,2,8,L|61:97,1,25,8|0,2:2|2:0,2:0:0:0: -42,168,257004,2,8,L|16:159,1,25,8|0,2:2|2:0,2:0:0:0: -42,227,257115,2,0,L|15:236,1,25,4|0,2:2|2:0,2:0:0:0: -76,275,257226,2,0,L|59:298,1,25,0|0,2:0|2:0,2:0:0:0: -133,294,257337,2,0,L|133:322,1,25,8|0,2:2|2:0,2:0:0:0: -189,275,257449,2,8,L|205:298,1,25,0|0,2:0|2:0,2:0:0:0: -224,227,257560,2,8,L|250:236,1,25,12|0,2:2|2:0,2:0:0:0: -224,168,257671,2,8,L|250:159,1,25,0|0,2:0|2:0,2:0:0:0: -190,120,257782,5,8,0:0:0:0: -133,101,257893,1,8,0:0:0:0: -77,120,258004,1,8,0:0:0:0: -224,227,258226,1,8,0:0:0:0: -258,275,258337,1,8,0:0:0:0: -315,294,258449,1,8,0:0:0:0: -371,275,258560,1,8,0:0:0:0: -406,227,258671,1,8,0:0:0:0: -412,167,258782,1,8,0:0:0:0: -391,110,258893,6,0,L|326:101,2,50,0|12|8,0:0|0:3|0:0,0:0:0:0: -315,194,259226,1,0,1:0:0:0: -315,194,259337,2,0,L|250:203,1,50,8|8,0:0|0:0,0:0:0:0: -88,95,259560,6,0,P|158:52|237:76,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: -362,269,259893,1,0,0:0:0:0: -272,288,260004,1,8,0:2:0:0: -194,241,260115,1,0,0:0:0:0: -171,153,260226,2,0,L|178:58,2,85.0000016212464,0|0|8,0:0|0:0|0:2,0:0:0:0: -106,216,260560,1,0,0:0:0:0: -24,177,260671,2,0,L|29:261,1,85.0000016212464 -165,349,260893,2,0,L|170:264,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -394,84,261115,6,0,P|343:147|251:128,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -394,84,261449,1,0,0:0:0:0: -425,169,261560,1,0,0:0:0:0: -388,252,261671,1,0,0:0:0:0: -303,285,261782,2,0,P|268:267|206:267,2,85.0000016212464,10|2|0,0:2|0:2|0:0,0:0:0:0: -454,347,262115,2,0,L|463:165,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: -297,56,262449,6,0,P|294:98|309:137,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -491,104,262671,2,0,P|443:101|409:118,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -223,263,262893,2,0,L|215:157,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -68,53,263115,1,8,0:2:0:0: -89,55,263226,1,0,0:0:0:0: -110,57,263337,1,0,0:0:0:0: -131,60,263449,1,0,0:0:0:0: -152,62,263560,6,0,P|237:73|284:142,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -87,182,263893,1,0,0:0:0:0: -87,182,264004,2,0,P|39:237|58:348,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -269,306,264337,1,0,0:0:0:0: -269,306,264449,2,0,L|186:290,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -283,137,264671,6,0,B|406:160|406:160|360:178,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: -165,193,265004,1,0,0:0:0:0: -165,193,265115,2,0,B|150:152|150:152|155:110,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -299,286,265337,2,0,P|339:271|391:274,2,85.0000016212464,10|2|0,0:2|0:2|0:0,0:0:0:0: -428,374,265671,1,2,0:2:0:0: -487,305,265782,1,8,0:2:0:0: -476,215,265893,1,0,0:0:0:0: -396,164,266004,6,0,L|274:178,1,85.0000016212464 -193,245,266226,2,0,L|108:235,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -210,73,266449,2,0,P|253:58|306:64,1,85.0000016212464 -438,263,266671,6,0,B|387:245|392:181|392:181|370:212|323:212,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -153,138,267004,1,0,0:0:0:0: -66,161,267115,1,8,0:2:0:0: -34,245,267226,1,0,0:0:0:0: -84,319,267337,2,0,P|168:311|263:307,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -334,211,267671,1,0,0:0:0:0: -334,211,267782,2,0,P|325:170|336:122,1,85.0000016212464 -458,77,268004,2,0,P|467:118|456:166,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -321,354,268226,6,0,P|253:306|164:315,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: -34,201,268560,1,0,0:0:0:0: -34,202,268671,2,0,L|129:194,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -263,87,268893,2,0,P|177:90|129:157,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -326,305,269226,1,2,0:2:0:0: -334,211,269337,1,8,0:2:0:0: -343,118,269449,1,0,0:0:0:0: -352,25,269560,6,0,L|362:109,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -223,176,269782,2,0,L|212:260,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -446,266,270004,2,0,P|408:240|357:234,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -113,288,270226,6,0,P|65:224|76:127,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -165,55,270560,1,0,0:0:0:0: -232,116,270671,1,8,0:2:0:0: -323,130,270782,1,0,0:0:0:0: -407,90,270893,2,0,P|426:124|432:182,1,85.0000016212464 -317,319,271115,5,8,0:2:0:0: -317,225,271226,1,0,0:0:0:0: -406,196,271337,1,0,0:0:0:0: -460,272,271449,1,0,0:0:0:0: -406,348,271560,1,8,0:2:0:0: -317,319,271671,1,0,0:0:0:0: -224,303,271782,2,0,P|178:303|138:324,1,85.0000016212464 -43,164,272004,5,10,0:2:0:0: -127,124,272115,1,0,0:0:0:0: -117,217,272226,1,2,0:2:0:0: -43,164,272337,5,0,0:0:0:0: -77,48,272449,1,10,0:2:0:0: -157,138,272560,1,0,0:0:0:0: -43,164,272671,2,2,P|39:120|56:80,1,85.0000016212464,2|0,0:2|0:0,0:1:0:0: -279,26,272893,6,0,L|416:16,1,85.0000016212464,10|8,0:2|0:2,0:0:0:0: -504,120,273115,2,0,L|367:130,1,85.0000016212464,0|8,0:0|0:2,0:0:0:0: -176,120,273337,5,10,0:2:0:0: -216,208,273449,1,8,0:2:0:0: -184,296,273560,1,8,0:2:0:0: -112,352,273671,1,8,0:2:0:0: -16,344,273782,6,0,L|224:328,1,170.000003242493,12|0,0:2|0:0,0:0:0:0: -464,288,274115,2,0,P|392:312|288:248,1,170.000003242493 -105,186,274449,5,0,0:0:0:0: -143,95,274560,1,0,0:0:0:0: -233,58,274671,1,8,0:2:0:0: -324,95,274782,1,0,3:0:0:0: -361,186,274893,1,0,0:0:0:0: -324,276,275004,1,0,0:0:0:0: -233,314,275115,6,0,L|128:320,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -0,216,275337,2,0,P|96:168|192:232,1,170.000003242493,0|8,0:0|0:2,0:0:0:0: -324,276,275671,1,0,0:0:0:0: -392,336,275782,2,0,P|413:299|414:257,1,85.0000016212464 -320,155,276004,6,0,P|272:217|324:276,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -424,368,276337,2,0,L|456:200,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: -360,24,276671,6,0,P|352:64|408:112,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -232,112,276893,2,0,P|192:104|144:160,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -200,296,277115,2,0,P|192:256|248:208,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -360,144,277337,5,8,0:2:0:0: -360,144,277449,1,0,0:0:0:0: -360,144,277560,1,0,0:0:0:0: -360,144,277671,1,0,0:0:0:0: -360,144,277782,2,0,L|336:328,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -256,352,278115,1,0,0:0:0:0: -168,328,278226,2,0,P|241:264|351:328,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -184,352,278560,5,0,0:0:0:0: -184,352,278671,2,0,L|80:360,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -307,274,278893,2,0,P|400:304|480:264,1,170.000003242493,2|8,0:2|0:2,0:0:0:0: -504,136,279226,5,0,0:0:0:0: -504,137,279337,2,0,L|415:124,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -232,64,279560,2,0,P|200:136|256:200,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -488,264,279893,5,2,0:2:0:0: -400,232,280004,1,8,0:2:0:0: -312,256,280115,1,0,0:0:0:0: -248,320,280226,6,0,P|280:352|336:344,1,85.0000016212464 -120,280,280449,2,0,L|24:272,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -216,192,280671,2,0,P|264:184|312:200,1,85.0000016212464 -160,104,280893,6,0,P|64:136|8:80,1,170.000003242493,8|0,0:2|0:0,0:0:0:0: -201,22,281226,2,0,L|217:214,1,170.000003242493 -376,336,281560,5,0,0:0:0:0: -416,248,281671,1,0,0:0:0:0: -320,264,281782,1,8,0:2:0:0: -376,336,281893,1,0,0:0:0:0: -416,248,282004,6,0,P|421:207|416:128,1,85.0000016212464 -312,56,282226,2,0,P|306:96|312:176,1,85.0000016212464,8|0,0:2|0:0,0:0:0:0: -200,256,282449,5,0,0:0:0:0: -185,163,282560,1,0,0:0:0:0: -92,148,282671,1,8,0:2:0:0: -51,231,282782,1,0,0:0:0:0: -116,298,282893,6,0,L|232:304,1,85.0000016212464 -448,264,283115,2,0,P|368:232|248:304,1,170.000003242493,10|0,0:2|0:0,0:0:0:0: -368,328,283449,2,0,L|392:96,1,170.000003242493,2|0,0:2|0:0,0:0:0:0: -288,32,283782,6,0,L|304:160,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -185,163,284004,2,0,L|128:80,1,85.0000016212464,10|0,0:2|0:0,0:0:0:0: -51,231,284226,2,0,L|160:264,1,85.0000016212464,2|0,0:2|0:0,0:0:0:0: -360,336,284449,6,0,L|424:352,1,42.5000008106232,10|0,3:2|3:0,0:0:0:0: -384,304,284560,2,0,L|440:320,1,42.5000008106232,0|4,3:0|3:2,0:0:0:0: -408,272,284671,2,0,L|472:288,1,42.5000008106232,2|0,3:2|3:0,0:0:0:0: -432,240,284782,2,0,L|496:256,1,42.5000008106232,0|0,3:0|3:0,0:0:0:0: -376,160,284893,6,0,L|312:176,1,42.5000008106232,10|0,3:2|3:0,0:0:0:0: -352,128,285004,2,0,L|296:144,1,42.5000008106232,0|0,3:0|3:0,0:0:0:0: -328,96,285115,2,0,L|264:112,1,42.5000008106232,2|0,3:2|3:0,0:0:0:0: -304,64,285226,2,0,L|240:80,1,42.5000008106232,0|0,3:0|3:0,0:0:0:0: -160,64,285337,6,0,L|160:8,1,42.5000008106232,8|0,3:2|3:0,0:0:0:0: -112,88,285449,2,0,L|72:48,1,42.5000008106232,0|0,3:0|3:0,0:0:0:0: -84,136,285560,2,2,L|28:136,1,42.5000008106232,4|0,3:2|2:0,0:1:0:0: -84,192,285671,2,0,L|44:231,1,42.5000008106232,0|0,2:0|2:0,2:0:0:0: -176,292,285782,6,0,L|176:348,1,42.5000008106232,8|0,2:2|2:0,2:0:0:0: -216,264,285893,2,0,L|255:303,1,42.5000008106232,0|0,2:0|2:0,2:0:0:0: -240,224,286004,2,2,L|296:224,1,42.5000008106232,2|0,2:2|2:0,2:1:0:0: -252,176,286115,2,0,L|291:136,1,42.5000008106232,0|0,2:0|2:0,2:0:0:0: -164,104,286226,5,8,0:2:0:0: -84,136,286337,1,8,2:0:0:0: -52,216,286449,1,8,2:0:0:0: -216,264,286671,1,8,0:0:0:0: -248,184,286782,1,8,0:0:0:0: -328,152,286893,2,0,L|328:104,2,42.5000008106232,8|0|0,3:2|3:0|3:0,0:0:0:0: -400,184,287115,1,8,0:0:0:0: -440,264,287226,5,0,0:0:0:0: -440,280,287449,1,4,0:3:0:0: -440,296,287671,1,0,0:0:0:0: -160,176,288004,6,0,L|337:155,1,150.000005722046,4|2,0:0|0:0,0:0:0:0: -335,290,288337,1,0,0:0:0:0: -335,290,288449,2,0,L|260:281,1,75.0000028610231,8|0,3:2|3:0,0:0:0:0: -136,148,288671,2,0,L|115:231,1,75.0000028610231,2|0,3:0|0:0,0:0:0:0: -254,138,288893,5,10,0:0:0:0: -223,70,289004,1,0,0:0:0:0: -160,33,289115,1,0,3:0:0:0: -86,39,289226,2,0,P|53:60|30:98,1,75.0000028610231,0|8,3:0|0:0,0:0:0:0: -117,220,289449,1,0,0:0:0:0: -117,220,289560,2,0,L|128:309,2,75.0000028610231,2|0|8,3:0|3:0|3:2,0:0:0:0: -228,182,289893,5,0,3:0:0:0: -268,192,290004,1,2,3:0:0:0: -300,161,290115,1,0,0:0:0:0: -341,171,290226,2,0,L|419:152,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: -243,96,290449,2,0,L|321:77,1,75.0000028610231,2|0,3:2|0:0,0:0:0:0: -140,30,290671,6,0,L|56:51,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: -223,123,290893,1,0,3:0:0:0: -223,123,291004,2,0,L|139:144,1,75.0000028610231,2|8,0:2|3:2,0:0:0:0: -306,216,291226,1,0,3:0:0:0: -233,234,291337,2,0,L|149:255,1,75.0000028610231,2|2,3:2|3:2,0:0:0:0: -60,82,291560,5,8,0:2:0:0: -114,95,291671,1,0,3:0:0:0: -169,109,291782,1,2,3:0:0:0: -223,123,291893,1,0,0:0:0:0: -277,108,292004,2,0,P|311:94|348:92,1,75.0000028610231,8|0,0:2|0:0,0:0:0:0: -479,209,292226,2,0,P|495:244|499:299,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: -285,299,292449,6,0,L|279:377,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: -407,190,292671,1,0,3:0:0:0: -407,190,292782,2,0,L|413:268,1,75.0000028610231,2|8,0:2|3:2,0:0:0:0: -253,128,293004,1,0,3:0:0:0: -248,203,293115,2,0,B|246:234|246:234|273:164|352:157,1,150.000005722046,2|8,3:2|0:2,0:0:0:0: -482,90,293449,5,0,0:0:0:0: -487,164,293560,2,0,L|406:144,1,75.0000028610231,2|0,0:3|0:0,0:0:0:0: -248,203,293782,1,8,3:2:0:0: -196,180,293893,1,0,3:0:0:0: -140,180,294004,1,0,3:0:0:0: -89,202,294115,1,0,3:0:0:0: -51,243,294226,6,0,B|12:200|46:133|46:133|43:157|54:173,1,150.000005722046,10|2,0:2|1:2,0:0:0:0: -92,319,294560,2,0,P|129:312|166:310,1,75.0000028610231,4|8,0:3|0:0,0:0:0:0: -317,351,294782,1,2,0:0:0:0: -399,338,294893,2,0,P|434:311|453:285,1,75.0000028610231,2|0,1:2|0:0,0:0:0:0: -281,104,295115,5,8,0:0:0:0: -247,147,295226,1,0,3:0:0:0: -248,203,295337,2,0,P|290:210|334:204,1,75.0000028610231,2|0,3:0|0:0,0:0:0:0: -281,104,295560,6,0,P|199:131|256:202,1,200,2|2,1:0|0:0,0:0:0:0: -301,78,295893,6,0,L|199:61,1,75.0000028610231,0|8,0:0|0:0,0:0:0:0: -322,206,296115,1,0,3:0:0:0: -378,203,296226,1,2,3:0:0:0: -434,200,296337,1,0,3:0:0:0: -490,197,296449,2,0,P|492:159|482:124,1,75.0000028610231,8|0,0:0|0:0,0:0:0:0: -384,359,296671,2,0,P|418:345|446:320,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: -295,164,296893,5,8,3:2:0:0: -242,144,297004,1,0,3:2:0:0: -187,149,297115,1,0,3:2:0:0: -140,179,297226,1,0,0:0:0:0: -112,227,297337,2,0,L|104:142,1,75.0000028610231,8|0,0:0|0:0,0:0:0:0: -0,63,297560,2,0,P|34:76|71:80,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: -259,18,297782,6,0,P|221:22|187:35,2,75.0000028610231,10|0|0,0:0|3:0|0:0,0:0:0:0: -383,73,298115,2,0,P|404:103|418:138,2,75.0000028610231,2|8|0,0:0|3:2|0:0,0:0:0:0: -242,144,298449,2,0,L|248:238,1,75.0000028610231,4|2,0:1|0:0,0:0:0:0: -409,298,298671,6,0,P|336:294|258:343,1,150.000005722046,12|0,0:0|3:0,0:0:0:0: -47,287,299115,1,10,0:0:0:0: -11,243,299226,1,0,3:0:0:0: -0,189,299337,1,2,3:0:0:0: -13,135,299449,1,0,0:0:0:0: -49,92,299560,6,0,B|80:80|80:80|136:91,1,75.0000028610231,10|0,0:0|3:0,0:0:0:0: -244,186,299782,1,0,3:0:0:0: -244,186,299893,2,0,B|213:198|213:198|172:189,1,75.0000028610231,2|8,0:0|0:0,0:0:0:0: -363,223,300115,1,0,0:0:0:0: -363,223,300226,2,0,L|357:126,1,75.0000028610231,10|0,1:2|0:0,0:0:0:0: -449,56,300449,6,0,L|455:13,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -403,162,300671,2,0,L|406:208,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -340,71,300893,2,0,L|296:67,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -254,153,301115,2,0,L|210:157,3,37.5000014305115,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -120,88,301337,6,0,L|88:62,1,37.5000014305115,10|0,0:0|0:0,0:0:0:0: -66,100,301449,2,0,L|21:88,1,37.5000014305115,8|0,0:0|0:0,0:0:0:0: -30,141,301560,2,0,L|-10:146,1,37.5000014305115,10|0,0:0|0:0,0:0:0:0: -23,196,301671,2,0,L|-21:218,1,37.5000014305115,10|0,0:0|0:0,0:0:0:0: -50,245,301782,1,8,0:0:0:0: -100,226,301893,1,10,0:0:0:0: -146,250,302004,1,10,0:0:0:0: -159,300,302115,1,8,0:0:0:0: -136,344,302226,6,0,L|112:152,1,150.000005722046,12|0,0:0|0:0,0:0:0:0: -192,80,302560,1,0,0:0:0:0: -192,80,302671,2,0,P|248:136|208:232,1,150.000005722046,8|2,3:2|3:0,0:0:0:0: -146,250,303004,5,0,0:0:0:0: -72,304,303115,2,0,P|56:264|56:224,1,75.0000028610231,10|0,0:0|0:0,0:0:0:0: -106,129,303337,6,0,P|158:179|146:250,1,150.000005722046,0|8,3:0|0:0,0:0:0:0: -72,152,303671,1,0,0:0:0:0: -72,152,303782,2,0,L|64:48,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: -168,0,304004,1,8,3:2:0:0: -168,0,304115,2,0,L|176:104,1,75.0000028610231,0|2,3:0|3:0,0:0:0:0: -224,184,304337,5,0,0:0:0:0: -159,247,304449,2,0,P|228:273|310:179,1,150.000005722046,10|2,3:2|3:2,0:0:0:0: -428,142,304782,1,0,0:0:0:0: -428,142,304893,2,0,L|336:147,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: -263,73,305115,5,0,3:0:0:0: -213,56,305226,1,4,0:3:0:0: -161,64,305337,1,8,3:2:0:0: -122,99,305449,1,0,3:0:0:0: -104,148,305560,2,0,L|122:225,1,75.0000028610231,2|2,3:2|3:2,0:0:0:0: -166,316,305782,6,0,B|96:318|90:334|90:334|65:342|12:324,1,150.000005722046,8|2,0:0|3:0,0:0:0:0: -309,200,306115,1,0,0:0:0:0: -309,200,306226,2,0,P|336:253|307:351,1,150.000005722046,8|2,0:0|3:0,0:0:0:0: -243,278,306560,5,0,3:0:0:0: -309,200,306671,2,0,L|391:196,1,75.0000028610231,10|0,3:2|3:0,0:0:0:0: -500,99,306893,5,0,3:0:0:0: -447,92,307004,1,2,0:2:0:0: -397,75,307115,1,8,3:2:0:0: -344,69,307226,1,0,3:0:0:0: -296,47,307337,2,0,L|207:35,1,75.0000028610231,2|0,3:2|0:0,0:0:0:0: -108,8,307560,6,0,B|110:50|110:50|124:100|124:100|105:170,1,150.000005722046,8|2,0:0|3:0,0:0:0:0: -78,336,307893,1,0,0:0:0:0: -78,336,308004,2,0,P|31:298|55:174,1,150.000005722046,8|0,3:2|3:0,0:0:0:0: -194,174,308337,1,0,3:0:0:0: -194,174,308449,2,0,L|186:258,1,75.0000028610231,10|0,0:2|0:0,0:0:0:0: -288,113,308671,5,2,1:2:0:0: -323,74,308782,1,2,0:0:0:0: -373,59,308893,1,8,0:0:0:0: -424,70,309004,1,2,0:0:0:0: -476,67,309115,5,2,1:2:0:0: -462,117,309226,1,0,0:0:0:0: -448,167,309337,1,8,0:0:0:0: -434,217,309449,1,0,3:0:0:0: -460,262,309560,5,2,3:0:0:0: -408,253,309671,1,0,0:0:0:0: -359,274,309782,2,0,P|343:319|405:383,1,75.0000028610231,2|0,1:0|0:0,0:0:0:0: -305,191,310004,1,2,0:0:0:0: -305,191,310115,2,0,L|329:18,1,150.000005722046,0|0,0:0|3:0,0:0:0:0: -216,296,310449,5,2,3:0:0:0: -165,310,310560,1,0,3:0:0:0: -118,290,310671,2,0,P|80:264|128:141,1,150.000005722046,8|2,0:0|3:0,0:0:0:0: -230,113,311004,5,0,0:0:0:0: -230,113,311115,2,0,L|-10:130,1,225.000008583069,8|0,3:2|0:0,0:0:0:0: -95,62,311560,6,0,P|132:80|151:116,1,75.0000028610231,8|0,0:0|0:0,0:0:0:0: -169,270,311782,1,2,3:0:0:0: -169,270,311893,2,0,P|248:233|338:273,1,150.000005722046,0|0,3:0|3:0,0:0:0:0: -405,327,312226,6,0,L|489:330,1,75.0000028610231,0|4,3:0|0:3,0:0:0:0: -200,285,312449,2,0,P|129:302|52:269,1,150.000005722046,8|4,0:0|0:1,0:0:0:0: -13,105,312782,5,0,0:0:0:0: -95,62,312893,2,0,L|328:80,1,225.000008583069,12|2,0:0|0:0,0:0:0:0: -488,272,313337,6,0,P|480:208|432:168,1,75.0000028610231,8|0,0:0|1:0,0:0:0:0: -360,168,313560,2,0,P|368:232|416:272,1,75.0000028610231,0|0,3:2|1:0,0:0:0:0: -464,312,313782,6,0,L|440:176,1,75.0000028610231,8|0,3:2|3:0,0:0:0:0: -320,144,314004,2,0,L|344:280,1,75.0000028610231,2|0,3:0|3:0,0:0:0:0: -288,352,314226,5,8,0:0:0:0: -248,320,314337,1,0,0:0:0:0: -208,304,314449,2,0,L|144:304,1,37.5000014305115,0|0,3:0|3:0,0:0:0:0: -168,304,314560,2,0,L|112:312,1,37.5000014305115,0|0,3:0|3:0,0:0:0:0: -112,320,314671,5,8,3:2:0:0: -64,312,314782,1,0,3:0:0:0: -32,272,314893,1,8,3:2:0:0: -40,216,315004,1,0,0:0:0:0: -72,176,315115,5,8,0:0:0:0: -120,160,315226,1,0,0:0:0:0: -168,144,315337,2,0,L|216:128,1,37.5000014305115,8|8,0:0|0:0,0:0:0:0: -203,132,315449,2,0,L|264:112,1,37.5000014305115,8|8,0:0|0:0,0:0:0:0: -264,136,315560,5,8,0:0:0:0: -296,96,315671,1,8,0:0:0:0: -288,48,315782,1,8,0:0:0:0: -256,8,315893,5,8,0:0:0:0: -256,352,316115,1,4,0:1:0:0: -256,8,316337,1,8,0:0:0:0: -219,91,316449,6,0,P|270:95|322:71,1,100,12|0,0:0|0:0,0:0:0:0: -434,29,316893,1,8,0:0:0:0: -437,66,317004,1,0,0:0:0:0: -440,103,317115,1,0,0:0:0:0: -371,215,317337,2,0,L|321:205,3,50,10|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -298,247,317782,2,0,L|248:256,3,50,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -216,221,318226,5,8,0:0:0:0: -178,217,318337,1,0,0:0:0:0: -143,229,318449,1,2,0:0:0:0: -23,142,318671,1,8,0:0:0:0: -161,78,318893,1,2,0:0:0:0: -56,224,319115,1,10,0:0:0:0: -36,255,319226,1,0,0:0:0:0: -31,292,319337,1,0,0:0:0:0: -40,328,319449,1,4,0:3:0:0: -62,358,319560,2,0,L|124:345,1,50,8|0,0:0|0:0,0:0:0:0: -249,272,319782,6,0,L|373:292,1,100,0|8,0:0|0:0,0:0:0:0: -219,363,320226,1,2,0:0:0:0: -260,226,320449,1,8,0:0:0:0: -380,331,320671,1,2,0:0:0:0: -462,168,320893,1,10,0:0:0:0: -420,173,321004,1,0,0:0:0:0: -379,178,321115,2,0,L|320:193,1,50,2|0,0:0|0:0,0:0:0:0: -200,259,321337,2,0,L|259:274,1,50,10|0,0:0|0:0,0:0:0:0: -380,331,321560,6,0,P|395:283|381:223,1,100,2|8,0:0|0:0,0:0:0:0: -338,49,322004,1,0,0:0:0:0: -330,190,322226,1,8,0:0:0:0: -443,275,322449,1,0,0:0:0:0: -426,118,322671,1,8,0:0:0:0: -394,97,322782,1,0,0:0:0:0: -360,83,322893,1,0,0:0:0:0: -323,76,323004,1,4,0:3:0:0: -285,78,323115,2,0,P|261:78|223:92,1,50,8|0,0:0|0:0,0:0:0:0: -165,223,323337,2,0,P|181:204|196:166,1,50 -65,126,323560,5,8,3:2:0:0: -43,156,323671,1,0,3:0:0:0: -30,191,323782,1,2,3:0:0:0: -27,229,323893,1,0,0:0:0:0: -34,265,324004,1,8,3:2:0:0: -69,278,324115,1,0,3:0:0:0: -106,283,324226,1,2,3:0:0:0: -144,280,324337,1,0,0:0:0:0: -179,269,324449,5,8,0:0:0:0: -205,294,324560,1,0,0:0:0:0: -205,331,324671,1,0,3:0:0:0: -179,356,324782,1,2,3:0:0:0: -143,356,324893,1,10,0:0:0:0: -120,319,325004,1,0,3:0:0:0: -111,276,325115,1,2,3:0:0:0: -118,233,325226,1,0,0:0:0:0: -139,195,325337,5,8,3:2:0:0: -168,203,325449,1,0,3:0:0:0: -199,204,325560,1,2,3:0:0:0: -230,196,325671,1,0,0:0:0:0: -256,180,325782,1,8,3:2:0:0: -282,164,325893,1,0,3:0:0:0: -313,156,326004,1,2,3:0:0:0: -344,157,326115,1,0,0:0:0:0: -374,166,326226,5,10,3:2:0:0: -334,137,326337,1,0,3:2:0:0: -318,90,326449,1,0,3:2:0:0: -331,43,326560,1,6,3:3:0:0: -370,12,326671,1,8,0:0:0:0: -419,9,326782,1,2,3:0:0:0: -461,35,326893,1,0,3:0:0:0: -480,80,327004,1,0,3:0:0:0: -470,128,327115,5,12,0:0:0:0: -448,166,327226,1,0,3:0:0:0: -444,209,327337,1,0,3:0:0:0: -458,250,327449,1,0,0:0:0:0: -487,282,327560,1,10,3:0:0:0: -475,241,327671,1,0,3:0:0:0: -442,213,327782,1,2,3:0:0:0: -400,208,327893,1,0,0:0:0:0: -361,227,328004,5,8,3:2:0:0: -336,258,328115,1,0,3:0:0:0: -275,214,328226,1,0,3:0:0:0: -261,246,328337,1,2,3:0:0:0: -192,227,328449,1,8,0:0:0:0: -186,256,328560,1,0,3:0:0:0: -114,258,328671,1,2,3:0:0:0: -113,282,328782,1,0,0:0:0:0: -45,304,328893,6,0,L|17:322,3,25,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -25,203,329115,2,0,L|-7:207,3,25,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -69,115,329337,2,0,L|37:105,3,25,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -154,70,329560,2,0,L|129:47,3,25,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -300,116,329782,6,0,L|336:103,1,25,8|0,3:2|3:0,0:0:0:0: -282,168,329893,2,0,L|305:198,1,25,8|0,3:2|0:0,0:0:0:0: -228,168,330004,2,0,L|206:199,1,25,8|0,3:2|3:2,0:0:0:0: -211,115,330115,2,0,L|174:104,1,25,12|0,3:0|3:0,0:0:0:0: -256,83,330226,5,8,0:0:0:0: -256,133,330337,1,8,0:0:0:0: -256,183,330449,1,8,0:0:0:0: -256,233,330560,1,4,0:0:0:0: From e303c4b4e6232bb9424c7a319602414fe86065cd Mon Sep 17 00:00:00 2001 From: myssto Date: Tue, 4 Aug 2026 00:12:59 -0500 Subject: [PATCH 25/27] feat: update osu performance test --- tests/performance.rs | 80 ++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/tests/performance.rs b/tests/performance.rs index 7a5bca18..7d8f122b 100644 --- a/tests/performance.rs +++ b/tests/performance.rs @@ -4,7 +4,7 @@ use rosu_pp::{ Beatmap, catch::{CatchPerformance, CatchPerformanceAttributes}, mania::{ManiaPerformance, ManiaPerformanceAttributes}, - osu::OsuPerformanceAttributes, + osu::{OsuPerformance, OsuPerformanceAttributes}, taiko::{TaikoPerformance, TaikoPerformanceAttributes}, }; @@ -34,6 +34,7 @@ macro_rules! test_cases { pp_aim: $pp_aim:expr, pp_flashlight: $pp_flashlight:expr, pp_speed: $pp_speed:expr, + pp_reading: $pp_reading:expr, effective_miss_count: $effective_miss_count:expr, speed_deviation: $speed_deviation:expr, combo_based_estimated_miss_count: $combo_based_estimated_miss_count:expr, @@ -49,6 +50,7 @@ macro_rules! test_cases { pp_aim: $pp_aim, pp_flashlight: $pp_flashlight, pp_speed: $pp_speed, + pp_reading: $pp_reading, effective_miss_count: $effective_miss_count, speed_deviation: $speed_deviation, combo_based_estimated_miss_count: $combo_based_estimated_miss_count, @@ -107,95 +109,101 @@ macro_rules! test_cases { #[test] fn basic_osu() { - #[cfg(target_os = "windows")] test_cases! { Osu: OSU { NM => { - pp: 287.9051448920619, + pp: 316.5901855625614, pp_acc: 98.99847982709288, - pp_aim: 113.66811014707582, + pp_aim: 148.75278891878943, pp_flashlight: 0.0, - pp_speed: 65.7316947411581, + pp_speed: 61.34653468094172, + pp_reading: 2.2291238201795176, effective_miss_count: 0.0, - speed_deviation: Some(11.559405011202584), + speed_deviation: Some(11.70045116819282), combo_based_estimated_miss_count: 0.0, score_based_estimated_miss_count: None, aim_estimated_slider_breaks: 0.0, speed_estimated_slider_breaks: 0.0, }; HD => { - pp: 315.8674097332546, - pp_acc: 106.91835821326032, - pp_aim: 125.5489356876975, + pp: 349.9881115302272, + pp_acc: 98.99847982709288, + pp_aim: 148.75278891878943, pp_flashlight: 0.0, - pp_speed: 72.9912208672784, + pp_speed: 61.34653468094172, + pp_reading: 41.59660781351789, effective_miss_count: 0.0, - speed_deviation: Some(11.559405011202584), + speed_deviation: Some(11.70045116819282), combo_based_estimated_miss_count: 0.0, score_based_estimated_miss_count: None, aim_estimated_slider_breaks: 0.0, speed_estimated_slider_breaks: 0.0, }; EZ HD => { - pp: 200.88128466771315, - pp_acc: 17.33989029835826, - pp_aim: 109.17177789930311, + pp: 330.5430804957736, + pp_acc: 16.05545397996135, + pp_aim: 88.9845069859366, pp_flashlight: 0.0, - pp_speed: 64.55964097206972, + pp_speed: 40.67344998245687, + pp_reading: 179.54117243675987, effective_miss_count: 0.0, - speed_deviation: Some(22.768253044002595), + speed_deviation: Some(23.04067406810845), combo_based_estimated_miss_count: 0.0, score_based_estimated_miss_count: None, aim_estimated_slider_breaks: 0.0, speed_estimated_slider_breaks: 0.0, }; HR => { - pp: 422.8822464661912, + pp: 468.21934604774174, pp_acc: 161.55575439788055, - pp_aim: 167.50210608714042, + pp_aim: 231.45791599856506, pp_flashlight: 0.0, - pp_speed: 78.89335639563441, + pp_speed: 61.86844909389746, + pp_reading: 3.0588804345706087, effective_miss_count: 0.0, - speed_deviation: Some(8.598712200750178), + speed_deviation: Some(8.609766678538842), combo_based_estimated_miss_count: 0.0, score_based_estimated_miss_count: None, aim_estimated_slider_breaks: 0.0, speed_estimated_slider_breaks: 0.0, }; DT => { - pp: 784.2400469306212, + pp: 861.1999380363726, pp_acc: 183.66566616694254, - pp_aim: 348.7917741691343, + pp_aim: 436.40604835642193, pp_flashlight: 0.0, - pp_speed: 224.8868678368528, + pp_speed: 198.35289926936036, + pp_reading: 33.10777055891541, effective_miss_count: 0.0, - speed_deviation: Some(7.6754769185728815), + speed_deviation: Some(7.66444640194172), combo_based_estimated_miss_count: 0.0, score_based_estimated_miss_count: None, aim_estimated_slider_breaks: 0.0, speed_estimated_slider_breaks: 0.0, }; FL => { - pp: 415.9768919360004, - pp_acc: 100.97844942363474, - pp_aim: 113.66811014707582, - pp_flashlight: 132.3188848707867, - pp_speed: 65.7316947411581, + pp: 444.5824653333625, + pp_acc: 98.99847982709288, + pp_aim: 148.75278891878943, + pp_flashlight: 137.54700871774983, + pp_speed: 61.34653468094172, + pp_reading: 2.2291238201795176, effective_miss_count: 0.0, - speed_deviation: Some(11.559405011202584), + speed_deviation: Some(11.70045116819282), combo_based_estimated_miss_count: 0.0, score_based_estimated_miss_count: None, aim_estimated_slider_breaks: 0.0, speed_estimated_slider_breaks: 0.0, }; HD FL => { - pp: 483.7752666636294, - pp_acc: 109.05672537752552, - pp_aim: 125.5489356876975, - pp_flashlight: 171.65397211175005, - pp_speed: 72.9912208672784, + pp: 512.2069389717595, + pp_acc: 98.99847982709288, + pp_aim: 148.75278891878943, + pp_flashlight: 172.5399803247157, + pp_speed: 61.34653468094172, + pp_reading: 41.59660781351789, effective_miss_count: 0.0, - speed_deviation: Some(11.559405011202584), + speed_deviation: Some(11.70045116819282), combo_based_estimated_miss_count: 0.0, score_based_estimated_miss_count: None, aim_estimated_slider_breaks: 0.0, From b5140c6575fbb13f90eb842f13a5a4695f54b838 Mon Sep 17 00:00:00 2001 From: myssto Date: Tue, 4 Aug 2026 02:38:26 -0500 Subject: [PATCH 26/27] feat: impl some missed changes --- src/osu/legacy_score_miss_calc.rs | 12 ++++++++++-- src/osu/performance/calculator.rs | 18 +++++++++--------- src/osu/utils/legacy_score.rs | 15 ++++----------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/osu/legacy_score_miss_calc.rs b/src/osu/legacy_score_miss_calc.rs index ae681791..22e3dbc9 100644 --- a/src/osu/legacy_score_miss_calc.rs +++ b/src/osu/legacy_score_miss_calc.rs @@ -137,9 +137,17 @@ impl<'a> OsuLegacyScoreMissCalculator<'a> { let mut miss_count = 0.0; + // * If sliders in the map are hard - it's likely for player to drop sliderends + // * If map has easy sliders - it's more likely for player to sliderbreak + let likely_missed_sliderend_portion = + 0.04 + 0.06 * self.attrs.aim_top_weighted_slider_factor.min(1.0).powf(2.0); + // * Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it - // * In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map - let full_combo_threshold = f64::from(attrs.max_combo) - 0.1 * f64::from(attrs.n_sliders); + // * In classic scores we can't know the amount of dropped sliders so we estimate it + let full_combo_threshold = f64::from(attrs.max_combo) + - (4.0 + + likely_missed_sliderend_portion + * f64::from(attrs.n_sliders).min(f64::from(attrs.n_sliders))); if f64::from(state.max_combo) < full_combo_threshold { miss_count = (full_combo_threshold / f64::from(state.max_combo).max(1.0)).powf(2.5); diff --git a/src/osu/performance/calculator.rs b/src/osu/performance/calculator.rs index 9aca530d..2819a9ed 100644 --- a/src/osu/performance/calculator.rs +++ b/src/osu/performance/calculator.rs @@ -67,16 +67,16 @@ impl OsuPerformanceCalculator<'_> { let combo_based_estimated_miss_count = self.calculate_combo_based_estimated_miss_count(); let mut score_based_estimated_miss_count = None; - let mut effective_miss_count = if using_classic_slider_acc - && state.legacy_total_score.is_some() - { - let legacy_score_miss_calc = OsuLegacyScoreMissCalculator::new(state, acc, mods, attrs); + let mut effective_miss_count = + if using_classic_slider_acc && !self.mods.sv2() && state.legacy_total_score.is_some() { + let legacy_score_miss_calc = + OsuLegacyScoreMissCalculator::new(state, acc, mods, attrs); - *score_based_estimated_miss_count.insert(legacy_score_miss_calc.calculate()) - } else { - // * Use combo-based miss count if this isn't a legacy score - combo_based_estimated_miss_count - }; + *score_based_estimated_miss_count.insert(legacy_score_miss_calc.calculate()) + } else { + // * Use combo-based miss count if this isn't a legacy score + combo_based_estimated_miss_count + }; effective_miss_count = effective_miss_count.max(f64::from(state.hitresults.misses)); effective_miss_count = effective_miss_count.min(f64::from(state.hitresults.total_hits())); diff --git a/src/osu/utils/legacy_score.rs b/src/osu/utils/legacy_score.rs index c98c42ba..94e4bbce 100644 --- a/src/osu/utils/legacy_score.rs +++ b/src/osu/utils/legacy_score.rs @@ -61,8 +61,7 @@ fn calculate_spinner_score(spinner: Spinner) -> f64 { #[derive(Default)] struct InnerNestedScorePerObject { - n_sliders: usize, - n_repeats: usize, + amount_of_big_ticks: usize, amount_of_small_ticks: usize, spinner_score: f64, object_count: usize, @@ -75,8 +74,8 @@ impl InnerNestedScorePerObject { match h.kind { OsuObjectKind::Circle => {} OsuObjectKind::Slider(ref slider) => { - self.n_sliders += 1; - self.n_repeats += slider.repeat_count(); + // * 1 for head, 1 for tail, plus repeats + self.amount_of_big_ticks += 2 + slider.repeat_count(); self.amount_of_small_ticks += slider.tick_count(); } OsuObjectKind::Spinner(spinner) => { @@ -89,13 +88,7 @@ impl InnerNestedScorePerObject { const BIG_TICK_SCORE: f64 = 30.0; const SMALL_TICK_SCORE: f64 = 10.0; - // * 1 for head, 1 for tail - let mut amount_of_big_ticks = self.n_sliders * 2; - - // * Add slider repeats - amount_of_big_ticks += self.n_repeats; - - let slider_score = amount_of_big_ticks as f64 * BIG_TICK_SCORE + let slider_score = self.amount_of_big_ticks as f64 * BIG_TICK_SCORE + self.amount_of_small_ticks as f64 * SMALL_TICK_SCORE; (slider_score + self.spinner_score) / self.object_count as f64 From 7514e5b94c3c2e0ca9e6510ebd24ceda61ea30cd Mon Sep 17 00:00:00 2001 From: myssto Date: Tue, 4 Aug 2026 02:45:31 -0500 Subject: [PATCH 27/27] chore: remove unused mock cs function --- src/util/traits.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/util/traits.rs b/src/util/traits.rs index 9da27173..4b4ecfea 100644 --- a/src/util/traits.rs +++ b/src/util/traits.rs @@ -1,13 +1,7 @@ -use std::iter::Sum; - /// Mimics the C# `IEnumerable` interface. pub trait IEnumerable: Sized { fn cs_where bool>(self, f: F) -> Self; - fn cs_sum(&self) -> S - where - S: for<'a> Sum<&'a T>; - fn cs_add_in_place(&mut self, item: T) -> usize where T: Ord; @@ -35,16 +29,6 @@ impl IEnumerable for Vec { self } - /// Computes the sum of a sequence of numeric values. - /// - /// - fn cs_sum(&self) -> S - where - S: for<'a> Sum<&'a T>, - { - self.iter().sum() - } - /// Adds the given item to the list according to standard sorting rules. Do not use on unsorted lists. /// ///