-
Notifications
You must be signed in to change notification settings - Fork 67
osu!standard parity for 2026q2 pp update #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
myssto
wants to merge
27
commits into
MaxOhn:pp-update
Choose a base branch
from
myssto:pp-update-2026q2
base: pp-update
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
3a59720
fix: update OsuDifficultyObject calculations
myssto 6441acd
feat: impl osu SnapAimEvaluator
myssto 436c6c2
feat: impl osu AgilityEvaluator
myssto 0d876c2
feat: impl osu FlowAimEvaluator
myssto 7cb0bc0
feat: impl osu speed evaluator group
myssto 8823804
fix: update osu flashlight evaluator
myssto 857dac7
feat: impl osu ReadingEvaluator
myssto f49347a
refactor: wip refactor of skill traits
myssto 28109d0
fix: various errors and formatting
myssto e19c790
refactor: finish refactoring osu skills
myssto 1653b5a
feat: update osu difficulty attributes
myssto 01ae6ab
feat: update osu performance calculator
myssto 53849ed
fix: update OsuStrains with new skills
myssto ffd9966
chore: formatting and fixing warnings
myssto 0e65cec
fix: starting debug
myssto e6519a4
fix: osu aim skill value parity
myssto 21e649f
fix: osu speed skill value parity
myssto 67fb9a9
fix: osu reading skill value parity
myssto dc6cc43
fix: remove debug change
myssto 3218ed6
chore: osu nm test passing + cleanup
myssto 7768822
fix: properly use base and clockrate adjusted object values
myssto b681d37
fix: flashlight difficulty value formula
myssto b2c2c76
fix: hd+fl bugs
myssto b9c7bad
chore: remove extra beatmap file
myssto e303c4b
feat: update osu performance test
myssto b5140c6
feat: impl some missed changes
myssto 7514e5b
chore: remove unused mock cs function
myssto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| use crate::{ | ||
| any::difficulty::skills_new::skill::Skill, | ||
| util::traits::{IEnumerable, IOrderedEnumerable}, | ||
| }; | ||
|
|
||
| pub trait HarmonicSkill: 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) | ||
| } | ||
|
|
||
| #[expect(unused_mut, reason = "staying in-sync with lazer")] | ||
| fn get_transformed_difficulties(&self, mut difficulties: Vec<f64>) -> Vec<f64> { | ||
| difficulties | ||
| } | ||
|
|
||
| fn into_transformed_difficulties(self) -> Vec<f64>; | ||
|
|
||
| /// Returns `(difficulty_value, object_weight_sum)`. | ||
| fn difficulty_value(transformed_object_difficulties: Vec<f64>) -> (f64, f64); | ||
|
|
||
| #[expect(dead_code, reason = "staying in-sync with existing skills")] | ||
| fn into_difficulty_value(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, | ||
| object_weight_sum: 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], | ||
| 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 (difficulty, object_weight_sum); | ||
| } | ||
|
|
||
| // * 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() | ||
| { | ||
| // * 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)); | ||
|
|
||
| object_weight_sum += weight; | ||
|
|
||
| difficulty += obj * weight; | ||
| } | ||
|
|
||
| (difficulty, object_weight_sum) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| use crate::util::{difficulty::logistic, float_ext::FloatExt, hint::unlikely}; | ||
|
|
||
| pub mod harmonic_skill; | ||
| pub mod skill; | ||
| pub mod strain_decay_skill; | ||
| pub mod strain_skill; | ||
| pub mod variable_length_strain_skill; | ||
|
|
||
| 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>, | ||
| ) -> 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 FloatExt::eq(consistent_top_obj, 0.0) { | ||
| 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( | ||
| 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) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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]; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| 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; | ||
|
|
||
| #[expect(dead_code, reason = "used by process_internal")] | ||
| 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>, | ||
| ); | ||
|
|
||
| #[expect(dead_code, reason = "used by start_new_section_from")] | ||
| fn calculate_initial_strain<'a>( | ||
| &self, | ||
| time: f64, | ||
| curr: &Self::DifficultyObject<'a>, | ||
| objects: &Self::DifficultyObjects<'a>, | ||
| ) -> f64; | ||
|
|
||
| fn into_current_strain_peaks(self) -> Vec<f64>; | ||
|
|
||
| fn get_current_strain_peaks(mut strain_peaks: Vec<f64>, current_section_peak: f64) -> Vec<f64> { | ||
| strain_peaks.push(current_section_peak); | ||
|
|
||
| strain_peaks | ||
| } | ||
|
|
||
| fn difficulty_value(current_strain_peaks: Vec<f64>) -> f64; | ||
|
|
||
| fn into_difficulty_value(self) -> f64; | ||
|
|
||
| fn cloned_difficulty_value(&self) -> f64; | ||
| } | ||
|
|
||
| pub fn strain_skill_difficulty_value(current_strain_peaks: Vec<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); | ||
|
|
||
| // * 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 | ||
| } |
111 changes: 111 additions & 0 deletions
111
src/any/difficulty/skills_new/variable_length_strain_skill.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| use crate::{ | ||
| any::difficulty::skills_new::skill::Skill, | ||
| util::traits::{IEnumerable, IOrderedEnumerable}, | ||
| }; | ||
|
|
||
| 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; | ||
|
|
||
| #[expect(dead_code, reason = "used by process_internal")] | ||
| 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>, | ||
| ); | ||
|
|
||
| #[expect(dead_code, reason = "used by start_new_section_from")] | ||
| fn calculate_initial_strain<'a>( | ||
| &self, | ||
| time: f64, | ||
| curr: &Self::DifficultyObject<'a>, | ||
| objects: &Self::DifficultyObjects<'a>, | ||
| ) -> f64; | ||
|
|
||
| fn into_current_strain_peaks(self) -> Vec<StrainPeak>; | ||
|
|
||
| fn get_current_strain_peaks( | ||
| mut strain_peaks: Vec<StrainPeak>, | ||
| current_section_peak: f64, | ||
| current_section_begin: f64, | ||
| current_section_end: f64, | ||
| ) -> Vec<StrainPeak> { | ||
| 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); | ||
| } | ||
|
|
||
| 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_ties_even(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl PartialEq for StrainPeak { | ||
| fn eq(&self, other: &Self) -> bool { | ||
| self.value == other.value && self.section_length == other.section_length | ||
| } | ||
| } | ||
|
|
||
| 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<std::cmp::Ordering> { | ||
| Some(self.cmp(other)) | ||
| } | ||
| } | ||
|
|
||
| impl IOrderedEnumerable<StrainPeak> for Vec<StrainPeak> { | ||
| fn cs_order_descending(mut self) -> Self { | ||
| self.sort_by(StrainPeak::cmp); | ||
|
|
||
| self | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is implementing these in the trait not an option? Relying on the implementation inside the macros being noticed is kinda meh, and I almost left a comment asking where the code disappeared to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried my best to base these off of the existing skill traits where presumably the design philosophy was "if it needs to mutate stateful variables on the struct that are filled in by the macro, implement in the macro instead of the trait".