From bf405fb9b009e854a51409f17dc1404be09f0b5c Mon Sep 17 00:00:00 2001 From: StanR <8269193+stanriders@users.noreply.github.com> Date: Mon, 4 May 2026 23:29:33 +0300 Subject: [PATCH] Another batch of small rhythm evaluation fixes (#37609) ## [Reduce rhythm effective ratio for patterns that are speeding up](https://github.com/ppy/osu/commit/56f66abf82b687d88e6fe83928d6dfa0f657b0a0) Assuming the same ratio difficulty, speeding up rhythms are a bit easier to play than slowing down ones ## [Reduce rhythm complexity sum if the final island is long](https://github.com/ppy/osu/commit/912d81cbfedee286b634c4bcb9c4e286d91a3d88) Currently due to how the rhythm complexity sum works rhythm difficulty gets applied to most of the object's island - historical decay isn't enough to counter difficulty carryover so we end up having non-zero rhythm difficulty on long consistent rhythm patterns (for example if a stream starts with an unusual ratio it will have non-zero rhythm difficulty regardless of its length, even if its hundreds of objects long). This applies a global difficulty reduction depending on the current object's island length. https://www.desmos.com/calculator/kvnpasbpt2 ## [Fix islands being initialised incorrectly if the rhythm section is longer than the historical cutoff](https://github.com/ppy/osu/commit/ddf0fe758b691ca6e2a157fe335a41c4048e7e9b) Due to the change above we now have to properly initialise islands at the start of the rhythm loop - currently if the object we're evaluating is a part of a >32 object consistent rhythm pattern (say a stream) it never actually gets its island properly initialised. It doesn't matter on the current system, but it does matter if we want to reduce complexity sum using island length as uninitialised island always has length of 1 so patterns that are >32 objects long stop getting their complexity reduced after the 32th object --------- Co-authored-by: James Wilson --- .../Evaluators/Speed/RhythmEvaluator.cs | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/Speed/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/Speed/RhythmEvaluator.cs index e2ee41162b..4d40289c40 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/Speed/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/Speed/RhythmEvaluator.cs @@ -16,8 +16,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed { private const int history_time_max = 5 * 1000; // 5 seconds private const int history_objects_max = 32; - private const double rhythm_overall_multiplier = 0.8; - private const double rhythm_ratio_multiplier = 32.0; + private const double rhythm_overall_multiplier = 0.95; + private const double rhythm_ratio_multiplier = 26.0; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -70,6 +70,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed double prevDelta = Math.Max(prevObj.DeltaTime, 1e-7); double lastDelta = Math.Max(lastObj.DeltaTime, 1e-7); + // 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 == int.MaxValue) + island = new Island((int)currDelta, deltaDifferenceEpsilon); + // 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) double deltaDifference = Math.Max(prevDelta, currDelta) / Math.Min(prevDelta, currDelta); @@ -96,14 +100,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed effectiveRatio = Math.Min(sliderEffectiveRatio, effectiveRatio); } + bool isSpeedingUp = prevDelta > currDelta + deltaDifferenceEpsilon; + + if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon) + { + // island is still progressing + island.AddDelta((int)currDelta); + } + if (firstDeltaSwitch) { - if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon) - { - // island is still progressing - island.AddDelta((int)currDelta); - } - else + if (Math.Abs(prevDelta - currDelta) > deltaDifferenceEpsilon) { // bpm change is into slider, this is easy acc window if (currObj.BaseObject is Slider) @@ -122,6 +129,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed if (previousIsland.DeltaCount == island.DeltaCount) effectiveRatio *= 0.5; + if (isSpeedingUp) + effectiveRatio *= 0.65; + var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island)); if (islandCount != default) @@ -140,7 +150,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed } else { - islandCounts.Add((island, 1)); + if (island.DeltaCount > 0) + { + islandCounts.Add((island, 1)); + } } // scale down the difficulty if the object is doubletappable @@ -182,6 +195,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed prevObj = currObj; } + // If the current island is long we don't want the sum to have as big of an effect + rhythmComplexitySum *= DifficultyCalculationUtils.ReverseLerp(island.DeltaCount, 22, 3); + return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though); }