From 79a3afe06feffe9db9aa60760a1509b01bfee3ba Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 19 Dec 2024 01:16:27 +1000 Subject: [PATCH 01/86] Implement considerations for Relax within osu!taiko diffcalc (#30591) --- .../Difficulty/TaikoDifficultyCalculator.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7f2558c406..b3efb7f46d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; + bool isRelax = mods.Any(h => h is TaikoModRelax); + Colour colour = (Colour)skills.First(x => x is Colour); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); Stamina stamina = (Stamina)skills.First(x => x is Stamina); @@ -88,15 +90,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina); + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { starRating *= 0.925; - // For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused. - if (colourRating < 2 && staminaRating > 8) + + // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. + if (isRelax) + starRating *= 0.60; + else if (colourRating < 2 && staminaRating > 8) starRating *= 0.80; } @@ -138,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina) + private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina, bool isRelax) { List peaks = new List(); @@ -152,6 +157,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + if (isRelax) + { + colourPeak = 0; // There is no colour difficulty in relax. + staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. + } + double peak = norm(1.5, colourPeak, staminaPeak); peak = norm(2, peak, rhythmPeak); From 0f2f25db532418ff5f8deba221ef14ed7a4867e7 Mon Sep 17 00:00:00 2001 From: YaniFR <58740803+YaniFR@users.noreply.github.com> Date: Wed, 18 Dec 2024 19:11:51 +0100 Subject: [PATCH 02/86] Adjust `DifficultyValue` curve to avoid lower star rating of osu!taiko being too inflated (#31067) * low sr * merge two line * update decimal * fix formatting --------- Co-authored-by: StanR --- .../Difficulty/TaikoPerformanceCalculator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c672b7a1d9..ed7d41bf72 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0; + double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0; + double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; From 4ca88ae2d66dd417a8380144b5fe5010821ad9ec Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 19 Dec 2024 21:32:59 +1000 Subject: [PATCH 03/86] Refactor `TaikoDifficultyCalculator` and add `DifficultStrain` attributes (#31191) * refactor + countdifficultstrain * norm in utils * adjust scaling shift * fix comment * revert all value changes * add the else back * remove cds comments --- .../Difficulty/TaikoDifficultyAttributes.cs | 13 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 75 ++++++++++--------- .../Utils/DifficultyCalculationUtils.cs | 9 +++ 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index c8f0448767..4a35c30e60 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -34,11 +34,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } - /// - /// The difficulty corresponding to the hardest parts of the map. - /// - [JsonProperty("peak_difficulty")] - public double PeakDifficulty { get; set; } + [JsonProperty("rhythm_difficult_strains")] + public double RhythmTopStrains { get; set; } + + [JsonProperty("colour_difficult_strains")] + public double ColourTopStrains { get; set; } + + [JsonProperty("stamina_difficult_strains")] + public double StaminaTopStrains { get; set; } /// /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b3efb7f46d..05081d471e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -53,18 +54,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - List difficultyHitObjects = new List(); - List centreObjects = new List(); - List rimObjects = new List(); - List noteObjects = new List(); + var difficultyHitObjects = new List(); + var centreObjects = new List(); + var rimObjects = new List(); + var noteObjects = new List(); + // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) { - difficultyHitObjects.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, - centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) - ); + difficultyHitObjects.Add(new TaikoDifficultyHitObject( + beatmap.HitObjects[i], + beatmap.HitObjects[i - 1], + beatmap.HitObjects[i - 2], + clockRate, + difficultyHitObjects, + centreObjects, + rimObjects, + noteObjects, + difficultyHitObjects.Count + )); } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); @@ -79,28 +87,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); - Colour colour = (Colour)skills.First(x => x is Colour); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Colour colour = (Colour)skills.First(x => x is Colour); Stamina stamina = (Stamina)skills.First(x => x is Stamina); Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); - double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); + double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); + double colourDifficultStrains = colour.CountTopWeightedStrains(); + double staminaDifficultStrains = stamina.CountTopWeightedStrains(); + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); - // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. + // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { starRating *= 0.925; - // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. + // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) starRating *= 0.60; + // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. else if (colourRating < 2 && staminaRating > 8) starRating *= 0.80; } @@ -112,11 +125,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { StarRating = starRating, Mods = mods, - StaminaDifficulty = staminaRating, - MonoStaminaFactor = monoStaminaFactor, RhythmDifficulty = rhythmRating, ColourDifficulty = colourRating, - PeakDifficulty = combinedRating, + StaminaDifficulty = staminaRating, + MonoStaminaFactor = monoStaminaFactor, + StaminaTopStrains = staminaDifficultStrains, + RhythmTopStrains = rhythmDifficultStrains, + ColourTopStrains = colourDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), @@ -125,17 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return attributes; } - /// - /// Applies a final re-scaling of the star rating. - /// - /// The raw star rating value before re-scaling. - private double rescale(double sr) - { - if (sr < 0) return sr; - - return 10.43 * Math.Log(sr / 8 + 1); - } - /// /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. /// @@ -153,8 +157,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 0; i < colourPeaks.Count; i++) { - double colourPeak = colourPeaks[i] * colour_skill_multiplier; double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double colourPeak = colourPeaks[i] * colour_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; if (isRelax) @@ -163,8 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. } - double peak = norm(1.5, colourPeak, staminaPeak); - peak = norm(2, peak, rhythmPeak); + double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak); // 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. @@ -185,10 +188,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } /// - /// Returns the p-norm of an n-dimensional vector. + /// Applies a final re-scaling of the star rating. /// - /// The value of p to calculate the norm for. - /// The coefficients of the vector. - private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + /// The raw star rating value before re-scaling. + private double rescale(double sr) + { + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); + } } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index b9efcd683d..df2d84d6f2 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; namespace osu.Game.Rulesets.Difficulty.Utils { @@ -46,5 +47,13 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// Exponent /// The output of logistic function public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent)); + + /// + /// Returns the p-norm of an n-dimensional vector (https://en.wikipedia.org/wiki/Norm_(mathematics)) + /// + /// The value of p to calculate the norm for. + /// The coefficients of the vector. + /// The p-norm of the vector. + public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); } } From ecd6b4192816391591ca8e96b77d80fe7c1fa948 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 20 Dec 2024 00:45:11 +1000 Subject: [PATCH 04/86] Increase `accscalingshift` and include `countok` in hit proportion (#31195) * revert acc scaling shift to previous values * increase variance in accuracy values across od * move return values, move nullcheck into return --------- Co-authored-by: James Wilson --- .../Difficulty/TaikoPerformanceCalculator.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ed7d41bf72..a93f4c66ab 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor; return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } @@ -134,6 +134,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + double? deviationGreatWindow = calcDeviationGreatWindow(); + double? deviationGoodWindow = calcDeviationGoodWindow(); + + return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); + // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. double? calcDeviationGreatWindow() { @@ -160,7 +165,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double n = totalHits; // Proportion of greats + goods hit. - double p = totalSuccessfulHits / n; + double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n; // We can be 99% confident that p is at least this value. double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); @@ -168,14 +173,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // We can be 99% confident that the deviation is not higher than: return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } - - double? deviationGreatWindow = calcDeviationGreatWindow(); - double? deviationGoodWindow = calcDeviationGoodWindow(); - - if (deviationGreatWindow is null) - return deviationGoodWindow; - - return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); } private int totalHits => countGreat + countOk + countMeh + countMiss; From d8c3d899ebb4660b97301f9a5d07902bb4598cbe Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 20 Dec 2024 03:22:16 +1000 Subject: [PATCH 05/86] remove particular condition on convert nerf (#31196) Co-authored-by: James Wilson --- .../Difficulty/TaikoDifficultyCalculator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 05081d471e..8f725d4f94 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -113,9 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) starRating *= 0.60; - // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. - else if (colourRating < 2 && staminaRating > 8) - starRating *= 0.80; } HitWindows hitWindows = new TaikoHitWindows(); From f722f94f26f0055f7a68bb867b60600aff6bac81 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 21 Dec 2024 04:32:51 +0500 Subject: [PATCH 06/86] Simplify osu! high-bpm acute angle jumps bonus (#30902) * Simplify osu! high-bpm acute angle jumps bonus * Add aim wiggle bonus * Add hitwindow-based aim velocity decrease * Revert "Add hitwindow-based aim velocity decrease" This reverts commit bcebe9662cfcb7a72805e48712525ef54ec9820e. * Move wiggle multiplier to a const, slightly decrease acute bonus multiplier * Make sure the previous object in the wiggle bonus is also part of the wiggle * Scale the wiggle bonus multiplayer down * Increase the acute angle jump bonus multiplier * Make wiggle bonus only apply on >150 bpm streams, make repetitive angle penalty * Reduce wiggle bonus multiplier to not break velocity>difficulty relation * Adjust wiggle falloff function to fix stability issues * Adjust wiggle consts * Update tests --- .../OsuDifficultyCalculatorTest.cs | 6 ++-- .../Difficulty/Evaluators/AimEvaluator.cs | 33 ++++++++++++------- .../Utils/DifficultyCalculationUtils.cs | 24 ++++++++++++++ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index efda3fa369..9798611488 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7171144000821119d, 239, "diffcalc-test")] + [TestCase(6.718709884850683d, 239, "diffcalc-test")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.9825709931204205d, 239, "diffcalc-test")] + [TestCase(9.4310274277499619d, 239, "diffcalc-test")] [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7171144000821119d, 239, "diffcalc-test")] + [TestCase(6.718709884850683d, 239, "diffcalc-test")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 9816f6d0a4..c3270f25f8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,9 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 1.95; + private const double acute_angle_multiplier = 2.35; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; + private const double wiggle_multiplier = 1.02; /// /// Evaluates the difficulty of aiming the current object, based on: @@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double acuteAngleBonus = 0; double sliderBonus = 0; double velocityChangeBonus = 0; + double wiggleBonus = 0; double aimStrain = currVelocity; // Start strain with regular velocity. @@ -79,22 +81,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double angleBonus = Math.Min(currVelocity, prevVelocity); wideAngleBonus = calcWideAngleBonus(currAngle); - acuteAngleBonus = calcAcuteAngleBonus(currAngle); - if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2. - acuteAngleBonus = 0; - else - { - acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. - * Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime - * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 - * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter. - } + // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter + acuteAngleBonus = calcAcuteAngleBonus(currAngle) * + angleBonus * + DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * + DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. - acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + + // Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150 + // https://www.desmos.com/calculator/iis7lgbppe + wiggleBonus = angleBonus + * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter) + * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8) + * DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)) + * DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter) + * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8) + * DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)); } } @@ -122,6 +129,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; } + aimStrain += wiggleBonus * wiggle_multiplier; + // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier); diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index df2d84d6f2..055d8a458b 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -55,5 +55,29 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The coefficients of the vector. /// The p-norm of the vector. public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + + /// + /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double Smootherstep(double x, double start, double end) + { + x = Math.Clamp((x - start) / (end - start), 0.0, 1.0); + + return x * x * x * (x * (6.0 * x - 15.0) + 10.0); + } + + /// + /// Reverse linear interpolation function (https://en.wikipedia.org/wiki/Linear_interpolation) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double ReverseLerp(double x, double start, double end) + { + return Math.Clamp((x - start) / (end - start), 0.0, 1.0); + } } } From f6a36f7b2e1427f858b087052bfe7f3dc50b2ab2 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Sat, 21 Dec 2024 20:19:14 +1000 Subject: [PATCH 07/86] Implement `Reading` Skill into osu!taiko (#31208) --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 43 ++++++++++++++++ .../Preprocessing/Reading/EffectiveBPM.cs | 50 +++++++++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 10 ++++ .../Difficulty/Skills/Reading.cs | 44 ++++++++++++++++ .../Difficulty/TaikoDifficultyAttributes.cs | 6 +++ .../Difficulty/TaikoDifficultyCalculator.cs | 22 +++++--- .../Difficulty/TaikoPerformanceCalculator.cs | 3 -- 7 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs new file mode 100644 index 0000000000..a6a1513842 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public static class ReadingEvaluator + { + private readonly struct VelocityRange + { + public double Min { get; } + public double Max { get; } + public double Center => (Max + Min) / 2; + public double Range => Max - Min; + + public VelocityRange(double min, double max) + { + Min = min; + Max = max; + } + } + + /// + /// Calculates the influence of higher slider velocities on hitobject difficulty. + /// The bonus is determined based on the EffectiveBPM, shifting within a defined range + /// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty. + /// + /// The hit object to evaluate. + /// The reading difficulty value for the given hit object. + public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject) + { + double effectiveBPM = noteObject.EffectiveBPM; + + var highVelocity = new VelocityRange(480, 640); + var midVelocity = new VelocityRange(360, 480); + + return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10)) + + 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs new file mode 100644 index 0000000000..17e05d5fbf --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading +{ + public class EffectiveBPMPreprocessor + { + private readonly IList noteObjects; + private readonly double globalSliderVelocity; + + public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects) + { + this.noteObjects = noteObjects; + globalSliderVelocity = beatmap.Difficulty.SliderMultiplier; + } + + /// + /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed. + /// + public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate) + { + foreach (var currentNoteObject in noteObjects) + { + double startTime = currentNoteObject.StartTime * clockRate; + + // Retrieve the timing point at the note's start time + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + + // Calculate the slider velocity at the note's start time. + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate); + currentNoteObject.CurrentSliderVelocity = currentSliderVelocity; + + currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; + } + } + + /// + /// Calculates the slider velocity based on control point info and clock rate. + /// + private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate) + { + var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); + return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 4aaee50c18..e741e4c9e7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -48,6 +48,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectColour Colour; + /// + /// The adjusted BPM of this hit object, based on its slider velocity and scroll speed. + /// + public double EffectiveBPM; + + /// + /// The current slider velocity of this hit object. + /// + public double CurrentSliderVelocity; + /// /// Creates a new difficulty hit object. /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs new file mode 100644 index 0000000000..9de058f289 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + /// + /// Calculates the reading coefficient of taiko difficulty. + /// + public class Reading : StrainDecaySkill + { + protected override double SkillMultiplier => 1.0; + protected override double StrainDecayBase => 0.4; + + private double currentStrain; + + public Reading(Mod[] mods) + : base(mods) + { + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + // Drum Rolls and Swells are exempt. + if (current.BaseObject is not Hit) + { + return 0.0; + } + + var taikoObject = (TaikoDifficultyHitObject)current; + + currentStrain *= StrainDecayBase; + currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier; + + return currentStrain; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 4a35c30e60..d3cdb379d5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } + /// + /// The difficulty corresponding to the reading skill. + /// + [JsonProperty("reading_difficulty")] + public double ReadingDifficulty { get; set; } + /// /// The difficulty corresponding to the colour skill. /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 8f725d4f94..0d6ecb8d3e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -22,7 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier; + private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; @@ -38,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return new Skill[] { new Rhythm(mods), + new Reading(mods), new Colour(mods), new Stamina(mods, false), new Stamina(mods, true) @@ -58,6 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var centreObjects = new List(); var rimObjects = new List(); var noteObjects = new List(); + EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects); // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) @@ -76,6 +80,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); return difficultyHitObjects; } @@ -88,11 +93,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Reading reading = (Reading)skills.First(x => x is Reading); Colour colour = (Colour)skills.First(x => x is Colour); Stamina stamina = (Stamina)skills.First(x => x is Stamina); Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double readingRating = reading.DifficultyValue() * reading_skill_multiplier; double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; @@ -102,13 +109,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double colourDifficultStrains = colour.CountTopWeightedStrains(); double staminaDifficultStrains = stamina.CountTopWeightedStrains(); - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); + double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { - starRating *= 0.925; + starRating *= 0.825; // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) @@ -123,6 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty StarRating = starRating, Mods = mods, RhythmDifficulty = rhythmRating, + ReadingDifficulty = readingRating, ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, @@ -144,17 +152,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina, bool isRelax) + private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax) { List peaks = new List(); - var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); + var readingPeaks = reading.GetCurrentStrainPeaks().ToList(); + var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); for (int i = 0; i < colourPeaks.Count; i++) { double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double readingPeak = readingPeaks[i] * reading_skill_multiplier; double colourPeak = colourPeaks[i] * colour_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; @@ -164,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. } - double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak); + double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak); // 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. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index a93f4c66ab..5da18e7963 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -87,9 +87,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModHidden)) difficultyValue *= 1.025; - if (score.Mods.Any(m => m is ModHardRock)) - difficultyValue *= 1.10; - if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus); From 6808a5a77cffdb5800fc6443823bcad80283f549 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 22 Dec 2024 04:45:29 +0500 Subject: [PATCH 08/86] Change slider drop penalty to use actual number of difficult sliders, fix slider drop penalty being too lenient (#31055) * Change slider drop penalty to use actual number of difficult sliders, fix slider nerf being too lenient * Move cubing to performance calculation * Add separate list for slider strains * Rename difficulty atttribute * Rename attribute in perfcalc * Check if AimDifficultSliderCount is more than 0, code quality fixes * Add `AimDifficultSliderCount` to the list of databased attributes * Code quality --------- Co-authored-by: James Wilson --- .../Difficulty/OsuDifficultyAttributes.cs | 9 ++++ .../Difficulty/OsuDifficultyCalculator.cs | 3 +- .../Difficulty/OsuPerformanceCalculator.cs | 49 +++++++++---------- .../Difficulty/Skills/Aim.cs | 24 +++++++++ .../Difficulty/DifficultyAttributes.cs | 1 + 5 files changed, 60 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index a3c0209a08..3b9a23df23 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -19,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("aim_difficulty")] public double AimDifficulty { get; set; } + /// + /// The number of s weighted by difficulty. + /// + [JsonProperty("aim_difficult_slider_count")] + public double AimDifficultSliderCount { get; set; } + /// /// The difficulty corresponding to the speed skill. /// @@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount); yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); + yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -125,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; + AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 575e03051c..ffdd4673e3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; double speedNotes = ((Speed)skills[2]).RelevantNoteCount(); - + double difficultSliders = ((Aim)skills[0]).GetDifficultSliders(); double flashlightRating = 0.0; if (mods.Any(h => h is OsuModFlashlight)) @@ -99,6 +99,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty StarRating = starRating, Mods = mods, AimDifficulty = aimRating, + AimDifficultSliderCount = difficultSliders, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, FlashlightDifficulty = flashlightRating, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 31b00dba2b..3610845533 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -135,7 +135,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty); + double aimDifficulty = attributes.AimDifficulty; + + if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) + { + double estimateImproperlyFollowedDifficultSliders; + + if (usingClassicSliderAccuracy) + { + // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders + int maximumPossibleDroppedSliders = totalImperfectHits; + estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, attributes.AimDifficultSliderCount); + } + else + { + // We add tick misses here since they too mean that the player didn't follow the slider properly + // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly + estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount); + } + + double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor; + aimDifficulty *= sliderNerfFactor; + } + + double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty); double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); @@ -163,30 +186,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. - double estimateDifficultSliders = attributes.SliderCount * 0.15; - - if (attributes.SliderCount > 0) - { - double estimateImproperlyFollowedDifficultSliders; - - if (usingClassicSliderAccuracy) - { - // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders - int maximumPossibleDroppedSliders = totalImperfectHits; - estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); - } - else - { - // We add tick misses here since they too mean that the player didn't follow the slider properly - // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly - estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders); - } - - double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor; - aimValue *= sliderNerfFactor; - } - aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index faf91e4652..400bc97fbc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Evaluators; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -26,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double skillMultiplier => 25.18; private double strainDecayBase => 0.15; + private readonly List sliderStrains = new List(); + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime); @@ -35,7 +40,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(current.DeltaTime); currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; + if (current.BaseObject is Slider) + { + sliderStrains.Add(currentStrain); + } + return currentStrain; } + + public double GetDifficultSliders() + { + if (sliderStrains.Count == 0) + return 0; + + double[] sortedStrains = sliderStrains.OrderDescending().ToArray(); + + double maxSliderStrain = sortedStrains.Max(); + if (maxSliderStrain == 0) + return 0; + + return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); + } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 7b6bc37a61..f5ed5a180b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25; protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; + protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; /// /// The mods which were applied to the beatmap. From 3ddeaf8460476e8e8c8386e584addd8f9594d0d1 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 24 Dec 2024 09:43:44 +0000 Subject: [PATCH 09/86] Use `lastAngle` when nerfing repeated angles on acute bonus (#31245) * Use `lastAngle` when nerfing repeated angles on acute bonus * Bump acute multiplier * Correct outdated wiggle bonus comment * Update test --------- Co-authored-by: StanR --- .../OsuDifficultyCalculatorTest.cs | 2 +- .../Difficulty/Evaluators/AimEvaluator.cs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 9798611488..c0a6d3a755 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.4310274277499619d, 239, "diffcalc-test")] + [TestCase(9.6343245007055653d, 239, "diffcalc-test")] [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index c3270f25f8..fdf94719ed 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 2.35; + private const double acute_angle_multiplier = 2.7; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; private const double wiggle_multiplier = 1.02; @@ -75,7 +75,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { double currAngle = osuCurrObj.Angle.Value; double lastAngle = osuLastObj.Angle.Value; - double lastLastAngle = osuLastLastObj.Angle.Value; // Rewarding angles, take the smaller velocity as base. double angleBonus = Math.Min(currVelocity, prevVelocity); @@ -90,11 +89,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); - // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse. + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); - // Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150 - // https://www.desmos.com/calculator/iis7lgbppe + // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle + // https://www.desmos.com/calculator/dp0v0nvowc wiggleBonus = angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter) * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8) From 824497d82c6f86eebf6421b1cdcf25beaf39f881 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 27 Dec 2024 23:30:30 +1000 Subject: [PATCH 10/86] Rewrite of the `Rhythm` Skill within osu!taiko (#31284) * implement bell curve into diffcalcutils * remove unneeded attributes * implement new rhythm skill * change dho variables * update dho rhythm * interval interface * implement rhythmevaluator * evenhitobjects * evenpatterns * evenrhythm * change attribute ordering * initial balancing * change naming to Same instead of Even * remove attribute bump for display * Fix diffcalc tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 +- .../Difficulty/Evaluators/RhythmEvaluator.cs | 149 +++++++++++++++++ .../Preprocessing/Rhythm/Data/SamePatterns.cs | 55 ++++++ .../Preprocessing/Rhythm/Data/SameRhythm.cs | 73 ++++++++ .../Rhythm/Data/SameRhythmHitObjects.cs | 94 +++++++++++ .../Preprocessing/Rhythm/IHasInterval.cs | 13 ++ .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 79 ++++++++- .../Preprocessing/TaikoDifficultyHitObject.cs | 51 ++---- .../Difficulty/Skills/Rhythm.cs | 157 ++---------------- .../Difficulty/TaikoDifficultyAttributes.cs | 28 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 20 ++- .../Utils/DifficultyCalculationUtils.cs | 10 ++ 12 files changed, 520 insertions(+), 217 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 09d6540f72..ba247c68d4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.0920212594351191d, 200, "diffcalc-test")] - [TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")] + [TestCase(3.0950934814938953d, 200, "diffcalc-test")] + [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.0789820318081444d, 200, "diffcalc-test")] - [TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")] + [TestCase(4.0839365008715403d, 200, "diffcalc-test")] + [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs new file mode 100644 index 0000000000..3a294f7123 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -0,0 +1,149 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public class RhythmEvaluator + { + /// + /// Multiplier for a given denominator term. + /// + private static double termPenalty(double ratio, int denominator, double power, double multiplier) + { + return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); + } + + /// + /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. + /// + private static double ratioDifficulty(double ratio, int terms = 8) + { + double difficulty = 0; + + for (int i = 1; i <= terms; ++i) + { + difficulty += termPenalty(ratio, i, 2, 1); + } + + difficulty += terms; + + // Give bonus to near-1 ratios + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7); + + // Penalize ratios that are VERY near 1 + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + + return difficulty / Math.Sqrt(8); + } + + /// + /// Determines if the changes in hit object intervals is consistent based on a given threshold. + /// + private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1) + { + double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3); + + double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6 + ? sameInterval(sameRhythmHitObjects, 4) + : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. + + // Scale penalties dynamically based on hit object duration relative to hitWindow. + double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5); + + return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling; + + double sameInterval(SameRhythmHitObjects startObject, int intervalCount) + { + List intervals = new List(); + var currentObject = startObject; + + for (int i = 0; i < intervalCount && currentObject != null; i++) + { + intervals.Add(currentObject.HitObjectInterval); + currentObject = currentObject.Previous; + } + + intervals.RemoveAll(interval => interval == null); + + if (intervals.Count < intervalCount) + return 1.0; // No penalty if there aren't enough valid intervals. + + for (int i = 0; i < intervals.Count; i++) + { + for (int j = i + 1; j < intervals.Count; j++) + { + double ratio = intervals[i]!.Value / intervals[j]!.Value; + if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty. + return 0.3; + } + } + + return 1.0; // No penalty if all intervals are different. + } + } + + private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + { + double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + + // If a previous interval exists and there are multiple hit objects in the sequence: + if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + { + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; + double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + + if (durationDifference > 0) + { + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + durationDifference / hitWindow, + midpointOffset: 0.7, + multiplier: 1.5, + maxValue: 1); + } + } + + // Apply consistency penalty. + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + + // Penalise patterns that can be hit within a single hit window. + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + sameRhythmHitObjects.Duration / hitWindow, + midpointOffset: 0.6, + multiplier: 1, + maxValue: 1); + + return Math.Pow(intervalDifficulty, 0.75); + } + + private static double evaluateDifficultyOf(SamePatterns samePatterns) + { + return ratioDifficulty(samePatterns.IntervalRatio); + } + + /// + /// Evaluate the difficulty of a hitobject considering its interval change. + /// + public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) + { + TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + double difficulty = 0.0d; + + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + + if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns + difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns); + + return difficulty; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs new file mode 100644 index 0000000000..50839c4561 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// Represents grouped by their 's interval. + /// + public class SamePatterns : SameRhythm + { + public SamePatterns? Previous { get; private set; } + + /// + /// The between children within this group. + /// If there is only one child, this will have the value of the first child's . + /// + public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; + + /// + /// The ratio of between this and the previous . In the + /// case where there is no previous , this will have a value of 1. + /// + public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; + + public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject; + + public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); + + private SamePatterns(SamePatterns? previous, List data, ref int i) + : base(data, ref i, 5) + { + Previous = previous; + + foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) + { + hitObject.Rhythm.SamePatterns = this; + } + } + + public static void GroupPatterns(List data) + { + List samePatterns = new List(); + + // Index does not need to be incremented, as it is handled within the SameRhythm constructor. + for (int i = 0; i < data.Count;) + { + SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; + samePatterns.Add(new SamePatterns(previous, data, ref i)); + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs new file mode 100644 index 0000000000..b1ca22595b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// A base class for grouping s by their interval. In edges where an interval change + /// occurs, the is added to the group with the smaller interval. + /// + public abstract class SameRhythm + where ChildType : IHasInterval + { + public IReadOnlyList Children { get; private set; } + + /// + /// Determines if the intervals between two child objects are within a specified margin of error, + /// indicating that the intervals are effectively "flat" or consistent. + /// + private bool isFlat(ChildType current, ChildType previous, double marginOfError) + { + return Math.Abs(current.Interval - previous.Interval) <= marginOfError; + } + + /// + /// Create a new from a list of s, and add + /// them to the list until the end of the group. + /// + /// The list of s. + /// + /// Index in to start adding children. This will be modified and should be passed into + /// the next 's constructor. + /// + /// + /// The margin of error for the interval, within of which no interval change is considered to have occured. + /// + protected SameRhythm(List data, ref int i, double marginOfError) + { + List children = new List(); + Children = children; + children.Add(data[i]); + i++; + + for (; i < data.Count - 1; i++) + { + // An interval change occured, add the current data if the next interval is larger. + if (!isFlat(data[i], data[i + 1], marginOfError)) + { + if (data[i + 1].Interval > data[i].Interval + marginOfError) + { + children.Add(data[i]); + i++; + } + + return; + } + + // No interval change occured + children.Add(data[i]); + } + + // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // If true, add the current object to the group and increment the index to process the next object. + if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError)) + { + children.Add(data[i]); + i++; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs new file mode 100644 index 0000000000..0ccc6da026 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// Represents a group of s with no rhythm variation. + /// + public class SameRhythmHitObjects : SameRhythm, IHasInterval + { + public TaikoDifficultyHitObject FirstHitObject => Children[0]; + + public SameRhythmHitObjects? Previous; + + /// + /// of the first hit object. + /// + public double StartTime => Children[0].StartTime; + + /// + /// The interval between the first and final hit object within this group. + /// + public double Duration => Children[^1].StartTime - Children[0].StartTime; + + /// + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . + /// + public double? HitObjectInterval; + + /// + /// The ratio of between this and the previous . In the + /// case where one or both of the is undefined, this will have a value of 1. + /// + public double HitObjectIntervalRatio = 1; + + /// + /// The interval between the of this and the previous . + /// + public double Interval { get; private set; } = double.PositiveInfinity; + + public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i) + : base(data, ref i, 5) + { + Previous = previous; + + foreach (var hitObject in Children) + { + hitObject.Rhythm.SameRhythmHitObjects = this; + + // Pass the HitObjectInterval to each child. + hitObject.HitObjectInterval = HitObjectInterval; + } + + calculateIntervals(); + } + + public static List GroupHitObjects(List data) + { + List flatPatterns = new List(); + + // Index does not need to be incremented, as it is handled within SameRhythm's constructor. + for (int i = 0; i < data.Count;) + { + SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; + flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i)); + } + + return flatPatterns; + } + + private void calculateIntervals() + { + // Calculate the average interval between hitobjects, or null if there are fewer than two. + HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1); + + // If both the current and previous intervals are available, calculate the ratio. + if (Previous?.HitObjectInterval != null && HitObjectInterval != null) + { + HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value; + } + + if (Previous == null) + { + return; + } + + Interval = StartTime - Previous.StartTime; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs new file mode 100644 index 0000000000..8f3917cbde --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +{ + /// + /// The interface for hitobjects that provide an interval value. + /// + public interface IHasInterval + { + double Interval { get; } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index a273d7e2ea..beb7bfe5f6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -1,35 +1,98 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { /// - /// Represents a rhythm change in a taiko map. + /// Stores rhythm data for a . /// public class TaikoDifficultyHitObjectRhythm { /// - /// The difficulty multiplier associated with this rhythm change. + /// The group of hit objects with consistent rhythm that this object belongs to. /// - public readonly double Difficulty; + public SameRhythmHitObjects? SameRhythmHitObjects; /// - /// The ratio of current - /// to previous for the rhythm change. + /// The larger pattern of rhythm groups that this object is part of. + /// + public SamePatterns? SamePatterns; + + /// + /// The ratio of current + /// to previous for the rhythm change. /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. /// public readonly double Ratio; + /// + /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. + /// + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// + private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = + { + new TaikoDifficultyHitObjectRhythm(1, 1), + new TaikoDifficultyHitObjectRhythm(2, 1), + new TaikoDifficultyHitObjectRhythm(1, 2), + new TaikoDifficultyHitObjectRhythm(3, 1), + new TaikoDifficultyHitObjectRhythm(1, 3), + new TaikoDifficultyHitObjectRhythm(3, 2), + new TaikoDifficultyHitObjectRhythm(2, 3), + new TaikoDifficultyHitObjectRhythm(5, 4), + new TaikoDifficultyHitObjectRhythm(4, 5) + }; + + /// + /// Initialises a new instance of s, + /// calculating the closest rhythm change and its associated difficulty for the current hit object. + /// + /// The current being processed. + public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current) + { + var previous = current.Previous(0); + + if (previous == null) + { + Ratio = 1; + return; + } + + TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime); + Ratio = closestRhythm.Ratio; + } + /// /// Creates an object representing a rhythm change. /// /// The numerator for . /// The denominator for - /// The difficulty multiplier associated with this rhythm change. - public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) + private TaikoDifficultyHitObjectRhythm(int numerator, int denominator) { Ratio = numerator / (double)denominator; - Difficulty = difficulty; + } + + /// + /// Determines the closest rhythm change from that matches the timing ratio + /// between the current and previous intervals. + /// + /// The time difference between the current hit object and the previous one. + /// The time difference between the previous hit object and the one before it. + /// The closest matching rhythm from . + private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime) + { + double ratio = currentDeltaTime / previousDeltaTime; + return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } } + diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index e741e4c9e7..dfcd08ed94 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; @@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// Represents a single hit object in taiko difficulty calculation. /// - public class TaikoDifficultyHitObject : DifficultyHitObject + public class TaikoDifficultyHitObject : DifficultyHitObject, IHasInterval { /// /// The list of all of the same colour as this in the beatmap. @@ -42,6 +41,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectRhythm Rhythm; + /// + /// The interval between this hit object and the surrounding hit objects in its rhythm group. + /// + public double? HitObjectInterval { get; set; } + /// /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. @@ -58,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public double CurrentSliderVelocity; + public double Interval => DeltaTime; + /// /// Creates a new difficulty hit object. /// @@ -81,7 +87,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor Colour = new TaikoDifficultyHitObjectColour(); - Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); + + // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm + Rhythm = new TaikoDifficultyHitObjectRhythm(this); switch ((hitObject as Hit)?.Type) { @@ -105,43 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } - /// - /// List of most common rhythm changes in taiko maps. - /// - /// - /// The general guidelines for the values are: - /// - /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, - /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). - /// - /// - private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = - { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style) - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - - /// - /// Returns the closest rhythm change from required to hit this object. - /// - /// The gameplay preceding this one. - /// The gameplay preceding . - /// The rate of the gameplay clock. - private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate) - { - double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - double ratio = DeltaTime / prevLength; - - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } - public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index e76af13686..4fe1ea693e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -16,158 +14,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Rhythm : StrainDecaySkill { - protected override double SkillMultiplier => 10; - protected override double StrainDecayBase => 0; + protected override double SkillMultiplier => 1.0; + protected override double StrainDecayBase => 0.4; - /// - /// The note-based decay for rhythm strain. - /// - /// - /// is not used here, as it's time- and not note-based. - /// - private const double strain_decay = 0.96; + private readonly double greatHitWindow; - /// - /// Maximum number of entries in . - /// - private const int rhythm_history_max_length = 8; - - /// - /// Contains the last changes in note sequence rhythms. - /// - private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); - - /// - /// Contains the rolling rhythm strain. - /// Used to apply per-note decay. - /// - private double currentStrain; - - /// - /// Number of notes since the last rhythm change has taken place. - /// - private int notesSinceRhythmChange; - - public Rhythm(Mod[] mods) + public Rhythm(Mod[] mods, double greatHitWindow) : base(mods) { + this.greatHitWindow = greatHitWindow; } protected override double StrainValueOf(DifficultyHitObject current) { - // drum rolls and swells are exempt. - if (!(current.BaseObject is Hit)) - { - resetRhythmAndStrain(); - return 0.0; - } + double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow); - currentStrain *= strain_decay; + // To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty. + difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 0.5; - TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - notesSinceRhythmChange += 1; - - // rhythm difficulty zero (due to rhythm not changing) => no rhythm strain. - if (hitObject.Rhythm.Difficulty == 0.0) - { - return 0.0; - } - - double objectStrain = hitObject.Rhythm.Difficulty; - - objectStrain *= repetitionPenalties(hitObject); - objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitObject.DeltaTime); - - // careful - needs to be done here since calls above read this value - notesSinceRhythmChange = 0; - - currentStrain += objectStrain; - return currentStrain; - } - - /// - /// Returns a penalty to apply to the current hit object caused by repeating rhythm changes. - /// - /// - /// Repetitions of more recent patterns are associated with a higher penalty. - /// - /// The current hit object being considered. - private double repetitionPenalties(TaikoDifficultyHitObject hitObject) - { - double penalty = 1; - - rhythmHistory.Enqueue(hitObject); - - for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++) - { - for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--) - { - if (!samePattern(start, mostRecentPatternsToCompare)) - continue; - - int notesSince = hitObject.Index - rhythmHistory[start].Index; - penalty *= repetitionPenalty(notesSince); - break; - } - } - - return penalty; - } - - /// - /// Determines whether the rhythm change pattern starting at is a repeat of any of the - /// . - /// - private bool samePattern(int start, int mostRecentPatternsToCompare) - { - for (int i = 0; i < mostRecentPatternsToCompare; i++) - { - if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm) - return false; - } - - return true; - } - - /// - /// Calculates a single rhythm repetition penalty. - /// - /// Number of notes since the last repetition of a rhythm change. - private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); - - /// - /// Calculates a penalty based on the number of notes since the last rhythm change. - /// Both rare and frequent rhythm changes are penalised. - /// - /// Number of notes since the last rhythm change. - private static double patternLengthPenalty(int patternLength) - { - double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); - } - - /// - /// Calculates a penalty for objects that do not require alternating hands. - /// - /// Time (in milliseconds) since the last hit object. - private double speedPenalty(double deltaTime) - { - if (deltaTime < 80) return 1; - if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime); - - resetRhythmAndStrain(); - return 0.0; - } - - /// - /// Resets the rolling strain value and counter. - /// - private void resetRhythmAndStrain() - { - currentStrain = 0.0; - notesSinceRhythmChange = 0; + return difficulty; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index d3cdb379d5..ef729e1f07 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -10,18 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyAttributes : DifficultyAttributes { - /// - /// The difficulty corresponding to the stamina skill. - /// - [JsonProperty("stamina_difficulty")] - public double StaminaDifficulty { get; set; } - - /// - /// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty. - /// - [JsonProperty("mono_stamina_factor")] - public double MonoStaminaFactor { get; set; } - /// /// The difficulty corresponding to the rhythm skill. /// @@ -40,8 +28,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } - [JsonProperty("rhythm_difficult_strains")] - public double RhythmTopStrains { get; set; } + /// + /// The difficulty corresponding to the stamina skill. + /// + [JsonProperty("stamina_difficulty")] + public double StaminaDifficulty { get; set; } + + /// + /// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty. + /// + [JsonProperty("mono_stamina_factor")] + public double MonoStaminaFactor { get; set; } + + [JsonProperty("reading_difficult_strains")] + public double ReadingTopStrains { get; set; } [JsonProperty("colour_difficult_strains")] public double ColourTopStrains { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 0d6ecb8d3e..f8ff6f6065 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier; private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; @@ -37,9 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { + HitWindows hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + return new Skill[] { - new Rhythm(mods), + new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate), new Reading(mods), new Colour(mods), new Stamina(mods, false), @@ -57,6 +61,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { + var hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + var difficultyHitObjects = new List(); var centreObjects = new List(); var rimObjects = new List(); @@ -79,7 +86,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty )); } + var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects); + TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + SamePatterns.GroupPatterns(groupedHitObjects); bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); return difficultyHitObjects; @@ -105,8 +115,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); - double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); double colourDifficultStrains = colour.CountTopWeightedStrains(); + double readingDifficultStrains = reading.CountTopWeightedStrains(); double staminaDifficultStrains = stamina.CountTopWeightedStrains(); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); @@ -134,9 +144,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, - StaminaTopStrains = staminaDifficultStrains, - RhythmTopStrains = rhythmDifficultStrains, + ReadingTopStrains = readingDifficultStrains, ColourTopStrains = colourDifficultStrains, + StaminaTopStrains = staminaDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index 055d8a458b..497a1f8234 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -56,6 +56,16 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The p-norm of the vector. public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + /// + /// Calculates a Gaussian-based bell curve function (https://en.wikipedia.org/wiki/Gaussian_function) + /// + /// Value to calculate the function for + /// The mean (center) of the bell curve + /// The width (spread) of the curve + /// Multiplier to adjust the curve's height + /// The output of the bell curve function of + public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2))); + /// /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) /// From 988ed374ae82528991f37516ee40098d2adf1af4 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Sun, 29 Dec 2024 19:29:57 +0000 Subject: [PATCH 11/86] Add basic difficulty & performance calculation for Autopilot mod on osu! ruleset (#21211) * Set speed distance to 0 * Reduce speed & flashlight, remove aim * Remove speed AR bonus * cleanup autopilot mod check in `SpeedEvaluator` * further decrease speed rating for extra hand availability * Pass all mods to the speed evaluator, zero out distance bonus instead of distance --------- Co-authored-by: tsunyoku Co-authored-by: StanR --- .../Difficulty/Evaluators/SpeedEvaluator.cs | 9 ++++++++- .../Difficulty/OsuDifficultyCalculator.cs | 6 ++++++ .../Difficulty/OsuPerformanceCalculator.cs | 6 ++++++ osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index a5f6468f17..e5e9769081 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -2,9 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators @@ -24,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators /// and how easily they can be cheesed. /// /// - public static double EvaluateDifficultyOf(DifficultyHitObject current) + public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList mods) { if (current.BaseObject is Spinner) return 0; @@ -56,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier; + if (mods.OfType().Any()) + distanceBonus = 0; + // Base difficulty with all bonuses double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index ffdd4673e3..d0f23735c3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -63,6 +63,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedRating = 0.0; flashlightRating *= 0.7; } + else if (mods.Any(h => h is OsuModAutopilot)) + { + speedRating *= 0.5; + aimRating = 0.0; + flashlightRating *= 0.4; + } double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating); double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 3610845533..df418fb3f8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -135,6 +135,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { + if (score.Mods.Any(h => h is OsuModAutopilot)) + return 0.0; + double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -211,6 +214,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + if (score.Mods.Any(h => h is OsuModAutopilot)) + approachRateFactor = 0.0; + speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. if (score.Mods.Any(m => m is OsuModBlinds)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index d2c4bbb618..5dae9a9fc5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); - currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; + currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier; currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); From 76ac11ff593bafc32a99a92368f79c94dac2f512 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 6 Jan 2025 20:08:14 +0500 Subject: [PATCH 12/86] Fix angle bonuses calculating repetition incorrectly, apply distance scaling to wide bonus (#31320) * Fix angle bonuses calculating repetition incorrectly, apply distance scaling to wide bonus * Buff speed to compensate for streams losing pp * Adjust speed multiplier * Adjust wide scaling * Fix tests --- .../OsuDifficultyCalculatorTest.cs | 18 ++++++++--------- .../Difficulty/Evaluators/AimEvaluator.cs | 20 ++++++++++--------- .../Difficulty/Evaluators/SpeedEvaluator.cs | 2 +- .../Difficulty/Skills/Speed.cs | 2 +- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index c0a6d3a755..842a34aaa8 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.718709884850683d, 239, "diffcalc-test")] - [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] - [TestCase(0.42630400627180914d, 4, "very-fast-slider")] + [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] + [TestCase(0.42912495021837549d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6343245007055653d, 239, "diffcalc-test")] - [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] - [TestCase(0.55231632896800109d, 4, "very-fast-slider")] + [TestCase(9.6358837846598835d, 239, "diffcalc-test")] + [TestCase(1.754888327422514d, 54, "zero-length-sliders")] + [TestCase(0.55601568006454294d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.718709884850683d, 239, "diffcalc-test")] - [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] - [TestCase(0.42630400627180914d, 4, "very-fast-slider")] + [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] + [TestCase(0.42912495021837549d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index fdf94719ed..cff2eae357 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -80,17 +80,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double angleBonus = Math.Min(currVelocity, prevVelocity); wideAngleBonus = calcWideAngleBonus(currAngle); + acuteAngleBonus = calcAcuteAngleBonus(currAngle); + + // Penalize angle repetition. + wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + + // Apply full wide angle bonus for distance more than one diameter + wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter - acuteAngleBonus = calcAcuteAngleBonus(currAngle) * - angleBonus * - DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * - DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); - - // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. - wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); - // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse. - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= angleBonus * + DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * + DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle // https://www.desmos.com/calculator/dp0v0nvowc diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index e5e9769081..769220ece0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers private const double min_speed_bonus = 200; // 200 BPM 1/4th private const double speed_balancing_factor = 40; - private const double distance_multiplier = 0.94; + private const double distance_multiplier = 0.9; /// /// Evaluates the difficulty of tapping the current object, based on: diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 5dae9a9fc5..f2e2c2ec5f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.430; + private double skillMultiplier => 1.45; private double strainDecayBase => 0.3; private double currentStrain; From 4095b2662bc67da4e3eeb90da0d747b2cc135dcb Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Tue, 7 Jan 2025 21:36:56 +1000 Subject: [PATCH 13/86] Add `consistentRatioPenalty` to the `Colour` skill. (#31285) * fix colour * review fix Co-authored-by: StanR * remove cancelled out operand * increase nerf, adjust tests * fix automated spacing issues * up penalty * adjust tests * apply review changes * fix nullable hell --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 +-- .../Difficulty/Evaluators/ColourEvaluator.cs | 54 ++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index ba247c68d4..de3bec5fcf 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.0950934814938953d, 200, "diffcalc-test")] - [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")] + [TestCase(2.837609165845338d, 200, "diffcalc-test")] + [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.0839365008715403d, 200, "diffcalc-test")] - [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")] + [TestCase(3.8005218640444949, 200, "diffcalc-test")] + [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 25428c8b2f..3ff5b87fb6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -36,18 +36,70 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } + /// + /// Calculates a consistency penalty based on the number of consecutive consistent intervals, + /// considering the delta time between each colour sequence. + /// + /// The current hitObject to consider. + /// The allowable margin of error for determining whether ratios are consistent. + /// The maximum objects to check per count of consistent ratio. + private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64) + { + int consistentRatioCount = 0; + double totalRatioCount = 0.0; + + TaikoDifficultyHitObject current = hitObject; + + for (int i = 0; i < maxObjectsToCheck; i++) + { + // Break if there is no valid previous object + if (current.Index <= 1) + break; + + var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); + + double currentRatio = current.Rhythm.Ratio; + double previousRatio = previousHitObject.Rhythm.Ratio; + + // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error. + if (Math.Abs(1 - currentRatio / previousRatio) <= threshold) + { + consistentRatioCount++; + totalRatioCount += currentRatio; + break; + } + + // Move to the previous object + current = previousHitObject; + } + + // Ensure no division by zero + double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80; + + return ratioPenalty; + } + + /// + /// Evaluate the difficulty of the first hitobject within a colour streak. + /// public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) { - TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour; + var taikoObject = (TaikoDifficultyHitObject)hitObject; + TaikoDifficultyHitObjectColour colour = taikoObject.Colour; double difficulty = 0.0d; if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak difficulty += EvaluateDifficultyOf(colour.MonoStreak); + if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); + if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern); + double consistencyPenalty = consistentRatioPenalty(taikoObject); + difficulty *= consistencyPenalty; + return difficulty; } } From 3b58d5e43565e9b16b94667972ba968dbea36ba1 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 7 Jan 2025 17:49:55 +0500 Subject: [PATCH 14/86] Clamp OD in performance calculation to fix negative OD gaining pp (#31447) Co-authored-by: James Wilson --- .../Difficulty/OsuPerformanceCalculator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index df418fb3f8..5cf7a56d8a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return aimValue; } @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); + speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -305,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the flashlight value with accuracy _slightly_. flashlightValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return flashlightValue; } From 392bb5718cbbab3a2b3738d460ea3cbbc4d46885 Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 8 Jan 2025 15:03:22 +0500 Subject: [PATCH 15/86] Simplify angle bonus formula (#31449) * Simplify angle bonus formula * Simplify further * Simplify acute too * Tests --- .../OsuDifficultyCalculatorTest.cs | 6 +++--- .../Difficulty/Evaluators/AimEvaluator.cs | 4 ++-- .../Difficulty/Utils/DifficultyCalculationUtils.cs | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 842a34aaa8..fbd865df47 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(6.7230435389286045d, 239, "diffcalc-test")] [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] [TestCase(0.42912495021837549d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6358837846598835d, 239, "diffcalc-test")] + [TestCase(9.6468019709446171d, 239, "diffcalc-test")] [TestCase(1.754888327422514d, 54, "zero-length-sliders")] [TestCase(0.55601568006454294d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(6.7230435389286045d, 239, "diffcalc-test")] [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] [TestCase(0.42912495021837549d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index cff2eae357..8c41240a24 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -142,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return aimStrain; } - private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2); + private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(30), double.DegreesToRadians(150)); - private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle); + private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(150), double.DegreesToRadians(30)); } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index 497a1f8234..aeccf2fd55 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -66,6 +66,19 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The output of the bell curve function of public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2))); + /// + /// Smoothstep function (https://en.wikipedia.org/wiki/Smoothstep) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double Smoothstep(double x, double start, double end) + { + x = Math.Clamp((x - start) / (end - start), 0.0, 1.0); + + return x * x * (3.0 - 2.0 * x); + } + /// /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) /// From db58ec864569889a17952149ff85a05d28a07133 Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 9 Jan 2025 14:57:48 +0500 Subject: [PATCH 16/86] Apply a bunch of balancing changes to aim (#31456) * Apply a bunch of balancing changes to aim * Update tests --------- Co-authored-by: James Wilson --- .../OsuDifficultyCalculatorTest.cs | 18 +++++++++--------- .../Difficulty/Evaluators/AimEvaluator.cs | 8 ++++---- .../Difficulty/Skills/Speed.cs | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index fbd865df47..9af5051f45 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7230435389286045d, 239, "diffcalc-test")] - [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] - [TestCase(0.42912495021837549d, 4, "very-fast-slider")] + [TestCase(6.6860329680488437d, 239, "diffcalc-test")] + [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6468019709446171d, 239, "diffcalc-test")] - [TestCase(1.754888327422514d, 54, "zero-length-sliders")] - [TestCase(0.55601568006454294d, 4, "very-fast-slider")] + [TestCase(9.6300773538770041d, 239, "diffcalc-test")] + [TestCase(1.7550155729445993d, 54, "zero-length-sliders")] + [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7230435389286045d, 239, "diffcalc-test")] - [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] - [TestCase(0.42912495021837549d, 4, "very-fast-slider")] + [TestCase(6.6860329680488437d, 239, "diffcalc-test")] + [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 8c41240a24..e279ed889a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 2.7; + private const double acute_angle_multiplier = 2.6; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; private const double wiggle_multiplier = 1.02; @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); @@ -142,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return aimStrain; } - private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(30), double.DegreesToRadians(150)); + private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140)); - private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(150), double.DegreesToRadians(30)); + private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40)); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f2e2c2ec5f..bdeea0e918 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.45; + private double skillMultiplier => 1.46; private double strainDecayBase => 0.3; private double currentStrain; From b21c6457b1a1febd004d508e3597815b64a2a6d4 Mon Sep 17 00:00:00 2001 From: Givikap120 <89256026+Givikap120@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:27:54 +0200 Subject: [PATCH 17/86] Punish speed PP for scores with high deviation (#30907) --- .../Difficulty/OsuDifficultyAttributes.cs | 31 ++++- .../Difficulty/OsuDifficultyCalculator.cs | 5 + .../Difficulty/OsuPerformanceAttributes.cs | 3 + .../Difficulty/OsuPerformanceCalculator.cs | 119 +++++++++++++++++- .../Difficulty/DifficultyAttributes.cs | 1 + 5 files changed, 149 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 3b9a23df23..395f581b65 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -62,21 +62,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). /// - /// - /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } /// /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). /// - /// - /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// [JsonProperty("overall_difficulty")] public double OverallDifficulty { get; set; } + /// + /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("great_hit_window")] + public double GreatHitWindow { get; set; } + + /// + /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("ok_hit_window")] + public double OkHitWindow { get; set; } + + /// + /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("meh_hit_window")] + public double MehHitWindow { get; set; } + /// /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. /// @@ -107,6 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_DIFFICULTY, StarRating); + yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); if (ShouldSerializeFlashlightDifficulty()) yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); @@ -117,6 +130,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); + + yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); + yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -128,12 +144,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; StarRating = values[ATTRIB_ID_DIFFICULTY]; + GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; + OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; + MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index d0f23735c3..5a61ea586a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -99,6 +99,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; + double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate; + double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate; OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { @@ -114,6 +116,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedDifficultStrainCount = speedDifficultyStrainCount, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, + GreatHitWindow = hitWindowGreat, + OkHitWindow = hitWindowOk, + MehHitWindow = hitWindowMeh, DrainRate = drainRate, MaxCombo = beatmap.GetMaxCombo(), HitCircleCount = hitCirclesCount, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 0aeaf7669f..de4491a31b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + [JsonProperty("speed_deviation")] + public double? SpeedDeviation { get; set; } + public override IEnumerable GetAttributesForDisplay() { foreach (var attribute in base.GetAttributesForDisplay()) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 5cf7a56d8a..91cd270966 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double? speedDeviation; + public OsuPerformanceCalculator() : base(new OsuRuleset()) { @@ -110,10 +113,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } + speedDeviation = calculateSpeedDeviation(osuAttributes); + double aimValue = computeAimValue(score, osuAttributes); double speedValue = computeSpeedValue(score, osuAttributes); double accuracyValue = computeAccuracyValue(score, osuAttributes); double flashlightValue = computeFlashlightValue(score, osuAttributes); + double totalValue = Math.Pow( Math.Pow(aimValue, 1.1) + @@ -129,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + SpeedDeviation = speedDeviation, Total = totalValue }; } @@ -198,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (score.Mods.Any(h => h is OsuModRelax)) + if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null) return 0.0; double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); @@ -230,6 +237,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } + double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes); + speedValue *= speedHighDeviationMultiplier; + // Calculate accuracy assuming the worst case scenario double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); @@ -240,9 +250,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with accuracy and OD. speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); - // Scale the speed value with # of 50s to punish doubletapping. - speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); - return speedValue; } @@ -310,12 +317,116 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } + /// + /// Estimates player's deviation on speed notes using , assuming worst-case. + /// Treats all speed notes as hit circles. + /// + private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return null; + + // Calculate accuracy assuming the worst case scenario + double speedNoteCount = attributes.SpeedNoteCount; + speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1; + + // Assume worst case: all mistakes were on speed notes + double relevantCountMiss = Math.Min(countMiss, speedNoteCount); + double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss); + double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh); + double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk); + + return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + } + + /// + /// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses, + /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings + /// will always return the same deviation. Misses are ignored because they are usually due to misaiming. + /// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution. + /// + private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss) + { + if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) + return null; + + double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; + + double hitWindowGreat = attributes.GreatHitWindow; + double hitWindowOk = attributes.OkHitWindow; + double hitWindowMeh = attributes.MehHitWindow; + + // The probability that a player hits a circle is unknown, but we can estimate it to be + // the number of greats on circles divided by the number of circles, and then add one + // to the number of circles as a bias correction. + double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh); + const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + + // Proportion of greats hit on circles, ignoring misses and 50s. + double p = relevantCountGreat / n; + + // We can be 99% confident that p is at least this value. + double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); + + // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. + // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: + double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + + double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) + / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + + deviation *= Math.Sqrt(1 - randomValue); + + // Value deviation approach as greatCount approaches 0 + double limitValue = hitWindowOk / Math.Sqrt(3); + + // If precision is not enough to compute true deviation - use limit value + if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue) + deviation = limitValue; + + // Then compute the variance for mehs. + double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3; + + // Find the total deviation. + deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); + + return deviation; + } + + // Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty + // https://www.desmos.com/calculator/dmogdhzofn + private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes) + { + if (speedDeviation == null) + return 0; + + double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); + + // Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty. + // This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value. + double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5); + + if (speedValue <= excessSpeedDifficultyCutoff) + return 1.0; + + const double scale = 50; + double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale); + + // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible + double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1); + adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp); + + return adjustedSpeedValue / speedValue; + } + // 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. private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); + private int totalHits => countGreat + countOk + countMeh + countMiss; + private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalImperfectHits => countOk + countMeh + countMiss; } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index f5ed5a180b..1d6cee043b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; + protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33; /// /// The mods which were applied to the beatmap. From c53188cf450bf5eb9efb903e5e295b7435971386 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 14 Jan 2025 18:18:02 +0500 Subject: [PATCH 18/86] Use total deviation to scale accuracy on aim, general aim buff (#31498) * Make aim accuracy scaling harsher * Use deviation-based scaling * Bring the balancing multiplier down * Adjust multipliers, fix incorrect deviation when using slider accuracy * Adjust multipliers * Update osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs Co-authored-by: James Wilson * Change high speed deviation threshold to 22-27 instead of 20-24 * Update tests --------- Co-authored-by: James Wilson --- .../OsuDifficultyCalculatorTest.cs | 12 ++-- .../Difficulty/Evaluators/AimEvaluator.cs | 2 +- .../Difficulty/OsuPerformanceAttributes.cs | 3 + .../Difficulty/OsuPerformanceCalculator.cs | 57 +++++++++++++++++-- .../Difficulty/Skills/Aim.cs | 2 +- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 9af5051f45..a68d9dad39 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.6860329680488437d, 239, "diffcalc-test")] - [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(6.7443067697205539d, 239, "diffcalc-test")] + [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6300773538770041d, 239, "diffcalc-test")] - [TestCase(1.7550155729445993d, 54, "zero-length-sliders")] + [TestCase(9.7058844423552308d, 239, "diffcalc-test")] + [TestCase(1.7724929629205366d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.6860329680488437d, 239, "diffcalc-test")] - [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(6.7443067697205539d, 239, "diffcalc-test")] + [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index e279ed889a..858ce673ee 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index de4491a31b..9c30c0f7c7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + [JsonProperty("total_deviation")] + public double? TotalDeviation { get; set; } + [JsonProperty("speed_deviation")] public double? SpeedDeviation { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 91cd270966..a03e3fd6ef 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; @@ -41,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double? totalDeviation; private double? speedDeviation; public OsuPerformanceCalculator() @@ -113,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } + totalDeviation = calculateTotalDeviation(osuAttributes); speedDeviation = calculateSpeedDeviation(osuAttributes); double aimValue = computeAimValue(score, osuAttributes); @@ -135,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + TotalDeviation = totalDeviation, SpeedDeviation = speedDeviation, Total = totalValue }; @@ -145,6 +149,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModAutopilot)) return 0.0; + if (totalDeviation == null) + return 0; + double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -196,9 +203,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - aimValue *= accuracy; - // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value)); return aimValue; } @@ -317,6 +322,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } + /// + /// Using estimates player's deviation on accuracy objects. + /// Returns deviation for circles and sliders if score was set with slideracc. + /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc. + /// + private double? calculateTotalDeviation(OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return null; + + int accuracyObjectCount = attributes.HitCircleCount; + + if (!usingClassicSliderAccuracy) + accuracyObjectCount += attributes.SliderCount; + + // Assume worst case: all mistakes was on accuracy objects + int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount); + int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss); + int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh); + int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk); + + // Calculate deviation on accuracy objects + double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + if (deviation == null) + return null; + + if (!usingClassicSliderAccuracy) + return deviation.Value; + + // If score was set without slider accuracy - also compute deviation with sliders + // Assume that all hits was 50s + int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount; + int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss); + int hitCountWithSliders = totalCountWithSliders - missCountWithSliders; + + double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0); + double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders)); + + // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case + return Math.Min(deviation.Value, deviationWithSliders); + } + /// /// Estimates player's deviation on speed notes using , assuming worst-case. /// Treats all speed notes as hit circles. @@ -412,8 +459,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty const double scale = 50; double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale); - // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible - double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1); + // 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible + double lerp = 1 - DifficultyCalculationUtils.ReverseLerp(speedDeviation.Value, 22.0, 27.0); adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp); return adjustedSpeedValue / speedValue; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 400bc97fbc..69211b610f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 25.18; + private double skillMultiplier => 25.7; private double strainDecayBase => 0.15; private readonly List sliderStrains = new List(); From 6cf15e3e5a2f5aa0df42886a60367eb2f184fe30 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 14 Jan 2025 18:27:25 +0000 Subject: [PATCH 19/86] Remove problematic total deviation scaling, rebalance aim (#31515) * Remove problematic total deviation scaling, rebalance aim * Fix tests --- .../OsuDifficultyCalculatorTest.cs | 12 ++--- .../Difficulty/Evaluators/AimEvaluator.cs | 2 +- .../Difficulty/OsuPerformanceAttributes.cs | 3 -- .../Difficulty/OsuPerformanceCalculator.cs | 52 ++----------------- .../Difficulty/Skills/Aim.cs | 2 +- 5 files changed, 11 insertions(+), 60 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index a68d9dad39..7cf5b0529f 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7443067697205539d, 239, "diffcalc-test")] - [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] + [TestCase(6.7331304290522747d, 239, "diffcalc-test")] + [TestCase(1.4602604078137214d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.7058844423552308d, 239, "diffcalc-test")] - [TestCase(1.7724929629205366d, 54, "zero-length-sliders")] + [TestCase(9.6779397290273756d, 239, "diffcalc-test")] + [TestCase(1.7691451263718989d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7443067697205539d, 239, "diffcalc-test")] - [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] + [TestCase(6.7331304290522747d, 239, "diffcalc-test")] + [TestCase(1.4602604078137214d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 858ce673ee..9a5533e536 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 9c30c0f7c7..de4491a31b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,9 +24,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } - [JsonProperty("total_deviation")] - public double? TotalDeviation { get; set; } - [JsonProperty("speed_deviation")] public double? SpeedDeviation { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index a03e3fd6ef..7013ee55c4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; - private double? totalDeviation; private double? speedDeviation; public OsuPerformanceCalculator() @@ -115,7 +114,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } - totalDeviation = calculateTotalDeviation(osuAttributes); speedDeviation = calculateSpeedDeviation(osuAttributes); double aimValue = computeAimValue(score, osuAttributes); @@ -138,7 +136,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, - TotalDeviation = totalDeviation, SpeedDeviation = speedDeviation, Total = totalValue }; @@ -149,9 +146,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModAutopilot)) return 0.0; - if (totalDeviation == null) - return 0; - double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -203,7 +197,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value)); + aimValue *= accuracy; + // It is important to consider accuracy difficulty when scaling with accuracy. + aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return aimValue; } @@ -322,48 +318,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } - /// - /// Using estimates player's deviation on accuracy objects. - /// Returns deviation for circles and sliders if score was set with slideracc. - /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc. - /// - private double? calculateTotalDeviation(OsuDifficultyAttributes attributes) - { - if (totalSuccessfulHits == 0) - return null; - - int accuracyObjectCount = attributes.HitCircleCount; - - if (!usingClassicSliderAccuracy) - accuracyObjectCount += attributes.SliderCount; - - // Assume worst case: all mistakes was on accuracy objects - int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount); - int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss); - int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh); - int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk); - - // Calculate deviation on accuracy objects - double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); - if (deviation == null) - return null; - - if (!usingClassicSliderAccuracy) - return deviation.Value; - - // If score was set without slider accuracy - also compute deviation with sliders - // Assume that all hits was 50s - int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount; - int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss); - int hitCountWithSliders = totalCountWithSliders - missCountWithSliders; - - double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0); - double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders)); - - // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case - return Math.Min(deviation.Value, deviationWithSliders); - } - /// /// Estimates player's deviation on speed notes using , assuming worst-case. /// Treats all speed notes as hit circles. diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 69211b610f..f04b679b73 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 25.7; + private double skillMultiplier => 25.6; private double strainDecayBase => 0.15; private readonly List sliderStrains = new List(); From 5bed7c22e351a60a9ae22b2f736da4871646911d Mon Sep 17 00:00:00 2001 From: Natelytle <92956514+Natelytle@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:12:08 -0500 Subject: [PATCH 20/86] Remove lower cap on deviation without misses (#31499) --- .../Difficulty/TaikoPerformanceCalculator.cs | 48 ++++--------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 5da18e7963..4933c9dee6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -123,53 +123,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes) { - if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0) + if (countGreat == 0 || attributes.GreatHitWindow <= 0) return null; - double h300 = attributes.GreatHitWindow; - double h100 = attributes.OkHitWindow; - const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). - double? deviationGreatWindow = calcDeviationGreatWindow(); - double? deviationGoodWindow = calcDeviationGoodWindow(); + double n = totalHits; - return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); + // Proportion of greats hit. + double p = countGreat / n; - // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. - double? calcDeviationGreatWindow() - { - if (countGreat == 0) return null; + // We can be 99% confident that p is at least this value. + double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - double n = totalHits; - - // Proportion of greats hit. - double p = countGreat / n; - - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - - // We can be 99% confident that the deviation is not higher than: - return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); - } - - // The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window. - // This will return a lower value than the first method when the number of 100s is high, but the miss count is low. - double? calcDeviationGoodWindow() - { - if (totalSuccessfulHits == 0) return null; - - double n = totalHits; - - // Proportion of greats + goods hit. - double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n; - - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - - // We can be 99% confident that the deviation is not higher than: - return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); - } + // We can be 99% confident that the deviation is not higher than: + return attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; From 0a21183e54648953b653e2e56b8150a11a93c69a Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Wed, 15 Jan 2025 20:34:21 +1000 Subject: [PATCH 21/86] reading mono nerf (#31510) --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs index 9de058f289..885131404a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -3,6 +3,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -34,6 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } var taikoObject = (TaikoDifficultyHitObject)current; + int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; + + currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5; currentStrain *= StrainDecayBase; currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier; From 974fa76987a445f0d0d18f823e11e0bb4ffec842 Mon Sep 17 00:00:00 2001 From: molneya <62799417+molneya@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:08:47 +0800 Subject: [PATCH 22/86] fix spinners not increasing cumulative strain time (#31525) Co-authored-by: StanR --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 5cb5a8f934..9d05f0b074 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var currentObj = (OsuDifficultyHitObject)current.Previous(i); var currentHitObject = (OsuHitObject)(currentObj.BaseObject); + cumulativeStrainTime += lastObj.StrainTime; + if (!(currentObj.BaseObject is Spinner)) { double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length; - cumulativeStrainTime += lastObj.StrainTime; - // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) smallDistNerf = Math.Min(1.0, jumpDistance / 75.0); From 9da8dcd8151009a2252c9b3f45d258f92a501895 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 16 Jan 2025 20:30:02 +1000 Subject: [PATCH 23/86] osu!taiko stamina balancing (#31337) * stamina considerations * remove consecutive note count * adjust multiplier * add back comment * adjust tests * adjusts tests post merge * use diffcalcutils --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- .../Difficulty/Evaluators/StaminaEvaluator.cs | 17 ++++++++--------- .../Difficulty/Skills/Stamina.cs | 9 ++++++--- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index de3bec5fcf..517f62b6f5 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.837609165845338d, 200, "diffcalc-test")] - [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")] + [TestCase(2.912326627861987d, 200, "diffcalc-test")] + [TestCase(2.912326627861987d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(3.8005218640444949, 200, "diffcalc-test")] - [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")] + [TestCase(3.9339069955362014d, 200, "diffcalc-test")] + [TestCase(3.9339069955362014d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 84d5de4c63..a273d91a38 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Interval is capped at a very small value to prevent infinite values. interval = Math.Max(interval, 1); - return 30 / interval; + return 20 / interval; } /// @@ -59,16 +59,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of // available fingers. TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); - - if (keyPrevious == null) - { - // There is no previous hit object hit by the current finger - return 0.0; - } + TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; + TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); double objectStrain = 0.5; // Add a base strain to all objects - objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); + if (taikoPrevious == null) return objectStrain; + + if (previousMono != null) + objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); + return objectStrain; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index f6914039f0..29f9f16033 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -44,10 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills var currentObject = current as TaikoDifficultyHitObject; int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; - if (singleColourStamina) - return currentStrain / (1 + Math.Exp(-(index - 10) / 2.0)); + double monolengthBonus = 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); - return currentStrain; + if (singleColourStamina) + return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); + + return currentStrain * monolengthBonus; } protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); From b9894f67ceac3ba42995cd81b0692c414620053f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 12:30:27 +0100 Subject: [PATCH 24/86] Bump NVika tool to 4.0.0 Code quality CI runs have suddenly started failing out of nowhere: - Passing run: https://github.com/ppy/osu/actions/runs/12806242929/job/35704267944#step:10:1 - Failing run: https://github.com/ppy/osu/actions/runs/12807108792/job/35707131634#step:10:1 In classic github fashion, they began rolling out another runner change wherein `ubuntu-latest` has started meaning `ubuntu-24.04` rather than `ubuntu-22.04`. `ubuntu-24.04` no longer has .NET 6 bundled. Therefore, upgrade NVika to 4.0.0 because that version is compatible with .NET 8. --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c4ba6e5143..6ec071be2f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "nvika": { - "version": "3.0.0", + "version": "4.0.0", "commands": [ "nvika" ] From a83f917d87c51f95d2778afce0048c08f8af125f Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 17 Jan 2025 07:14:05 +1000 Subject: [PATCH 25/86] osu!taiko star rating and performance points rebalance (#31338) * rebalance * revert pp scaling change * further rebalancing * comment * adjust tests --- .../TaikoDifficultyCalculatorTest.cs | 8 +++--- .../Difficulty/TaikoDifficultyAttributes.cs | 4 +-- .../Difficulty/TaikoDifficultyCalculator.cs | 27 +++++++++++++------ .../Difficulty/TaikoPerformanceCalculator.cs | 8 +++--- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 517f62b6f5..b4cbe03511 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.912326627861987d, 200, "diffcalc-test")] - [TestCase(2.912326627861987d, 200, "diffcalc-test-strong")] + [TestCase(3.3172381854905493d, 200, "diffcalc-test")] + [TestCase(3.3172381854905493d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(3.9339069955362014d, 200, "diffcalc-test")] - [TestCase(3.9339069955362014d, 200, "diffcalc-test-strong")] + [TestCase(4.4640702427013101d, 200, "diffcalc-test")] + [TestCase(4.4640702427013101d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index ef729e1f07..37e6996e5a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("mono_stamina_factor")] public double MonoStaminaFactor { get; set; } - [JsonProperty("reading_difficult_strains")] - public double ReadingTopStrains { get; set; } + [JsonProperty("rhythm_difficult_strains")] + public double RhythmTopStrains { get; set; } [JsonProperty("colour_difficult_strains")] public double ColourTopStrains { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index f8ff6f6065..3ad9d17526 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -24,10 +24,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 0.65 * difficulty_multiplier; private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; - private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; + private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier; + + private double strainLengthBonus; + private double patternMultiplier; public override int Version => 20241007; @@ -116,8 +119,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); double colourDifficultStrains = colour.CountTopWeightedStrains(); - double readingDifficultStrains = reading.CountTopWeightedStrains(); - double staminaDifficultStrains = stamina.CountTopWeightedStrains(); + double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); + // Due to constraints of strain in cases where difficult strain values don't shift with range changes, we manually apply clockrate. + double staminaDifficultStrains = stamina.CountTopWeightedStrains() * clockRate; + + // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm. + patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10); + + strainLengthBonus = 1 + + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); @@ -125,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { - starRating *= 0.825; + starRating *= 0.7; // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) @@ -144,7 +155,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, - ReadingTopStrains = readingDifficultStrains, + RhythmTopStrains = rhythmDifficultStrains, ColourTopStrains = colourDifficultStrains, StaminaTopStrains = staminaDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, @@ -173,10 +184,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 0; i < colourPeaks.Count; i++) { - double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier; double readingPeak = readingPeaks[i] * reading_skill_multiplier; double colourPeak = colourPeaks[i] * colour_skill_multiplier; - double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus; if (isRelax) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 4933c9dee6..c29ea3ba73 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -73,8 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0; - double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0); + double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.110) - 4.0; + double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1250.0); + + difficultyValue *= 1 + 0.10 * Math.Max(0, attributes.StarRating - 10); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; @@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } From a42c03cea457b9e6786983d77d966a461d1a10ed Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 17 Jan 2025 21:15:22 +1000 Subject: [PATCH 26/86] osu!taiko further considerations for rhythm (#31339) * further considerations for rhythm * new rhythm balancing * fix license header * use isNormal to validate ratio * adjust tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++-- .../Difficulty/Evaluators/RhythmEvaluator.cs | 48 +++++++++++++------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index b4cbe03511..d760b9aef6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3172381854905493d, 200, "diffcalc-test")] - [TestCase(3.3172381854905493d, 200, "diffcalc-test-strong")] + [TestCase(3.3167800835687551d, 200, "diffcalc-test")] + [TestCase(3.3167800835687551d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4640702427013101d, 200, "diffcalc-test")] - [TestCase(4.4640702427013101d, 200, "diffcalc-test-strong")] + [TestCase(4.4631326105105122d, 200, "diffcalc-test")] + [TestCase(4.4631326105105122d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 3a294f7123..7d58eada5e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -21,27 +21,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); } + /// + /// Validates the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. + /// + private static double validateRatio(double ratio) + { + return double.IsNormal(ratio) ? ratio : 0; + } + /// /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. /// private static double ratioDifficulty(double ratio, int terms = 8) { double difficulty = 0; + ratio = validateRatio(ratio); for (int i = 1; i <= terms; ++i) { - difficulty += termPenalty(ratio, i, 2, 1); + difficulty += termPenalty(ratio, i, 4, 1); } - difficulty += terms; + difficulty += terms / (1 + ratio); // Give bonus to near-1 ratios - difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7); + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); // Penalize ratios that are VERY near 1 - difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); - return difficulty / Math.Sqrt(8); + difficulty = Math.Max(difficulty, 0); + difficulty /= Math.Sqrt(8); + + return difficulty; } /// @@ -55,10 +67,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators ? sameInterval(sameRhythmHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. - // Scale penalties dynamically based on hit object duration relative to hitWindow. - double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5); + // The duration penalty is based on hit object duration relative to hitWindow. + double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5); - return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling; + return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; double sameInterval(SameRhythmHitObjects startObject, int intervalCount) { @@ -82,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { double ratio = intervals[i]!.Value / intervals[j]!.Value; if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty. - return 0.3; + return 0.80; } } @@ -95,6 +107,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + // If a previous interval exists and there are multiple hit objects in the sequence: if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) { @@ -111,9 +125,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } } - // Apply consistency penalty. - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); - // Penalise patterns that can be hit within a single hit window. intervalDifficulty *= DifficultyCalculationUtils.Logistic( sameRhythmHitObjects.Duration / hitWindow, @@ -137,11 +148,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; double difficulty = 0.0d; + double sameRhythm = 0; + double samePattern = 0; + double intervalPenalty = 0; + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects - difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + { + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + } if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns); + samePattern += 1.15 * evaluateDifficultyOf(rhythm.SamePatterns); + + difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } From 5b4ba9225d7810c21a2456c9824e2a3fe621306a Mon Sep 17 00:00:00 2001 From: Natelytle <92956514+Natelytle@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:37:34 -0500 Subject: [PATCH 27/86] Move error function from osu.Game.Utils to osu.Game.Rulesets.Difficulty.Utils (#31520) * Move error function implementation to osu.Game.Rulesets.Difficulty.Utils * Rename ErrorFunction.cs to DifficultyCalculationUtils_ErrorFunction.cs --- .../Difficulty/OsuPerformanceCalculator.cs | 5 ++--- .../Difficulty/TaikoPerformanceCalculator.cs | 6 +++--- .../Difficulty/Utils/DifficultyCalculationUtils.cs | 2 +- .../Utils/DifficultyCalculationUtils_ErrorFunction.cs} | 7 ++----- 4 files changed, 8 insertions(+), 12 deletions(-) rename osu.Game/{Utils/SpecialFunctions.cs => Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs} (99%) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 7013ee55c4..f191180630 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -371,10 +370,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: - double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + double deviation = hitWindowGreat / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) - / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + / (deviation * DifficultyCalculationUtils.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); deviation *= Math.Sqrt(1 - randomValue); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c29ea3ba73..9e7bf7cb7a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; -using osu.Game.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty { @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double accScalingExponent = 2 + attributes.MonoStaminaFactor; double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); + return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); // We can be 99% confident that the deviation is not higher than: - return attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + return attributes.GreatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index aeccf2fd55..78df8a139b 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -6,7 +6,7 @@ using System.Linq; namespace osu.Game.Rulesets.Difficulty.Utils { - public static class DifficultyCalculationUtils + public static partial class DifficultyCalculationUtils { /// /// Converts BPM value into milliseconds diff --git a/osu.Game/Utils/SpecialFunctions.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs similarity index 99% rename from osu.Game/Utils/SpecialFunctions.cs rename to osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs index 795a84a973..4b89cbe7cc 100644 --- a/osu.Game/Utils/SpecialFunctions.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs @@ -3,7 +3,6 @@ // All code is referenced from the following: // https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/SpecialFunctions/Erf.cs -// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/Optimization/NelderMeadSimplex.cs /* Copyright (c) 2002-2022 Math.NET @@ -14,12 +13,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI using System; -namespace osu.Game.Utils +namespace osu.Game.Rulesets.Difficulty.Utils { - public class SpecialFunctions + public partial class DifficultyCalculationUtils { - private const double sqrt2_pi = 2.5066282746310005024157652848110452530069867406099d; - /// /// ************************************** /// COEFFICIENTS FOR METHOD ErfImp * From 8354cd5f93a7c1989dbee48fb0c1403b96a7b420 Mon Sep 17 00:00:00 2001 From: Eloise Date: Sat, 18 Jan 2025 13:52:47 +0000 Subject: [PATCH 28/86] Penalise the reading difficulty of high velocity notes using "note density" (#31512) * Penalise reading difficulty of high velocity notes at high densities * Use System for math functions * Lawtrohux changes * Clean up density penalty comment * Swap midVelocity and highVelocity back around * code quality pass --------- Co-authored-by: Jay Lawton Co-authored-by: StanR --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs index a6a1513842..2a08f65c7b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -31,13 +32,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The reading difficulty value for the given hit object. public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject) { - double effectiveBPM = noteObject.EffectiveBPM; - var highVelocity = new VelocityRange(480, 640); var midVelocity = new VelocityRange(360, 480); - return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10)) - + 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + // Apply a cap to prevent outlier values on maps that exceed the editor's parameters. + double effectiveBPM = Math.Max(1.0, noteObject.EffectiveBPM); + + double midVelocityDifficulty = 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + + // Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note. + double expectedDeltaTime = 21000.0 / effectiveBPM; + double objectDensity = expectedDeltaTime / Math.Max(1.0, noteObject.DeltaTime); + + // High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi + double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15); + + double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic + (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); + + return midVelocityDifficulty + highVelocityDifficulty; } } } From 67723b3e5201f8b10e2aaac8831c4f4960e934ba Mon Sep 17 00:00:00 2001 From: "Bastien D." <37190278+bastoo0@users.noreply.github.com> Date: Sat, 18 Jan 2025 20:26:23 +0100 Subject: [PATCH 29/86] Fix osu!catch "buzz slider" SR abuse (#31126) * Implement fix for catch buzz sliders SR abuse * Run formatting --------- Co-authored-by: StanR --- .../Difficulty/Skills/Movement.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 54b85f1745..2d1adbd056 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float? lastPlayerPosition; private float lastDistanceMoved; + private float lastExactDistanceMoved; private double lastStrainTime; + private bool isBuzzSliderTriggered; /// /// The speed multiplier applied to the player's catcher. @@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills float distanceMoved = playerPosition - lastPlayerPosition.Value; + // For the exact position we consider that the catcher is in the correct position for both objects + float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value; + double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier); double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); @@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values + distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) + * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values + } + + // There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than + // the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets + // We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified. + // To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius) + if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime) + { + if (isBuzzSliderTriggered) + distanceAddition = 0; + else + isBuzzSliderTriggered = true; + } + else + { + isBuzzSliderTriggered = false; } lastPlayerPosition = playerPosition; lastDistanceMoved = distanceMoved; lastStrainTime = catchCurrent.StrainTime; + lastExactDistanceMoved = exactDistanceMoved; return distanceAddition / weightedStrainTime; } From e320f17fafa8d904bb7a436971feabaeb3f64e3b Mon Sep 17 00:00:00 2001 From: James Wilson Date: Sun, 19 Jan 2025 15:47:39 +0000 Subject: [PATCH 30/86] Remove redundant angle check (#31566) --- osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 7cf5b0529f..defd02b830 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6779397290273756d, 239, "diffcalc-test")] + [TestCase(9.6779746353001634d, 239, "diffcalc-test")] [TestCase(1.7691451263718989d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 9a5533e536..d1c92ed6a7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same. { - if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null) + if (osuCurrObj.Angle != null && osuLastObj.Angle != null) { double currAngle = osuCurrObj.Angle.Value; double lastAngle = osuLastObj.Angle.Value; From e04727afb13d5478608987e1080270a54bee66ed Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Mon, 20 Jan 2025 07:55:34 +1000 Subject: [PATCH 31/86] Improve convert considerations in osu!taiko (#31546) * return a higher finger count * implement isConvert * diffcalc cleanup * harshen monostaminafactor accuracy curve * readd comment * adjusts tests --- .../TaikoDifficultyCalculatorTest.cs | 8 ++--- .../Difficulty/Evaluators/StaminaEvaluator.cs | 2 +- .../Difficulty/Skills/Stamina.cs | 7 +++-- .../Difficulty/TaikoDifficultyCalculator.cs | 31 ++++++------------- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index d760b9aef6..6f5c26816f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3167800835687551d, 200, "diffcalc-test")] - [TestCase(3.3167800835687551d, 200, "diffcalc-test-strong")] + [TestCase(3.3056113401782845d, 200, "diffcalc-test")] + [TestCase(3.3056113401782845d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4631326105105122d, 200, "diffcalc-test")] - [TestCase(4.4631326105105122d, 200, "diffcalc-test-strong")] + [TestCase(4.4473902679506896d, 200, "diffcalc-test")] + [TestCase(4.4473902679506896d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index a273d91a38..b39ad953a4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 2; } - return 4; + return 8; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 29f9f16033..aea491aca3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private double strainDecayBase => 0.4; private readonly bool singleColourStamina; + private readonly bool isConvert; private double currentStrain; @@ -28,10 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Mods for use in skill calculations. /// Reads when Stamina is from a single coloured pattern. - public Stamina(Mod[] mods, bool singleColourStamina) + /// Determines if the currently evaluated beatmap is converted. + public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert) : base(mods) { this.singleColourStamina = singleColourStamina; + this.isConvert = isConvert; } private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); @@ -45,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills var currentObject = current as TaikoDifficultyHitObject; int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; - double monolengthBonus = 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); + double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); if (singleColourStamina) return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 3ad9d17526..efd3001764 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double strainLengthBonus; private double patternMultiplier; + private bool isConvert; + public override int Version => 20241007; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) @@ -44,13 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty HitWindows hitWindows = new HitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; + return new Skill[] { new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate), new Reading(mods), new Colour(mods), - new Stamina(mods, false), - new Stamina(mods, true) + new Stamina(mods, false, isConvert), + new Stamina(mods, true, isConvert) }; } @@ -130,19 +134,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); - double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); + double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); double starRating = rescale(combinedRating * 1.4); - // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. - if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) - { - starRating *= 0.7; - - // For maps with relax, multiple inputs are more likely to be abused. - if (isRelax) - starRating *= 0.60; - } - HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); @@ -173,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax) + private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax, bool isConvert) { List peaks = new List(); @@ -186,14 +180,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier; double readingPeak = readingPeaks[i] * reading_skill_multiplier; - double colourPeak = colourPeaks[i] * colour_skill_multiplier; + double colourPeak = isRelax ? 0 : colourPeaks[i] * colour_skill_multiplier; // There is no colour difficulty in relax. double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus; - - if (isRelax) - { - colourPeak = 0; // There is no colour difficulty in relax. - staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. - } + staminaPeak /= isConvert || isRelax ? 1.5 : 1.0; // Available finger count is increased by 150%, thus we adjust accordingly. double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9e7bf7cb7a..bcd3693119 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3); return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } From 2d0bc6cb62bd9fe84b7fffb8019ff2e503a6ffc1 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Mon, 20 Jan 2025 08:40:09 +1000 Subject: [PATCH 32/86] Rebalance stamina length bonus in osu!taiko (#31556) * adjust straincount to assume 1300 * remove comment --------- Co-authored-by: StanR --- .../Difficulty/TaikoDifficultyCalculator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index efd3001764..b1dcf2d7a0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -124,14 +124,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double colourDifficultStrains = colour.CountTopWeightedStrains(); double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); - // Due to constraints of strain in cases where difficult strain values don't shift with range changes, we manually apply clockrate. - double staminaDifficultStrains = stamina.CountTopWeightedStrains() * clockRate; + double staminaDifficultStrains = stamina.CountTopWeightedStrains(); // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm. patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10); strainLengthBonus = 1 - + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + + Math.Min(Math.Max((staminaDifficultStrains - 1000) / 3700, 0), 0.15) + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); From e57565435ed58fc4e549559350886df1fa4d4189 Mon Sep 17 00:00:00 2001 From: Eloise Date: Mon, 20 Jan 2025 08:40:52 +0000 Subject: [PATCH 33/86] osu!taiko new rhythm penalty for long intervals using stamina difficulty (#31573) * Replace long interval nerf with a new one that uses stamina difficulty * Turn tabs into spaces * Update unit tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 6f5c26816f..76b86eb4d6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3056113401782845d, 200, "diffcalc-test")] - [TestCase(3.3056113401782845d, 200, "diffcalc-test-strong")] + [TestCase(3.305554470092722d, 200, "diffcalc-test")] + [TestCase(3.305554470092722d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4473902679506896d, 200, "diffcalc-test")] - [TestCase(4.4473902679506896d, 200, "diffcalc-test-strong")] + [TestCase(4.4472572672057815d, 200, "diffcalc-test")] + [TestCase(4.4472572672057815d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 4fe1ea693e..45d0d0a548 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow); // To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty. - difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 0.5; + double staminaDifficulty = StaminaEvaluator.EvaluateDifficultyOf(current) - 0.5; // Remove base strain + difficulty *= DifficultyCalculationUtils.Logistic(staminaDifficulty, 1 / 15.0, 50.0); return difficulty; } From 22e839d62b646f6f42b129df83336694547bef8e Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 20 Jan 2025 14:39:35 +0500 Subject: [PATCH 34/86] Replace indexed skill access with `skills.OfType<...>().Single()` (#30034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace indexed skill access with `skills.First(s is ...)` * Fix comment * Further refactoring to remove casts --------- Co-authored-by: Dan Balasescu Co-authored-by: Bartłomiej Dach --- .../Difficulty/CatchDifficultyCalculator.cs | 3 ++- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 24 ++++++++++--------- .../Difficulty/Skills/Aim.cs | 10 ++++---- .../Difficulty/Skills/Stamina.cs | 8 +++---- .../Difficulty/TaikoDifficultyCalculator.cs | 10 ++++---- 6 files changed, 30 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 7d21409ee8..99df2731ff 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { - StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier, + StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.GetMaxCombo(), diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index ff9aa4aa7b..1efa7cb42f 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { - StarRating = skills[0].DifficultyValue() * difficulty_multiplier, + StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier, Mods = mods, // In osu-stable mania, rate-adjustment mods don't affect the hit window. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5a61ea586a..1505c51592 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -36,20 +36,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (beatmap.HitObjects.Count == 0) return new OsuDifficultyAttributes { Mods = mods }; - double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; - double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; - double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; - double speedNotes = ((Speed)skills[2]).RelevantNoteCount(); - double difficultSliders = ((Aim)skills[0]).GetDifficultSliders(); - double flashlightRating = 0.0; - - if (mods.Any(h => h is OsuModFlashlight)) - flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier; + var aim = skills.OfType().Single(a => a.IncludeSliders); + double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier; + double aimDifficultyStrainCount = aim.CountTopWeightedStrains(); + double difficultSliders = aim.GetDifficultSliders(); + var aimWithoutSliders = skills.OfType().Single(a => !a.IncludeSliders); + double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; - double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains(); - double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains(); + var speed = skills.OfType().Single(); + double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier; + double speedNotes = speed.RelevantNoteCount(); + double speedDifficultyStrainCount = speed.CountTopWeightedStrains(); + + var flashlight = skills.OfType().SingleOrDefault(); + double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier; if (mods.Any(m => m is OsuModTouchDevice)) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index f04b679b73..89adda302c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Aim : OsuStrainSkill { - public Aim(Mod[] mods, bool withSliders) + public readonly bool IncludeSliders; + + public Aim(Mod[] mods, bool includeSliders) : base(mods) { - this.withSliders = withSliders; + IncludeSliders = includeSliders; } - private readonly bool withSliders; - private double currentStrain; private double skillMultiplier => 25.6; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { currentStrain *= strainDecay(current.DeltaTime); - currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; + currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier; if (current.BaseObject is Slider) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index aea491aca3..12e1396dd7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private double skillMultiplier => 1.1; private double strainDecayBase => 0.4; - private readonly bool singleColourStamina; + public readonly bool SingleColourStamina; private readonly bool isConvert; private double currentStrain; @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert) : base(mods) { - this.singleColourStamina = singleColourStamina; + SingleColourStamina = singleColourStamina; this.isConvert = isConvert; } @@ -50,12 +50,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); - if (singleColourStamina) + if (SingleColourStamina) return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); return currentStrain * monolengthBonus; } - protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); + protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b1dcf2d7a0..bcd26a06bc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -109,11 +109,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); - Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); - Reading reading = (Reading)skills.First(x => x is Reading); - Colour colour = (Colour)skills.First(x => x is Colour); - Stamina stamina = (Stamina)skills.First(x => x is Stamina); - Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); + var rhythm = skills.OfType().Single(); + var reading = skills.OfType().Single(); + var colour = skills.OfType().Single(); + var stamina = skills.OfType().Single(s => !s.SingleColourStamina); + var singleColourStamina = skills.OfType().Single(s => s.SingleColourStamina); double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; double readingRating = reading.DifficultyValue() * reading_skill_multiplier; From a77dfb106834e8818574e81b1d7880d38c0e929b Mon Sep 17 00:00:00 2001 From: James Wilson Date: Mon, 20 Jan 2025 12:04:31 +0000 Subject: [PATCH 35/86] Use correct `HitWindows` class for osu!taiko hit windows in difficulty calculator (#31579) * Use correct `HitWindows` class for osu!taiko hit windows in difficulty calculator * Remove redundant (and incorrect) hit window creation * Balance rhythm against hit window changes --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 7d58eada5e..e7d82453eb 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators intervalDifficulty *= DifficultyCalculationUtils.Logistic( durationDifference / hitWindow, midpointOffset: 0.7, - multiplier: 1.5, + multiplier: 1.0, maxValue: 1); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index bcd26a06bc..f3b976f970 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { - HitWindows hitWindows = new HitWindows(); + HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; @@ -68,9 +68,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - var hitWindows = new HitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - var difficultyHitObjects = new List(); var centreObjects = new List(); var rimObjects = new List(); From c8b05ce114a00e9123ba5b3ac8930f1fafde88a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 13:40:55 +0900 Subject: [PATCH 36/86] Tidy up code quality of `RhythmEvaluator` --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 149 ++++++++---------- 1 file changed, 68 insertions(+), 81 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index e7d82453eb..22321a8f6e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -14,48 +14,64 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators public class RhythmEvaluator { /// - /// Multiplier for a given denominator term. + /// Evaluate the difficulty of a hitobject considering its interval change. /// - private static double termPenalty(double ratio, int denominator, double power, double multiplier) + public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) { - return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); - } + TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + double difficulty = 0.0d; - /// - /// Validates the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. - /// - private static double validateRatio(double ratio) - { - return double.IsNormal(ratio) ? ratio : 0; - } + double sameRhythm = 0; + double samePattern = 0; + double intervalPenalty = 0; - /// - /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. - /// - private static double ratioDifficulty(double ratio, int terms = 8) - { - double difficulty = 0; - ratio = validateRatio(ratio); - - for (int i = 1; i <= terms; ++i) + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects { - difficulty += termPenalty(ratio, i, 4, 1); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); } - difficulty += terms / (1 + ratio); + if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns + samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio); - // Give bonus to near-1 ratios - difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); - - // Penalize ratios that are VERY near 1 - difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); - - difficulty = Math.Max(difficulty, 0); - difficulty /= Math.Sqrt(8); + difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } + private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + { + double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + + // If a previous interval exists and there are multiple hit objects in the sequence: + if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + { + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; + double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + + if (durationDifference > 0) + { + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + durationDifference / hitWindow, + midpointOffset: 0.7, + multiplier: 1.0, + maxValue: 1); + } + } + + // Penalise patterns that can be hit within a single hit window. + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + sameRhythmHitObjects.Duration / hitWindow, + midpointOffset: 0.6, + multiplier: 1, + maxValue: 1); + + return Math.Pow(intervalDifficulty, 0.75); + } + /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// @@ -102,68 +118,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } } - private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) - { - double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); - double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; - - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); - - // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) - { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; - double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; - - if (durationDifference > 0) - { - intervalDifficulty *= DifficultyCalculationUtils.Logistic( - durationDifference / hitWindow, - midpointOffset: 0.7, - multiplier: 1.0, - maxValue: 1); - } - } - - // Penalise patterns that can be hit within a single hit window. - intervalDifficulty *= DifficultyCalculationUtils.Logistic( - sameRhythmHitObjects.Duration / hitWindow, - midpointOffset: 0.6, - multiplier: 1, - maxValue: 1); - - return Math.Pow(intervalDifficulty, 0.75); - } - - private static double evaluateDifficultyOf(SamePatterns samePatterns) - { - return ratioDifficulty(samePatterns.IntervalRatio); - } - /// - /// Evaluate the difficulty of a hitobject considering its interval change. + /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. /// - public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) + private static double ratioDifficulty(double ratio, int terms = 8) { - TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; - double difficulty = 0.0d; + double difficulty = 0; - double sameRhythm = 0; - double samePattern = 0; - double intervalPenalty = 0; + // Validate the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. + ratio = double.IsNormal(ratio) ? ratio : 0; - if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + for (int i = 1; i <= terms; ++i) { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + difficulty += termPenalty(ratio, i, 4, 1); } - if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - samePattern += 1.15 * evaluateDifficultyOf(rhythm.SamePatterns); + difficulty += terms / (1 + ratio); - difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; + // Give bonus to near-1 ratios + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + + // Penalize ratios that are VERY near 1 + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); + + difficulty = Math.Max(difficulty, 0); + difficulty /= Math.Sqrt(8); return difficulty; } + + /// + /// Multiplier for a given denominator term. + /// + private static double termPenalty(double ratio, int denominator, double power, double multiplier) => + -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); } } From fa20bc6631b084b4fbd3b97c3cd257a005379b0e Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:04 +0000 Subject: [PATCH 37/86] Remove `EffectiveBPMPreprocessor` --- .../Preprocessing/Reading/EffectiveBPM.cs | 50 ------------------- .../Preprocessing/TaikoDifficultyHitObject.cs | 27 +++++++++- .../Difficulty/TaikoDifficultyCalculator.cs | 13 +++-- 3 files changed, 32 insertions(+), 58 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs deleted file mode 100644 index 17e05d5fbf..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading -{ - public class EffectiveBPMPreprocessor - { - private readonly IList noteObjects; - private readonly double globalSliderVelocity; - - public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects) - { - this.noteObjects = noteObjects; - globalSliderVelocity = beatmap.Difficulty.SliderMultiplier; - } - - /// - /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed. - /// - public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate) - { - foreach (var currentNoteObject in noteObjects) - { - double startTime = currentNoteObject.StartTime * clockRate; - - // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); - - // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate); - currentNoteObject.CurrentSliderVelocity = currentSliderVelocity; - - currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; - } - } - - /// - /// Calculates the slider velocity based on control point info and clock rate. - /// - private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate) - { - var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); - return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index dfcd08ed94..34c4871a42 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -76,11 +77,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The list of rim (kat) s in the current beatmap. /// The list of s that is a hit (i.e. not a drumroll or swell) in the current beatmap. /// The position of this in the list. + /// The control point info of the beatmap. + /// The global slider velocity of the beatmap. public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, - List noteObjects, int index) + List noteObjects, int index, + ControlPointInfo controlPointInfo, + double globalSliderVelocity) : base(hitObject, lastObject, clockRate, objects, index) { noteDifficultyHitObjects = noteObjects; @@ -111,6 +116,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing NoteIndex = noteObjects.Count; noteObjects.Add(this); } + + double startTime = hitObject.StartTime * clockRate; + + // Retrieve the timing point at the note's start time + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + + // Calculate the slider velocity at the note's start time. + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, startTime, clockRate); + CurrentSliderVelocity = currentSliderVelocity; + + EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; + } + + /// + /// Calculates the slider velocity based on control point info and clock rate. + /// + private static double calculateSliderVelocity(ControlPointInfo controlPointInfo, double globalSliderVelocity, double startTime, double clockRate) + { + var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); + return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; } public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index f3b976f970..1d3075e4ac 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; @@ -72,7 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var centreObjects = new List(); var rimObjects = new List(); var noteObjects = new List(); - EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects); // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) @@ -86,15 +84,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty centreObjects, rimObjects, noteObjects, - difficultyHitObjects.Count + difficultyHitObjects.Count, + beatmap.ControlPointInfo, + beatmap.Difficulty.SliderMultiplier )); } - var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects); - TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - SamePatterns.GroupPatterns(groupedHitObjects); - bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); + + var groupedHitObjects = SameRhythmGroupedHitObjects.GroupHitObjects(noteObjects); + SamePatternsGroupedHitObjects.GroupPatterns(groupedHitObjects); return difficultyHitObjects; } From dbe36887f6da2649e9c55e265d6e4eb15429929a Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:27 +0000 Subject: [PATCH 38/86] Refactor `ColourEvaluator` --- .../Difficulty/Evaluators/ColourEvaluator.cs | 41 ++++++------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 3ff5b87fb6..c0e90e83c1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -10,32 +10,8 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { - public class ColourEvaluator + public static class ColourEvaluator { - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(MonoStreak monoStreak) - { - return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) - { - return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern) - { - return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); - } - /// /// Calculates a consistency penalty based on the number of consecutive consistent intervals, /// considering the delta time between each colour sequence. @@ -89,18 +65,27 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double difficulty = 0.0d; if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak - difficulty += EvaluateDifficultyOf(colour.MonoStreak); + difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak); if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern - difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); + difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern); + difficulty += evaluateReadingHitPatternDifficulty(colour.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; return difficulty; } + + private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) => + DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5; + + private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) => + DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateReadingHitPatternDifficulty(alternatingMonoPattern.Parent); + + private static double evaluateReadingHitPatternDifficulty(RepeatingHitPatterns repeatingHitPattern) => + 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } } From 9919179b0b914aba42499467cba38ee2d311034b Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:46 +0000 Subject: [PATCH 39/86] Format `ReadingEvaluator` --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs index 2a08f65c7b..5871979613 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15); - double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic - (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); + double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) + * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); return midVelocityDifficulty + highVelocityDifficulty; } From b8c79d58a731943f46433298db8eb0523ec850b7 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:25:28 +0000 Subject: [PATCH 40/86] Refactor `StaminaEvaluator` --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index b39ad953a4..a9884b2328 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -8,8 +8,34 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { - public class StaminaEvaluator + public static class StaminaEvaluator { + /// + /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the + /// maximum possible interval between two hits using the same key, by alternating available fingers for each colour. + /// + public static double EvaluateDifficultyOf(DifficultyHitObject current) + { + if (current.BaseObject is not Hit) + { + return 0.0; + } + + // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of + // available fingers. + TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; + TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; + TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); + + double objectStrain = 0.5; // Add a base strain to all objects + if (taikoPrevious == null) return objectStrain; + + if (previousMono != null) + objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); + + return objectStrain; + } + /// /// Applies a speed bonus dependent on the time since the last hit performed using this finger. /// @@ -44,31 +70,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 8; } - - /// - /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the - /// maximum possible interval between two hits using the same key, by alternating available fingers for each colour. - /// - public static double EvaluateDifficultyOf(DifficultyHitObject current) - { - if (current.BaseObject is not Hit) - { - return 0.0; - } - - // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of - // available fingers. - TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; - TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); - - double objectStrain = 0.5; // Add a base strain to all objects - if (taikoPrevious == null) return objectStrain; - - if (previousMono != null) - objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); - - return objectStrain; - } } } From ef8867704adaeb813bce65fe1e44844aea86ddce Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:28:15 +0000 Subject: [PATCH 41/86] Add xmldoc to explain `IHasInterval.Interval` --- .../Difficulty/Preprocessing/Rhythm/IHasInterval.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs index 8f3917cbde..32b148da2e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs @@ -8,6 +8,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// public interface IHasInterval { + /// + /// The interval between 2 objects start times. + /// double Interval { get; } } } From 20a76d832df7986c623f9e7fecd468fc012782eb Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:29:07 +0000 Subject: [PATCH 42/86] Rename rhythm preprocessing objects to be clearer with intent --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 38 +++++++++---------- ...Rhythm.cs => IntervalGroupedHitObjects.cs} | 31 ++++++--------- ...ns.cs => SamePatternsGroupedHitObjects.cs} | 28 +++++++------- ...ects.cs => SameRhythmGroupedHitObjects.cs} | 30 +++++++-------- .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 4 +- 5 files changed, 60 insertions(+), 71 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythm.cs => IntervalGroupedHitObjects.cs} (62%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SamePatterns.cs => SamePatternsGroupedHitObjects.cs} (50%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythmHitObjects.cs => SameRhythmGroupedHitObjects.cs} (70%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 22321a8f6e..8accc6124c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -25,32 +25,32 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double samePattern = 0; double intervalPenalty = 0; - if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow); } - if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio); + if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects + samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio); difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } - private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow) { - double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); - double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval; - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow); // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1) { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; - double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count; + double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious; if (durationDifference > 0) { @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Penalise patterns that can be hit within a single hit window. intervalDifficulty *= DifficultyCalculationUtils.Logistic( - sameRhythmHitObjects.Duration / hitWindow, + sameRhythmGroupedHitObjects.Duration / hitWindow, midpointOffset: 0.6, multiplier: 1, maxValue: 1); @@ -75,20 +75,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// - private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1) + private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) { - double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3); + double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3); - double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6 - ? sameInterval(sameRhythmHitObjects, 4) + double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6 + ? sameInterval(sameRhythmGroupedHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. // The duration penalty is based on hit object duration relative to hitWindow. - double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5); + double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5); return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; - double sameInterval(SameRhythmHitObjects startObject, int intervalCount) + double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount) { List intervals = new List(); var currentObject = startObject; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs similarity index 62% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs index b1ca22595b..930b3fc0e4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { @@ -10,35 +10,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// A base class for grouping s by their interval. In edges where an interval change /// occurs, the is added to the group with the smaller interval. /// - public abstract class SameRhythm - where ChildType : IHasInterval + public abstract class IntervalGroupedHitObjects + where TChildType : IHasInterval { - public IReadOnlyList Children { get; private set; } + public IReadOnlyList Children { get; private set; } /// - /// Determines if the intervals between two child objects are within a specified margin of error, - /// indicating that the intervals are effectively "flat" or consistent. - /// - private bool isFlat(ChildType current, ChildType previous, double marginOfError) - { - return Math.Abs(current.Interval - previous.Interval) <= marginOfError; - } - - /// - /// Create a new from a list of s, and add + /// Create a new from a list of s, and add /// them to the list until the end of the group. /// /// The list of s. /// /// Index in to start adding children. This will be modified and should be passed into - /// the next 's constructor. + /// the next 's constructor. /// /// /// The margin of error for the interval, within of which no interval change is considered to have occured. /// - protected SameRhythm(List data, ref int i, double marginOfError) + protected IntervalGroupedHitObjects(List data, ref int i, double marginOfError) { - List children = new List(); + List children = new List(); Children = children; children.Add(data[i]); i++; @@ -46,9 +37,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data for (; i < data.Count - 1; i++) { // An interval change occured, add the current data if the next interval is larger. - if (!isFlat(data[i], data[i + 1], marginOfError)) + if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) { - if (data[i + 1].Interval > data[i].Interval + marginOfError) + if (Precision.DefinitelyBigger(data[i].Interval, data[i + 1].Interval, marginOfError)) { children.Add(data[i]); i++; @@ -63,7 +54,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError)) + if (data.Count > 2 && Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) { children.Add(data[i]); i++; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs similarity index 50% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index 50839c4561..d4cbc9c1f9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -7,21 +7,21 @@ using System.Linq; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// - /// Represents grouped by their 's interval. + /// Represents grouped by their 's interval. /// - public class SamePatterns : SameRhythm + public class SamePatternsGroupedHitObjects : IntervalGroupedHitObjects { - public SamePatterns? Previous { get; private set; } + public SamePatternsGroupedHitObjects? Previous { get; private set; } /// - /// The between children within this group. - /// If there is only one child, this will have the value of the first child's . + /// The between children within this group. + /// If there is only one child, this will have the value of the first child's . /// public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; /// - /// The ratio of between this and the previous . In the - /// case where there is no previous , this will have a value of 1. + /// The ratio of between this and the previous . In the + /// case where there is no previous , this will have a value of 1. /// public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; @@ -29,26 +29,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); - private SamePatterns(SamePatterns? previous, List data, ref int i) + private SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List data, ref int i) : base(data, ref i, 5) { Previous = previous; foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) { - hitObject.Rhythm.SamePatterns = this; + hitObject.Rhythm.SamePatternsGroupedHitObjects = this; } } - public static void GroupPatterns(List data) + public static void GroupPatterns(List data) { - List samePatterns = new List(); + List samePatterns = new List(); - // Index does not need to be incremented, as it is handled within the SameRhythm constructor. + // Index does not need to be incremented, as it is handled within the IntervalGroupedHitObjects constructor. for (int i = 0; i < data.Count;) { - SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; - samePatterns.Add(new SamePatterns(previous, data, ref i)); + SamePatternsGroupedHitObjects? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; + samePatterns.Add(new SamePatternsGroupedHitObjects(previous, data, ref i)); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs similarity index 70% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 0ccc6da026..0b59433a2e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmHitObjects : SameRhythm, IHasInterval + public class SameRhythmGroupedHitObjects : IntervalGroupedHitObjects, IHasInterval { public TaikoDifficultyHitObject FirstHitObject => Children[0]; - public SameRhythmHitObjects? Previous; + public SameRhythmGroupedHitObjects? Previous; /// /// of the first hit object. @@ -26,30 +26,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public double Duration => Children[^1].StartTime - Children[0].StartTime; /// - /// The interval in ms of each hit object in this . This is only defined if there is - /// more than two hit objects in this . + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . /// public double? HitObjectInterval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// public double HitObjectIntervalRatio = 1; - /// - /// The interval between the of this and the previous . - /// - public double Interval { get; private set; } = double.PositiveInfinity; + /// + public double Interval { get; private set; } - public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i) + public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List data, ref int i) : base(data, ref i, 5) { Previous = previous; foreach (var hitObject in Children) { - hitObject.Rhythm.SameRhythmHitObjects = this; + hitObject.Rhythm.SameRhythmGroupedHitObjects = this; // Pass the HitObjectInterval to each child. hitObject.HitObjectInterval = HitObjectInterval; @@ -58,15 +56,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data calculateIntervals(); } - public static List GroupHitObjects(List data) + public static List GroupHitObjects(List data) { - List flatPatterns = new List(); + List flatPatterns = new List(); - // Index does not need to be incremented, as it is handled within SameRhythm's constructor. + // Index does not need to be incremented, as it is handled within IntervalGroupedHitObjects's constructor. for (int i = 0; i < data.Count;) { - SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; - flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i)); + SameRhythmGroupedHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; + flatPatterns.Add(new SameRhythmGroupedHitObjects(previous, data, ref i)); } return flatPatterns; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index beb7bfe5f6..351015ae08 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The group of hit objects with consistent rhythm that this object belongs to. /// - public SameRhythmHitObjects? SameRhythmHitObjects; + public SameRhythmGroupedHitObjects? SameRhythmGroupedHitObjects; /// /// The larger pattern of rhythm groups that this object is part of. /// - public SamePatterns? SamePatterns; + public SamePatternsGroupedHitObjects? SamePatternsGroupedHitObjects; /// /// The ratio of current From e0882d2a53d5452bb539bb9b16a0019b3f4094d2 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:33:40 +0000 Subject: [PATCH 43/86] Make `rescale` a static method --- .../Difficulty/TaikoDifficultyCalculator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 1d3075e4ac..e07a965ab0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -203,9 +203,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// Applies a final re-scaling of the star rating. /// /// The raw star rating value before re-scaling. - private double rescale(double sr) + private static double rescale(double sr) { - if (sr < 0) return sr; + if (sr < 0) + return sr; return 10.43 * Math.Log(sr / 8 + 1); } From 764b0001efc8ec7bc9aff48c525ee78f47b468aa Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:56:51 +0000 Subject: [PATCH 44/86] Fix typo in `ColourEvaluator` --- .../Difficulty/Evaluators/ColourEvaluator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index c0e90e83c1..166c01f507 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += evaluateReadingHitPatternDifficulty(colour.RepeatingHitPattern); + difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; @@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5; private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) => - DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateReadingHitPatternDifficulty(alternatingMonoPattern.Parent); + DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent); - private static double evaluateReadingHitPatternDifficulty(RepeatingHitPatterns repeatingHitPattern) => + private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) => 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } } From 1c4bc6dffd64126ab1b380ab0e6d11ff17c16a32 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 15:00:23 +0000 Subject: [PATCH 45/86] Revert `Precision.DefinitelyBigger` usage --- .../Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs index 930b3fc0e4..cc389d4091 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data // An interval change occured, add the current data if the next interval is larger. if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) { - if (Precision.DefinitelyBigger(data[i].Interval, data[i + 1].Interval, marginOfError)) + if (data[i + 1].Interval > data[i].Interval + marginOfError) { children.Add(data[i]); i++; From 14c68bcc583d1e980225da3f022176412ede3cb8 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 15:58:33 +0000 Subject: [PATCH 46/86] Replace weird `IntervalGroupedHitObjects` inheritance layer --- .../Rhythm/Data/IntervalGroupedHitObjects.cs | 64 ------------------- .../Data/SamePatternsGroupedHitObjects.cs | 27 ++------ .../Data/SameRhythmGroupedHitObjects.cs | 57 ++++------------- .../TaikoRhythmDifficultyPreprocessor.cs | 63 ++++++++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 1 + .../Difficulty/TaikoDifficultyCalculator.cs | 6 +- .../Rhythm => Utils}/IHasInterval.cs | 4 +- .../Difficulty/Utils/IntervalGroupingUtils.cs | 64 +++++++++++++++++++ 8 files changed, 152 insertions(+), 134 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs rename osu.Game.Rulesets.Taiko/Difficulty/{Preprocessing/Rhythm => Utils}/IHasInterval.cs (73%) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs deleted file mode 100644 index cc389d4091..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Utils; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data -{ - /// - /// A base class for grouping s by their interval. In edges where an interval change - /// occurs, the is added to the group with the smaller interval. - /// - public abstract class IntervalGroupedHitObjects - where TChildType : IHasInterval - { - public IReadOnlyList Children { get; private set; } - - /// - /// Create a new from a list of s, and add - /// them to the list until the end of the group. - /// - /// The list of s. - /// - /// Index in to start adding children. This will be modified and should be passed into - /// the next 's constructor. - /// - /// - /// The margin of error for the interval, within of which no interval change is considered to have occured. - /// - protected IntervalGroupedHitObjects(List data, ref int i, double marginOfError) - { - List children = new List(); - Children = children; - children.Add(data[i]); - i++; - - for (; i < data.Count - 1; i++) - { - // An interval change occured, add the current data if the next interval is larger. - if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) - { - if (data[i + 1].Interval > data[i].Interval + marginOfError) - { - children.Add(data[i]); - i++; - } - - return; - } - - // No interval change occured - children.Add(data[i]); - } - - // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. - // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) - { - children.Add(data[i]); - i++; - } - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index d4cbc9c1f9..cb22b2ef82 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -9,9 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents grouped by their 's interval. /// - public class SamePatternsGroupedHitObjects : IntervalGroupedHitObjects + public class SamePatternsGroupedHitObjects { - public SamePatternsGroupedHitObjects? Previous { get; private set; } + public IReadOnlyList Children { get; } + + public SamePatternsGroupedHitObjects? Previous { get; } /// /// The between children within this group. @@ -29,27 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); - private SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List data, ref int i) - : base(data, ref i, 5) + public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List children) { Previous = previous; - - foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) - { - hitObject.Rhythm.SamePatternsGroupedHitObjects = this; - } - } - - public static void GroupPatterns(List data) - { - List samePatterns = new List(); - - // Index does not need to be incremented, as it is handled within the IntervalGroupedHitObjects constructor. - for (int i = 0; i < data.Count;) - { - SamePatternsGroupedHitObjects? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; - samePatterns.Add(new SamePatternsGroupedHitObjects(previous, data, ref i)); - } + Children = children; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 0b59433a2e..dc6cf45d23 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -3,14 +3,17 @@ using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmGroupedHitObjects : IntervalGroupedHitObjects, IHasInterval + public class SameRhythmGroupedHitObjects : IHasInterval { + public List Children { get; private set; } + public TaikoDifficultyHitObject FirstHitObject => Children[0]; public SameRhythmGroupedHitObjects? Previous; @@ -40,53 +43,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// public double Interval { get; private set; } - public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List data, ref int i) - : base(data, ref i, 5) + public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List children) { Previous = previous; + Children = children; - foreach (var hitObject in Children) - { - hitObject.Rhythm.SameRhythmGroupedHitObjects = this; + // Calculate the average interval between hitobjects, or null if there are fewer than two + HitObjectInterval = Children.Count < 2 ? null : Duration / (Children.Count - 1); - // Pass the HitObjectInterval to each child. - hitObject.HitObjectInterval = HitObjectInterval; - } + // Calculate the ratio between this group's interval and the previous group's interval + HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null + ? HitObjectInterval.Value / Previous.HitObjectInterval.Value + : 1; - calculateIntervals(); - } - - public static List GroupHitObjects(List data) - { - List flatPatterns = new List(); - - // Index does not need to be incremented, as it is handled within IntervalGroupedHitObjects's constructor. - for (int i = 0; i < data.Count;) - { - SameRhythmGroupedHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; - flatPatterns.Add(new SameRhythmGroupedHitObjects(previous, data, ref i)); - } - - return flatPatterns; - } - - private void calculateIntervals() - { - // Calculate the average interval between hitobjects, or null if there are fewer than two. - HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1); - - // If both the current and previous intervals are available, calculate the ratio. - if (Previous?.HitObjectInterval != null && HitObjectInterval != null) - { - HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value; - } - - if (Previous == null) - { - return; - } - - Interval = StartTime - Previous.StartTime; + // Calculate the interval from the previous group's start time + Interval = Previous != null ? StartTime - Previous.StartTime : 0; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs new file mode 100644 index 0000000000..fa2135caf3 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +{ + public static class TaikoRhythmDifficultyPreprocessor + { + public static void ProcessAndAssign(List hitObjects) + { + var rhythmGroups = createSameRhythmGroupedHitObjects(hitObjects); + + foreach (var rhythmGroup in rhythmGroups) + { + foreach (var hitObject in rhythmGroup.Children) + { + hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; + hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; + } + } + + var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups); + + foreach (var patternGroup in patternGroups) + { + foreach (var hitObject in patternGroup.AllHitObjects) + { + hitObject.Rhythm.SamePatternsGroupedHitObjects = patternGroup; + } + } + } + + private static List createSameRhythmGroupedHitObjects(List hitObjects) + { + var rhythmGroups = new List(); + var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); + + foreach (var group in groups) + { + var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; + rhythmGroups.Add(new SameRhythmGroupedHitObjects(previous, group)); + } + + return rhythmGroups; + } + + private static List createSamePatternGroupedHitObjects(List rhythmGroups) + { + var patternGroups = new List(); + var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); + + foreach (var group in groups) + { + var previous = patternGroups.Count > 0 ? patternGroups[^1] : null; + patternGroups.Add(new SamePatternsGroupedHitObjects(previous, group)); + } + + return patternGroups; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 34c4871a42..0c668797cd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index e07a965ab0..acd654f9b8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -91,9 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - - var groupedHitObjects = SameRhythmGroupedHitObjects.GroupHitObjects(noteObjects); - SamePatternsGroupedHitObjects.GroupPatterns(groupedHitObjects); + TaikoRhythmDifficultyPreprocessor.ProcessAndAssign(noteObjects); return difficultyHitObjects; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs similarity index 73% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs index 32b148da2e..8f80bb6079 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { /// - /// The interface for hitobjects that provide an interval value. + /// The interface for objects that provide an interval value. /// public interface IHasInterval { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs new file mode 100644 index 0000000000..22ded8a966 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + public static class IntervalGroupingUtils + { + public static List> GroupByInterval(IReadOnlyList data, double marginOfError = 5) where T : IHasInterval + { + var groups = new List>(); + if (data.Count == 0) + return groups; + + int i = 0; + + while (i < data.Count) + { + var group = createGroup(data, ref i, marginOfError); + groups.Add(group); + } + + return groups; + } + + private static List createGroup(IReadOnlyList data, ref int i, double marginOfError) where T : IHasInterval + { + var children = new List { data[i] }; + i++; + + for (; i < data.Count - 1; i++) + { + // An interval change occured, add the current data if the next interval is larger. + if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) + { + if (data[i + 1].Interval > data[i].Interval + marginOfError) + { + children.Add(data[i]); + i++; + } + + return children; + } + + // No interval change occurred + children.Add(data[i]); + } + + // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // If true, add the current object to the group and increment the index to process the next object. + if (data.Count > 2 && i < data.Count && + Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) + { + children.Add(data[i]); + i++; + } + + return children; + } + } +} From 2c0d6b14c82969a850b292f785a678016e06ed26 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 22 Jan 2025 13:24:30 +0000 Subject: [PATCH 47/86] Fix incorrect namespace --- .../Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs | 1 + .../Difficulty/Utils/IntervalGroupingUtils.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index fa2135caf3..cd56d835dc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 22ded8a966..3b6f5406b4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using osu.Framework.Utils; -using osu.Game.Rulesets.Taiko.Difficulty.Utils; -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { public static class IntervalGroupingUtils { From 753e9ef7c79f85d027557295c0c60fb4fa09210c Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 22 Jan 2025 13:26:12 +0000 Subject: [PATCH 48/86] Keep old behaviour of `double.PositiveInfinity` being the default for `Interval` --- .../Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index dc6cf45d23..4f7023059f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data : 1; // Calculate the interval from the previous group's start time - Interval = Previous != null ? StartTime - Previous.StartTime : 0; + Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity; } } } From 8f17a44976439ba30c8ee13f1200d72821847c5a Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 23 Jan 2025 10:29:04 +0000 Subject: [PATCH 49/86] Remove unused default value --- .../Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 4f7023059f..b77176b49d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// - public double HitObjectIntervalRatio = 1; + public double HitObjectIntervalRatio; /// public double Interval { get; private set; } From a7aa553445738068eb8075043cb64187ed6b73dc Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 26 Jan 2025 16:21:07 +0000 Subject: [PATCH 50/86] Fix incorrect `startTime` calculation --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 0c668797cd..486841b995 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -118,13 +118,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteObjects.Add(this); } - double startTime = hitObject.StartTime * clockRate; - // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(hitObject.StartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, startTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, hitObject.StartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 13c956c2482ee8ff81e83f283de9f17910ad189d Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 26 Jan 2025 20:15:13 +0000 Subject: [PATCH 51/86] Account for floating point errors --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 486841b995..f9ca2707ab 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -118,11 +118,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteObjects.Add(this); } + // Using `hitObject.StartTime` causes floating point error differences + double normalizedStartTime = StartTime * clockRate; + // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(hitObject.StartTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalizedStartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, hitObject.StartTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalizedStartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 71b89c390fe7d672ec8f1f61bbea31352315a4fb Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Mon, 27 Jan 2025 12:54:22 +0000 Subject: [PATCH 52/86] Rename class, rename children to hit objects and groups, make fields un-settable --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 12 ++++---- .../Data/SamePatternsGroupedHitObjects.cs | 22 +++++++------- ...ects.cs => SameRhythmHitObjectGrouping.cs} | 30 +++++++++---------- .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 2 +- .../TaikoRhythmDifficultyPreprocessor.cs | 10 +++---- 5 files changed, 38 insertions(+), 38 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythmGroupedHitObjects.cs => SameRhythmHitObjectGrouping.cs} (65%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 8accc6124c..f4686f2fe3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return difficulty; } - private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow) + private static double evaluateDifficultyOf(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow) { double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio); double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval; @@ -47,9 +47,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow); // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1) + if (previousInterval != null && sameRhythmGroupedHitObjects.HitObjects.Count > 1) { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count; + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.HitObjects.Count; double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious; if (durationDifference > 0) @@ -75,11 +75,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// - private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) + private static double repeatedIntervalPenalty(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) { double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3); - double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6 + double shortIntervalPenalty = sameRhythmGroupedHitObjects.HitObjects.Count < 6 ? sameInterval(sameRhythmGroupedHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; - double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount) + double sameInterval(SameRhythmHitObjectGrouping startObject, int intervalCount) { List intervals = new List(); var currentObject = startObject; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index cb22b2ef82..938cb4670f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -7,34 +7,34 @@ using System.Linq; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// - /// Represents grouped by their 's interval. + /// Represents grouped by their 's interval. /// public class SamePatternsGroupedHitObjects { - public IReadOnlyList Children { get; } + public IReadOnlyList Groups { get; } public SamePatternsGroupedHitObjects? Previous { get; } /// - /// The between children within this group. - /// If there is only one child, this will have the value of the first child's . + /// The between groups . + /// If there is only one group, this will have the value of the first group's . /// - public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; + public double GroupInterval => Groups.Count > 1 ? Groups[1].Interval : Groups[0].Interval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where there is no previous , this will have a value of 1. /// - public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; + public double IntervalRatio => GroupInterval / Previous?.GroupInterval ?? 1.0d; - public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject; + public TaikoDifficultyHitObject FirstHitObject => Groups[0].FirstHitObject; - public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); + public IEnumerable AllHitObjects => Groups.SelectMany(hitObject => hitObject.HitObjects); - public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List children) + public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List groups) { Previous = previous; - Children = children; + Groups = groups; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs similarity index 65% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs index b77176b49d..9caa9b9958 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs @@ -10,46 +10,46 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmGroupedHitObjects : IHasInterval + public class SameRhythmHitObjectGrouping : IHasInterval { - public List Children { get; private set; } + public readonly List HitObjects; - public TaikoDifficultyHitObject FirstHitObject => Children[0]; + public TaikoDifficultyHitObject FirstHitObject => HitObjects[0]; - public SameRhythmGroupedHitObjects? Previous; + public readonly SameRhythmHitObjectGrouping? Previous; /// /// of the first hit object. /// - public double StartTime => Children[0].StartTime; + public double StartTime => HitObjects[0].StartTime; /// /// The interval between the first and final hit object within this group. /// - public double Duration => Children[^1].StartTime - Children[0].StartTime; + public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime; /// - /// The interval in ms of each hit object in this . This is only defined if there is - /// more than two hit objects in this . + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . /// - public double? HitObjectInterval; + public readonly double? HitObjectInterval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// - public double HitObjectIntervalRatio; + public readonly double HitObjectIntervalRatio; /// - public double Interval { get; private set; } + public double Interval { get; } - public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List children) + public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List hitObjects) { Previous = previous; - Children = children; + HitObjects = hitObjects; // Calculate the average interval between hitobjects, or null if there are fewer than two - HitObjectInterval = Children.Count < 2 ? null : Duration / (Children.Count - 1); + HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1); // Calculate the ratio between this group's interval and the previous group's interval HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index 351015ae08..3503a836fa 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The group of hit objects with consistent rhythm that this object belongs to. /// - public SameRhythmGroupedHitObjects? SameRhythmGroupedHitObjects; + public SameRhythmHitObjectGrouping? SameRhythmGroupedHitObjects; /// /// The larger pattern of rhythm groups that this object is part of. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index cd56d835dc..3ebc0c25b7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var rhythmGroup in rhythmGroups) { - foreach (var hitObject in rhythmGroup.Children) + foreach (var hitObject in rhythmGroup.HitObjects) { hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; @@ -33,21 +33,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm } } - private static List createSameRhythmGroupedHitObjects(List hitObjects) + private static List createSameRhythmGroupedHitObjects(List hitObjects) { - var rhythmGroups = new List(); + var rhythmGroups = new List(); var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); foreach (var group in groups) { var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; - rhythmGroups.Add(new SameRhythmGroupedHitObjects(previous, group)); + rhythmGroups.Add(new SameRhythmHitObjectGrouping(previous, group)); } return rhythmGroups; } - private static List createSamePatternGroupedHitObjects(List rhythmGroups) + private static List createSamePatternGroupedHitObjects(List rhythmGroups) { var patternGroups = new List(); var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); From f3c17f1c2b73e4f12fd00b130bd8326ca17a74e6 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Mon, 27 Jan 2025 12:56:33 +0000 Subject: [PATCH 53/86] Use correct English --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index f9ca2707ab..d6a2d5874e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -119,13 +119,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } // Using `hitObject.StartTime` causes floating point error differences - double normalizedStartTime = StartTime * clockRate; + double normalisedStartTime = StartTime * clockRate; // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalizedStartTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalisedStartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalizedStartTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 46144960e50fc49867bccaed2ee035e983a05718 Mon Sep 17 00:00:00 2001 From: "Rian (Reza Mouna Hendrian)" <52914632+Rian8337@users.noreply.github.com> Date: Thu, 30 Jan 2025 03:06:05 +0800 Subject: [PATCH 54/86] Remove unnecessary strain sorting in difficult slider count (#31724) --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 89adda302c..6f1b680211 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -53,13 +53,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (sliderStrains.Count == 0) return 0; - double[] sortedStrains = sliderStrains.OrderDescending().ToArray(); - - double maxSliderStrain = sortedStrains.Max(); + double maxSliderStrain = sliderStrains.Max(); if (maxSliderStrain == 0) return 0; - return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); + return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); } } } From 2ee480c442436bb442b8b6171e2f42b86c3cbfa8 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 30 Jan 2025 13:58:38 +0000 Subject: [PATCH 55/86] Clamp `estimateImproperlyFollowedDifficultSliders` between 0 and `attributes.AimDifficultSliderCount` (#31736) --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index f191180630..dc2df39cdb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { // We add tick misses here since they too mean that the player didn't follow the slider properly // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly - estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount); + estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, attributes.AimDifficultSliderCount); } double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor; From fa844b0ebc783222beadd1e6889dada450823219 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:01:59 +0900 Subject: [PATCH 56/86] Rename `Colour` / `Rhythm` related fields and classes --- .../Difficulty/Evaluators/ColourEvaluator.cs | 18 +++++----- .../Difficulty/Evaluators/RhythmEvaluator.cs | 12 +++---- .../Difficulty/Evaluators/StaminaEvaluator.cs | 4 +-- ...yHitObjectColour.cs => TaikoColourData.cs} | 2 +- .../TaikoColourDifficultyPreprocessor.cs | 10 +++--- ...yHitObjectRhythm.cs => TaikoRhythmData.cs} | 35 +++++++++---------- .../TaikoRhythmDifficultyPreprocessor.cs | 4 +-- .../Preprocessing/TaikoDifficultyHitObject.cs | 8 ++--- .../Difficulty/Skills/Reading.cs | 2 +- .../Difficulty/Skills/Stamina.cs | 2 +- 10 files changed, 48 insertions(+), 49 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/{TaikoDifficultyHitObjectColour.cs => TaikoColourData.cs} (96%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/{TaikoDifficultyHitObjectRhythm.cs => TaikoRhythmData.cs} (75%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 166c01f507..b715dfc37a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); - double currentRatio = current.Rhythm.Ratio; - double previousRatio = previousHitObject.Rhythm.Ratio; + double currentRatio = current.RhythmData.Ratio; + double previousRatio = previousHitObject.RhythmData.Ratio; // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error. if (Math.Abs(1 - currentRatio / previousRatio) <= threshold) @@ -61,17 +61,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) { var taikoObject = (TaikoDifficultyHitObject)hitObject; - TaikoDifficultyHitObjectColour colour = taikoObject.Colour; + TaikoColourData colourData = taikoObject.ColourData; double difficulty = 0.0d; - if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak - difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak); + if (colourData.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak + difficulty += evaluateMonoStreakDifficulty(colourData.MonoStreak); - if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern - difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); + if (colourData.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern + difficulty += evaluateAlternatingMonoPatternDifficulty(colourData.AlternatingMonoPattern); - if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern); + if (colourData.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern + difficulty += evaluateRepeatingHitPatternsDifficulty(colourData.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index f4686f2fe3..3b3aea07f3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -18,21 +18,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) { - TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + TaikoRhythmData rhythmData = ((TaikoDifficultyHitObject)hitObject).RhythmData; double difficulty = 0.0d; double sameRhythm = 0; double samePattern = 0; double intervalPenalty = 0; - if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects + if (rhythmData.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythmData.SameRhythmGroupedHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythmData.SameRhythmGroupedHitObjects, hitWindow); } - if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects - samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio); + if (rhythmData.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects + samePattern += 1.15 * ratioDifficulty(rhythmData.SamePatternsGroupedHitObjects.IntervalRatio); difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index a9884b2328..32ed8ec189 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -55,8 +55,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// private static int availableFingersFor(TaikoDifficultyHitObject hitObject) { - DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange; - DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange; + DifficultyHitObject? previousColourChange = hitObject.ColourData.PreviousColourChange; + DifficultyHitObject? nextColourChange = hitObject.ColourData.NextColourChange; if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs index abf6fb3672..81201b6584 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// /// Stores colour compression information for a . /// - public class TaikoDifficultyHitObjectColour + public class TaikoColourData { /// /// The that encodes this note. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 18a299ae92..3c6ef7c53c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public static class TaikoColourDifficultyPreprocessor { /// - /// Processes and encodes a list of s into a list of s, - /// assigning the appropriate s to each . + /// Processes and encodes a list of s into a list of s, + /// assigning the appropriate s to each . /// public static void ProcessAndAssign(List hitObjects) { @@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour foreach (var hitObject in monoStreak.HitObjects) { - hitObject.Colour.RepeatingHitPattern = repeatingHitPattern; - hitObject.Colour.AlternatingMonoPattern = monoPattern; - hitObject.Colour.MonoStreak = monoStreak; + hitObject.ColourData.RepeatingHitPattern = repeatingHitPattern; + hitObject.ColourData.AlternatingMonoPattern = monoPattern; + hitObject.ColourData.MonoStreak = monoStreak; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs similarity index 75% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index 3503a836fa..d895dcfc55 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// Stores rhythm data for a . /// - public class TaikoDifficultyHitObjectRhythm + public class TaikoRhythmData { /// /// The group of hit objects with consistent rhythm that this object belongs to. @@ -39,25 +39,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). /// /// - private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = + private static readonly TaikoRhythmData[] common_rhythms = { - new TaikoDifficultyHitObjectRhythm(1, 1), - new TaikoDifficultyHitObjectRhythm(2, 1), - new TaikoDifficultyHitObjectRhythm(1, 2), - new TaikoDifficultyHitObjectRhythm(3, 1), - new TaikoDifficultyHitObjectRhythm(1, 3), - new TaikoDifficultyHitObjectRhythm(3, 2), - new TaikoDifficultyHitObjectRhythm(2, 3), - new TaikoDifficultyHitObjectRhythm(5, 4), - new TaikoDifficultyHitObjectRhythm(4, 5) + new TaikoRhythmData(1, 1), + new TaikoRhythmData(2, 1), + new TaikoRhythmData(1, 2), + new TaikoRhythmData(3, 1), + new TaikoRhythmData(1, 3), + new TaikoRhythmData(3, 2), + new TaikoRhythmData(2, 3), + new TaikoRhythmData(5, 4), + new TaikoRhythmData(4, 5) }; /// - /// Initialises a new instance of s, + /// Initialises a new instance of s, /// calculating the closest rhythm change and its associated difficulty for the current hit object. /// /// The current being processed. - public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current) + public TaikoRhythmData(TaikoDifficultyHitObject current) { var previous = current.Previous(0); @@ -67,8 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm return; } - TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime); - Ratio = closestRhythm.Ratio; + TaikoRhythmData closestRhythmData = getClosestRhythm(current.DeltaTime, previous.DeltaTime); + Ratio = closestRhythmData.Ratio; } /// @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The numerator for . /// The denominator for - private TaikoDifficultyHitObjectRhythm(int numerator, int denominator) + private TaikoRhythmData(int numerator, int denominator) { Ratio = numerator / (double)denominator; } @@ -88,11 +88,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// The time difference between the current hit object and the previous one. /// The time difference between the previous hit object and the one before it. /// The closest matching rhythm from . - private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime) + private TaikoRhythmData getClosestRhythm(double currentDeltaTime, double previousDeltaTime) { double ratio = currentDeltaTime / previousDeltaTime; return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } } - diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 3ebc0c25b7..45cc29c99e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { foreach (var hitObject in rhythmGroup.HitObjects) { - hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; + hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup; hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; } } @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { foreach (var hitObject in patternGroup.AllHitObjects) { - hitObject.Rhythm.SamePatternsGroupedHitObjects = patternGroup; + hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index d6a2d5874e..5c5503c25d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// The rhythm required to hit this hit object. /// - public readonly TaikoDifficultyHitObjectRhythm Rhythm; + public readonly TaikoRhythmData RhythmData; /// /// The interval between this hit object and the surrounding hit objects in its rhythm group. @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. /// - public readonly TaikoDifficultyHitObjectColour Colour; + public readonly TaikoColourData ColourData; /// /// The adjusted BPM of this hit object, based on its slider velocity and scroll speed. @@ -92,10 +92,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteDifficultyHitObjects = noteObjects; // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor - Colour = new TaikoDifficultyHitObjectColour(); + ColourData = new TaikoColourData(); // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm - Rhythm = new TaikoDifficultyHitObjectRhythm(this); + RhythmData = new TaikoRhythmData(this); switch ((hitObject as Hit)?.Type) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs index 885131404a..7be1107b70 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } var taikoObject = (TaikoDifficultyHitObject)current; - int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; + int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 12e1396dd7..0e1f3d41cf 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // Safely prevents previous strains from shifting as new notes are added. var currentObject = current as TaikoDifficultyHitObject; - int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; + int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); From 709ad02a517606b07b6a4aaf3f55e611a94219c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:09:51 +0900 Subject: [PATCH 57/86] Simplify `TaikoRhythmData`'s ratio computation --- .../Preprocessing/Rhythm/TaikoRhythmData.cs | 68 +++++++------------ 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index d895dcfc55..6c4a332624 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -27,30 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// to previous for the rhythm change. /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. /// - public readonly double Ratio; - - /// - /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. - /// /// - /// The general guidelines for the values are: - /// - /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, - /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). - /// + /// This is snapped to the closest matching . /// - private static readonly TaikoRhythmData[] common_rhythms = - { - new TaikoRhythmData(1, 1), - new TaikoRhythmData(2, 1), - new TaikoRhythmData(1, 2), - new TaikoRhythmData(3, 1), - new TaikoRhythmData(1, 3), - new TaikoRhythmData(3, 2), - new TaikoRhythmData(2, 3), - new TaikoRhythmData(5, 4), - new TaikoRhythmData(4, 5) - }; + public readonly double Ratio; /// /// Initialises a new instance of s, @@ -67,31 +47,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm return; } - TaikoRhythmData closestRhythmData = getClosestRhythm(current.DeltaTime, previous.DeltaTime); - Ratio = closestRhythmData.Ratio; + double actualRatio = current.DeltaTime / previous.DeltaTime; + double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First(); + + Ratio = closestRatio; } /// - /// Creates an object representing a rhythm change. + /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. /// - /// The numerator for . - /// The denominator for - private TaikoRhythmData(int numerator, int denominator) + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// + private static readonly double[] common_ratios = new[] { - Ratio = numerator / (double)denominator; - } - - /// - /// Determines the closest rhythm change from that matches the timing ratio - /// between the current and previous intervals. - /// - /// The time difference between the current hit object and the previous one. - /// The time difference between the previous hit object and the one before it. - /// The closest matching rhythm from . - private TaikoRhythmData getClosestRhythm(double currentDeltaTime, double previousDeltaTime) - { - double ratio = currentDeltaTime / previousDeltaTime; - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } + 1.0 / 1, + 2.0 / 1, + 1.0 / 2, + 3.0 / 1, + 1.0 / 3, + 3.0 / 2, + 2.0 / 3, + 5.0 / 4, + 4.0 / 5 + }; } } From fc933902844ce21ffa6961920dd96bbe47d94fa1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:10:15 +0900 Subject: [PATCH 58/86] Remove unused `HitObjectInterval` --- .../Rhythm/TaikoRhythmDifficultyPreprocessor.cs | 5 ----- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 45cc29c99e..8b126f85ce 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -16,10 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var rhythmGroup in rhythmGroups) { foreach (var hitObject in rhythmGroup.HitObjects) - { hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup; - hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; - } } var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups); @@ -27,9 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var patternGroup in patternGroups) { foreach (var hitObject in patternGroup.AllHitObjects) - { hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup; - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 5c5503c25d..489b36b259 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoRhythmData RhythmData; - /// - /// The interval between this hit object and the surrounding hit objects in its rhythm group. - /// - public double? HitObjectInterval { get; set; } - /// /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. From 325483192a26f41d7019c4cf28c22fe91da1f1e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:13:04 +0900 Subject: [PATCH 59/86] Tidy up xmldoc and remove another unused field --- .../Preprocessing/TaikoDifficultyHitObject.cs | 52 ++++++++----------- .../Difficulty/TaikoDifficultyCalculator.cs | 1 - .../Difficulty/Utils/IHasInterval.cs | 2 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 489b36b259..f407e13ff1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; @@ -39,13 +40,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly int NoteIndex; /// - /// The rhythm required to hit this hit object. + /// Rhythm data used by . + /// This is populated via . /// public readonly TaikoRhythmData RhythmData; /// - /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used - /// by other skills in the future. + /// Colour data used by and . + /// This is populated via . /// public readonly TaikoColourData ColourData; @@ -54,19 +56,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public double EffectiveBPM; - /// - /// The current slider velocity of this hit object. - /// - public double CurrentSliderVelocity; - - public double Interval => DeltaTime; - /// /// Creates a new difficulty hit object. /// /// The gameplay associated with this difficulty object. /// The gameplay preceding . - /// The gameplay preceding . /// The rate of the gameplay clock. Modified by speed-changing mods. /// The list of all s in the current beatmap. /// The list of centre (don) s in the current beatmap. @@ -75,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The position of this in the list. /// The control point info of the beatmap. /// The global slider velocity of the beatmap. - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, @@ -86,29 +80,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { noteDifficultyHitObjects = noteObjects; - // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor ColourData = new TaikoColourData(); - - // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm RhythmData = new TaikoRhythmData(this); - switch ((hitObject as Hit)?.Type) + if (hitObject is Hit hit) { - case HitType.Centre: - MonoIndex = centreHitObjects.Count; - centreHitObjects.Add(this); - monoDifficultyHitObjects = centreHitObjects; - break; + switch (hit.Type) + { + case HitType.Centre: + MonoIndex = centreHitObjects.Count; + centreHitObjects.Add(this); + monoDifficultyHitObjects = centreHitObjects; + break; - case HitType.Rim: - MonoIndex = rimHitObjects.Count; - rimHitObjects.Add(this); - monoDifficultyHitObjects = rimHitObjects; - break; - } + case HitType.Rim: + MonoIndex = rimHitObjects.Count; + rimHitObjects.Add(this); + monoDifficultyHitObjects = rimHitObjects; + break; + } - if (hitObject is Hit) - { NoteIndex = noteObjects.Count; noteObjects.Add(this); } @@ -121,7 +112,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // Calculate the slider velocity at the note's start time. double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate); - CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; } @@ -142,5 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1)); public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1)); + + public double Interval => DeltaTime; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index acd654f9b8..6b9986bd68 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty difficultyHitObjects.Add(new TaikoDifficultyHitObject( beatmap.HitObjects[i], beatmap.HitObjects[i - 1], - beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, centreObjects, diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs index 8f80bb6079..a42940180c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils public interface IHasInterval { /// - /// The interval between 2 objects start times. + /// The interval – ie delta time – between this object and a known previous object. /// double Interval { get; } } From 8447679db9f038b5ddfefbe7337d87ea38000c22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:41:31 +0900 Subject: [PATCH 60/86] Initial tidy-up pass on `IntervalGroupingUtils` --- .../TaikoRhythmDifficultyPreprocessor.cs | 17 +++----- .../Difficulty/Utils/IntervalGroupingUtils.cs | 41 ++++++++----------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 8b126f85ce..5bc0fdbc03 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Utils; @@ -31,13 +32,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm private static List createSameRhythmGroupedHitObjects(List hitObjects) { var rhythmGroups = new List(); - var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); - foreach (var group in groups) - { - var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; - rhythmGroups.Add(new SameRhythmHitObjectGrouping(previous, group)); - } + foreach (var grouped in IntervalGroupingUtils.GroupByInterval(hitObjects)) + rhythmGroups.Add(new SameRhythmHitObjectGrouping(rhythmGroups.LastOrDefault(), grouped)); return rhythmGroups; } @@ -45,13 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm private static List createSamePatternGroupedHitObjects(List rhythmGroups) { var patternGroups = new List(); - var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); - foreach (var group in groups) - { - var previous = patternGroups.Count > 0 ? patternGroups[^1] : null; - patternGroups.Add(new SamePatternsGroupedHitObjects(previous, group)); - } + foreach (var grouped in IntervalGroupingUtils.GroupByInterval(rhythmGroups)) + patternGroups.Add(new SamePatternsGroupedHitObjects(patternGroups.LastOrDefault(), grouped)); return patternGroups; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 3b6f5406b4..f04dec1c08 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -8,56 +8,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { public static class IntervalGroupingUtils { - public static List> GroupByInterval(IReadOnlyList data, double marginOfError = 5) where T : IHasInterval + public static List> GroupByInterval(IReadOnlyList objects) where T : IHasInterval { var groups = new List>(); - if (data.Count == 0) - return groups; int i = 0; - - while (i < data.Count) - { - var group = createGroup(data, ref i, marginOfError); - groups.Add(group); - } + while (i < objects.Count) + groups.Add(createNextGroup(objects, ref i)); return groups; } - private static List createGroup(IReadOnlyList data, ref int i, double marginOfError) where T : IHasInterval + private static List createNextGroup(IReadOnlyList objects, ref int i) where T : IHasInterval { - var children = new List { data[i] }; + const double margin_of_error = 5; + + var groupedObjects = new List { objects[i] }; i++; - for (; i < data.Count - 1; i++) + for (; i < objects.Count - 1; i++) { - // An interval change occured, add the current data if the next interval is larger. - if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) + // An interval change occured, add the current object if the next interval is larger. + if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error)) { - if (data[i + 1].Interval > data[i].Interval + marginOfError) + if (objects[i + 1].Interval > objects[i].Interval + margin_of_error) { - children.Add(data[i]); + groupedObjects.Add(objects[i]); i++; } - return children; + return groupedObjects; } // No interval change occurred - children.Add(data[i]); + groupedObjects.Add(objects[i]); } - // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error. // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && i < data.Count && - Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) + if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error)) { - children.Add(data[i]); + groupedObjects.Add(objects[i]); i++; } - return children; + return groupedObjects; } } } From 40ea7ff2383248c4e3cdbd2c042cf692792f7bd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:48:48 +0900 Subject: [PATCH 61/86] Add better documentation for interval change code --- .../Difficulty/Utils/IntervalGroupingUtils.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index f04dec1c08..7bd7aa7677 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -28,9 +28,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils for (; i < objects.Count - 1; i++) { - // An interval change occured, add the current object if the next interval is larger. if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error)) { + // When an interval change occurs, include the object with the differing interval in the case it increased + // See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale. if (objects[i + 1].Interval > objects[i].Interval + margin_of_error) { groupedObjects.Add(objects[i]); From 4f6fd68a9195d170eeca5983e0a76d5e5fcc78b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 13:54:35 +0900 Subject: [PATCH 62/86] Fix inspections --- .../Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index 6c4a332624..247fb06dc0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm } double actualRatio = current.DeltaTime / previous.DeltaTime; - double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First(); + double closestRatio = common_ratios.MinBy(r => Math.Abs(r - actualRatio)); Ratio = closestRatio; } @@ -63,8 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). /// /// - private static readonly double[] common_ratios = new[] - { + private static readonly double[] common_ratios = + [ 1.0 / 1, 2.0 / 1, 1.0 / 2, @@ -74,6 +74,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm 2.0 / 3, 5.0 / 4, 4.0 / 5 - }; + ]; } } From 9f90ebb2f774bd023befdc73a849fe087cca9550 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Fri, 7 Feb 2025 10:21:12 +0000 Subject: [PATCH 63/86] Calculate hit windows in performance calculator instead of databased difficulty attributes (#31735) * Calculate hit windows in performance calculator instead of databased difficulty attributes * Apply mods to beatmap difficulty in osu! performance calculator * Remove `GreatHitWindow` difficulty attribute for osu!mania * Remove use of approach rate and overall difficulty attributes for osu! * Remove use of hit window difficulty attributes in osu!taiko * Remove use of approach rate attribute in osu!catch * Remove unused attribute IDs * Code quality * Fix `computeDeviationUpperBound` being called before `greatHitWindow` is set --- .../Difficulty/CatchDifficultyAttributes.cs | 12 --- .../Difficulty/CatchDifficultyCalculator.cs | 4 - .../Difficulty/CatchPerformanceCalculator.cs | 17 ++++- .../Difficulty/ManiaDifficultyAttributes.cs | 12 --- .../Difficulty/ManiaDifficultyCalculator.cs | 29 -------- .../Difficulty/OsuDifficultyAttributes.cs | 41 ---------- .../Difficulty/OsuDifficultyCalculator.cs | 15 ---- .../Difficulty/OsuPerformanceCalculator.cs | 74 +++++++++++++------ .../Difficulty/TaikoDifficultyAttributes.cs | 22 ------ .../Difficulty/TaikoDifficultyCalculator.cs | 5 -- .../Difficulty/TaikoPerformanceCalculator.cs | 30 ++++++-- .../Difficulty/DifficultyAttributes.cs | 6 -- 12 files changed, 92 insertions(+), 175 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 5c64643fd4..82c3cfe735 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyAttributes : DifficultyAttributes { - /// - /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("approach_rate")] - public double ApproachRate { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) @@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Todo: osu!catch should not output star rating in the 'aim' attribute. yield return (ATTRIB_ID_AIM, StarRating); - yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_AIM]; - ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 99df2731ff..6434adb63c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -36,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (beatmap.HitObjects.Count == 0) return new CatchDifficultyAttributes { Mods = mods }; - // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier, Mods = mods, - ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.GetMaxCombo(), }; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 55232a9598..62a9fe250e 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -3,6 +3,9 @@ using System; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (catchAttributes.MaxCombo > 0) value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0); - double approachRate = catchAttributes.ApproachRate; + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + + // this is the same as osu!, so there's potential to share the implementation... maybe + double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + + double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0; + double approachRateFactor = 1.0; if (approachRate > 9.0) approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9 diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index db60e757e1..512d98f713 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -10,22 +9,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaDifficultyAttributes : DifficultyAttributes { - /// - /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods do not affect the hit window at all in osu-stable. - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) yield return v; yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -33,7 +22,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 1efa7cb42f..06b8018f2b 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty private const double difficulty_multiplier = 0.018; private readonly bool isForCurrentRuleset; - private readonly double originalOverallDifficulty; public override int Version => 20241007; @@ -35,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty : base(ruleset, beatmap) { isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); - originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty { StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier, Mods = mods, - // In osu-stable mania, rate-adjustment mods don't affect the hit window. - // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. - GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), }; @@ -124,29 +119,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty }).ToArray(); } } - - private double getHitWindow300(Mod[] mods) - { - if (isForCurrentRuleset) - { - double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty)); - return applyModAdjustments(34 + 3 * od, mods); - } - - if (Math.Round(originalOverallDifficulty) > 4) - return applyModAdjustments(34, mods); - - return applyModAdjustments(47, mods); - - static double applyModAdjustments(double value, Mod[] mods) - { - if (mods.Any(m => m is ManiaModHardRock)) - value /= 1.4; - else if (mods.Any(m => m is ManiaModEasy)) - value *= 1.4; - - return value; - } - } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 395f581b65..f7d8c649c1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -59,36 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("speed_difficult_strain_count")] public double SpeedDifficultStrainCount { get; set; } - /// - /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("approach_rate")] - public double ApproachRate { get; set; } - - /// - /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("overall_difficulty")] - public double OverallDifficulty { get; set; } - - /// - /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - - /// - /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("ok_hit_window")] - public double OkHitWindow { get; set; } - - /// - /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("meh_hit_window")] - public double MehHitWindow { get; set; } - /// /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. /// @@ -116,10 +86,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_AIM, AimDifficulty); yield return (ATTRIB_ID_SPEED, SpeedDifficulty); - yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); - yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); if (ShouldSerializeFlashlightDifficulty()) yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); @@ -130,9 +97,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); - - yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); - yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -141,18 +105,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty AimDifficulty = values[ATTRIB_ID_AIM]; SpeedDifficulty = values[ATTRIB_ID_SPEED]; - OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; - ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; - OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; - MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 1505c51592..30339fbaa7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -90,20 +88,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); - HitWindows hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - - double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; - double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate; - double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate; - OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { StarRating = starRating, @@ -116,11 +106,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SliderFactor = sliderFactor, AimDifficultStrainCount = aimDifficultyStrainCount, SpeedDifficultStrainCount = speedDifficultyStrainCount, - ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, - OverallDifficulty = (80 - hitWindowGreat) / 6, - GreatHitWindow = hitWindowGreat, - OkHitWindow = hitWindowOk, - MehHitWindow = hitWindowMeh, DrainRate = drainRate, MaxCombo = beatmap.GetMaxCombo(), HitCircleCount = hitCirclesCount, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index dc2df39cdb..09ec890926 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -4,10 +4,15 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -41,6 +46,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double clockRate; + private double greatHitWindow; + private double okHitWindow; + private double mehHitWindow; + private double overallDifficulty; + private double approachRate; + private double? speedDeviation; public OsuPerformanceCalculator() @@ -64,6 +76,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss); effectiveMissCount = countMiss; + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; + + HitWindows hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(difficulty.OverallDifficulty); + + greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate; + okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate; + mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate; + + double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + + overallDifficulty = (80 - greatHitWindow) / 6; + approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5; + if (osuAttributes.SliderCount > 0) { if (usingClassicSliderAccuracy) @@ -106,8 +138,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // https://www.desmos.com/calculator/bc9eybdthb // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0 // this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11) - double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0); - double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0); + double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0); + double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0); // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); @@ -178,10 +210,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount); double approachRateFactor = 0.0; - if (attributes.ApproachRate > 10.33) - approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); - else if (attributes.ApproachRate < 8.0) - approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate); + if (approachRate > 10.33) + approachRateFactor = 0.3 * (approachRate - 10.33); + else if (approachRate < 8.0) + approachRateFactor = 0.05 * (8.0 - approachRate); if (score.Mods.Any(h => h is OsuModRelax)) approachRateFactor = 0.0; @@ -193,12 +225,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); + aimValue *= 1.0 + 0.04 * (12.0 - approachRate); } aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; return aimValue; } @@ -218,8 +250,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount); double approachRateFactor = 0.0; - if (attributes.ApproachRate > 10.33) - approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + if (approachRate > 10.33) + approachRateFactor = 0.3 * (approachRate - 10.33); if (score.Mods.Any(h => h is OsuModAutopilot)) approachRateFactor = 0.0; @@ -234,7 +266,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); + speedValue *= 1.0 + 0.04 * (12.0 - approachRate); } double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes); @@ -248,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); + speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2); return speedValue; } @@ -275,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. - double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; + double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); @@ -312,7 +344,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the flashlight value with accuracy _slightly_. flashlightValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; return flashlightValue; } @@ -352,10 +384,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; - double hitWindowGreat = attributes.GreatHitWindow; - double hitWindowOk = attributes.OkHitWindow; - double hitWindowMeh = attributes.MehHitWindow; - // The probability that a player hits a circle is unknown, but we can estimate it to be // the number of greats on circles divided by the number of circles, and then add one // to the number of circles as a bias correction. @@ -370,22 +398,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: - double deviation = hitWindowGreat / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); + double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); - double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) - / (deviation * DifficultyCalculationUtils.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2)) + / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation))); deviation *= Math.Sqrt(1 - randomValue); // Value deviation approach as greatCount approaches 0 - double limitValue = hitWindowOk / Math.Sqrt(3); + double limitValue = okHitWindow / Math.Sqrt(3); // If precision is not enough to compute true deviation - use limit value if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue) deviation = limitValue; // Then compute the variance for mehs. - double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3; + double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3; // Find the total deviation. deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 37e6996e5a..b43468ab18 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -49,32 +49,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("stamina_difficult_strains")] public double StaminaTopStrains { get; set; } - /// - /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - - /// - /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("ok_hit_window")] - public double OkHitWindow { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) yield return v; yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); - yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor); } @@ -83,8 +63,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; - OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR]; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6b9986bd68..7bc050d2df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -129,9 +129,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); double starRating = rescale(combinedRating * 1.4); - HitWindows hitWindows = new TaikoHitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes { StarRating = starRating, @@ -144,8 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty RhythmTopStrains = rhythmDifficultStrains, ColourTopStrains = colourDifficultStrains, StaminaTopStrains = staminaDifficultStrains, - GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, - OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), }; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index bcd3693119..9e049df87c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -4,11 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Difficulty @@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countMiss; private double? estimatedUnstableRate; + private double clockRate; + private double greatHitWindow; + private double effectiveMissCount; public TaikoPerformanceCalculator() @@ -36,7 +42,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10; + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; + + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + HitWindows hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(difficulty.OverallDifficulty); + + greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate; + + estimatedUnstableRate = computeDeviationUpperBound() * 10; // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -104,7 +124,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) { - if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null) + if (greatHitWindow <= 0 || estimatedUnstableRate == null) return 0; double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; @@ -123,9 +143,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that /// two SS scores on the same map with the same settings will always return the same deviation. /// - private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes) + private double? computeDeviationUpperBound() { - if (countGreat == 0 || attributes.GreatHitWindow <= 0) + if (countGreat == 0 || greatHitWindow <= 0) return null; const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). @@ -139,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); // We can be 99% confident that the deviation is not higher than: - return attributes.GreatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); + return greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 1d6cee043b..59511973f7 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -17,21 +17,15 @@ namespace osu.Game.Rulesets.Difficulty { protected const int ATTRIB_ID_AIM = 1; protected const int ATTRIB_ID_SPEED = 3; - protected const int ATTRIB_ID_OVERALL_DIFFICULTY = 5; - protected const int ATTRIB_ID_APPROACH_RATE = 7; protected const int ATTRIB_ID_MAX_COMBO = 9; protected const int ATTRIB_ID_DIFFICULTY = 11; - protected const int ATTRIB_ID_GREAT_HIT_WINDOW = 13; - protected const int ATTRIB_ID_SCORE_MULTIPLIER = 15; protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23; protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25; - protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; - protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33; /// /// The mods which were applied to the beatmap. From d4ce71267256590ff170281b17ef471fdb497653 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 17:57:01 +0900 Subject: [PATCH 64/86] Add note about weird taiko iteration --- .../Difficulty/Utils/IntervalGroupingUtils.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 7bd7aa7677..5ab58ad4f3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { const double margin_of_error = 5; + // This never compares the first two elements in the group. + // This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329) var groupedObjects = new List { objects[i] }; i++; From 340e081965355fc1f36acdd1acce1d7c6b773780 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 18:05:08 +0900 Subject: [PATCH 65/86] Rename buzz variable per review --- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 2d1adbd056..559e9dafa0 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float lastDistanceMoved; private float lastExactDistanceMoved; private double lastStrainTime; - private bool isBuzzSliderTriggered; + private bool isInBuzzSection; /// /// The speed multiplier applied to the player's catcher. @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills // To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius) if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime) { - if (isBuzzSliderTriggered) + if (isInBuzzSection) distanceAddition = 0; else - isBuzzSliderTriggered = true; + isInBuzzSection = true; } else { - isBuzzSliderTriggered = false; + isInBuzzSection = false; } lastPlayerPosition = playerPosition; From 4e66536ae8a65219c971202addb6394c6744d1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Feb 2025 15:52:05 +0100 Subject: [PATCH 66/86] Fix failed scores with no hits on beatmaps with ridiculous mod combinations showing hundreds of pp points awarded (#31741) See https://discord.com/channels/188630481301012481/1097318920991559880/1334716356582572074. On `master` this is actually worse and shows thousands of pp points, so I guess `pp-dev` is a comparable improvement, but still flagrantly wrong. The reason why `pp-dev` is better is the `speedDeviation == null` guard at the start of `computeSpeedValue()` which turns off the rest of the calculation, therefore not exposing the bug where `relevantTotalDiff` can go negative. I still guarded it in this commit just for safety's sake given it is clear it can do very wrong stuff. --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 09ec890926..a667d12a44 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= speedHighDeviationMultiplier; // Calculate accuracy assuming the worst case scenario - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; + double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount); double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); @@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); + betterAccuracyPercentage = ((countGreat - Math.Max(totalHits - amountHitObjectsWithAccuracy, 0)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); else betterAccuracyPercentage = 0; From f868f03e1b75556418c8cfd6576781c3324d3fd7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Feb 2025 16:38:55 +0900 Subject: [PATCH 67/86] Fix host change sounds playing when exiting multiplayer rooms --- .../Online/Multiplayer/MultiplayerClient.cs | 6 +++++ .../Multiplayer/MultiplayerRoomSounds.cs | 27 ++++++------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 97161cce48..2d445ea25a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -51,6 +51,11 @@ namespace osu.Game.Online.Multiplayer /// public event Action? UserKicked; + /// + /// Invoked when the room's host is changed. + /// + public event Action? HostChanged; + /// /// Invoked when a new item is added to the playlist. /// @@ -531,6 +536,7 @@ namespace osu.Game.Online.Multiplayer Room.Host = user; APIRoom.Host = user?.User; + HostChanged?.Invoke(user); RoomUpdated?.Invoke(); }, false); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs index d53e485c86..cdf4e96bad 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -20,7 +19,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private Sample? userJoinedSample; private Sample? userLeftSample; private Sample? userKickedSample; - private MultiplayerRoomUser? host; [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -35,25 +33,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - client.RoomUpdated += onRoomUpdated; client.UserJoined += onUserJoined; client.UserLeft += onUserLeft; client.UserKicked += onUserKicked; - updateState(); - } - - private void onRoomUpdated() => Scheduler.AddOnce(updateState); - - private void updateState() - { - if (EqualityComparer.Default.Equals(host, client.Room?.Host)) - return; - - // only play sound when the host changes from an already-existing host. - if (host != null) - Scheduler.AddOnce(() => hostChangedSample?.Play()); - - host = client.Room?.Host; + client.HostChanged += onHostChanged; } private void onUserJoined(MultiplayerRoomUser user) @@ -65,16 +48,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(() => userKickedSample?.Play()); + private void onHostChanged(MultiplayerRoomUser? host) + { + if (host != null) + Scheduler.AddOnce(() => hostChangedSample?.Play()); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (client.IsNotNull()) { - client.RoomUpdated -= onRoomUpdated; client.UserJoined -= onUserJoined; client.UserLeft -= onUserLeft; client.UserKicked -= onUserKicked; + client.HostChanged -= onHostChanged; } } } From a690b0bae993f06edc45fabc6ea2b5153219cc1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 12:05:23 +0100 Subject: [PATCH 68/86] Adjust rounding tolerance in distance snap grid ring colour logic --- osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 88e28df8e3..8322c67def 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // in case 2, we want *flooring* to occur, to prevent a possible off-by-one // because of the rounding snapping forward by a chunk of time significantly too high to be considered a rounding error. // the tolerance margin chosen here is arbitrary and can be adjusted if more cases of this are found. - if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.005)) + if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.01)) beatIndex = (int)Math.Floor(fractionalBeatIndex); var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); From de78518fea14b4c12c5a2db4bc74d65335f05521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 12:52:59 +0100 Subject: [PATCH 69/86] Fix "use current distance snap" button incorrectly factoring in last object with velocity Closes https://github.com/ppy/osu/issues/32003. --- osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs | 6 +++++- osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs index 3c0889d027..45ce3206d2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -14,7 +16,9 @@ namespace osu.Game.Rulesets.Osu.Edit { public override double ReadCurrentDistanceSnap(HitObject before, HitObject after) { - float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); + var lastObjectWithVelocity = EditorBeatmap.HitObjects.TakeWhile(ho => ho != after).OfType().LastOrDefault(); + + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime, lastObjectWithVelocity); float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); return actualDistance / expectedDistance; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index d0b279f201..4129a6fb2c 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit private EditorClock editorClock { get; set; } = null!; [Resolved] - private EditorBeatmap editorBeatmap { get; set; } = null!; + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } = null!; @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Edit } }); - DistanceSpacingMultiplier.Value = editorBeatmap.DistanceSpacing; + DistanceSpacingMultiplier.Value = EditorBeatmap.DistanceSpacing; DistanceSpacingMultiplier.BindValueChanged(multiplier => { distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit if (multiplier.NewValue != multiplier.OldValue) onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); - editorBeatmap.DistanceSpacing = multiplier.NewValue; + EditorBeatmap.DistanceSpacing = multiplier.NewValue; }, true); DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true); @@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Edit public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null) { - return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 + return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / beatSnapProvider.BeatDivisor); } From c77fed637c89aab0e6374c307b4935fe1d6f9097 Mon Sep 17 00:00:00 2001 From: ziv_vy <134942175+ziv-vy@users.noreply.github.com> Date: Sun, 23 Feb 2025 01:01:39 +0200 Subject: [PATCH 70/86] Update MouseSettingsStrings.cs CAPITALISED ONE GODDAMN LETTER --- osu.Game/Localisation/MouseSettingsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index e61af07364..9609c2dd90 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString HighPrecisionMouse => new TranslatableString(getKey(@"high_precision_mouse"), @"High precision mouse"); /// - /// "Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as "Raw Input"." + /// "Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." /// - public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as ""Raw Input""."); + public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); /// /// "Confine mouse cursor to window" From d95f31dc5af463423a72981f4328fbd0f0b6c654 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 22 Feb 2025 15:21:54 -0800 Subject: [PATCH 71/86] Also fix operating system terminology --- osu.Game/Localisation/MouseSettingsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index 9609c2dd90..c92c3b6ddc 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString HighPrecisionMouse => new TranslatableString(getKey(@"high_precision_mouse"), @"High precision mouse"); /// - /// "Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." + /// "Attempts to bypass any operating system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." /// - public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); + public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operating system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); /// /// "Confine mouse cursor to window" From f4b427ee66bd169ab7270f9a4ef8f467d4ac4572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:15:20 +0100 Subject: [PATCH 72/86] Add failing test case --- .../TestSceneModDifficultyAdjustSettings.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs index b40d0b10d2..30470c9c17 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { @@ -220,6 +221,29 @@ namespace osu.Game.Tests.Visual.UserInterface checkBindableAtValue("Circle Size", null); } + [Test] + public void TestResetToDefaultViaDoubleClickingNub() + { + setBeatmapWithDifficultyParameters(5); + + setSliderValue("Circle Size", 3); + setExtendedLimits(true); + + checkSliderAtValue("Circle Size", 3); + checkBindableAtValue("Circle Size", 3); + + AddStep("double click circle size nub", () => + { + var nub = this.ChildrenOfType.SliderNub>().First(); + InputManager.MoveMouseTo(nub); + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + checkSliderAtValue("Circle Size", 5); + checkBindableAtValue("Circle Size", null); + } + [Test] public void TestModSettingChangeTracker() { From d8cb3b68d3ea90268bb142a2ef6e1de782f2040a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:34:52 +0100 Subject: [PATCH 73/86] Add "Team" channel type The lack of this bricks chat completely due to newtonsoft deserialisation errors: 2025-02-24 08:32:58 [verbose]: Processing response from https://dev.ppy.sh/api/v2/chat/updates failed with Newtonsoft.Json.JsonSerializationException: Error converting value "TEAM" to type 'osu.Game.Online.Chat.ChannelType'. Path 'presence[39].type', line 1, position 13765. 2025-02-24 08:32:58 [verbose]: ---> System.ArgumentException: Requested value 'TEAM' was not found. 2025-02-24 08:32:58 [verbose]: at Newtonsoft.Json.Utilities.EnumUtils.ParseEnum(Type enumType, NamingStrategy namingStrategy, String value, Boolean disallowNumber) 2025-02-24 08:32:58 [verbose]: at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType) --- osu.Game/Online/Chat/ChannelType.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs index bd628e90c4..4fb890c2cc 100644 --- a/osu.Game/Online/Chat/ChannelType.cs +++ b/osu.Game/Online/Chat/ChannelType.cs @@ -14,5 +14,6 @@ namespace osu.Game.Online.Chat Group, System, Announce, + Team, } } From be8ec759488e3bb5e5479341c8de400a80f3e9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:39:27 +0100 Subject: [PATCH 74/86] Display team chat channel in separate group --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index f027888962..6e874e4ed8 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.ChannelList public ChannelGroup AnnounceChannelGroup { get; private set; } = null!; public ChannelGroup PublicChannelGroup { get; private set; } = null!; + public ChannelGroup TeamChannelGroup { get; private set; } = null!; public ChannelGroup PrivateChannelGroup { get; private set; } = null!; private OsuScrollContainer scroll = null!; @@ -82,6 +83,7 @@ namespace osu.Game.Overlays.Chat.ChannelList AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), selector = new ChannelListItem(ChannelListingChannel), + TeamChannelGroup = new ChannelGroup("TEAM", false), // TODO: replace with osu-web localisable string once available PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), }, }, @@ -156,6 +158,9 @@ namespace osu.Game.Overlays.Chat.ChannelList case ChannelType.Announce: return AnnounceChannelGroup; + case ChannelType.Team: + return TeamChannelGroup; + default: return PublicChannelGroup; } From 194f05d2588fa7f23287b85c3968219102e33a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:43:46 +0100 Subject: [PATCH 75/86] Add icons to chat channel group headers Matches web in appearance. Cross-reference: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19 --- .../Overlays/Chat/ChannelList/ChannelList.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index 6e874e4ed8..ae68c9c82e 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -80,11 +81,12 @@ namespace osu.Game.Overlays.Chat.ChannelList RelativeSizeAxes = Axes.X, } }, - AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), - PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), + // cross-reference for icons: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19 + AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), FontAwesome.Solid.Bullhorn, false), + PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), FontAwesome.Solid.Comments, false), selector = new ChannelListItem(ChannelListingChannel), - TeamChannelGroup = new ChannelGroup("TEAM", false), // TODO: replace with osu-web localisable string once available - PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), + TeamChannelGroup = new ChannelGroup("TEAM", FontAwesome.Solid.Users, false), // TODO: replace with osu-web localisable string once available + PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), FontAwesome.Solid.Envelope, true), }, }, }, @@ -179,7 +181,7 @@ namespace osu.Game.Overlays.Chat.ChannelList private readonly bool sortByRecent; public readonly ChannelListItemFlow ItemFlow; - public ChannelGroup(LocalisableString label, bool sortByRecent) + public ChannelGroup(LocalisableString label, IconUsage icon, bool sortByRecent) { this.sortByRecent = sortByRecent; Direction = FillDirection.Vertical; @@ -189,11 +191,26 @@ namespace osu.Game.Overlays.Chat.ChannelList Children = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Text = label, - Margin = new MarginPadding { Left = 18, Bottom = 5 }, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = label, + Margin = new MarginPadding { Left = 18, Bottom = 5 }, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + }, + new SpriteIcon + { + Icon = icon, + Size = new Vector2(12), + }, + } }, ItemFlow = new ChannelListItemFlow(sortByRecent) { From 4ac4b308e10d041dec5960f808ce2d295171f3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:48:03 +0100 Subject: [PATCH 76/86] Add visual test coverage of team channels --- .../Visual/Online/TestSceneChannelList.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs index 5f77e084da..8f8cf036f1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -115,6 +115,12 @@ namespace osu.Game.Tests.Visual.Online channelList.AddChannel(createRandomPrivateChannel()); }); + AddStep("Add Team Channels", () => + { + for (int i = 0; i < 10; i++) + channelList.AddChannel(createRandomTeamChannel()); + }); + AddStep("Add Announce Channels", () => { for (int i = 0; i < 2; i++) @@ -189,5 +195,16 @@ namespace osu.Game.Tests.Visual.Online Id = id, }; } + + private Channel createRandomTeamChannel() + { + int id = TestResources.GetNextTestID(); + return new Channel + { + Name = $"Team {id}", + Type = ChannelType.Team, + Id = id, + }; + } } } From 0312467c8840067054c6326d9da821329b7bf01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 12:30:37 +0100 Subject: [PATCH 77/86] Fix hash comparison being case sensitive when choosing files for partial beatmap submission Noticed when investigating https://github.com/ppy/osu/issues/32059, and also a likely cause for user reports like https://discord.com/channels/188630481301012481/1097318920991559880/1342962553101357066. Honestly I have no solid defence, Your Honour. I guess this just must not have been tested on the client side, only relied on server-side testing. --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 66139bacec..13981bcb69 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -285,7 +285,7 @@ namespace osu.Game.Screens.Edit.Submission continue; } - if (localHash != onlineHash) + if (!localHash.Equals(onlineHash, StringComparison.OrdinalIgnoreCase)) filesToUpdate.Add(filename); } From 41db3c1501bbfc40f1eb9952fa9d332319ff347b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 14:30:55 +0100 Subject: [PATCH 78/86] Fix taiko swell ending samples playing at results sometimes Closes https://github.com/ppy/osu/issues/32052. Sooooo... this is going to be a rant... To understand why this is going to require a rant, dear reader, please do the following: 1. Read the issue thread and follow the reproduction scenario (download map linked, fire up autoplay, seek near end, wait for results, hear the sample spam). 2. Now exit out to song select, *hide the toolbar*, and attempt reproducing the issue again. 3. Depending on ambient mood, laugh or cry. Now, *why on earth* would the *TOOLBAR* have any bearing on anything? Well, the chain of failure is something like this: - The toolbar hides for the duration of gameplay, naturally. - When progressing to results, the toolbar gets automatically unhidden. - This triggers invalidations on `ScrollingHitObjectContainer`. I'm not precisely sure which property it is that triggers the invalidations, but one clearly does. It may be position or size or whichever. - When the invalidation is triggered on `layoutCache`, the next `Update()` call is going to recompute lifetimes for ALL hitobject entries. - In case of swells, it happens that the calculated lifetime end of the swell is larger than what it actually ended up being determined as at the instant of judging the swell, and thus, the swell is *resurrected*, reassigned a DHO, and the DHO calls `UpdateState()` and plays the sample again despite the `samplePlayed` flag in `LegacySwell`, because all of that is ephemeral state that does not survive a hitobject getting resurrected. Now I *could* just fix this locally to the swell, maybe, by having some time lenience check, but the fact that hitobjects can be resurrected by the *toolbar* appearing, of all possible causes in the world, feels just completely wrong. So I'm adding a local check in SHOC to not overwrite lifetime ends of judged object entries. The reason why I'm making that check specific to end time is that I can see valid reasons why you would want to recompute lifetime *start* even on a judged object (such as playfield geometry changing in a significant way). I can't think of a valid reason to do that to lifetime *end*. --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 7841e65935..8b0076afa1 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -247,7 +247,12 @@ namespace osu.Game.Rulesets.UI.Scrolling // It is required that we set a lifetime end here to ensure that in scenarios like loading a Player instance to a seeked // location in a beatmap doesn't churn every hit object into a DrawableHitObject. Even in a pooled scenario, the overhead // of this can be quite crippling. - entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value; + // + // However, additionally do not attempt to alter lifetime of judged entries. + // This is to prevent freak accidents like objects suddenly becoming alive because of this estimate assigning a later lifetime + // than the object itself decided it should have when it underwent judgement. + if (!entry.Judged) + entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value; } private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null) From e8f7bcb6e625a6360b2bd4487186ac075e07ddc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:06:02 +0100 Subject: [PATCH 79/86] Only show team channel section when there is a team channel --- osu.Game.Tests/Visual/Online/TestSceneChannelList.cs | 6 +----- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 7 +++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs index 8f8cf036f1..364240502a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -115,11 +115,7 @@ namespace osu.Game.Tests.Visual.Online channelList.AddChannel(createRandomPrivateChannel()); }); - AddStep("Add Team Channels", () => - { - for (int i = 0; i < 10; i++) - channelList.AddChannel(createRandomTeamChannel()); - }); + AddStep("Add Team Channel", () => channelList.AddChannel(createRandomTeamChannel())); AddStep("Add Announce Channels", () => { diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index ae68c9c82e..c0fc349c2c 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -106,6 +106,7 @@ namespace osu.Game.Overlays.Chat.ChannelList }; selector.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); + updateVisibility(); } public void AddChannel(Channel channel) @@ -170,10 +171,8 @@ namespace osu.Game.Overlays.Chat.ChannelList private void updateVisibility() { - if (AnnounceChannelGroup.ItemFlow.Children.Count == 0) - AnnounceChannelGroup.Hide(); - else - AnnounceChannelGroup.Show(); + AnnounceChannelGroup.Alpha = AnnounceChannelGroup.ItemFlow.Any() ? 1 : 0; + TeamChannelGroup.Alpha = TeamChannelGroup.ItemFlow.Any() ? 1 : 0; } public partial class ChannelGroup : FillFlowContainer From e13aa4a99b353f994e9bcf6c6df58d18f466bf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:10:20 +0100 Subject: [PATCH 80/86] Do not allow leaving team channels --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 8 ++++++-- osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index c0fc349c2c..0a89775cc7 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -114,9 +114,13 @@ namespace osu.Game.Overlays.Chat.ChannelList if (channelMap.ContainsKey(channel)) return; - ChannelListItem item = new ChannelListItem(channel); + ChannelListItem item = new ChannelListItem(channel) + { + CanLeave = channel.Type != ChannelType.Team + }; item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); - item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); + if (item.CanLeave) + item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); ChannelGroup group = getGroupFromChannel(channel); channelMap.Add(channel, item); diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index b197fe199d..6107f130ec 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Chat.ChannelList public partial class ChannelListItem : OsuClickableContainer, IFilterable { public event Action? OnRequestSelect; + + public bool CanLeave { get; init; } = true; public event Action? OnRequestLeave; public readonly Channel Channel; @@ -160,7 +162,7 @@ namespace osu.Game.Overlays.Chat.ChannelList private ChannelListItemCloseButton? createCloseButton() { - if (isSelector) + if (isSelector || !CanLeave) return null; return new ChannelListItemCloseButton From c82cf4092879167f60bd76729730350d882c248f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:24:18 +0100 Subject: [PATCH 81/86] Do not give swell ticks any visual representation Why is this a thing at all? How has it survived this long? I don't know. As far as I can tell this only manifests on selected beatmaps with "slow swells" that spend the entire beatmap moving in the background. On other beatmaps the tick is faded out, probably due to the initial transform application that normally "works" but fails hard on these slow swells. Can be seen on https://osu.ppy.sh/beatmapsets/1432454#taiko/2948222. --- .../Objects/Drawables/DrawableSwellTick.cs | 7 +------ .../Objects/Drawables/DrawableTaikoHitObject.cs | 6 +++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 04dd01e066..88554ba257 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -4,9 +4,7 @@ #nullable disable using JetBrains.Annotations; -using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -25,8 +23,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override void UpdateInitialTransforms() => this.FadeOut(); - public void TriggerResult(bool hit) { HitObject.StartTime = Time.Current; @@ -43,7 +39,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(KeyBindingPressEvent e) => false; - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), - _ => new TickPiece()); + protected override SkinnableDrawable CreateMainPiece() => null; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 0cf9651965..520ac2ba80 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -154,9 +154,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (MainPiece != null) Content.Remove(MainPiece, true); - Content.Add(MainPiece = CreateMainPiece()); + MainPiece = CreateMainPiece(); + + if (MainPiece != null) + Content.Add(MainPiece); } + [CanBeNull] protected abstract SkinnableDrawable CreateMainPiece(); } } From 1f562ab47d5f6959f66e0091ec82b778c3539f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:18:19 +0100 Subject: [PATCH 82/86] Fix double-clicking difficulty adjust sliders not resetting the value to default correctly - Closes https://github.com/ppy/osu/issues/31888 - Supersedes / closes https://github.com/ppy/osu/pull/32060 --- .../Mods/OsuModDifficultyAdjust.cs | 9 +------- .../UserInterface/RoundedSliderBar.cs | 18 +++++++++++---- .../Mods/DifficultyAdjustSettingsControl.cs | 23 +++++++++++++------ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index f35b1abc42..10282ff988 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -63,13 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl { - protected override RoundedSliderBar CreateSlider(BindableNumber current) => - new ApproachRateSlider - { - RelativeSizeAxes = Axes.X, - Current = current, - KeyboardStep = 0.1f, - }; + protected override RoundedSliderBar CreateSlider(BindableNumber current) => new ApproachRateSlider(); /// /// A slider bar with more detailed approach rate info for its given value diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index aeab7c34b2..9a0183da64 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Overlays; using Vector2 = osuTK.Vector2; @@ -52,10 +53,21 @@ namespace osu.Game.Graphics.UserInterface } } + /// + /// The action to use to reset the value of to the default. + /// Triggered on double click. + /// + public Action ResetToDefault { get; internal set; } + public RoundedSliderBar() { Height = Nub.HEIGHT; RangePadding = Nub.DEFAULT_EXPANDED_SIZE / 2; + ResetToDefault = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }; Children = new Drawable[] { new Container @@ -102,11 +114,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, - OnDoubleClicked = () => - { - if (!Current.Disabled) - Current.SetDefault(); - }, + OnDoubleClicked = () => ResetToDefault.Invoke(), }, }, hoverClickSounds = new HoverClickSounds() diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index d04d7636ec..6697a8d848 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs @@ -31,12 +31,7 @@ namespace osu.Game.Rulesets.Mods protected sealed override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent, CreateSlider); - protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar - { - RelativeSizeAxes = Axes.X, - Current = current, - KeyboardStep = 0.1f, - }; + protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar(); /// /// Guards against beatmap values displayed on slider bars being transferred to user override. @@ -111,7 +106,21 @@ namespace osu.Game.Rulesets.Mods { InternalChildren = new Drawable[] { - createSlider(currentNumber) + createSlider(currentNumber).With(slider => + { + slider.RelativeSizeAxes = Axes.X; + slider.Current = currentNumber; + slider.KeyboardStep = 0.1f; + // this looks redundant, but isn't because of the various games this component plays + // (`Current` is nullable and represents the underlying setting value, + // `currentNumber` is not nullable and represents what is getting displayed, + // therefore without this, double-clicking the slider would reset `currentNumber` to its bogus default of 0). + slider.ResetToDefault = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }; + }) }; AutoSizeAxes = Axes.Y; From e97c2fee0d2a57d2d13c2e20a76370daa325cd4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Feb 2025 12:57:38 +0100 Subject: [PATCH 83/86] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d49acd7b27..d4b49e492a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 5ca49e80f6..d10a3d649a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From abc12abdedfbb315996d5c16e5556cc9837d1e17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 16:48:18 +0900 Subject: [PATCH 84/86] Fix `PlayerTeamFlag` skinnable component not showing team details during replay For now, let's fetch on demand. Note that song select local leaderboard has the same issue. I feel we should be doing a lot more cached lookups (probaly with persisting across game restarts). Maybe even replacing the realm user storage. An issue for another day. --- osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs index f8ef03c58c..3f72099a45 100644 --- a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs @@ -3,8 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Skinning; @@ -40,10 +42,19 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load() + private void load(UserLookupCache userLookupCache) { if (gameplayState != null) - flag.Team = gameplayState.Score.ScoreInfo.User.Team; + { + if (gameplayState.Score.ScoreInfo.User.Team != null) + flag.Team = gameplayState.Score.ScoreInfo.User.Team; + else + { + // We only store very basic information about a user to realm, so there's a high chance we don't have the team information. + userLookupCache.GetUserAsync(gameplayState.Score.ScoreInfo.User.Id) + .ContinueWith(task => Schedule(() => flag.Team = task.GetResultSafely()?.Team)); + } + } else { apiUser = api.LocalUser.GetBoundCopy(); From e8b7ec0f9537db864b712ebc28ba63afabe3eeb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 17:01:48 +0900 Subject: [PATCH 85/86] Adjust leaderboard score design slightly This design is about to get replaced, so I'm just making some minor adjustments since a lot of people complained about the font size in the last update. Of note, I'm only changing the font size which is one pt size lower than we'd usually use. Also overlapping the mod icons to create a bit more space (since there's already cases where they don't fit). Closes https://github.com/ppy/osu/issues/32055 as far as I'm concerned. I can read everything fine at 0.8x UI scale. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index fc30f158f0..7306c2d21e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -271,6 +271,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, + Spacing = new Vector2(-10, 0), Direction = FillDirection.Horizontal, ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) }) }, @@ -425,7 +426,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true); + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From c45a403fe2b87db7b43d3500fe25e348b88e27ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 18:00:18 +0900 Subject: [PATCH 86/86] Mostly revert sizes --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 7306c2d21e..0db03efb68 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -395,7 +395,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreLeft, Text = statistic.Value, Spacing = new Vector2(-1, 0), - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true) + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold, fixedWidth: true) }, }, }; @@ -426,7 +426,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30));