diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs index c7851cc12f02..cc836374fc75 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs @@ -19,13 +19,13 @@ public EmptyFreeformDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap b { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skills) { return new DifficultyAttributes(mods, 0); } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty(); + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) => Array.Empty(); } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs index 852576958446..1e54287c68be 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs @@ -19,13 +19,13 @@ public PippidonDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatma { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skills) { return new DifficultyAttributes(mods, 0); } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty(); + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) => Array.Empty(); } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs index 17139218a52d..bd6725bda280 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs @@ -19,13 +19,13 @@ public EmptyScrollingDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skills) { return new DifficultyAttributes(mods, 0); } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty(); + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) => Array.Empty(); } } diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs index 852576958446..1e54287c68be 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs @@ -19,13 +19,13 @@ public PippidonDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatma { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skills) { return new DifficultyAttributes(mods, 0); } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty(); + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) => Array.Empty(); } } diff --git a/osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs b/osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs index 01e50827ba78..ca7bdcd5ecfa 100644 --- a/osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs +++ b/osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs @@ -65,6 +65,18 @@ private WorkingBeatmap readBeatmap(ZipArchiveReader archiveReader, string beatma [Benchmark] public void CalculateDifficultyMania() => maniaCalculator.Calculate(); + [Benchmark] + public void CalculateTimedDifficultyOsu() => osuCalculator.CalculateTimed(); + + [Benchmark] + public void CalculateTimedDifficultyTaiko() => taikoCalculator.CalculateTimed(); + + [Benchmark] + public void CalculateTimedDifficultyCatch() => catchCalculator.CalculateTimed(); + + [Benchmark] + public void CalculateTimedDifficultyMania() => maniaCalculator.CalculateTimed(); + [Benchmark] public void CalculateDifficultyOsuHundredTimes() { diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 826395da69f0..8e7c59051796 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -3,7 +3,6 @@ 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; @@ -30,14 +29,14 @@ public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skillAttributes) { if (beatmap.HitObjects.Count == 0) return new CatchDifficultyAttributes { Mods = mods }; CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { - StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier, + StarRating = Math.Sqrt(skillAttributes[0].Difficulty) * difficulty_multiplier, Mods = mods, MaxCombo = beatmap.GetMaxCombo(), }; @@ -74,11 +73,11 @@ protected override IEnumerable CreateDifficultyHitObjects(I return objects; } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) { - return new Skill[] + return new ISkill[] { - new Movement(mods), + new Movement(mods, difficultyHitObjects), }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 332ef7e17bf9..ce1751d50216 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -17,8 +17,8 @@ public class Movement : StrainDecaySkill protected override int SectionLength => 750; - public Movement(Mod[] mods) - : base(mods) + public Movement(Mod[] mods, DifficultyHitObject[] difficultyHitObjects) + : base(mods, difficultyHitObjects) { } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 9b8e79edd167..b001fc4e1aa3 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -37,7 +37,7 @@ public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skillAttributes) { if (beatmap.HitObjects.Count == 0) return new ManiaDifficultyAttributes { Mods = mods }; @@ -47,7 +47,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { - StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier, + StarRating = skillAttributes[0].Difficulty * difficulty_multiplier, Mods = mods, MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), }; @@ -91,9 +91,9 @@ protected override IEnumerable CreateDifficultyHitObjects(I // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required. protected override IEnumerable SortObjects(IEnumerable input) => input; - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) => new ISkill[] { - new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns) + new Strain(mods, difficultyHitObjects, ((ManiaBeatmap)Beatmap).TotalColumns) }; protected override Mod[] DifficultyAdjustmentMods diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 5b682fdd9fae..883d45177daa 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -23,8 +23,8 @@ public class Strain : StrainDecaySkill private double highestIndividualStrain; private double overallStrain; - public Strain(Mod[] mods, int totalColumns) - : base(mods) + public Strain(Mod[] mods, DifficultyHitObject[] difficultyHitObjects, int totalColumns) + : base(mods, difficultyHitObjects) { individualStrains = new double[totalColumns]; overallStrain = 1; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index c034906aa2ab..3a217df4db02 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -25,6 +25,14 @@ public class OsuDifficultyCalculatorTest : DifficultyCalculatorTest public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); + [TestCase(6.5243170265483581d, 239, "diffcalc-test")] + [TestCase(1.3280410795791415d, 54, "zero-length-sliders")] + [TestCase(0.40867325147697559d, 4, "very-fast-slider")] + [TestCase(0.87058175794353554d, 6, "nan-slider")] + [TestCase(6.3059767387139756d, 2359, "801165")] // real world test + public void TestTimed(double expectedStarRating, int expectedMaxCombo, string name) + => base.TestTimed(expectedStarRating, expectedMaxCombo, name); + [TestCase(9.4677607900646308d, 239, "diffcalc-test")] [TestCase(1.6856612715618886d, 54, "zero-length-sliders")] [TestCase(0.53588473186572561d, 4, "very-fast-slider")] diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 07679dbb6a8c..13ec43e7fd24 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -28,37 +28,19 @@ public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skillAttributes) { if (beatmap.HitObjects.Count == 0) return new OsuDifficultyAttributes { Mods = mods }; - var aim = skills.OfType().Single(a => a.IncludeSliders); - var aimWithoutSliders = skills.OfType().Single(a => !a.IncludeSliders); - var speed = skills.OfType().Single(); - var flashlight = skills.OfType().SingleOrDefault(); - var reading = skills.OfType().Single(); + var aimAttributes = skillAttributes.OfType().Single(a => a.WithSliders); + var aimWithoutSlidersAttributes = skillAttributes.OfType().Single(a => !a.WithSliders); + var speedAttributes = skillAttributes.OfType().Single(); + var flashlightAttributes = skillAttributes.OfType().SingleOrDefault(); + var readingAttributes = skillAttributes.OfType().Single(); - double aimDifficultyValue = aim.DifficultyValue(); - double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue(); - double speedDifficultyValue = speed.DifficultyValue(); - double readingDifficultyValue = reading.DifficultyValue(); - - double aimDifficultStrainCount = aim.CountTopWeightedStrains(aimDifficultyValue); - double speedDifficultStrainCount = speed.CountTopWeightedObjectDifficulties(speedDifficultyValue); - double readingDifficultNoteCount = reading.CountTopWeightedObjectDifficulties(readingDifficultyValue); - - double speedNotes = speed.RelevantObjectCount(); - - double aimNoSlidersTopWeightedSliderCount = aimWithoutSliders.CountTopWeightedSliders(aimNoSlidersDifficultyValue); - double aimNoSlidersDifficultStrainCount = aimWithoutSliders.CountTopWeightedStrains(aimNoSlidersDifficultyValue); - - double aimTopWeightedSliderFactor = aimNoSlidersTopWeightedSliderCount / Math.Max(1, aimNoSlidersDifficultStrainCount - aimNoSlidersTopWeightedSliderCount); - - double speedTopWeightedSliderCount = speed.CountTopWeightedSliders(speedDifficultyValue); - double speedTopWeightedSliderFactor = speedTopWeightedSliderCount / Math.Max(1, speedDifficultStrainCount - speedTopWeightedSliderCount); - - double difficultSliders = aim.GetDifficultSliders(); + double aimTopWeightedSliderFactor = aimWithoutSlidersAttributes.TopWeightedSlidersCount / Math.Max(1, aimWithoutSlidersAttributes.TopWeightedStrainsCount - aimWithoutSlidersAttributes.TopWeightedSlidersCount); + double speedTopWeightedSliderFactor = speedAttributes.TopWeightedSlidersCount / Math.Max(1, speedAttributes.TopWeightedObjectDifficultiesCount - speedAttributes.TopWeightedSlidersCount); int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); @@ -66,20 +48,20 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat int totalHits = beatmap.HitObjects.Count; - double aimRating = calculateAimDifficultyRating(aimDifficultyValue); - double aimNoSlidersRating = calculateAimDifficultyRating(aimNoSlidersDifficultyValue); + double aimRating = calculateAimDifficultyRating(aimAttributes.Difficulty); + double aimNoSlidersRating = calculateAimDifficultyRating(aimWithoutSlidersAttributes.Difficulty); - double sliderFactor = aimDifficultyValue > 0 + double sliderFactor = aimAttributes.Difficulty > 0 ? aimNoSlidersRating / aimRating : 1; - double speedRating = calculateDifficultyRating(speedDifficultyValue); - double readingRating = calculateDifficultyRating(readingDifficultyValue); + double speedRating = calculateDifficultyRating(speedAttributes.Difficulty); + double readingRating = calculateDifficultyRating(readingAttributes.Difficulty); double flashlightRating = 0.0; - if (flashlight is not null) - flashlightRating = calculateDifficultyRating(flashlight.DifficultyValue()); + if (flashlightAttributes is not null) + flashlightRating = calculateDifficultyRating(flashlightAttributes.Difficulty); double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits); double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(WorkingBeatmap.Beatmap); @@ -102,15 +84,15 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat StarRating = starRating, Mods = mods, AimDifficulty = aimRating, - AimDifficultSliderCount = difficultSliders, + AimDifficultSliderCount = aimAttributes.DifficultSlidersCount, SpeedDifficulty = speedRating, - SpeedNoteCount = speedNotes, + SpeedNoteCount = speedAttributes.RelevantObjectCount, FlashlightDifficulty = flashlightRating, ReadingDifficulty = readingRating, SliderFactor = sliderFactor, - AimDifficultStrainCount = aimDifficultStrainCount, - SpeedDifficultStrainCount = speedDifficultStrainCount, - ReadingDifficultNoteCount = readingDifficultNoteCount, + AimDifficultStrainCount = aimAttributes.TopWeightedStrainsCount, + SpeedDifficultStrainCount = speedAttributes.TopWeightedObjectDifficultiesCount, + ReadingDifficultNoteCount = readingAttributes.TopWeightedObjectDifficultiesCount, AimTopWeightedSliderFactor = aimTopWeightedSliderFactor, SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor, MaxCombo = beatmap.GetMaxCombo(), @@ -162,18 +144,18 @@ protected override IEnumerable CreateDifficultyHitObjects(I return objects; } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) { - var skills = new List + var skills = new List { - new Aim(mods, true), - new Aim(mods, false), - new Speed(mods), - new Reading(mods) + new Aim(mods, difficultyHitObjects, true), + new Aim(mods, difficultyHitObjects, false), + new Speed(mods, difficultyHitObjects), + new Reading(mods, difficultyHitObjects) }; if (mods.Any(h => h is OsuModFlashlight)) - skills.Add(new Flashlight(mods, beatmap.HitObjects.Count)); + skills.Add(new Flashlight(mods, difficultyHitObjects, beatmap.HitObjects.Count)); return skills.ToArray(); } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 30a20d2827d5..95c3f2ab12aa 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -16,6 +16,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { + public class AimAttributes : VariableLengthStrainSkillAttributes + { + public required bool WithSliders { get; init; } + public required double DifficultSlidersCount { get; init; } + public required double TopWeightedSlidersCount { get; init; } + } + /// /// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances. /// @@ -23,8 +30,8 @@ public class Aim : VariableLengthStrainSkill { public readonly bool IncludeSliders; - public Aim(Mod[] mods, bool includeSliders) - : base(mods) + public Aim(Mod[] mods, DifficultyHitObject[] difficultyHitObjects, bool includeSliders) + : base(mods, difficultyHitObjects) { IncludeSliders = includeSliders; } @@ -130,7 +137,40 @@ private static double calculateSnapFlowProbability(double ratio) return DiffUtils.Logistic(-k * Math.Log(ratio)); } - public double GetDifficultSliders() + public override ISkillAttributes Process() + { + var baseAttributes = (VariableLengthStrainSkillAttributes)base.Process(); + + return new AimAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount, + WithSliders = IncludeSliders, + DifficultSlidersCount = getDifficultSliders(), + TopWeightedSlidersCount = countTopWeightedSliders(baseAttributes.Difficulty) + }; + } + + public override IEnumerable ProcessTimed() + { + foreach (var baseTimedAttributes in base.ProcessTimed()) + { + var baseAttributes = (VariableLengthStrainSkillAttributes)baseTimedAttributes.Attributes; + + yield return new TimedSkillAttributes(new AimAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount, + WithSliders = IncludeSliders, + DifficultSlidersCount = getDifficultSliders(), + TopWeightedSlidersCount = countTopWeightedSliders(baseAttributes.Difficulty) + }, baseTimedAttributes.Time); + } + } + + private double getDifficultSliders() { if (sliderStrains.Count == 0) return 0; @@ -143,7 +183,7 @@ public double GetDifficultSliders() return sliderStrains.Sum(strain => DiffUtils.Logistic(strain / maxSliderStrain, 0.5, 12.0)); } - public double CountTopWeightedSliders(double difficultyValue) + private double countTopWeightedSliders(double difficultyValue) { if (sliderStrains.Count == 0) return 0; @@ -157,7 +197,7 @@ public double CountTopWeightedSliders(double difficultyValue) return sliderStrains.Sum(s => DiffUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1)); } - public override double DifficultyValue() + protected override double Aggregate() { double difficulty = 0; double time = 0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 85666881ad70..34b9436ee2f5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -2,6 +2,7 @@ // 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.Skills; @@ -13,6 +14,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { + public class FlashlightAttributes : StrainSkillAttributes; + /// /// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled. /// @@ -20,8 +23,8 @@ public class Flashlight : StrainSkill { private readonly int totalObjects; - public Flashlight(Mod[] mods, int totalObjects) - : base(mods) + public Flashlight(Mod[] mods, DifficultyHitObject[] difficultyHitObjects, int totalObjects) + : base(mods, difficultyHitObjects) { this.totalObjects = totalObjects; } @@ -75,7 +78,36 @@ private double calculateAdjustedDifficulty(DifficultyHitObject current) return difficulty; } - public override double DifficultyValue() + public override ISkillAttributes Process() + { + var baseAttributes = (StrainSkillAttributes)base.Process(); + + return new FlashlightAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount + }; + } + + public override IEnumerable ProcessTimed() + { + foreach (var baseTimedAttributes in base.ProcessTimed()) + { + var baseAttributes = (StrainSkillAttributes)baseTimedAttributes.Attributes; + + yield return new TimedSkillAttributes(new FlashlightAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount + }, baseTimedAttributes.Time); + } + } + + protected override double Aggregate() { double sum = GetCurrentStrainPeaks().Sum(); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs index 92f054573210..bd329b83e5a1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs @@ -16,14 +16,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { - public class Reading : Skill + public class ReadingAttributes : ISkillAttributes { + public required double Difficulty { get; init; } + public required List ObjectDifficulties { get; init; } + public required double TopWeightedObjectDifficultiesCount { get; init; } + } + + public class Reading : ISkill + { + public IReadOnlyList Mods { get; init; } + public IReadOnlyList DifficultyHitObjects { get; init; } + private readonly bool hasHiddenMod; - private double harmonicWeightSum; - public Reading(Mod[] mods) - : base(mods) + public Reading(Mod[] mods, DifficultyHitObject[] difficultyHitObjects) { + Mods = mods; + DifficultyHitObjects = difficultyHitObjects; hasHiddenMod = mods.OfType().Any(m => !m.OnlyFadeApproachCircles.Value); } @@ -34,7 +44,7 @@ public Reading(Mod[] mods) private double strainDecay(double ms) => DiffUtils.Pow(0.8, ms / 1000); - protected override double ProcessInternal(DifficultyHitObject current) + private double objectDifficultyOf(DifficultyHitObject current) { const double skill_multiplier = 2.5; const double reduced_difficulty_duration = 60 * 1000; @@ -79,18 +89,6 @@ private double calculateAdjustedDifficulty(DifficultyHitObject current) return difficulty; } - public override double DifficultyValue() - { - if (ObjectDifficulties.Count == 0) - return 0; - - var difficulties = GetTransformedDifficulties(ObjectDifficulties); - - (double difficulty, harmonicWeightSum) = HarmonicSeries.Aggregate(difficulties); - - return difficulty; - } - protected List GetTransformedDifficulties(List difficulties) { difficulties = difficulties.Where(v => v > 0).ToList(); @@ -106,9 +104,51 @@ protected List GetTransformedDifficulties(List difficulties) return difficulties; } - public double CountTopWeightedObjectDifficulties(double difficultyValue) + public ISkillAttributes Process() + { + var objectDifficulties = new List(); + + foreach (var difficultyHitObject in DifficultyHitObjects) + { + objectDifficulties.Add(objectDifficultyOf(difficultyHitObject)); + } + + var difficulties = GetTransformedDifficulties(objectDifficulties); + + (double difficulty, double harmonicWeightSum) = HarmonicSeries.Aggregate(difficulties); + + return new ReadingAttributes + { + Difficulty = difficulty, + ObjectDifficulties = objectDifficulties, + TopWeightedObjectDifficultiesCount = countTopWeightedDifficulties(difficulties, harmonicWeightSum, difficulty) + }; + } + + public IEnumerable ProcessTimed() + { + var objectDifficulties = new List(); + + foreach (var difficultyHitObject in DifficultyHitObjects) + { + objectDifficulties.Add(objectDifficultyOf(difficultyHitObject)); + + var difficulties = GetTransformedDifficulties(objectDifficulties); + + (double difficulty, double harmonicWeightSum) = HarmonicSeries.Aggregate(difficulties); + + yield return new TimedSkillAttributes(new ReadingAttributes + { + Difficulty = difficulty, + ObjectDifficulties = objectDifficulties, + TopWeightedObjectDifficultiesCount = countTopWeightedDifficulties(difficulties, harmonicWeightSum, difficulty) + }, difficultyHitObject.EndTime); + } + } + + private double countTopWeightedDifficulties(List difficulties, double harmonicWeightSum, double difficultyValue) { - if (ObjectDifficulties.Count == 0) + if (difficulties.Count == 0) return 0.0; if (harmonicWeightSum == 0) @@ -119,7 +159,7 @@ public double CountTopWeightedObjectDifficulties(double difficultyValue) if (consistentTopNote == 0) return 0; - return ObjectDifficulties.Sum(d => DiffUtils.Logistic(d / consistentTopNote, 1.15, 5, 1.1)); + return difficulties.Sum(d => DiffUtils.Logistic(d / consistentTopNote, 1.15, 5, 1.1)); } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f79359e6578d..6d59f2ec1201 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -15,24 +15,38 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { + public class SpeedAttributes : ISkillAttributes + { + public required double Difficulty { get; init; } + public required List ObjectDifficulties { get; init; } + public required double RelevantObjectCount { get; init; } + public required double TopWeightedSlidersCount { get; init; } + public required double TopWeightedObjectDifficultiesCount { get; init; } + } + /// /// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit. /// - public class Speed : Skill + public class Speed : ISkill { + public IReadOnlyList Mods { get; init; } + public IReadOnlyList DifficultyHitObjects { get; init; } + private readonly List sliderStrains = new List(); private double currentStrain; - private double harmonicWeightSum; - public Speed(Mod[] mods) - : base(mods) + private const double harmonic_scale = 20.0; + + public Speed(Mod[] mods, DifficultyHitObject[] difficultyHitObjects) { + Mods = mods; + DifficultyHitObjects = difficultyHitObjects; } private double strainDecay(double ms) => DiffUtils.Pow(0.3, ms / 1000); - protected override double ProcessInternal(DifficultyHitObject current) + private double objectDifficultyOf(DifficultyHitObject current) { const double skill_multiplier = 1.16; @@ -64,32 +78,64 @@ private double calculateAdjustedDifficulty(DifficultyHitObject current) return difficulty; } - public override double DifficultyValue() + public ISkillAttributes Process() { - if (ObjectDifficulties.Count == 0) - return 0; - - (double difficulty, harmonicWeightSum) = HarmonicSeries.Aggregate(ObjectDifficulties, harmonicScale: 20); + var objectDifficulties = new List(); + + foreach (var difficultyHitObject in DifficultyHitObjects) + { + objectDifficulties.Add(objectDifficultyOf(difficultyHitObject)); + } + + (double difficulty, double harmonicWeightSum) = HarmonicSeries.Aggregate(objectDifficulties, harmonicScale: harmonic_scale); + + return new SpeedAttributes + { + Difficulty = difficulty, + ObjectDifficulties = objectDifficulties, + RelevantObjectCount = relevantObjectCount(objectDifficulties), + TopWeightedSlidersCount = countTopWeightedDifficulties(sliderStrains, harmonicWeightSum, difficulty), + TopWeightedObjectDifficultiesCount = countTopWeightedDifficulties(objectDifficulties, harmonicWeightSum, difficulty) + }; + } - return difficulty; + public IEnumerable ProcessTimed() + { + var objectDifficulties = new List(); + + foreach (var difficultyHitObject in DifficultyHitObjects) + { + objectDifficulties.Add(objectDifficultyOf(difficultyHitObject)); + + (double difficulty, double harmonicWeightSum) = HarmonicSeries.Aggregate(objectDifficulties, harmonicScale: harmonic_scale); + + yield return new TimedSkillAttributes(new SpeedAttributes + { + Difficulty = difficulty, + ObjectDifficulties = objectDifficulties, + RelevantObjectCount = relevantObjectCount(objectDifficulties), + TopWeightedSlidersCount = countTopWeightedDifficulties(sliderStrains, harmonicWeightSum, difficulty), + TopWeightedObjectDifficultiesCount = countTopWeightedDifficulties(objectDifficulties, harmonicWeightSum, difficulty) + }, difficultyHitObject.EndTime); + } } - public double RelevantObjectCount() + private double relevantObjectCount(List objectDifficulties) { - if (ObjectDifficulties.Count == 0) + if (objectDifficulties.Count == 0) return 0; - double maxStrain = ObjectDifficulties.Max(); + double maxStrain = objectDifficulties.Max(); if (maxStrain == 0) return 0; - return ObjectDifficulties.Sum(strain => DiffUtils.Logistic(strain / maxStrain, 0.5, 12.0)); + return objectDifficulties.Sum(strain => DiffUtils.Logistic(strain / maxStrain, 0.5, 12.0)); } - public virtual double CountTopWeightedObjectDifficulties(double difficultyValue) + private double countTopWeightedDifficulties(List difficulties, double harmonicWeightSum, double difficultyValue) { - if (ObjectDifficulties.Count == 0) + if (difficulties.Count == 0) return 0.0; if (harmonicWeightSum == 0) @@ -100,24 +146,7 @@ public virtual double CountTopWeightedObjectDifficulties(double difficultyValue) if (consistentTopObject == 0) return 0; - return ObjectDifficulties.Sum(d => DiffUtils.Logistic(d / consistentTopObject, 0.88, 10, 1.1)); - } - - public double CountTopWeightedSliders(double difficultyValue) - { - if (sliderStrains.Count == 0) - return 0; - - if (harmonicWeightSum == 0) - return 0.0; - - double consistentTopObject = difficultyValue / harmonicWeightSum; // What would the top note be if all note values were identical - - if (consistentTopObject == 0) - return 0; - - // Use a weighted sum of all notes. Constants are arbitrary and give nice values - return sliderStrains.Sum(s => DiffUtils.Logistic(s / consistentTopObject, 0.88, 10, 1.1)); + return difficulties.Sum(d => DiffUtils.Logistic(d / consistentTopObject, 0.88, 10, 1.1)); } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index a4b33b7c1551..1ddb8034f955 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -24,6 +24,11 @@ public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); + [TestCase(3.3190848563395079d, 200, "diffcalc-test")] + [TestCase(3.3190848563395079d, 200, "diffcalc-test-strong")] + public void TestTimed(double expectedStarRating, int expectedMaxCombo, string name) + => base.TestTimed(expectedStarRating, expectedMaxCombo, name); + protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap); protected override Ruleset CreateRuleset() => new TaikoRuleset(); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 2d45b5eed078..107293f3bd58 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.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.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; @@ -8,6 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + public class ColourAttributes : StrainSkillAttributes; + /// /// Calculates the colour coefficient of taiko difficulty. /// @@ -20,8 +23,8 @@ public class Colour : StrainDecaySkill // slower maps. protected override double StrainDecayBase => 0.8; - public Colour(Mod[] mods) - : base(mods) + public Colour(Mod[] mods, DifficultyHitObject[] difficultyHitObjects) + : base(mods, difficultyHitObjects) { } @@ -29,5 +32,34 @@ protected override double StrainValueOf(DifficultyHitObject current) { return ColourEvaluator.EvaluateDifficultyOf(current); } + + public override ISkillAttributes Process() + { + var baseAttributes = (StrainSkillAttributes)base.Process(); + + return new ColourAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount + }; + } + + public override IEnumerable ProcessTimed() + { + foreach (var baseTimedAttributes in base.ProcessTimed()) + { + var baseAttributes = (StrainSkillAttributes)baseTimedAttributes.Attributes; + + yield return new TimedSkillAttributes(new ColourAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount + }, baseTimedAttributes.Time); + } + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs index 7e3006dfbf19..4eaafbc549e5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.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.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; @@ -11,6 +12,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + public class ReadingAttributes : StrainSkillAttributes; + /// /// Calculates the reading coefficient of taiko difficulty. /// @@ -21,8 +24,8 @@ public class Reading : StrainDecaySkill private double currentStrain; - public Reading(Mod[] mods) - : base(mods) + public Reading(Mod[] mods, DifficultyHitObject[] difficultyHitObjects) + : base(mods, difficultyHitObjects) { } @@ -44,5 +47,34 @@ protected override double StrainValueOf(DifficultyHitObject current) return currentStrain; } + + public override ISkillAttributes Process() + { + var baseAttributes = (StrainSkillAttributes)base.Process(); + + return new ReadingAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount + }; + } + + public override IEnumerable ProcessTimed() + { + foreach (var baseTimedAttributes in base.ProcessTimed()) + { + var baseAttributes = (StrainSkillAttributes)baseTimedAttributes.Attributes; + + yield return new TimedSkillAttributes(new ReadingAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount + }, baseTimedAttributes.Time); + } + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index d04d50ee880d..f691f3910f55 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.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.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; @@ -9,6 +10,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + public class RhythmAttributes : StrainSkillAttributes; + /// /// Calculates the rhythm coefficient of taiko difficulty. /// @@ -17,8 +20,8 @@ public class Rhythm : StrainDecaySkill protected override double SkillMultiplier => 1.0; protected override double StrainDecayBase => 0.4; - public Rhythm(Mod[] mods) - : base(mods) + public Rhythm(Mod[] mods, DifficultyHitObject[] difficultyHitObjects) + : base(mods, difficultyHitObjects) { } @@ -32,5 +35,34 @@ protected override double StrainValueOf(DifficultyHitObject current) return difficulty; } + + public override ISkillAttributes Process() + { + var baseAttributes = (StrainSkillAttributes)base.Process(); + + return new RhythmAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount + }; + } + + public override IEnumerable ProcessTimed() + { + foreach (var baseTimedAttributes in base.ProcessTimed()) + { + var baseAttributes = (StrainSkillAttributes)baseTimedAttributes.Attributes; + + yield return new TimedSkillAttributes(new RhythmAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount + }, baseTimedAttributes.Time); + } + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 023d7b6574a0..8b3bffec1db2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.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.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; @@ -10,6 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + public class StaminaAttributes : StrainSkillAttributes + { + public required bool SingleColourStamina { get; init; } + } + /// /// Calculates the stamina coefficient of taiko difficulty. /// @@ -27,10 +33,11 @@ public class Stamina : StrainSkill /// Creates a skill. /// /// Mods for use in skill calculations. + /// Difficulty hit objects for use in skill calculations. /// Reads when Stamina is from a single coloured pattern. /// Determines if the currently evaluated beatmap is converted. - public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert) - : base(mods) + public Stamina(Mod[] mods, DifficultyHitObject[] difficultyHitObjects, bool singleColourStamina, bool isConvert) + : base(mods, difficultyHitObjects) { SingleColourStamina = singleColourStamina; this.isConvert = isConvert; @@ -64,5 +71,36 @@ protected override double CalculateInitialStrain(double time, DifficultyHitObjec SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); + + public override ISkillAttributes Process() + { + var baseAttributes = (StrainSkillAttributes)base.Process(); + + return new StaminaAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount, + SingleColourStamina = SingleColourStamina + }; + } + + public override IEnumerable ProcessTimed() + { + foreach (var baseTimedAttributes in base.ProcessTimed()) + { + var baseAttributes = (StrainSkillAttributes)baseTimedAttributes.Attributes; + + yield return new TimedSkillAttributes(new StaminaAttributes + { + Difficulty = baseAttributes.Difficulty, + ObjectDifficulties = baseAttributes.ObjectDifficulties, + StrainPeaks = baseAttributes.StrainPeaks, + TopWeightedStrainsCount = baseAttributes.TopWeightedStrainsCount, + SingleColourStamina = SingleColourStamina + }, baseTimedAttributes.Time); + } + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 9e745c6dcc35..9c095c75f50e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -40,18 +40,18 @@ public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) { } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) { isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; isRelax = mods.Any(h => h is TaikoModRelax); - return new Skill[] + return new ISkill[] { - new Rhythm(mods), - new Reading(mods), - new Colour(mods), - new Stamina(mods, false, isConvert), - new Stamina(mods, true, isConvert) + new Rhythm(mods, difficultyHitObjects), + new Reading(mods, difficultyHitObjects), + new Colour(mods, difficultyHitObjects), + new Stamina(mods, difficultyHitObjects, false, isConvert), + new Stamina(mods, difficultyHitObjects, true, isConvert) }; } @@ -95,34 +95,35 @@ protected override IEnumerable CreateDifficultyHitObjects(I return difficultyHitObjects; } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skillAttributes) { if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; - 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); + var rhythmAttributes = skillAttributes.OfType().Single(); + var readingAttributes = skillAttributes.OfType().Single(); + var colourAttributes = skillAttributes.OfType().Single(); + var staminaAttributes = skillAttributes.OfType().Single(s => !s.SingleColourStamina); + var singleColourStaminaAttributes = skillAttributes.OfType().Single(s => s.SingleColourStamina); - double staminaDifficultyValue = stamina.DifficultyValue(); + double staminaDifficultyValue = staminaAttributes.Difficulty; - double rhythmSkill = rhythm.DifficultyValue() * rhythm_skill_multiplier; - double readingSkill = reading.DifficultyValue() * reading_skill_multiplier; - double colourSkill = colour.DifficultyValue() * colour_skill_multiplier; + double rhythmSkill = rhythmAttributes.Difficulty * rhythm_skill_multiplier; + double readingSkill = readingAttributes.Difficulty * reading_skill_multiplier; + double colourSkill = colourAttributes.Difficulty * colour_skill_multiplier; double staminaSkill = staminaDifficultyValue * stamina_skill_multiplier; - double monoStaminaSkill = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; + double monoStaminaSkill = singleColourStaminaAttributes.Difficulty * stamina_skill_multiplier; double monoStaminaFactor = staminaSkill == 0 ? 1 : DiffUtils.Pow(monoStaminaSkill / staminaSkill, 5); - double staminaDifficultStrains = stamina.CountTopWeightedStrains(staminaDifficultyValue); + double staminaDifficultStrains = staminaAttributes.TopWeightedStrainsCount; // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm. patternMultiplier = DiffUtils.Pow(staminaSkill * colourSkill, 0.10); strainLengthBonus = 1 + 0.15 * DiffUtils.ReverseLerp(staminaDifficultStrains, 1000, 1555); - double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, out double consistencyFactor); + double combinedRating = combinedDifficultyValue(rhythmAttributes, readingAttributes, colourAttributes, staminaAttributes, out double consistencyFactor); + double starRating = rescale(combinedRating * 1.4); // Calculate proportional contribution of each skill to the combinedRating. @@ -159,13 +160,14 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat /// 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, out double consistencyFactor) + private double combinedDifficultyValue(StrainSkillAttributes rhythmAttributes, StrainSkillAttributes readingAttributes, StrainSkillAttributes colourAttributes, StrainSkillAttributes staminaAttributes, + out double consistencyFactor) { List peaks = combinePeaks( - rhythm.GetCurrentStrainPeaks().ToList(), - reading.GetCurrentStrainPeaks().ToList(), - colour.GetCurrentStrainPeaks().ToList(), - stamina.GetCurrentStrainPeaks().ToList() + rhythmAttributes.StrainPeaks, + readingAttributes.StrainPeaks, + colourAttributes.StrainPeaks, + staminaAttributes.StrainPeaks ); if (peaks.Count == 0) @@ -184,10 +186,10 @@ private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour co } List hitObjectStrainPeaks = combinePeaks( - rhythm.GetObjectDifficulties(), - reading.GetObjectDifficulties(), - colour.GetObjectDifficulties(), - stamina.GetObjectDifficulties() + rhythmAttributes.ObjectDifficulties, + readingAttributes.ObjectDifficulties, + colourAttributes.ObjectDifficulties, + staminaAttributes.ObjectDifficulties ); if (hitObjectStrainPeaks.Count == 0) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index eecabbab6684..f748f5c04d25 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -218,7 +218,7 @@ public TestLegacyDifficultyCalculator(params Mod[] mods) protected override Mod[] DifficultyAdjustmentMods { get; } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skills) { throw new NotImplementedException(); } @@ -228,7 +228,7 @@ protected override IEnumerable CreateDifficultyHitObjects(I throw new NotImplementedException(); } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) { throw new NotImplementedException(); } diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs index 4da5ba13cb6b..7de3f7edae2a 100644 --- a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs +++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs @@ -173,7 +173,7 @@ public TestDifficultyCalculator(IWorkingBeatmap beatmap) { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skills) => new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() }; protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) @@ -194,21 +194,37 @@ protected override IEnumerable CreateDifficultyHitObjects(I return objects; } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { new PassThroughSkill(mods) }; + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) => new ISkill[] { new PassThroughSkill(mods, difficultyHitObjects) }; - private class PassThroughSkill : Skill + private class PassThroughSkill : ISkill { - public PassThroughSkill(Mod[] mods) - : base(mods) + public class EmptySkillAttributes : ISkillAttributes { + public double Difficulty { get; init; } = 0; + public List ObjectDifficulties { get; init; } = []; } - protected override double ProcessInternal(DifficultyHitObject current) + public PassThroughSkill(IReadOnlyList mods, IReadOnlyList difficultyHitObjects) { - return 0; + Mods = mods; + DifficultyHitObjects = difficultyHitObjects; } - public override double DifficultyValue() => 1; + public IReadOnlyList Mods { get; init; } + public IReadOnlyList DifficultyHitObjects { get; init; } + + public ISkillAttributes Process() + { + return new EmptySkillAttributes(); + } + + public IEnumerable ProcessTimed() + { + foreach (var difficultyHitObject in DifficultyHitObjects) + { + yield return new TimedSkillAttributes(new EmptySkillAttributes(), difficultyHitObject.EndTime); + } + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs index a841de592da0..7122e872ed92 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -208,14 +208,14 @@ public TestDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skills) => new DifficultyAttributes(mods, mods.OfType().SingleOrDefault()?.Difficulty.Value ?? 0); protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Array.Empty(); - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) - => Array.Empty(); + protected override ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects) + => Array.Empty(); } private class TestPerformanceCalculator : PerformanceCalculator diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index bd7f3c2d6b55..a502c8106b65 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -33,8 +33,6 @@ public abstract class DifficultyCalculator /// protected readonly IWorkingBeatmap WorkingBeatmap; - private Mod[] playableMods; - private readonly IRulesetInfo ruleset; /// @@ -70,24 +68,15 @@ public DifficultyAttributes Calculate([NotNull] IEnumerable mods, Cancellat cancellationToken = timedCancellationSource.Token; cancellationToken.ThrowIfCancellationRequested(); - // ReSharper disable once PossiblyMistakenUseOfCancellationToken - preProcess(mods, cancellationToken); - var skills = CreateSkills(Beatmap, playableMods); + var playableMods = mods.Select(m => m.DeepClone()).ToArray(); - if (!Beatmap.HitObjects.Any()) - return CreateDifficultyAttributes(Beatmap, playableMods, skills); + // ReSharper disable once PossiblyMistakenUseOfCancellationToken + preProcess(playableMods, cancellationToken); - foreach (var hitObject in getDifficultyHitObjects()) - { - foreach (var skill in skills) - { - cancellationToken.ThrowIfCancellationRequested(); - skill.Process(hitObject); - } - } + var skillAttributes = ProcessSkills(Beatmap, playableMods, cancellationToken); - return CreateDifficultyAttributes(Beatmap, playableMods, skills); + return CreateDifficultyAttributes(Beatmap, playableMods, skillAttributes); } /// @@ -112,39 +101,94 @@ public List CalculateTimed([NotNull] IEnumerable cancellationToken = timedCancellationSource.Token; cancellationToken.ThrowIfCancellationRequested(); + + var playableMods = mods.Select(m => m.DeepClone()).ToArray(); + // ReSharper disable once PossiblyMistakenUseOfCancellationToken - preProcess(mods, cancellationToken); + preProcess(playableMods, cancellationToken); var attribs = new List(); if (!Beatmap.HitObjects.Any()) return attribs; - var skills = CreateSkills(Beatmap, playableMods); - var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); - var difficultyObjects = getDifficultyHitObjects().ToArray(); + var timedSkillAttributes = ProcessSkillsTimed(Beatmap, playableMods, cancellationToken); - int currentIndex = 0; + foreach (var skillAttributes in timedSkillAttributes) + { + attribs.Add(new TimedDifficultyAttributes(skillAttributes.Key, CreateDifficultyAttributes(Beatmap, playableMods, skillAttributes.Value))); + } - foreach (var obj in Beatmap.HitObjects) + return attribs; + } + + /// + /// Calculates per-skill difficulty of the beatmap using a specific mod combination. + /// + /// Beatmap to calculate difficulty for. + /// The mods that have been applied to the beatmap. + /// The cancellation token. + /// List of . + public virtual List ProcessSkills(IBeatmap beatmap, Mod[] playableMods, CancellationToken cancellationToken = default) + { + var difficultyHitObjects = getDifficultyHitObjects(playableMods); + + var skills = CreateSkills(beatmap, playableMods, difficultyHitObjects.ToArray()); + var skillAttributes = new List(); + + foreach (var skill in skills) { - progressiveBeatmap.HitObjects.Add(obj); + cancellationToken.ThrowIfCancellationRequested(); + skillAttributes.Add(skill.Process()); + } - while (currentIndex < difficultyObjects.Length && difficultyObjects[currentIndex].BaseObject.GetEndTime() <= obj.GetEndTime()) - { - foreach (var skill in skills) - { - cancellationToken.ThrowIfCancellationRequested(); - skill.Process(difficultyObjects[currentIndex]); - } + return skillAttributes; + } + + /// + /// Calculates per-skill difficulty of the beatmap using a specific mod combination and returns at every relevant time value. + /// + /// Beatmap to calculate difficulty for. + /// The mods that have been applied to the beatmap. + /// The cancellation token. + /// Lists of per-skill at different time points. + public virtual Dictionary> ProcessSkillsTimed(IBeatmap beatmap, Mod[] playableMods, CancellationToken cancellationToken = default) + { + var difficultyHitObjects = getDifficultyHitObjects(playableMods); + + var skills = CreateSkills(beatmap, playableMods, difficultyHitObjects.ToArray()); + var timedSkillAttributes = new List(); + + foreach (var skill in skills) + { + cancellationToken.ThrowIfCancellationRequested(); + timedSkillAttributes.Add(skill.ProcessTimed().ToArray()); + } + + // Skills have authority over what times they want to calculate skill attributes at + // so we want to try to calculate difficulty attributes for every timing point available + var times = timedSkillAttributes.SelectMany(x => x.Select(y => y.Time)).Distinct().ToList(); - currentIndex++; + var timedAttributes = new Dictionary>(); + + foreach (double time in times) + { + cancellationToken.ThrowIfCancellationRequested(); + var attributes = new List(); + + foreach (var attribute in timedSkillAttributes) + { + // Different skills might calculate skill attributes at different times + // take the latest skill attributes for every skill to make sure we always pass all skill attributes to CalculateDifficultyAttributes + var closestAttributes = attribute.LastOrDefault(x => x.Time <= time); + if (closestAttributes != null) + attributes.Add(closestAttributes.Attributes); } - attribs.Add(new TimedDifficultyAttributes(obj.GetEndTime(), CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills))); + timedAttributes.Add(time, attributes); } - return attribs; + return timedAttributes; } /// @@ -173,16 +217,15 @@ public IEnumerable CalculateAllLegacyCombinations(Cancella /// /// Retrieves the s to calculate against. /// - private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, playableMods)); + private IEnumerable getDifficultyHitObjects(Mod[] playableMods) => SortObjects(CreateDifficultyHitObjects(Beatmap, playableMods)); /// /// Performs required tasks before every calculation. /// - /// The original list of s. + /// /// The cancellation token. - private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken) + private void preProcess([NotNull] Mod[] playableMods, CancellationToken cancellationToken) { - playableMods = mods.Select(m => m.DeepClone()).ToArray(); Beatmap = WorkingBeatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); } @@ -274,7 +317,7 @@ static IEnumerable createDifficultyAdjustmentModCombinations(ReadOnlyMemory /// This may differ from in the case of timed calculation. /// The s that difficulty was calculated with. /// The skills which processed the beatmap. - protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills); + protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, IReadOnlyList skills); /// /// Enumerates s to be processed from s in the . @@ -285,149 +328,13 @@ static IEnumerable createDifficultyAdjustmentModCombinations(ReadOnlyMemory protected abstract IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods); /// - /// Creates the s to calculate the difficulty of an . + /// Creates the s to calculate the difficulty of an . /// /// The whose difficulty will be calculated. /// This may differ from in the case of timed calculation. /// Mods to calculate difficulty with. - /// The s. - protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods); - - /// - /// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time. - /// - private class ProgressiveCalculationBeatmap : IBeatmap - { - private readonly IBeatmap baseBeatmap; - - public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap) - { - this.baseBeatmap = baseBeatmap; - } - - public readonly List HitObjects = new List(); - - IReadOnlyList IBeatmap.HitObjects => HitObjects; - - #region Delegated IBeatmap implementation - - public BeatmapInfo BeatmapInfo - { - get => baseBeatmap.BeatmapInfo; - set => baseBeatmap.BeatmapInfo = value; - } - - public ControlPointInfo ControlPointInfo - { - get => baseBeatmap.ControlPointInfo; - set => baseBeatmap.ControlPointInfo = value; - } - - public BeatmapMetadata Metadata => baseBeatmap.Metadata; - - public BeatmapDifficulty Difficulty - { - get => baseBeatmap.Difficulty; - set => baseBeatmap.Difficulty = value; - } - - public SortedList Breaks - { - get => baseBeatmap.Breaks; - set => baseBeatmap.Breaks = value; - } - - public double TotalBreakTime => baseBeatmap.TotalBreakTime; - public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); - public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); - public int BeatmapVersion => baseBeatmap.BeatmapVersion; - public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); - - public double AudioLeadIn - { - get => baseBeatmap.AudioLeadIn; - set => baseBeatmap.AudioLeadIn = value; - } - - public float StackLeniency - { - get => baseBeatmap.StackLeniency; - set => baseBeatmap.StackLeniency = value; - } - - public bool SpecialStyle - { - get => baseBeatmap.SpecialStyle; - set => baseBeatmap.SpecialStyle = value; - } - - public bool LetterboxInBreaks - { - get => baseBeatmap.LetterboxInBreaks; - set => baseBeatmap.LetterboxInBreaks = value; - } - - public bool WidescreenStoryboard - { - get => baseBeatmap.WidescreenStoryboard; - set => baseBeatmap.WidescreenStoryboard = value; - } - - public bool EpilepsyWarning - { - get => baseBeatmap.EpilepsyWarning; - set => baseBeatmap.EpilepsyWarning = value; - } - - public bool SamplesMatchPlaybackRate - { - get => baseBeatmap.SamplesMatchPlaybackRate; - set => baseBeatmap.SamplesMatchPlaybackRate = value; - } - - public double DistanceSpacing - { - get => baseBeatmap.DistanceSpacing; - set => baseBeatmap.DistanceSpacing = value; - } - - public int GridSize - { - get => baseBeatmap.GridSize; - set => baseBeatmap.GridSize = value; - } - - public double TimelineZoom - { - get => baseBeatmap.TimelineZoom; - set => baseBeatmap.TimelineZoom = value; - } - - public CountdownType Countdown - { - get => baseBeatmap.Countdown; - set => baseBeatmap.Countdown = value; - } - - public int CountdownOffset - { - get => baseBeatmap.CountdownOffset; - set => baseBeatmap.CountdownOffset = value; - } - - public int[] Bookmarks - { - get => baseBeatmap.Bookmarks; - set => baseBeatmap.Bookmarks = value; - } - - public double[] SliderVelocityPresets - { - get => baseBeatmap.SliderVelocityPresets; - set => baseBeatmap.SliderVelocityPresets = value; - } - - #endregion - } + /// + /// The s. + protected abstract ISkill[] CreateSkills(IBeatmap beatmap, Mod[] mods, DifficultyHitObject[] difficultyHitObjects); } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/ISkill.cs b/osu.Game/Rulesets/Difficulty/Skills/ISkill.cs new file mode 100644 index 000000000000..273df5966186 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Skills/ISkill.cs @@ -0,0 +1,35 @@ +// 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; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Difficulty.Skills +{ + /// + /// Skill difficulty calculation class. + /// + public interface ISkill + { + /// + /// Mods for use in skill calculations. + /// + IReadOnlyList Mods { get; init; } + + /// + /// s for use in skill calculations. + /// + IReadOnlyList DifficultyHitObjects { get; init; } + + /// + /// Processes and calculates with relevant information to calculate . + /// + ISkillAttributes Process(); + + /// + /// Processes and calculates an array of that can be then used to calculate . + /// + IEnumerable ProcessTimed(); + } +} diff --git a/osu.Game/Rulesets/Difficulty/Skills/ISkillAttributes.cs b/osu.Game/Rulesets/Difficulty/Skills/ISkillAttributes.cs new file mode 100644 index 000000000000..d1c40d8d917e --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Skills/ISkillAttributes.cs @@ -0,0 +1,23 @@ +// 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; + +namespace osu.Game.Rulesets.Difficulty.Skills +{ + /// + /// Structure representing output of processing. + /// + public interface ISkillAttributes + { + /// + /// Calculated difficulty value. + /// + double Difficulty { get; init; } + + /// + /// Difficulty values of . + /// + List ObjectDifficulties { get; init; } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs deleted file mode 100644 index cf45104c942c..000000000000 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ /dev/null @@ -1,54 +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.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Rulesets.Difficulty.Skills -{ - /// - /// A bare minimal abstract skill for fully custom skill implementations. - /// - /// - /// This class should be considered a "processing" class and not persisted. - /// - public abstract class Skill - { - /// - /// Mods for use in skill calculations. - /// - protected IReadOnlyList Mods => mods; - - /// - /// List of calculated per-object difficulties, populated by Process - /// - protected readonly List ObjectDifficulties = new List(); - - private readonly Mod[] mods; - - protected Skill(Mod[] mods) - { - this.mods = mods; - } - - /// - /// Process a . - /// - /// The to process. - public void Process(DifficultyHitObject current) - { - double difficultyValue = ProcessInternal(current); - ObjectDifficulties.Add(difficultyValue); - } - - protected abstract double ProcessInternal(DifficultyHitObject current); - - /// - /// Returns the calculated difficulty value representing all s that have been processed up to this point. - /// - public abstract double DifficultyValue(); - - public IReadOnlyList GetObjectDifficulties() => ObjectDifficulties; - } -} diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs index 431d2faba2ed..a7dab24e332f 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -29,8 +29,8 @@ public abstract class StrainDecaySkill : StrainSkill /// protected double CurrentStrain { get; private set; } - protected StrainDecaySkill(Mod[] mods) - : base(mods) + protected StrainDecaySkill(Mod[] mods, DifficultyHitObject[] difficultyHitObjects) + : base(mods, difficultyHitObjects) { } diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 269888ebd7ed..b5efd448a216 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -10,11 +10,19 @@ namespace osu.Game.Rulesets.Difficulty.Skills { + public class StrainSkillAttributes : ISkillAttributes + { + public required double Difficulty { get; init; } + public required List ObjectDifficulties { get; init; } + public required List StrainPeaks { get; init; } + public required double TopWeightedStrainsCount { get; init; } + } + /// /// Used to processes strain values of s, keep track of strain levels caused by the processed objects /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects. /// - public abstract class StrainSkill : Skill + public abstract class StrainSkill : ISkill { /// /// The weight by which each strain value decays. @@ -31,9 +39,13 @@ public abstract class StrainSkill : Skill private readonly List strainPeaks = new List(); - protected StrainSkill(Mod[] mods) - : base(mods) + public IReadOnlyList Mods { get; init; } + public IReadOnlyList DifficultyHitObjects { get; init; } + + protected StrainSkill(Mod[] mods, DifficultyHitObject[] difficultyHitObjects) { + Mods = mods; + DifficultyHitObjects = difficultyHitObjects; } /// @@ -44,7 +56,7 @@ protected StrainSkill(Mod[] mods) /// /// Process a and update current strain values accordingly. /// - protected sealed override double ProcessInternal(DifficultyHitObject current) + protected double ProcessObject(DifficultyHitObject current) { // The first object doesn't generate a strain, so we begin with an incremented section end if (current.Index == 0) @@ -67,18 +79,18 @@ protected sealed override double ProcessInternal(DifficultyHitObject current) /// Calculates the number of strains weighted against the top strain. /// The result is scaled by clock rate as it affects the total number of strains. /// - public double CountTopWeightedStrains(double difficultyValue) + protected double CountTopWeightedStrains(List objectDifficulties, double difficultyValue) { - if (ObjectDifficulties.Count == 0) + if (objectDifficulties.Count == 0) return 0.0; double consistentTopStrain = difficultyValue * (1 - DecayWeight); // What would the top strain be if all strain values were identical if (consistentTopStrain == 0) - return ObjectDifficulties.Count; + return objectDifficulties.Count; // Use a weighted sum of all strains. Constants are arbitrary and give nice values - return ObjectDifficulties.Sum(s => DiffUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1)); + return objectDifficulties.Sum(s => DiffUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1)); } /// @@ -113,12 +125,12 @@ private void startNewSectionFrom(double time, DifficultyHitObject current) /// Returns a live enumerable of the peak strains for each section of the beatmap, /// including the peak of the current section. /// - public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak); + protected IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak); /// /// Returns the calculated difficulty value representing all s that have been processed up to this point. /// - public override double DifficultyValue() + protected virtual double Aggregate() { double difficulty = 0; double weight = 1; @@ -137,5 +149,45 @@ public override double DifficultyValue() return difficulty; } + + public virtual ISkillAttributes Process() + { + var objectDifficulties = new List(); + + foreach (var difficultyHitObject in DifficultyHitObjects) + { + objectDifficulties.Add(ProcessObject(difficultyHitObject)); + } + + double difficulty = Aggregate(); + + return new StrainSkillAttributes + { + Difficulty = difficulty, + ObjectDifficulties = objectDifficulties, + StrainPeaks = GetCurrentStrainPeaks().ToList(), + TopWeightedStrainsCount = CountTopWeightedStrains(objectDifficulties, difficulty) + }; + } + + public virtual IEnumerable ProcessTimed() + { + var objectDifficulties = new List(); + + foreach (var difficultyHitObject in DifficultyHitObjects) + { + objectDifficulties.Add(ProcessObject(difficultyHitObject)); + + double difficulty = Aggregate(); + + yield return new TimedSkillAttributes(new StrainSkillAttributes + { + Difficulty = difficulty, + ObjectDifficulties = objectDifficulties, + StrainPeaks = GetCurrentStrainPeaks().ToList(), + TopWeightedStrainsCount = CountTopWeightedStrains(objectDifficulties, difficulty) + }, difficultyHitObject.EndTime); + } + } } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/TimedSkillAttributes.cs b/osu.Game/Rulesets/Difficulty/Skills/TimedSkillAttributes.cs new file mode 100644 index 000000000000..6e9f18ff2901 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Skills/TimedSkillAttributes.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Rulesets.Difficulty.Skills +{ + /// + /// Wraps a object and adds a time value for which the attribute is valid. + /// + public class TimedSkillAttributes : IComparable + { + public readonly double Time; + public readonly ISkillAttributes Attributes; + + public TimedSkillAttributes(ISkillAttributes attributes, double time) + { + Attributes = attributes; + Time = time; + } + + public int CompareTo(TimedSkillAttributes? other) => Time.CompareTo(other?.Time); + } +} diff --git a/osu.Game/Rulesets/Difficulty/Skills/VariableLengthStrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/VariableLengthStrainSkill.cs index 30b55ae68afe..2c6ef74d2c82 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/VariableLengthStrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/VariableLengthStrainSkill.cs @@ -11,6 +11,13 @@ namespace osu.Game.Rulesets.Difficulty.Skills { + public class VariableLengthStrainSkillAttributes : ISkillAttributes + { + public required double Difficulty { get; init; } + public required List ObjectDifficulties { get; init; } + public required double TopWeightedStrainsCount { get; init; } + } + /// /// Similar to , but instead of strains having a fixed length, strains can be any length. /// A new is created for each . @@ -19,7 +26,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// This class intends to replace eventually as it fixes bugs with that implementation. /// Has not yet been applied globally as it changes resultant PP values in ways which may require discretion. /// - public abstract class VariableLengthStrainSkill : Skill + public abstract class VariableLengthStrainSkill : ISkill { /// /// The weight by which each strain value decays. @@ -54,15 +61,20 @@ public abstract class VariableLengthStrainSkill : Skill /// private readonly List<(double StrainValue, double StartTime)> queuedStrains = new List<(double, double)>(); + public IReadOnlyList Mods { get; init; } + public IReadOnlyList DifficultyHitObjects { get; init; } + /// /// Create a new . /// /// The mods. + /// /// The weight by which each strain value decays. /// The maximum length of each strain section. - protected VariableLengthStrainSkill(Mod[] mods, double decayWeight = 0.9, int maxSectionLength = 400) - : base(mods) + protected VariableLengthStrainSkill(Mod[] mods, DifficultyHitObject[] difficultyHitObjects, double decayWeight = 0.9, int maxSectionLength = 400) { + Mods = mods; + DifficultyHitObjects = difficultyHitObjects; DecayWeight = decayWeight; MaxSectionLength = maxSectionLength; @@ -77,7 +89,7 @@ protected VariableLengthStrainSkill(Mod[] mods, double decayWeight = 0.9, int ma /// /// Process a and update current strain values accordingly. /// - protected sealed override double ProcessInternal(DifficultyHitObject current) + protected double ProcessObject(DifficultyHitObject current) { // If we're on the first object, set up the first section to end `MaxSectionLength` after it. if (current.Index == 0) @@ -231,18 +243,58 @@ public IEnumerable GetCurrentStrainPeaks() /// Calculates the number of strains weighted against the top strain. /// The result is scaled by clock rate as it affects the total number of strains. /// - public virtual double CountTopWeightedStrains(double difficultyValue) + protected virtual double CountTopWeightedStrains(List objectDifficulties, double difficultyValue) { - if (ObjectDifficulties.Count == 0) + if (objectDifficulties.Count == 0) return 0.0; double consistentTopStrain = difficultyValue * (1 - DecayWeight); // What would the top strain be if all strain values were identical if (consistentTopStrain == 0) - return ObjectDifficulties.Count; + return objectDifficulties.Count; // Use a weighted sum of all strains. Constants are arbitrary and give nice values - return ObjectDifficulties.Sum(s => DiffUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1)); + return objectDifficulties.Sum(s => DiffUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1)); + } + + protected abstract double Aggregate(); + + public virtual ISkillAttributes Process() + { + var objectDifficulties = new List(); + + foreach (var difficultyHitObject in DifficultyHitObjects) + { + objectDifficulties.Add(ProcessObject(difficultyHitObject)); + } + + double difficulty = Aggregate(); + + return new VariableLengthStrainSkillAttributes + { + Difficulty = difficulty, + ObjectDifficulties = objectDifficulties, + TopWeightedStrainsCount = CountTopWeightedStrains(objectDifficulties, difficulty) + }; + } + + public virtual IEnumerable ProcessTimed() + { + var objectDifficulties = new List(); + + foreach (var difficultyHitObject in DifficultyHitObjects) + { + objectDifficulties.Add(ProcessObject(difficultyHitObject)); + + double difficulty = Aggregate(); + + yield return new TimedSkillAttributes(new VariableLengthStrainSkillAttributes + { + Difficulty = difficulty, + ObjectDifficulties = objectDifficulties, + TopWeightedStrainsCount = CountTopWeightedStrains(objectDifficulties, difficulty) + }, difficultyHitObject.EndTime); + } } /// diff --git a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs index 855be9b0930a..b9659bfe0692 100644 --- a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs +++ b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs @@ -43,6 +43,15 @@ protected void Test(double? expectedStarRating, int expectedMaxCombo, string nam Assert.That(timedAttributes.Last().Attributes, Is.EqualTo(attributes).UsingPropertiesComparer()); } + protected void TestTimed(double? expectedStarRating, int expectedMaxCombo, string name, params Mod[] mods) + { + var attributes = CreateDifficultyCalculator(GetBeatmap(name)).CalculateTimed(mods).Last().Attributes; + + // Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences. + Assert.That(attributes.StarRating, Is.EqualTo(expectedStarRating).Within(0.00001)); + Assert.That(attributes.MaxCombo, Is.EqualTo(expectedMaxCombo)); + } + protected IWorkingBeatmap GetBeatmap(string name) { using (var resStream = openResource($"{resource_namespace}.{name}.osu"))