From 1e2c1323ff861386a3272f7db3ee1fefc45da1fb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 17:05:49 +0900 Subject: [PATCH 1/8] Show star difficulty attribute inclusive of mods --- .../Components/BeatmapAttributeText.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 63ba6d1581fb..d6246b4bcf81 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -33,7 +34,12 @@ public partial class BeatmapAttributeText : FontAdjustableSkinComponent [Resolved] private IBindable beatmap { get; set; } = null!; + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + private readonly OsuSpriteText text; + private IBindable? difficultyBindable; + private CancellationTokenSource? difficultyCancellationSource; public BeatmapAttributeText() { @@ -55,7 +61,18 @@ protected override void LoadComplete() Attribute.BindValueChanged(_ => updateText()); Template.BindValueChanged(_ => updateText()); - beatmap.BindValueChanged(b => updateText()); + + beatmap.BindValueChanged(b => + { + difficultyCancellationSource?.Cancel(); + difficultyCancellationSource = new CancellationTokenSource(); + + difficultyBindable?.UnbindAll(); + difficultyBindable = difficultyCache.GetBindableDifficulty(b.NewValue.BeatmapInfo, difficultyCancellationSource.Token); + difficultyBindable.BindValueChanged(_ => updateText()); + + updateText(); + }, true); updateText(); } @@ -172,7 +189,9 @@ private LocalisableString getValueString(BeatmapAttribute attribute) return ((double)beatmap.Value.BeatmapInfo.Difficulty.ApproachRate).ToLocalisableString(@"F2"); case BeatmapAttribute.StarRating: - return beatmap.Value.BeatmapInfo.StarRating.ToLocalisableString(@"F2"); + return difficultyBindable?.Value is StarDifficulty starDifficulty + ? starDifficulty.Stars.ToLocalisableString(@"F2") + : @"..."; default: throw new ArgumentOutOfRangeException(); @@ -182,6 +201,15 @@ private LocalisableString getValueString(BeatmapAttribute attribute) protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + difficultyCancellationSource?.Cancel(); + difficultyCancellationSource?.Dispose(); + difficultyCancellationSource = null; + } } // WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END. From 400142545df498fa57f375a3b67e5150ee9f6c07 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 17:26:02 +0900 Subject: [PATCH 2/8] Show difficulty values inclusive of mods --- .../Components/BeatmapAttributeText.cs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index d6246b4bcf81..ed44d4b44c49 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -19,6 +20,9 @@ using osu.Game.Localisation; using osu.Game.Localisation.SkinComponents; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; namespace osu.Game.Skinning.Components { @@ -34,6 +38,12 @@ public partial class BeatmapAttributeText : FontAdjustableSkinComponent [Resolved] private IBindable beatmap { get; set; } = null!; + [Resolved] + private IBindable> mods { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; @@ -74,6 +84,9 @@ protected override void LoadComplete() updateText(); }, true); + mods.BindValueChanged(_ => updateText()); + ruleset.BindValueChanged(_ => updateText()); + updateText(); } @@ -177,16 +190,16 @@ private LocalisableString getValueString(BeatmapAttribute attribute) return beatmap.Value.BeatmapInfo.BPM.ToLocalisableString(@"F2"); case BeatmapAttribute.CircleSize: - return ((double)beatmap.Value.BeatmapInfo.Difficulty.CircleSize).ToLocalisableString(@"F2"); + return computeDifficulty().CircleSize.ToLocalisableString(@"F2"); case BeatmapAttribute.HPDrain: - return ((double)beatmap.Value.BeatmapInfo.Difficulty.DrainRate).ToLocalisableString(@"F2"); + return computeDifficulty().DrainRate.ToLocalisableString(@"F2"); case BeatmapAttribute.Accuracy: - return ((double)beatmap.Value.BeatmapInfo.Difficulty.OverallDifficulty).ToLocalisableString(@"F2"); + return computeDifficulty().OverallDifficulty.ToLocalisableString(@"F2"); case BeatmapAttribute.ApproachRate: - return ((double)beatmap.Value.BeatmapInfo.Difficulty.ApproachRate).ToLocalisableString(@"F2"); + return computeDifficulty().ApproachRate.ToLocalisableString(@"F2"); case BeatmapAttribute.StarRating: return difficultyBindable?.Value is StarDifficulty starDifficulty @@ -196,6 +209,22 @@ private LocalisableString getValueString(BeatmapAttribute attribute) default: throw new ArgumentOutOfRangeException(); } + + BeatmapDifficulty computeDifficulty() + { + BeatmapDifficulty difficulty = new BeatmapDifficulty(beatmap.Value.BeatmapInfo.Difficulty); + + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(difficulty); + + if (ruleset.Value is RulesetInfo rulesetInfo) + { + double rate = ModUtils.CalculateRateWithMods(mods.Value); + difficulty = rulesetInfo.CreateInstance().GetRateAdjustedDisplayDifficulty(difficulty, rate); + } + + return difficulty; + } } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); From 9ea15a39619068deed002b50d794b9eacaef8b64 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 17:32:32 +0900 Subject: [PATCH 3/8] Show bpm and length inclusive of mods --- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index ed44d4b44c49..6cfed8b945c4 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -181,13 +181,13 @@ private LocalisableString getValueString(BeatmapAttribute attribute) return beatmap.Value.BeatmapInfo.Metadata.Source; case BeatmapAttribute.Length: - return TimeSpan.FromMilliseconds(beatmap.Value.BeatmapInfo.Length).ToFormattedDuration(); + return Math.Round(beatmap.Value.BeatmapInfo.Length / ModUtils.CalculateRateWithMods(mods.Value)).ToFormattedDuration(); case BeatmapAttribute.RankedStatus: return beatmap.Value.BeatmapInfo.Status.GetLocalisableDescription(); case BeatmapAttribute.BPM: - return beatmap.Value.BeatmapInfo.BPM.ToLocalisableString(@"F2"); + return FormatUtils.RoundBPM(beatmap.Value.BeatmapInfo.BPM, ModUtils.CalculateRateWithMods(mods.Value)).ToLocalisableString(@"F2"); case BeatmapAttribute.CircleSize: return computeDifficulty().CircleSize.ToLocalisableString(@"F2"); From f9a9ceb41c76fd67b90a129cc97d0b4018afa2d9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 17:42:40 +0900 Subject: [PATCH 4/8] Also bind to mod setting changes --- .../Skinning/Components/BeatmapAttributeText.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 6cfed8b945c4..ccee90410e85 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -50,6 +50,7 @@ public partial class BeatmapAttributeText : FontAdjustableSkinComponent private readonly OsuSpriteText text; private IBindable? difficultyBindable; private CancellationTokenSource? difficultyCancellationSource; + private ModSettingChangeTracker? modSettingTracker; public BeatmapAttributeText() { @@ -84,7 +85,17 @@ protected override void LoadComplete() updateText(); }, true); - mods.BindValueChanged(_ => updateText()); + mods.BindValueChanged(m => + { + modSettingTracker?.Dispose(); + modSettingTracker = new ModSettingChangeTracker(m.NewValue) + { + SettingChanged = _ => updateText() + }; + + updateText(); + }, true); + ruleset.BindValueChanged(_ => updateText()); updateText(); From b40c31af3adc922d2c47ecd0f810e179aece085f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 20:36:15 +0900 Subject: [PATCH 5/8] Store difficulty to reduce flickering The computation usually finishes in a few milliseconds anyway. --- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index ccee90410e85..316b20035cd2 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -51,6 +51,7 @@ public partial class BeatmapAttributeText : FontAdjustableSkinComponent private IBindable? difficultyBindable; private CancellationTokenSource? difficultyCancellationSource; private ModSettingChangeTracker? modSettingTracker; + private StarDifficulty? starDifficulty; public BeatmapAttributeText() { @@ -80,7 +81,11 @@ protected override void LoadComplete() difficultyBindable?.UnbindAll(); difficultyBindable = difficultyCache.GetBindableDifficulty(b.NewValue.BeatmapInfo, difficultyCancellationSource.Token); - difficultyBindable.BindValueChanged(_ => updateText()); + difficultyBindable.BindValueChanged(d => + { + starDifficulty = d.NewValue; + updateText(); + }); updateText(); }, true); @@ -213,9 +218,7 @@ private LocalisableString getValueString(BeatmapAttribute attribute) return computeDifficulty().ApproachRate.ToLocalisableString(@"F2"); case BeatmapAttribute.StarRating: - return difficultyBindable?.Value is StarDifficulty starDifficulty - ? starDifficulty.Stars.ToLocalisableString(@"F2") - : @"..."; + return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2"); default: throw new ArgumentOutOfRangeException(); From c0eda3606cb6be03938f3df155e3648e8f46f7bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Oct 2024 22:01:55 +0900 Subject: [PATCH 6/8] Add mod-related tests --- .../TestSceneBeatmapAttributeText.cs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs index bf959d9862d8..01659a265421 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -1,14 +1,28 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Models; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Skinning.Components; using osu.Game.Tests.Beatmaps; @@ -30,6 +44,8 @@ public TestSceneBeatmapAttributeText() [SetUp] public void Setup() => Schedule(() => { + SelectedMods.SetDefault(); + Ruleset.SetDefault(); Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) { BeatmapInfo = @@ -93,6 +109,126 @@ public void TestChangeBeatmap() AddAssert("check new title", getText, () => Is.EqualTo("Title: Another")); } + [Test] + public void TestWithMods() + { + AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + BPM = 100, + Length = 30000, + Difficulty = + { + ApproachRate = 10, + CircleSize = 9 + } + } + })); + + test(BeatmapAttribute.BPM, new OsuModDoubleTime(), "BPM: 100.00", "BPM: 150.00"); + test(BeatmapAttribute.Length, new OsuModDoubleTime(), "Length: 00:30", "Length: 00:20"); + test(BeatmapAttribute.ApproachRate, new OsuModDoubleTime(), "Approach Rate: 10.00", "Approach Rate: 11.00"); + test(BeatmapAttribute.CircleSize, new OsuModHardRock(), "Circle Size: 9.00", "Circle Size: 10.00"); + + void test(BeatmapAttribute attribute, Mod mod, string before, string after) + { + AddStep($"set attribute: {attribute}", () => text.Attribute.Value = attribute); + AddAssert("check text is correct", getText, () => Is.EqualTo(before)); + + AddStep("add DT mod", () => SelectedMods.Value = new[] { mod }); + AddAssert("check text is correct", getText, () => Is.EqualTo(after)); + AddStep("clear mods", () => SelectedMods.SetDefault()); + } + } + + [Test] + public void TestStarRating() + { + AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo); + AddStep("set star rating attribute", () => text.Attribute.Value = BeatmapAttribute.StarRating); + AddAssert("check star rating is 0", getText, () => Is.EqualTo("Star Rating: 0.00")); + + // Adding mod + TestMod mod = null!; + AddStep("add mod with difficulty 1", () => SelectedMods.Value = new[] { mod = new TestMod { Difficulty = { Value = 1 } } }); + AddUntilStep("check star rating is 1", getText, () => Is.EqualTo("Star Rating: 1.00")); + + // Changing mod setting + AddStep("change mod difficulty to 2", () => mod.Difficulty.Value = 2); + AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00")); + } + private string getText() => text.ChildrenOfType().Single().Text.ToString(); + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => new[] + { + new TestMod() + }; + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) + => new OsuRuleset().CreateBeatmapConverter(beatmap); + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) + => new TestDifficultyCalculator(new TestRuleset().RulesetInfo, beatmap); + + public override PerformanceCalculator CreatePerformanceCalculator() + => new TestPerformanceCalculator(new TestRuleset()); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) + => null!; + + public override string Description => string.Empty; + public override string ShortName => string.Empty; + } + + private class TestDifficultyCalculator : DifficultyCalculator + { + public TestDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + } + + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + => new DifficultyAttributes(mods, mods.OfType().SingleOrDefault()?.Difficulty.Value ?? 0); + + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) + => Array.Empty(); + + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) + => Array.Empty(); + } + + private class TestPerformanceCalculator : PerformanceCalculator + { + public TestPerformanceCalculator(Ruleset ruleset) + : base(ruleset) + { + } + + protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) + => new PerformanceAttributes { Total = score.Mods.OfType().SingleOrDefault()?.Performance.Value ?? 0 }; + } + + private class TestMod : Mod + { + [SettingSource("difficulty")] + public BindableDouble Difficulty { get; } = new BindableDouble(0); + + [SettingSource("performance")] + public BindableDouble Performance { get; } = new BindableDouble(0); + + [JsonConstructor] + public TestMod() + { + } + + public override string Name => string.Empty; + public override LocalisableString Description => string.Empty; + public override double ScoreMultiplier => 1.0; + public override string Acronym => "Test"; + } } } From 17702ead0bcde62988b2146cfd8db671b64da09d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 22 Oct 2024 14:20:10 +0900 Subject: [PATCH 7/8] Fix ruleset not being reset correctly in tests --- .../Visual/UserInterface/TestSceneBeatmapAttributeText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs index 01659a265421..91525e02c141 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -45,7 +45,7 @@ public TestSceneBeatmapAttributeText() public void Setup() => Schedule(() => { SelectedMods.SetDefault(); - Ruleset.SetDefault(); + Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) { BeatmapInfo = From 5f6395059807f2c0eca9c354dd30e03952bce876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 Nov 2024 11:26:59 +0100 Subject: [PATCH 8/8] Add missing disposal --- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 9176c42da81c..4e35c90ba641 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -257,6 +257,8 @@ protected override void Dispose(bool isDisposing) difficultyCancellationSource?.Cancel(); difficultyCancellationSource?.Dispose(); difficultyCancellationSource = null; + + modSettingTracker?.Dispose(); } }