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 312d3d5e9a..6c49efa6b9 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs @@ -19,9 +19,9 @@ namespace osu.Game.Rulesets.EmptyFreeform { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { - return new DifficultyAttributes(mods, 0); + return new DifficultyAttributes(); } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.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 f6addab279..3fe024086c 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs @@ -19,9 +19,9 @@ namespace osu.Game.Rulesets.Pippidon { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { - return new DifficultyAttributes(mods, 0); + return new DifficultyAttributes(); } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.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 a4dc1762d5..7060872bda 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,9 +19,9 @@ namespace osu.Game.Rulesets.EmptyScrolling { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { - return new DifficultyAttributes(mods, 0); + return new DifficultyAttributes(); } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.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 f6addab279..3fe024086c 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,9 +19,9 @@ namespace osu.Game.Rulesets.Pippidon { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { - return new DifficultyAttributes(mods, 0); + return new DifficultyAttributes(); } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty(); diff --git a/osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs b/osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs new file mode 100644 index 0000000000..39eb5b78da --- /dev/null +++ b/osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using BenchmarkDotNet.Attributes; +using osu.Framework.IO.Stores; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; +using osu.Game.Tests.Resources; + +namespace osu.Game.Benchmarks +{ + public class BenchmarkDifficultyCalculation : BenchmarkTest + { + private FlatWorkingBeatmap beatmap = null!; + + private IDifficultyAttributes osuAttributes = null!; + private IDifficultyAttributes taikoAttributes = null!; + private IDifficultyAttributes catchAttributes = null!; + private IDifficultyAttributes maniaAttributes = null!; + + public override void SetUp() + { + using var resources = new DllResourceStore(typeof(TestResources).Assembly); + using var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz"); + using var zipReader = new ZipArchiveReader(archive); + + using var beatmapStream = new MemoryStream(); + zipReader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream); + beatmapStream.Seek(0, SeekOrigin.Begin); + var reader = new LineBufferedReader(beatmapStream); + var decoder = Decoder.GetDecoder(reader); + + beatmap = new FlatWorkingBeatmap(decoder.Decode(reader)); + + // Prepare difficulty attributes for an isolated performance calculation in every mode. + osuAttributes = DifficultyOsu(); + taikoAttributes = DifficultyTaiko(); + catchAttributes = DifficultyCatch(); + maniaAttributes = DifficultyMania(); + } + + [Benchmark] + public IDifficultyAttributes DifficultyOsu() => new OsuRuleset().CreateDifficultyCalculator(beatmap).Calculate(); + + [Benchmark] + public IDifficultyAttributes DifficultyTaiko() => new TaikoRuleset().CreateDifficultyCalculator(beatmap).Calculate(); + + [Benchmark] + public IDifficultyAttributes DifficultyCatch() => new CatchRuleset().CreateDifficultyCalculator(beatmap).Calculate(); + + [Benchmark] + public IDifficultyAttributes DifficultyMania() => new ManiaRuleset().CreateDifficultyCalculator(beatmap).Calculate(); + + [Benchmark] + public void PerformanceOsu() + { + Ruleset ruleset = new OsuRuleset(); + ScoreInfo score = new ScoreInfo(beatmap.BeatmapInfo, ruleset.RulesetInfo); + ruleset.CreatePerformanceCalculator()!.Calculate(score, osuAttributes); + } + + [Benchmark] + public void PerformanceTaiko() + { + Ruleset ruleset = new TaikoRuleset(); + ScoreInfo score = new ScoreInfo(beatmap.BeatmapInfo, ruleset.RulesetInfo); + ruleset.CreatePerformanceCalculator()!.Calculate(score, taikoAttributes); + } + + [Benchmark] + public void PerformanceCatch() + { + Ruleset ruleset = new CatchRuleset(); + ScoreInfo score = new ScoreInfo(beatmap.BeatmapInfo, ruleset.RulesetInfo); + ruleset.CreatePerformanceCalculator()!.Calculate(score, catchAttributes); + } + + [Benchmark] + public void PerformanceMania() + { + Ruleset ruleset = new ManiaRuleset(); + ScoreInfo score = new ScoreInfo(beatmap.BeatmapInfo, ruleset.RulesetInfo); + ruleset.CreatePerformanceCalculator()!.Calculate(score, maniaAttributes); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 5c64643fd4..4f4ea014d0 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -8,8 +8,15 @@ using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Catch.Difficulty { - public class CatchDifficultyAttributes : DifficultyAttributes + [JsonObject(MemberSerialization.OptIn)] + public struct CatchDifficultyAttributes : IDifficultyAttributes { + /// + public double StarRating { get; set; } + + /// + public int MaxCombo { get; set; } + /// /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). /// @@ -19,22 +26,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty [JsonProperty("approach_rate")] public double ApproachRate { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() + public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { - foreach (var v in base.ToDatabaseAttributes()) - yield return v; - + yield return (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo); // 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); + yield return (IDifficultyAttributes.ATTRIB_ID_AIM, StarRating); + yield return (IDifficultyAttributes.ATTRIB_ID_APPROACH_RATE, ApproachRate); } - public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) + public void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { - base.FromDatabaseAttributes(values, onlineInfo); - - StarRating = values[ATTRIB_ID_AIM]; - ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; + MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO]; + StarRating = values[IDifficultyAttributes.ATTRIB_ID_AIM]; + ApproachRate = values[IDifficultyAttributes.ATTRIB_ID_APPROACH_RATE]; } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 7d21409ee8..8978d764ef 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -30,10 +30,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) - return new CatchDifficultyAttributes { Mods = mods }; + return new CatchDifficultyAttributes(); // 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; @@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills[0].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/CatchPerformanceAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceAttributes.cs index 1335fc2d23..3673e79159 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceAttributes.cs @@ -1,11 +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; +using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Catch.Difficulty { - public class CatchPerformanceAttributes : PerformanceAttributes + public struct CatchPerformanceAttributes : IPerformanceAttributes { + /// + /// Calculated score performance points. + /// + [JsonProperty("pp")] + public double Total { get; set; } + + public IEnumerable GetAttributesForDisplay() + { + yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total); + } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 55232a9598..a8a0e3c444 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { } - protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) + protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes) { var catchAttributes = (CatchDifficultyAttributes)attributes; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index db60e757e1..7929ab53ee 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -8,8 +8,15 @@ using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Mania.Difficulty { - public class ManiaDifficultyAttributes : DifficultyAttributes + [JsonObject(MemberSerialization.OptIn)] + public struct ManiaDifficultyAttributes : IDifficultyAttributes { + /// + public double StarRating { get; set; } + + /// + public int MaxCombo { get; set; } + /// /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). /// @@ -19,21 +26,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() + public 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 (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo); + yield return (IDifficultyAttributes.ATTRIB_ID_DIFFICULTY, StarRating); + yield return (IDifficultyAttributes.ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } - public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) + public void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { - base.FromDatabaseAttributes(values, onlineInfo); - - StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; + MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO]; + StarRating = values[IDifficultyAttributes.ATTRIB_ID_DIFFICULTY]; + GreatHitWindow = values[IDifficultyAttributes.ATTRIB_ID_GREAT_HIT_WINDOW]; } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index ff9aa4aa7b..7f3cb867a4 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -38,10 +38,10 @@ namespace osu.Game.Rulesets.Mania.Difficulty originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) - return new ManiaDifficultyAttributes { Mods = mods }; + return new ManiaDifficultyAttributes(); HitWindows hitWindows = new ManiaHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); @@ -49,7 +49,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { StarRating = skills[0].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), diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index 64f8b026c2..6a90a6f117 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -7,16 +7,20 @@ using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Mania.Difficulty { - public class ManiaPerformanceAttributes : PerformanceAttributes + public struct ManiaPerformanceAttributes : IPerformanceAttributes { + /// + /// Calculated score performance points. + /// + [JsonProperty("pp")] + public double Total { get; set; } + [JsonProperty("difficulty")] public double Difficulty { get; set; } - public override IEnumerable GetAttributesForDisplay() + public IEnumerable GetAttributesForDisplay() { - foreach (var attribute in base.GetAttributesForDisplay()) - yield return attribute; - + yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total); yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 778d569cf2..b01bcc2d04 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty { } - protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) + protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes) { var maniaAttributes = (ManiaDifficultyAttributes)attributes; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index efda3fa369..760374b5a2 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -34,6 +34,18 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); + [Test] + public void TestFlashlightDifficultyNullability() + { + IWorkingBeatmap beatmap = GetBeatmap("diffcalc-test"); + + OsuDifficultyAttributes attributes = (OsuDifficultyAttributes)CreateDifficultyCalculator(beatmap).Calculate(); + Assert.IsNull(attributes.FlashlightDifficulty); + + attributes = (OsuDifficultyAttributes)CreateDifficultyCalculator(GetBeatmap("diffcalc-test")).Calculate([new OsuModFlashlight()]); + Assert.IsNotNull(attributes.FlashlightDifficulty); + } + protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap); protected override Ruleset CreateRuleset() => new OsuRuleset(); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index a3c0209a08..d97bf4cdf5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -2,17 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Difficulty { - public class OsuDifficultyAttributes : DifficultyAttributes + [JsonObject(MemberSerialization.OptIn)] + public struct OsuDifficultyAttributes : IDifficultyAttributes { + /// + public double StarRating { get; set; } + + /// + public int MaxCombo { get; set; } + /// /// The difficulty corresponding to the aim skill. /// @@ -33,10 +38,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double SpeedNoteCount { get; set; } /// - /// The difficulty corresponding to the flashlight skill. + /// The difficulty corresponding to the flashlight skill. A null value indicates the non-existence of . /// [JsonProperty("flashlight_difficulty")] - public double FlashlightDifficulty { get; set; } + public double? FlashlightDifficulty { get; set; } /// /// Describes how much of is contributed to by hitcircles or sliders. @@ -90,41 +95,38 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// public int SpinnerCount { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() + public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { - foreach (var v in base.ToDatabaseAttributes()) - yield return v; - - 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 (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo); + yield return (IDifficultyAttributes.ATTRIB_ID_AIM, AimDifficulty); + yield return (IDifficultyAttributes.ATTRIB_ID_SPEED, SpeedDifficulty); + yield return (IDifficultyAttributes.ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); + yield return (IDifficultyAttributes.ATTRIB_ID_APPROACH_RATE, ApproachRate); + yield return (IDifficultyAttributes.ATTRIB_ID_DIFFICULTY, StarRating); if (ShouldSerializeFlashlightDifficulty()) - yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); + yield return (IDifficultyAttributes.ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty!); - yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); + yield return (IDifficultyAttributes.ATTRIB_ID_SLIDER_FACTOR, SliderFactor); - 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 (IDifficultyAttributes.ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount); + yield return (IDifficultyAttributes.ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); + yield return (IDifficultyAttributes.ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); } - public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) + public void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { - base.FromDatabaseAttributes(values, onlineInfo); - - 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]; - 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]; + MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO]; + AimDifficulty = values[IDifficultyAttributes.ATTRIB_ID_AIM]; + SpeedDifficulty = values[IDifficultyAttributes.ATTRIB_ID_SPEED]; + OverallDifficulty = values[IDifficultyAttributes.ATTRIB_ID_OVERALL_DIFFICULTY]; + ApproachRate = values[IDifficultyAttributes.ATTRIB_ID_APPROACH_RATE]; + StarRating = values[IDifficultyAttributes.ATTRIB_ID_DIFFICULTY]; + FlashlightDifficulty = values.GetValueOrDefault(IDifficultyAttributes.ATTRIB_ID_FLASHLIGHT); + SliderFactor = values[IDifficultyAttributes.ATTRIB_ID_SLIDER_FACTOR]; + AimDifficultStrainCount = values[IDifficultyAttributes.ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; + SpeedDifficultStrainCount = values[IDifficultyAttributes.ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; + SpeedNoteCount = values[IDifficultyAttributes.ATTRIB_ID_SPEED_NOTE_COUNT]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; @@ -138,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // unless the fields are also renamed. [UsedImplicitly] - public bool ShouldSerializeFlashlightDifficulty() => Mods.Any(m => m is ModFlashlight); + public bool ShouldSerializeFlashlightDifficulty() => FlashlightDifficulty is not null; #endregion } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 575e03051c..83c3e126a4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -31,17 +31,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) - return new OsuDifficultyAttributes { Mods = mods }; + return new OsuDifficultyAttributes(); 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 flashlightRating = 0.0; + double flashlightRating = 0; if (mods.Any(h => h is OsuModFlashlight)) flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier; @@ -97,11 +97,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { StarRating = starRating, - Mods = mods, AimDifficulty = aimRating, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, - FlashlightDifficulty = flashlightRating, + FlashlightDifficulty = mods.Any(x => x is OsuModFlashlight) ? flashlightRating : null, SliderFactor = sliderFactor, AimDifficultStrainCount = aimDifficultyStrainCount, SpeedDifficultStrainCount = speedDifficultyStrainCount, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 0aeaf7669f..aefe416d87 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -7,8 +7,14 @@ using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Osu.Difficulty { - public class OsuPerformanceAttributes : PerformanceAttributes + public struct OsuPerformanceAttributes : IPerformanceAttributes { + /// + /// Calculated score performance points. + /// + [JsonProperty("pp")] + public double Total { get; set; } + [JsonProperty("aim")] public double Aim { get; set; } @@ -24,11 +30,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } - public override IEnumerable GetAttributesForDisplay() + public IEnumerable GetAttributesForDisplay() { - foreach (var attribute in base.GetAttributesForDisplay()) - yield return attribute; - + yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total); yield return new PerformanceDisplayAttribute(nameof(Aim), "Aim", Aim); yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed); yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 31b00dba2b..edd78eb00d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { } - protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) + protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes) { var osuAttributes = (OsuDifficultyAttributes)attributes; @@ -285,7 +285,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (!score.Mods.Any(h => h is OsuModFlashlight)) return 0.0; - double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty); + double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty!.Value); // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index c8f0448767..279e0dba4d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -8,8 +8,15 @@ using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Taiko.Difficulty { - public class TaikoDifficultyAttributes : DifficultyAttributes + [JsonObject(MemberSerialization.OptIn)] + public struct TaikoDifficultyAttributes : IDifficultyAttributes { + /// + public double StarRating { get; set; } + + /// + public int MaxCombo { get; set; } + /// /// The difficulty corresponding to the stamina skill. /// @@ -58,25 +65,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("ok_hit_window")] public double OkHitWindow { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() + public 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); + yield return (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo); + yield return (IDifficultyAttributes.ATTRIB_ID_DIFFICULTY, StarRating); + yield return (IDifficultyAttributes.ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); + yield return (IDifficultyAttributes.ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); + yield return (IDifficultyAttributes.ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor); } - public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) + public void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { - 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]; + MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO]; + StarRating = values[IDifficultyAttributes.ATTRIB_ID_DIFFICULTY]; + GreatHitWindow = values[IDifficultyAttributes.ATTRIB_ID_GREAT_HIT_WINDOW]; + OkHitWindow = values[IDifficultyAttributes.ATTRIB_ID_OK_HIT_WINDOW]; + MonoStaminaFactor = values[IDifficultyAttributes.ATTRIB_ID_MONO_STAMINA_FACTOR]; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7f2558c406..6c2a0de4c1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -72,10 +72,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return difficultyHitObjects; } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) - return new TaikoDifficultyAttributes { Mods = mods }; + return new TaikoDifficultyAttributes(); Colour colour = (Colour)skills.First(x => x is Colour); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); @@ -106,7 +106,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes { StarRating = starRating, - Mods = mods, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, RhythmDifficulty = rhythmRating, diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 7c74e43db1..496311a9aa 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -7,8 +7,14 @@ using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Taiko.Difficulty { - public class TaikoPerformanceAttributes : PerformanceAttributes + public struct TaikoPerformanceAttributes : IPerformanceAttributes { + /// + /// Calculated score performance points. + /// + [JsonProperty("pp")] + public double Total { get; set; } + [JsonProperty("difficulty")] public double Difficulty { get; set; } @@ -21,11 +27,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("estimated_unstable_rate")] public double? EstimatedUnstableRate { get; set; } - public override IEnumerable GetAttributesForDisplay() + public IEnumerable GetAttributesForDisplay() { - foreach (var attribute in base.GetAttributesForDisplay()) - yield return attribute; - + yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total); yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c672b7a1d9..7cb1232e01 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) + protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes) { var taikoAttributes = (TaikoDifficultyAttributes)attributes; diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 6b1b883ce7..7c63bbd9a3 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -222,7 +222,7 @@ namespace osu.Game.Tests.NonVisual protected override Mod[] DifficultyAdjustmentMods { get; } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { throw new NotImplementedException(); } diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs index f860cd097a..c5441dd43c 100644 --- a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs +++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs @@ -172,7 +172,7 @@ namespace osu.Game.Tests.NonVisual { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) => new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() }; protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs index 5acd6cb084..1a676ba13d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -208,8 +208,8 @@ namespace osu.Game.Tests.Visual.UserInterface { } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) - => new DifficultyAttributes(mods, mods.OfType().SingleOrDefault()?.Difficulty.Value ?? 0); + protected override IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + => new DifficultyAttributes { StarRating = mods.OfType().SingleOrDefault()?.Difficulty.Value ?? 0 }; protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Array.Empty(); @@ -225,7 +225,7 @@ namespace osu.Game.Tests.Visual.UserInterface { } - protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) + protected override IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes) => new PerformanceAttributes { Total = score.Mods.OfType().SingleOrDefault()?.Performance.Value ?? 0 }; } diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs index 9f7a92fe46..8199810fdc 100644 --- a/osu.Game/Beatmaps/StarDifficulty.cs +++ b/osu.Game/Beatmaps/StarDifficulty.cs @@ -22,18 +22,18 @@ namespace osu.Game.Beatmaps /// The difficulty attributes computed for the given beatmap. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// - public readonly DifficultyAttributes? DifficultyAttributes; + public readonly IDifficultyAttributes? DifficultyAttributes; /// /// The performance attributes computed for a perfect score on the given beatmap. /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. /// - public readonly PerformanceAttributes? PerformanceAttributes; + public readonly IPerformanceAttributes? PerformanceAttributes; /// /// Creates a structure. /// - public StarDifficulty(DifficultyAttributes difficulty, PerformanceAttributes performance) + public StarDifficulty(IDifficultyAttributes difficulty, IPerformanceAttributes performance) { Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0; MaxCombo = difficulty.MaxCombo; @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps /// /// Creates a structure with a pre-populated star difficulty and max combo - /// in scenarios where computing is not feasible (i.e. when working with online sources). + /// in scenarios where computing is not feasible (i.e. when working with online sources). /// public StarDifficulty(double starDifficulty, int maxCombo) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 59b1ac22bc..6d5545a3e0 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Difficulty /// /// The cancellation token. /// A structure describing the difficulty of the beatmap. - public DifficultyAttributes Calculate(CancellationToken cancellationToken = default) + public IDifficultyAttributes Calculate(CancellationToken cancellationToken = default) => Calculate(Array.Empty(), cancellationToken); /// @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Difficulty /// The mods that should be applied to the beatmap. /// The cancellation token. /// A structure describing the difficulty of the beatmap. - public DifficultyAttributes Calculate([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) + public IDifficultyAttributes Calculate([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); preProcess(mods, cancellationToken); @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Difficulty /// This can only be used to compute difficulties for legacy mod combinations. /// /// A collection of structures describing the difficulty of the beatmap for each mod combination. - public IEnumerable CalculateAllLegacyCombinations(CancellationToken cancellationToken = default) + public IEnumerable<(Mod[] Mods, IDifficultyAttributes Attributes)> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default) { var rulesetInstance = ruleset.CreateInstance(); @@ -148,11 +148,13 @@ namespace osu.Game.Rulesets.Difficulty { Mod classicMod = rulesetInstance.CreateMod(); - var finalCombination = ModUtils.FlattenMod(combination); + IEnumerable finalCombination = ModUtils.FlattenMod(combination); if (classicMod != null) finalCombination = finalCombination.Append(classicMod); - yield return Calculate(finalCombination.ToArray(), cancellationToken); + Mod[] finalMods = finalCombination.ToArray(); + + yield return (finalMods, Calculate(finalMods, cancellationToken)); } } @@ -263,14 +265,14 @@ namespace osu.Game.Rulesets.Difficulty protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty(); /// - /// Creates to describe beatmap's calculated difficulty. + /// Creates to describe beatmap's calculated difficulty. /// /// The whose difficulty was calculated. /// This may differ from in the case of timed calculation. /// The s that difficulty was calculated with. /// The skills which processed the beatmap. /// The rate at which the gameplay clock is run at. - protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate); + protected abstract IDifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate); /// /// Enumerates s to be processed from s in the . diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/IDifficultyAttributes.cs similarity index 61% rename from osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs rename to osu.Game/Rulesets/Difficulty/IDifficultyAttributes.cs index 7b6bc37a61..d907692f27 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/IDifficultyAttributes.cs @@ -1,19 +1,16 @@ // 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 Newtonsoft.Json; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty { /// /// Describes the difficulty of a beatmap, as output by a . /// - [JsonObject(MemberSerialization.OptIn)] - public class DifficultyAttributes + public interface IDifficultyAttributes { protected const int ATTRIB_ID_AIM = 1; protected const int ATTRIB_ID_SPEED = 3; @@ -31,11 +28,6 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; - /// - /// The mods which were applied to the beatmap. - /// - public Mod[] Mods { get; set; } = Array.Empty(); - /// /// The combined star rating of all skills. /// @@ -49,42 +41,40 @@ namespace osu.Game.Rulesets.Difficulty public int MaxCombo { get; set; } /// - /// Creates new . - /// - public DifficultyAttributes() - { - } - - /// - /// Creates new . - /// - /// The mods which were applied to the beatmap. - /// The combined star rating of all skills. - public DifficultyAttributes(Mod[] mods, double starRating) - { - Mods = mods; - StarRating = starRating; - } - - /// - /// Converts this to osu-web compatible database attribute mappings. + /// Converts this to osu-web compatible database attribute mappings. /// /// /// See: osu_difficulty_attribs table. /// - public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() - { - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); - } + public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes(); /// - /// Reads osu-web database attribute mappings into this object. + /// Reads osu-web database attribute mappings into this object. /// /// The attribute mappings. /// The where more information about the beatmap may be extracted from (such as AR/CS/OD/etc). - public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) + public void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo); + } + + /// + /// Represents a full, minimal implementation of . + /// + public class DifficultyAttributes : IDifficultyAttributes + { + public double StarRating { get; set; } + + public int MaxCombo { get; set; } + + public IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; + yield return (IDifficultyAttributes.ATTRIB_ID_MAX_COMBO, MaxCombo); + yield return (IDifficultyAttributes.ATTRIB_ID_AIM, StarRating); + } + + public void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) + { + MaxCombo = (int)values[IDifficultyAttributes.ATTRIB_ID_MAX_COMBO]; + StarRating = values[IDifficultyAttributes.ATTRIB_ID_AIM]; } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/IPerformanceAttributes.cs similarity index 64% rename from osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs rename to osu.Game/Rulesets/Difficulty/IPerformanceAttributes.cs index e8c4c71913..9d861c59d1 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/IPerformanceAttributes.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json; namespace osu.Game.Rulesets.Difficulty { - public class PerformanceAttributes + public interface IPerformanceAttributes { /// /// Calculated score performance points. @@ -19,7 +19,17 @@ namespace osu.Game.Rulesets.Difficulty /// Some attributes may be omitted if they are not meant for display. /// /// - public virtual IEnumerable GetAttributesForDisplay() + public IEnumerable GetAttributesForDisplay(); + } + + /// + /// Represents a full, minimal implementation of . + /// + public class PerformanceAttributes : IPerformanceAttributes + { + public double Total { get; set; } + + public IEnumerable GetAttributesForDisplay() { yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total); } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs index 6e41855ca3..31b9909999 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs @@ -11,19 +11,19 @@ namespace osu.Game.Rulesets.Difficulty /// /// Actual gameplay performance. /// - public PerformanceAttributes Performance { get; set; } + public IPerformanceAttributes Performance { get; set; } /// /// Performance of a perfect play for comparison. /// - public PerformanceAttributes PerfectPerformance { get; set; } + public IPerformanceAttributes PerfectPerformance { get; set; } /// /// Create a new performance breakdown. /// /// Actual gameplay performance. /// Performance of a perfect play for comparison. - public PerformanceBreakdown(PerformanceAttributes performance, PerformanceAttributes perfectPerformance) + public PerformanceBreakdown(IPerformanceAttributes performance, IPerformanceAttributes perfectPerformance) { Performance = performance; PerfectPerformance = perfectPerformance; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 966da0ff12..b7ff4b9812 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -17,20 +17,20 @@ namespace osu.Game.Rulesets.Difficulty Ruleset = ruleset; } - public Task CalculateAsync(ScoreInfo score, DifficultyAttributes attributes, CancellationToken cancellationToken) + public Task CalculateAsync(ScoreInfo score, IDifficultyAttributes attributes, CancellationToken cancellationToken) => Task.Run(() => CreatePerformanceAttributes(score, attributes), cancellationToken); - public PerformanceAttributes Calculate(ScoreInfo score, DifficultyAttributes attributes) + public IPerformanceAttributes Calculate(ScoreInfo score, IDifficultyAttributes attributes) => CreatePerformanceAttributes(score, attributes); - public PerformanceAttributes Calculate(ScoreInfo score, IWorkingBeatmap beatmap) + public IPerformanceAttributes Calculate(ScoreInfo score, IWorkingBeatmap beatmap) => Calculate(score, Ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods)); /// - /// Creates to describe a score's performance. + /// Creates to describe a score's performance. /// /// The score to create the attributes for. /// The difficulty attributes for the beatmap relating to the score. - protected abstract PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes); + protected abstract IPerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, IDifficultyAttributes attributes); } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs index a654652ef8..1ff66f5ea3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Difficulty public class PerformanceDisplayAttribute { /// - /// Name of the attribute property in . + /// Name of the attribute property in . /// public string PropertyName { get; } diff --git a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs index a07827d50b..b6a7aa26da 100644 --- a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs @@ -8,7 +8,7 @@ using System; namespace osu.Game.Rulesets.Difficulty { /// - /// Wraps a object and adds a time value for which the attribute is valid. + /// Wraps a object and adds a time value for which the attribute is valid. /// Output by DifficultyCalculator.CalculateTimed methods. /// public class TimedDifficultyAttributes : IComparable @@ -21,14 +21,14 @@ namespace osu.Game.Rulesets.Difficulty /// /// The attributes. /// - public readonly DifficultyAttributes Attributes; + public readonly IDifficultyAttributes Attributes; /// /// Creates new . /// /// The non-clock-adjusted time value at which the attributes take effect. /// The attributes. - public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) + public TimedDifficultyAttributes(double time, IDifficultyAttributes attributes) { Time = time; Attributes = attributes; diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 25c1387220..a48124fb68 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Play.HUD } [CanBeNull] - private DifficultyAttributes getAttributeAtTime(JudgementResult judgement) + private IDifficultyAttributes getAttributeAtTime(JudgementResult judgement) { if (timedAttributes == null || timedAttributes.Count == 0) return null; diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index f9c8c93dec..9524613b68 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -176,8 +176,8 @@ namespace osu.Game.Screens.Ranking.Statistics var perfectDisplayAttributes = breakdown.PerfectPerformance.GetAttributesForDisplay(); setTotalValues( - displayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total)), - perfectDisplayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total)) + displayAttributes.First(a => a.PropertyName == nameof(IPerformanceAttributes.Total)), + perfectDisplayAttributes.First(a => a.PropertyName == nameof(IPerformanceAttributes.Total)) ); var rowDimensions = new List(); @@ -185,7 +185,7 @@ namespace osu.Game.Screens.Ranking.Statistics foreach (PerformanceDisplayAttribute attr in displayAttributes) { - if (attr.PropertyName == nameof(PerformanceAttributes.Total)) continue; + if (attr.PropertyName == nameof(IPerformanceAttributes.Total)) continue; var row = createAttributeRow(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)); diff --git a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs index 16434406b5..a7238f349f 100644 --- a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs +++ b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs @@ -25,14 +25,14 @@ namespace osu.Game.Tests.Beatmaps protected void Test(double expectedStarRating, int expectedMaxCombo, string name, params Mod[] mods) { - var attributes = CreateDifficultyCalculator(getBeatmap(name)).Calculate(mods); + var attributes = CreateDifficultyCalculator(GetBeatmap(name)).Calculate(mods); // 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)); } - private IWorkingBeatmap getBeatmap(string name) + protected IWorkingBeatmap GetBeatmap(string name) { using (var resStream = openResource($"{resource_namespace}.{name}.osu")) using (var stream = new LineBufferedReader(resStream))