From 426ca00516e61be0c4b1e35ec3c154e857931475 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Sun, 17 Nov 2024 17:48:16 +0100 Subject: [PATCH 001/152] Add osu!taiko mod `Simplified Rhythm` --- .../Mods/TaikoModSimplifiedRhythm.cs | 133 ++++++++++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 1 + 2 files changed, 134 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs new file mode 100644 index 0000000000..14b819163b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Mods +{ + public class TaikoModSimplifiedRhythm : Mod, IApplicableToBeatmap + { + public override string Name => "Simplified Rhythm"; + public override string Acronym => "SR"; + public override double ScoreMultiplier => 0.6; + public override LocalisableString Description => "Simplify tricky rhythms!"; + public override ModType Type => ModType.DifficultyReduction; + + [SettingSource("One-third conversion", "Converts 1/3 snap to 1/2 snap.")] + public Bindable EnableOneThird { get; } = new BindableBool(false); + + [SettingSource("One-sixth conversion", "Converts 1/6 snap to 1/4 snap.")] + public Bindable EnableOneSixth { get; } = new BindableBool(true); + + [SettingSource("One-eighth conversion", "Converts 1/8 snap to 1/4 snap.")] + public Bindable EnableOneEighth { get; } = new BindableBool(false); + + public void ApplyToBeatmap(IBeatmap beatmap) + { + var taikoBeatmap = (TaikoBeatmap)beatmap; + var controlPointInfo = taikoBeatmap.ControlPointInfo; + List toRemove = []; + + // Snap conversions for rhythms + var snapConversions = new Dictionary() + { + { 8, 4 }, // 1/8 snap to 1/4 snap + { 6, 4 }, // 1/6 snap to 1/4 snap + { 3, 2 }, // 1/3 snap to 1/2 snap + }; + + double beatLength = controlPointInfo.TimingPointAt(0).BeatLength; + int patternStartIndex = 0; + bool inPattern = false; + + List hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast().ToList(); + + foreach (var snapConversion in snapConversions) + { + // Skip processing if the corresponding conversion is disabled + if (!shouldProcessRhythm(snapConversion.Key)) + continue; + + for (int i = 0; i < hits.Count; i++) + { + double snapValue = i < hits.Count - 1 + ? getSnapBetweenNotes(controlPointInfo, hits[i], hits[i + 1]) + : 1; // No next note, default to a safe 1/1 snap + if (snapValue == snapConversion.Key) + { + if (!inPattern) + { + patternStartIndex = i; + } + inPattern = true; + } + // check if end of pattern or if we're on the last note + if ((inPattern && snapValue != snapConversion.Key) || i == hits.Count) + { + // End of the pattern + inPattern = false; + + // Iterate through the pattern + for (int j = patternStartIndex; j <= i; j++) + { + int currentHitPosition = j - patternStartIndex; + + if (snapConversion.Key == 8) + { + // 1/8: Remove the second note + if (currentHitPosition % 2 == 1) + { + toRemove.Add(hits[j]); + } + } + else + { + // 1/6 and 1/3: Adjust the second note and remove the third + if (currentHitPosition % 3 == 1) + { + hits[j].StartTime = hits[j - 1].StartTime + controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / Convert.ToDouble(snapConversion.Value); + } + else if (currentHitPosition % 3 == 2) + { + toRemove.Add(hits[j]); + } + } + } + } + } + + // Remove queued notes + taikoBeatmap.HitObjects = taikoBeatmap.HitObjects.Except(toRemove).ToList(); + } + } + + private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote) + { + double gapMs = Math.Max(currentNote.StartTime, nextNote.StartTime) - Math.Min(currentNote.StartTime, nextNote.StartTime); + var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime); + + return controlPointInfo.GetClosestBeatDivisor(gapMs + currentTimingPoint.Time); + } + + private bool shouldProcessRhythm(int snap) + { + return snap switch + { + 3 => EnableOneThird.Value, + 6 => EnableOneSixth.Value, + 8 => EnableOneEighth.Value, + _ => false + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 70e429a344..0280992b9d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -130,6 +130,7 @@ namespace osu.Game.Rulesets.Taiko new TaikoModEasy(), new TaikoModNoFail(), new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()), + new TaikoModSimplifiedRhythm(), }; case ModType.DifficultyIncrease: From 28b911f6ac0594ec0064d2850f713a61266aefae Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Sun, 17 Nov 2024 17:53:11 +0100 Subject: [PATCH 002/152] format --- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index 14b819163b..17de560300 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Mods // Snap conversions for rhythms var snapConversions = new Dictionary() { - { 8, 4 }, // 1/8 snap to 1/4 snap + { 8, 4 }, // 1/8 snap to 1/4 snap { 6, 4 }, // 1/6 snap to 1/4 snap { 3, 2 }, // 1/3 snap to 1/2 snap }; @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Mods 3 => EnableOneThird.Value, 6 => EnableOneSixth.Value, 8 => EnableOneEighth.Value, - _ => false + _ => false, }; } } From 617f8cce4a8e4ef86d925483541c2d4a32c798be Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Sun, 17 Nov 2024 17:54:27 +0100 Subject: [PATCH 003/152] better names --- .../Mods/TaikoModSimplifiedRhythm.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index 17de560300..b316260752 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -24,13 +24,13 @@ namespace osu.Game.Rulesets.Taiko.Mods public override ModType Type => ModType.DifficultyReduction; [SettingSource("One-third conversion", "Converts 1/3 snap to 1/2 snap.")] - public Bindable EnableOneThird { get; } = new BindableBool(false); + public Bindable OneThirdConversion { get; } = new BindableBool(false); [SettingSource("One-sixth conversion", "Converts 1/6 snap to 1/4 snap.")] - public Bindable EnableOneSixth { get; } = new BindableBool(true); + public Bindable OneSixthConversion { get; } = new BindableBool(true); [SettingSource("One-eighth conversion", "Converts 1/8 snap to 1/4 snap.")] - public Bindable EnableOneEighth { get; } = new BindableBool(false); + public Bindable OneEighthConversion { get; } = new BindableBool(false); public void ApplyToBeatmap(IBeatmap beatmap) { @@ -123,9 +123,9 @@ namespace osu.Game.Rulesets.Taiko.Mods { return snap switch { - 3 => EnableOneThird.Value, - 6 => EnableOneSixth.Value, - 8 => EnableOneEighth.Value, + 3 => OneThirdConversion.Value, + 6 => OneSixthConversion.Value, + 8 => OneEighthConversion.Value, _ => false, }; } From db081760498f616d80d2acbcea3cd945fa629dc1 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Sun, 17 Nov 2024 22:54:38 +0100 Subject: [PATCH 004/152] fix CI --- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index b316260752..c0a0c10b6b 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -36,17 +36,16 @@ namespace osu.Game.Rulesets.Taiko.Mods { var taikoBeatmap = (TaikoBeatmap)beatmap; var controlPointInfo = taikoBeatmap.ControlPointInfo; - List toRemove = []; + List toRemove = new List(); // Snap conversions for rhythms - var snapConversions = new Dictionary() + var snapConversions = new Dictionary { { 8, 4 }, // 1/8 snap to 1/4 snap { 6, 4 }, // 1/6 snap to 1/4 snap { 3, 2 }, // 1/3 snap to 1/2 snap }; - double beatLength = controlPointInfo.TimingPointAt(0).BeatLength; int patternStartIndex = 0; bool inPattern = false; @@ -63,6 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Mods double snapValue = i < hits.Count - 1 ? getSnapBetweenNotes(controlPointInfo, hits[i], hits[i + 1]) : 1; // No next note, default to a safe 1/1 snap + if (snapValue == snapConversion.Key) { if (!inPattern) @@ -71,6 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Mods } inPattern = true; } + // check if end of pattern or if we're on the last note if ((inPattern && snapValue != snapConversion.Key) || i == hits.Count) { @@ -95,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Mods // 1/6 and 1/3: Adjust the second note and remove the third if (currentHitPosition % 3 == 1) { - hits[j].StartTime = hits[j - 1].StartTime + controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / Convert.ToDouble(snapConversion.Value); + hits[j].StartTime = hits[j - 1].StartTime + (controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / Convert.ToDouble(snapConversion.Value)); } else if (currentHitPosition % 3 == 2) { From 72210bf9fef29c50a4a9d7eea474b40a05c089c1 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Sun, 17 Nov 2024 23:34:30 +0100 Subject: [PATCH 005/152] blank line --- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index c0a0c10b6b..d54bb44d59 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { patternStartIndex = i; } + inPattern = true; } From 45ed2fdec297be5d8a0372d37878102097677a61 Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 18 Nov 2024 12:00:28 +0100 Subject: [PATCH 006/152] apply review --- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index d54bb44d59..cb3cde309b 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -46,13 +46,14 @@ namespace osu.Game.Rulesets.Taiko.Mods { 3, 2 }, // 1/3 snap to 1/2 snap }; - int patternStartIndex = 0; bool inPattern = false; List hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast().ToList(); foreach (var snapConversion in snapConversions) { + int patternStartIndex = 0; + // Skip processing if the corresponding conversion is disabled if (!shouldProcessRhythm(snapConversion.Key)) continue; @@ -73,8 +74,8 @@ namespace osu.Game.Rulesets.Taiko.Mods inPattern = true; } - // check if end of pattern or if we're on the last note - if ((inPattern && snapValue != snapConversion.Key) || i == hits.Count) + // check if end of pattern + if (inPattern && snapValue != snapConversion.Key) { // End of the pattern inPattern = false; @@ -109,7 +110,7 @@ namespace osu.Game.Rulesets.Taiko.Mods } // Remove queued notes - taikoBeatmap.HitObjects = taikoBeatmap.HitObjects.Except(toRemove).ToList(); + taikoBeatmap.HitObjects.RemoveAll(obj => toRemove.Contains(obj)); } } From 9bea112370e0ce9dc15911477f759df672327093 Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 18 Nov 2024 12:09:13 +0100 Subject: [PATCH 007/152] better terminology --- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index cb3cde309b..57851173b8 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -23,13 +23,13 @@ namespace osu.Game.Rulesets.Taiko.Mods public override LocalisableString Description => "Simplify tricky rhythms!"; public override ModType Type => ModType.DifficultyReduction; - [SettingSource("One-third conversion", "Converts 1/3 snap to 1/2 snap.")] + [SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")] public Bindable OneThirdConversion { get; } = new BindableBool(false); - [SettingSource("One-sixth conversion", "Converts 1/6 snap to 1/4 snap.")] + [SettingSource("1/6 to 1/4 conversion", "Converts 1/6 patterns to 1/4 rhythm.")] public Bindable OneSixthConversion { get; } = new BindableBool(true); - [SettingSource("One-eighth conversion", "Converts 1/8 snap to 1/4 snap.")] + [SettingSource("1/8 to 1/4 conversion", "Converts 1/8 patterns to 1/4 rhythm.")] public Bindable OneEighthConversion { get; } = new BindableBool(false); public void ApplyToBeatmap(IBeatmap beatmap) From a7b4f975ca7038e4ea33c73883921da90f9b60c7 Mon Sep 17 00:00:00 2001 From: Hivie Date: Mon, 18 Nov 2024 12:10:23 +0100 Subject: [PATCH 008/152] rename mod --- .../{TaikoModSimplifiedRhythm.cs => TaikoModSimplified.cs} | 6 +++--- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game.Rulesets.Taiko/Mods/{TaikoModSimplifiedRhythm.cs => TaikoModSimplified.cs} (96%) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs rename to osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs index 57851173b8..0dfa5a998a 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs @@ -15,10 +15,10 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModSimplifiedRhythm : Mod, IApplicableToBeatmap + public class TaikoModSimplified : Mod, IApplicableToBeatmap { - public override string Name => "Simplified Rhythm"; - public override string Acronym => "SR"; + public override string Name => "Simplified"; + public override string Acronym => "SF"; public override double ScoreMultiplier => 0.6; public override LocalisableString Description => "Simplify tricky rhythms!"; public override ModType Type => ModType.DifficultyReduction; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 0280992b9d..f57d2a20f5 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko new TaikoModEasy(), new TaikoModNoFail(), new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()), - new TaikoModSimplifiedRhythm(), + new TaikoModSimplified(), }; case ModType.DifficultyIncrease: From 3e8b26c483d426e64841f5ebe2199fe5796aa27c Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Wed, 20 Nov 2024 01:49:01 +0100 Subject: [PATCH 009/152] simplify operation --- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs index 0dfa5a998a..78eaf3199d 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Mods private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote) { - double gapMs = Math.Max(currentNote.StartTime, nextNote.StartTime) - Math.Min(currentNote.StartTime, nextNote.StartTime); + double gapMs = nextNote.StartTime - currentNote.StartTime; var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime); return controlPointInfo.GetClosestBeatDivisor(gapMs + currentTimingPoint.Time); From 38e76d41b52207401e16aa9fd9fe275228b593ca Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Wed, 20 Nov 2024 01:49:14 +0100 Subject: [PATCH 010/152] add unit tests --- .../Mods/TestSceneTaikoModSimplified.cs | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs new file mode 100644 index 0000000000..4d8b2a4b4d --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs @@ -0,0 +1,133 @@ +// 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; + +namespace osu.Game.Rulesets.Taiko.Tests.Mods +{ + public partial class TestSceneTaikoModSimplified : TaikoModTestScene + { + [Test] + public void TestOneThirdConversion() + { + CreateModTest(new ModTestData + { + Mod = new TaikoModSimplified + { + OneThirdConversion = { Value = true }, + }, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Hit { StartTime = 1000, Type = HitType.Centre }, + new Hit { StartTime = 1500, Type = HitType.Centre }, + new Hit { StartTime = 2000, Type = HitType.Centre }, + new Hit { StartTime = 2333, Type = HitType.Centre }, + new Hit { StartTime = 2666, Type = HitType.Rim }, + new Hit { StartTime = 3000, Type = HitType.Centre }, + new Hit { StartTime = 3500, Type = HitType.Centre }, + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1200), + new TaikoReplayFrame(1500, TaikoAction.LeftCentre), + new TaikoReplayFrame(1700), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2200), + new TaikoReplayFrame(2500, TaikoAction.LeftCentre), + new TaikoReplayFrame(2700), + new TaikoReplayFrame(3000, TaikoAction.LeftCentre), + new TaikoReplayFrame(3200), + new TaikoReplayFrame(3500, TaikoAction.LeftCentre), + new TaikoReplayFrame(3700), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 + }); + } + + [Test] + public void TestOneSixthConversion() => CreateModTest(new ModTestData + { + Mod = new TaikoModSimplified + { + OneSixthConversion = { Value = true } + }, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Hit { StartTime = 1000, Type = HitType.Centre }, + new Hit { StartTime = 1250, Type = HitType.Centre }, + new Hit { StartTime = 1500, Type = HitType.Centre }, + new Hit { StartTime = 1666, Type = HitType.Centre }, + new Hit { StartTime = 1833, Type = HitType.Rim }, + new Hit { StartTime = 2000, Type = HitType.Centre }, + new Hit { StartTime = 2250, Type = HitType.Centre }, + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1200), + new TaikoReplayFrame(1250, TaikoAction.LeftCentre), + new TaikoReplayFrame(1450), + new TaikoReplayFrame(1500, TaikoAction.LeftCentre), + new TaikoReplayFrame(1600), + new TaikoReplayFrame(1750, TaikoAction.LeftCentre), + new TaikoReplayFrame(1800), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2200), + new TaikoReplayFrame(2250, TaikoAction.LeftCentre), + new TaikoReplayFrame(2450), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 + }); + + [Test] + public void TestOneEighthConversion() => CreateModTest(new ModTestData + { + Mod = new TaikoModSimplified + { + OneEighthConversion = { Value = true } + }, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Hit { StartTime = 1000, Type = HitType.Centre }, + new Hit { StartTime = 1250, Type = HitType.Centre }, + new Hit { StartTime = 1500, Type = HitType.Centre }, + new Hit { StartTime = 1625, Type = HitType.Rim }, + new Hit { StartTime = 1750, Type = HitType.Centre }, + new Hit { StartTime = 2000, Type = HitType.Centre }, + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1200), + new TaikoReplayFrame(1250, TaikoAction.LeftCentre), + new TaikoReplayFrame(1450), + new TaikoReplayFrame(1500, TaikoAction.LeftCentre), + new TaikoReplayFrame(1700), + new TaikoReplayFrame(1750, TaikoAction.LeftCentre), + new TaikoReplayFrame(1900), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 5 + }); + } +} From d4f29487d3aad90cdc1ae44643a109206d2ddd35 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Fri, 22 Nov 2024 10:26:49 +0100 Subject: [PATCH 011/152] fix tests --- .../Mods/TestSceneTaikoModSimplified.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs index 4d8b2a4b4d..825c8cb1ad 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs @@ -24,15 +24,15 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods OneThirdConversion = { Value = true }, }, Autoplay = false, - Beatmap = new Beatmap + CreateBeatmap = () => new Beatmap { HitObjects = new List { new Hit { StartTime = 1000, Type = HitType.Centre }, new Hit { StartTime = 1500, Type = HitType.Centre }, new Hit { StartTime = 2000, Type = HitType.Centre }, - new Hit { StartTime = 2333, Type = HitType.Centre }, - new Hit { StartTime = 2666, Type = HitType.Rim }, + new Hit { StartTime = 2333, Type = HitType.Centre }, // mod moves this to 2500 + new Hit { StartTime = 2666, Type = HitType.Rim }, // mod removes this new Hit { StartTime = 3000, Type = HitType.Centre }, new Hit { StartTime = 3500, Type = HitType.Centre }, }, @@ -64,15 +64,15 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods OneSixthConversion = { Value = true } }, Autoplay = false, - Beatmap = new Beatmap + CreateBeatmap = () => new Beatmap { HitObjects = new List { new Hit { StartTime = 1000, Type = HitType.Centre }, new Hit { StartTime = 1250, Type = HitType.Centre }, new Hit { StartTime = 1500, Type = HitType.Centre }, - new Hit { StartTime = 1666, Type = HitType.Centre }, - new Hit { StartTime = 1833, Type = HitType.Rim }, + new Hit { StartTime = 1666, Type = HitType.Centre }, // mod moves this to 1750 + new Hit { StartTime = 1833, Type = HitType.Rim }, // mod removes this new Hit { StartTime = 2000, Type = HitType.Centre }, new Hit { StartTime = 2250, Type = HitType.Centre }, }, @@ -103,14 +103,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods OneEighthConversion = { Value = true } }, Autoplay = false, - Beatmap = new Beatmap + CreateBeatmap = () => new Beatmap { HitObjects = new List { new Hit { StartTime = 1000, Type = HitType.Centre }, new Hit { StartTime = 1250, Type = HitType.Centre }, new Hit { StartTime = 1500, Type = HitType.Centre }, - new Hit { StartTime = 1625, Type = HitType.Rim }, + new Hit { StartTime = 1625, Type = HitType.Rim }, // mod removes this new Hit { StartTime = 1750, Type = HitType.Centre }, new Hit { StartTime = 2000, Type = HitType.Centre }, }, From 93e7afd5f35bae93108b2318077ac56a92d27fea Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Fri, 22 Nov 2024 11:21:48 +0100 Subject: [PATCH 012/152] improve conversion process to reduce breakage in rare cases --- .../Mods/TestSceneTaikoModSimplified.cs | 10 +++++----- osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs index 825c8cb1ad..8ce6698857 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs @@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods new Hit { StartTime = 1000, Type = HitType.Centre }, new Hit { StartTime = 1500, Type = HitType.Centre }, new Hit { StartTime = 2000, Type = HitType.Centre }, - new Hit { StartTime = 2333, Type = HitType.Centre }, // mod moves this to 2500 - new Hit { StartTime = 2666, Type = HitType.Rim }, // mod removes this + new Hit { StartTime = 2333, Type = HitType.Rim }, // mod removes this + new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500 new Hit { StartTime = 3000, Type = HitType.Centre }, new Hit { StartTime = 3500, Type = HitType.Centre }, }, @@ -71,8 +71,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods new Hit { StartTime = 1000, Type = HitType.Centre }, new Hit { StartTime = 1250, Type = HitType.Centre }, new Hit { StartTime = 1500, Type = HitType.Centre }, - new Hit { StartTime = 1666, Type = HitType.Centre }, // mod moves this to 1750 - new Hit { StartTime = 1833, Type = HitType.Rim }, // mod removes this + new Hit { StartTime = 1666, Type = HitType.Rim }, // mod removes this + new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750 new Hit { StartTime = 2000, Type = HitType.Centre }, new Hit { StartTime = 2250, Type = HitType.Centre }, }, @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods new Hit { StartTime = 1000, Type = HitType.Centre }, new Hit { StartTime = 1250, Type = HitType.Centre }, new Hit { StartTime = 1500, Type = HitType.Centre }, - new Hit { StartTime = 1625, Type = HitType.Rim }, // mod removes this + new Hit { StartTime = 1625, Type = HitType.Rim }, // mod removes this new Hit { StartTime = 1750, Type = HitType.Centre }, new Hit { StartTime = 2000, Type = HitType.Centre }, }, diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs index 78eaf3199d..70e76ed8f3 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs @@ -95,15 +95,16 @@ namespace osu.Game.Rulesets.Taiko.Mods } else { - // 1/6 and 1/3: Adjust the second note and remove the third + // 1/6 and 1/3: Remove the second note and adjust the third if (currentHitPosition % 3 == 1) - { - hits[j].StartTime = hits[j - 1].StartTime + (controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / Convert.ToDouble(snapConversion.Value)); - } - else if (currentHitPosition % 3 == 2) { toRemove.Add(hits[j]); } + else if (currentHitPosition % 3 == 2 && j < hits.Count - 1) + { + double offset = controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / Convert.ToDouble(snapConversion.Value); + hits[j].StartTime = hits[j + 1].StartTime - offset; + } } } } From ea4cbb5c36f31804077a0f39cc52691dc7edea41 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Fri, 22 Nov 2024 11:33:33 +0100 Subject: [PATCH 013/152] rename mod to `Quarterize` --- ...ikoModSimplified.cs => TestSceneTaikoModQuarterize.cs} | 8 ++++---- .../Mods/{TaikoModSimplified.cs => TaikoModQuarterize.cs} | 6 +++--- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game.Rulesets.Taiko.Tests/Mods/{TestSceneTaikoModSimplified.cs => TestSceneTaikoModQuarterize.cs} (96%) rename osu.Game.Rulesets.Taiko/Mods/{TaikoModSimplified.cs => TaikoModQuarterize.cs} (96%) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs similarity index 96% rename from osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs rename to osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs index 8ce6698857..3e5e620073 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplified.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs @@ -12,14 +12,14 @@ using osu.Game.Rulesets.Taiko.Replays; namespace osu.Game.Rulesets.Taiko.Tests.Mods { - public partial class TestSceneTaikoModSimplified : TaikoModTestScene + public partial class TestSceneTaikoModQuarterize : TaikoModTestScene { [Test] public void TestOneThirdConversion() { CreateModTest(new ModTestData { - Mod = new TaikoModSimplified + Mod = new TaikoModQuarterize { OneThirdConversion = { Value = true }, }, @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [Test] public void TestOneSixthConversion() => CreateModTest(new ModTestData { - Mod = new TaikoModSimplified + Mod = new TaikoModQuarterize { OneSixthConversion = { Value = true } }, @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [Test] public void TestOneEighthConversion() => CreateModTest(new ModTestData { - Mod = new TaikoModSimplified + Mod = new TaikoModQuarterize { OneEighthConversion = { Value = true } }, diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs rename to osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs index 70e76ed8f3..c486c6d8b2 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplified.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs @@ -15,10 +15,10 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModSimplified : Mod, IApplicableToBeatmap + public class TaikoModQuarterize : Mod, IApplicableToBeatmap { - public override string Name => "Simplified"; - public override string Acronym => "SF"; + public override string Name => "Quarterize"; + public override string Acronym => "QR"; public override double ScoreMultiplier => 0.6; public override LocalisableString Description => "Simplify tricky rhythms!"; public override ModType Type => ModType.DifficultyReduction; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index f57d2a20f5..cce7f61d2f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko new TaikoModEasy(), new TaikoModNoFail(), new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()), - new TaikoModSimplified(), + new TaikoModQuarterize(), }; case ModType.DifficultyIncrease: From ad21b7f3412ac791b8035b58ff0872762cfb1f5f Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 22 Nov 2024 13:48:15 +0100 Subject: [PATCH 014/152] cleaner expression --- osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs index c486c6d8b2..e2ab4853fa 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs @@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Taiko.Mods List toRemove = new List(); // Snap conversions for rhythms - var snapConversions = new Dictionary + var snapConversions = new Dictionary { - { 8, 4 }, // 1/8 snap to 1/4 snap - { 6, 4 }, // 1/6 snap to 1/4 snap - { 3, 2 }, // 1/3 snap to 1/2 snap + { 8, 4.0 }, // 1/8 snap to 1/4 snap + { 6, 4.0 }, // 1/6 snap to 1/4 snap + { 3, 2.0 }, // 1/3 snap to 1/2 snap }; bool inPattern = false; @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Taiko.Mods } else if (currentHitPosition % 3 == 2 && j < hits.Count - 1) { - double offset = controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / Convert.ToDouble(snapConversion.Value); + double offset = controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / snapConversion.Value; hits[j].StartTime = hits[j + 1].StartTime - offset; } } From 25bb6cbf9b3e4f33e32225f764dd686e296c7123 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 22 Nov 2024 14:18:09 +0100 Subject: [PATCH 015/152] remove unused import --- osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs index e2ab4853fa..af319b1d41 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; From 4918b6141257a19f0ff636fc29e0635784863623 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Wed, 4 Dec 2024 21:14:51 +0100 Subject: [PATCH 016/152] minor cleanup --- osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs index af319b1d41..a3ef7125f6 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs @@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { var taikoBeatmap = (TaikoBeatmap)beatmap; var controlPointInfo = taikoBeatmap.ControlPointInfo; + List hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast().ToList(); List toRemove = new List(); // Snap conversions for rhythms @@ -47,8 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Mods bool inPattern = false; - List hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast().ToList(); - foreach (var snapConversion in snapConversions) { int patternStartIndex = 0; @@ -73,10 +72,10 @@ namespace osu.Game.Rulesets.Taiko.Mods inPattern = true; } - // check if end of pattern + // Check if end of pattern if (inPattern && snapValue != snapConversion.Key) { - // End of the pattern + // End pattern inPattern = false; // Iterate through the pattern From f3eb21cfec6f7d979797188814494a1a55df0a48 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Wed, 4 Dec 2024 21:15:22 +0100 Subject: [PATCH 017/152] make tests pass condition more rigid --- .../Mods/TestSceneTaikoModQuarterize.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs index 3e5e620073..dd36f8cb0e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods new TaikoReplayFrame(3500, TaikoAction.LeftCentre), new TaikoReplayFrame(3700), }, - PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 + PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1 }); } @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods new TaikoReplayFrame(2250, TaikoAction.LeftCentre), new TaikoReplayFrame(2450), }, - PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 + PassCondition = () => Player.ScoreProcessor.Combo.Value == 6 && Player.ScoreProcessor.Accuracy.Value == 1 }); [Test] @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods new TaikoReplayFrame(1900), new TaikoReplayFrame(2000, TaikoAction.LeftCentre), }, - PassCondition = () => Player.ScoreProcessor.Combo.Value == 5 + PassCondition = () => Player.ScoreProcessor.Combo.Value == 5 && Player.ScoreProcessor.Accuracy.Value == 1 }); } } From d66227c9e520008604a83dc61412f02dc7f71b83 Mon Sep 17 00:00:00 2001 From: Hivie Date: Thu, 10 Apr 2025 12:39:20 +0100 Subject: [PATCH 018/152] revert to original name --- ...Quarterize.cs => TestSceneTaikoModSimplifiedRhythm.cs} | 8 ++++---- ...{TaikoModQuarterize.cs => TaikoModSimplifiedRhythm.cs} | 6 +++--- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game.Rulesets.Taiko.Tests/Mods/{TestSceneTaikoModQuarterize.cs => TestSceneTaikoModSimplifiedRhythm.cs} (96%) rename osu.Game.Rulesets.Taiko/Mods/{TaikoModQuarterize.cs => TaikoModSimplifiedRhythm.cs} (96%) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs similarity index 96% rename from osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs rename to osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs index dd36f8cb0e..09ff5fe266 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs @@ -12,14 +12,14 @@ using osu.Game.Rulesets.Taiko.Replays; namespace osu.Game.Rulesets.Taiko.Tests.Mods { - public partial class TestSceneTaikoModQuarterize : TaikoModTestScene + public partial class TestSceneTaikoModSimplifiedRhythm : TaikoModTestScene { [Test] public void TestOneThirdConversion() { CreateModTest(new ModTestData { - Mod = new TaikoModQuarterize + Mod = new TaikoModSimplifiedRhythm { OneThirdConversion = { Value = true }, }, @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [Test] public void TestOneSixthConversion() => CreateModTest(new ModTestData { - Mod = new TaikoModQuarterize + Mod = new TaikoModSimplifiedRhythm { OneSixthConversion = { Value = true } }, @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [Test] public void TestOneEighthConversion() => CreateModTest(new ModTestData { - Mod = new TaikoModQuarterize + Mod = new TaikoModSimplifiedRhythm { OneEighthConversion = { Value = true } }, diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs rename to osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index a3ef7125f6..fc162d4b7b 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -14,10 +14,10 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModQuarterize : Mod, IApplicableToBeatmap + public class TaikoModSimplifiedRhythm : Mod, IApplicableToBeatmap { - public override string Name => "Quarterize"; - public override string Acronym => "QR"; + public override string Name => "Simplified Rhythm"; + public override string Acronym => "SR"; public override double ScoreMultiplier => 0.6; public override LocalisableString Description => "Simplify tricky rhythms!"; public override ModType Type => ModType.DifficultyReduction; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index cce7f61d2f..0280992b9d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko new TaikoModEasy(), new TaikoModNoFail(), new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()), - new TaikoModQuarterize(), + new TaikoModSimplifiedRhythm(), }; case ModType.DifficultyIncrease: From 2a332896c1f19f89d4f2719803c88e1cd32f51a0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 05:38:13 -0400 Subject: [PATCH 019/152] Update sheared button flow test case to be useful --- .../UserInterface/TestSceneShearedButtons.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs index 8db22f2d65..bdec96f446 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs @@ -13,7 +13,6 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface @@ -183,32 +182,31 @@ namespace osu.Game.Tests.Visual.UserInterface Origin = Anchor.Centre, Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Scale = new Vector2(2.5f), Children = new Drawable[] { - new ShearedButton(120) + new ShearedButton { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "Test", + Text = "Button", Action = () => { }, - Padding = new MarginPadding(), + Height = 30, }, - new ShearedButton(120, 40) + new ShearedButton { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "Test", + Text = "Button", Action = () => { }, - Padding = new MarginPadding { Left = -1f }, + Height = 30, }, - new ShearedButton(120, 70) + new ShearedButton { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "Test", + Text = "Button", Action = () => { }, - Padding = new MarginPadding { Left = 3f }, + Height = 30, }, } } From ac547353763c481f79c94dbe44c2255572337e7f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 06:02:52 -0400 Subject: [PATCH 020/152] Update sheared slider bar test scene --- .../TestSceneShearedSliderBar.cs | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index c3038ddb3d..28f22f1b6c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -3,40 +3,34 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; +using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneShearedSliderBar : OsuManualInputManagerTestScene + public partial class TestSceneShearedSliderBar : ThemeComparisonTestScene { - [Cached] - private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple); - private ShearedSliderBar slider = null!; - [SetUpSteps] - public void SetUpSteps() + protected override Drawable CreateContent() => slider = new ShearedSliderBar { - AddStep("create slider", () => Child = slider = new ShearedSliderBar + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = new BindableDouble(5) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Current = new BindableDouble(5) - { - Precision = 0.1, - MinValue = 0, - MaxValue = 15 - }, - RelativeSizeAxes = Axes.X, - Width = 0.4f - }); - } + Precision = 0.1, + MinValue = 0, + MaxValue = 15 + }, + RelativeSizeAxes = Axes.X, + Width = 0.4f + }; [Test] public void TestNubDoubleClickRevertToDefault() @@ -69,6 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1)); + AddStep("enable slider", () => slider.Current.Disabled = false); } } } From c71f3dee28357ce76c7a2e529190bf47b19dd741 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:30:08 -0400 Subject: [PATCH 021/152] Update beatmap leaderboard score design to match new metrics --- ...cs => TestSceneBeatmapLeaderboardScore.cs} | 76 +++++--- .../SelectV2/BeatmapLeaderboardScore.cs | 180 ++++++++++++------ 2 files changed, 171 insertions(+), 85 deletions(-) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneLeaderboardScore.cs => TestSceneBeatmapLeaderboardScore.cs} (80%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs similarity index 80% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index b59a31c173..c82f20a758 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; @@ -28,7 +29,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneLeaderboardScore : SongSelectComponentsTestScene + public partial class TestSceneBeatmapLeaderboardScore : SongSelectComponentsTestScene { [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -44,18 +45,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("create content", () => { - Children = new Drawable[] + Child = new PopoverContainer { - fillFlow = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 2f), - Shear = OsuGame.SHEAR, - }, - drawWidthText = new OsuSpriteText(), + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = OsuGame.SHEAR, + }, + drawWidthText = new OsuSpriteText(), + } }; foreach (var scoreInfo in getTestScores()) @@ -78,17 +84,22 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("create content", () => { - Children = new Drawable[] + Child = new PopoverContainer { - fillFlow = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 2f), - }, - drawWidthText = new OsuSpriteText(), + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + }, + drawWidthText = new OsuSpriteText(), + } }; foreach (var scoreInfo in getTestScores()) @@ -112,18 +123,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("create content", () => { - Children = new Drawable[] + Child = new PopoverContainer { - fillFlow = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 2f), - Shear = OsuGame.SHEAR, - }, - drawWidthText = new OsuSpriteText(), + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = OsuGame.SHEAR, + }, + drawWidthText = new OsuSpriteText(), + } }; var scoreInfo = new ScoreInfo diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index c9413a9414..cefb3aec54 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -24,6 +25,7 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; @@ -57,17 +59,18 @@ namespace osu.Game.Screens.SelectV2 public int? Rank { get; init; } public bool IsPersonalBest { get; init; } - private const float expanded_right_content_width = 210; - private const float grade_width = 40; - private const float username_min_width = 125; - private const float statistics_regular_min_width = 175; - private const float statistics_compact_min_width = 100; - private const float rank_label_width = 65; + private const float expanded_right_content_width = 200; + private const float grade_width = 35; + private const float username_min_width = 120; + private const float statistics_regular_min_width = 165; + private const float statistics_compact_min_width = 90; + private const float rank_label_width = 60; private readonly ScoreInfo score; private readonly bool sheared; - private const int height = 60; + public const int HEIGHT = 50; + private const int corner_radius = 10; private const int transition_duration = 200; @@ -75,6 +78,10 @@ namespace osu.Game.Screens.SelectV2 private Colour4 backgroundColour; private ColourInfo totalScoreBackgroundGradient; + private static readonly Color4 personal_best_gradient_left = Color4Extensions.FromHex("#66FFCC"); + private static readonly Color4 personal_best_gradient_right = Color4Extensions.FromHex("#51A388"); + private ColourInfo personalBestGradient; + [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -111,7 +118,8 @@ namespace osu.Game.Screens.SelectV2 private Box totalScoreBackground = null!; private FillFlowContainer statisticsContainer = null!; - private RankLabel rankLabel = null!; + private Container personalBestIndicator = null!; + private Container rankLabelStandalone = null!; private Container rankLabelOverlay = null!; public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); @@ -124,7 +132,7 @@ namespace osu.Game.Screens.SelectV2 Shear = sheared ? OsuGame.SHEAR : Vector2.Zero; RelativeSizeAxes = Axes.X; - Height = height; + Height = HEIGHT; } [BackgroundDependencyLoader] @@ -132,9 +140,10 @@ namespace osu.Game.Screens.SelectV2 { var user = score.User; - foregroundColour = IsPersonalBest ? colourProvider.Background1 : colourProvider.Background5; - backgroundColour = IsPersonalBest ? colourProvider.Background2 : colourProvider.Background4; + foregroundColour = colourProvider.Background5; + backgroundColour = colourProvider.Background3; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); + personalBestGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left, personal_best_gradient_right); statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score) { @@ -167,14 +176,24 @@ namespace osu.Game.Screens.SelectV2 { new Drawable[] { - new Container + rankLabelStandalone = new Container { - AutoSizeAxes = Axes.X, + Width = rank_label_width, RelativeSizeAxes = Axes.Y, - Child = rankLabel = new RankLabel(Rank, sheared) + Children = new Drawable[] { - Width = rank_label_width, - RelativeSizeAxes = Axes.Y, + personalBestIndicator = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = -10f }, + Alpha = IsPersonalBest ? 1 : 0, + Colour = personalBestGradient, + Child = new Box { RelativeSizeAxes = Axes.Both }, + }, + new RankLabel(Rank, sheared, darkText: IsPersonalBest) + { + RelativeSizeAxes = Axes.Both, + } }, }, createCentreContent(user), @@ -203,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 switch (s.NewValue) { case ScoringMode.Standardised: - rightContent.Width = 180f; + rightContent.Width = 170; break; case ScoringMode.Classic: @@ -224,15 +243,15 @@ namespace osu.Game.Screens.SelectV2 modsContainer.Padding = new MarginPadding { Top = 4f }; modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Take(Math.Min(maxMods, score.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) { - Scale = new Vector2(0.375f) + Scale = new Vector2(0.3125f) }); if (score.Mods.Length > maxMods) { modsContainer.Remove(modsContainer[^1], true); - modsContainer.Add(new MoreModSwitchTiny(score.Mods.Length - maxMods + 1) + modsContainer.Add(new MoreModSwitchTiny(score.Mods) { - Scale = new Vector2(0.375f), + Scale = new Vector2(0.3125f), }); } } @@ -291,7 +310,7 @@ namespace osu.Game.Screens.SelectV2 }) { RelativeSizeAxes = Axes.None, - Size = new Vector2(height) + Size = new Vector2(HEIGHT) }, rankLabelOverlay = new Container { @@ -304,7 +323,7 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Both, Colour = Colour4.Black.Opacity(0.5f), }, - new RankLabel(Rank, sheared) + new RankLabel(Rank, sheared, false) { AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -337,18 +356,19 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(24, 16), + Size = new Vector2(20, 14), }, new UpdateableTeamFlag(user.Team) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(40, 20), + Size = new Vector2(30, 15), }, new DateLabel(score.Date) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Colour = colourProvider.Content2, UseFullGlyphHeight = false, } } @@ -358,7 +378,7 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X, Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Text = user.Username, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) + Font = OsuFont.Style.Heading2, } } }, @@ -441,7 +461,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.Centre, Spacing = new Vector2(-2), Colour = DrawableRank.GetRankNameColour(score.Rank), - Font = OsuFont.Numeric.With(size: 16), + Font = OsuFont.Numeric.With(size: 14), Text = DrawableRank.GetRankName(score.Rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), @@ -490,16 +510,21 @@ namespace osu.Game.Screens.SelectV2 UseFullGlyphHeight = false, Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Current = scoreManager.GetBindableTotalScoreString(score), - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), + Spacing = new Vector2(-1.5f), + Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), }, - modsContainer = new FillFlowContainer + new InputBlockingContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(2f, 0f), + Child = modsContainer = new FillFlowContainer + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f, 0f), + }, }, } } @@ -523,9 +548,9 @@ namespace osu.Game.Screens.SelectV2 Alpha = 0; - content.MoveToY(75); - avatar.MoveToX(75); - nameLabel.MoveToX(150); + content.MoveToY(60); + avatar.MoveToX(60); + nameLabel.MoveToX(125); this.FadeIn(200); content.MoveToY(0, 800, Easing.OutQuint); @@ -568,10 +593,12 @@ namespace osu.Game.Screens.SelectV2 private void updateState() { var lightenedGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0).Lighten(0.2f), backgroundColour.Lighten(0.2f)); + var personalBestLightenedGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left.Lighten(0.2f), personal_best_gradient_right.Lighten(0.2f)); foreground.FadeColour(IsHovered ? foregroundColour.Lighten(0.2f) : foregroundColour, transition_duration, Easing.OutQuint); background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); + personalBestIndicator.FadeColour(IsHovered ? personalBestLightenedGradient : personalBestGradient, transition_duration, Easing.OutQuint); if (IsHovered && currentMode != DisplayMode.Full) rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint); @@ -590,9 +617,9 @@ namespace osu.Game.Screens.SelectV2 if (currentMode != mode) { if (mode >= DisplayMode.Full) - rankLabel.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + rankLabelStandalone.FadeIn(transition_duration, Easing.OutQuint).ResizeWidthTo(rank_label_width, transition_duration, Easing.OutQuint); else - rankLabel.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-rankLabel.DrawWidth, transition_duration, Easing.OutQuint); + rankLabelStandalone.FadeOut(transition_duration, Easing.OutQuint).ResizeWidthTo(0, transition_duration, Easing.OutQuint); if (mode >= DisplayMode.Regular) { @@ -615,13 +642,13 @@ namespace osu.Game.Screens.SelectV2 private DisplayMode getCurrentDisplayMode() { - if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width) + if (DrawWidth >= HEIGHT + username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width) return DisplayMode.Full; - if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width) + if (DrawWidth >= HEIGHT + username_min_width + statistics_regular_min_width + expanded_right_content_width) return DisplayMode.Regular; - if (DrawWidth >= height + username_min_width + statistics_compact_min_width + expanded_right_content_width) + if (DrawWidth >= HEIGHT + username_min_width + statistics_compact_min_width + expanded_right_content_width) return DisplayMode.Compact; return DisplayMode.Minimal; @@ -642,7 +669,7 @@ namespace osu.Game.Screens.SelectV2 public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 16, weight: FontWeight.Medium, italics: true); + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); @@ -677,7 +704,7 @@ namespace osu.Game.Screens.SelectV2 { Colour = colourProvider.Content2, Text = statisticInfo.Name, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), }, value = new OsuSpriteText { @@ -685,7 +712,7 @@ namespace osu.Game.Screens.SelectV2 // since the accuracy is sometimes longer than its name. BypassAutoSizeAxes = Axes.X, Text = statisticInfo.Value, - Font = OsuFont.GetFont(size: 19, weight: FontWeight.Medium), + Font = OsuFont.Style.Body, } } }; @@ -697,21 +724,32 @@ namespace osu.Game.Screens.SelectV2 private partial class RankLabel : Container, IHasTooltip { - public RankLabel(int? rank, bool sheared) + private readonly bool darkText; + private readonly OsuSpriteText text; + + public RankLabel(int? rank, bool sheared, bool darkText) { + this.darkText = darkText; if (rank >= 1000) TooltipText = $"#{rank:N0}"; - Child = new OsuSpriteText + Child = text = new OsuSpriteText { Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), - Text = rank == null ? "-" : rank.Value.FormatRank().Insert(0, "#") + Font = OsuFont.Style.Heading2, + Text = rank == null ? "-" : rank.Value.FormatRank().Insert(0, "#"), + Shadow = !darkText, }; } + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + text.Colour = darkText ? colourProvider.Background3 : colourProvider.Content1; + } + public LocalisableString TooltipText { get; } } @@ -732,17 +770,17 @@ namespace osu.Game.Screens.SelectV2 public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); } - private sealed partial class MoreModSwitchTiny : CompositeDrawable + private sealed partial class MoreModSwitchTiny : CompositeDrawable, IHasPopover { - private readonly int count; + private readonly IReadOnlyList mods; - public MoreModSwitchTiny(int count) + public MoreModSwitchTiny(IReadOnlyList mods) { - this.count = count; + this.mods = mods; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider) { Size = new Vector2(ModSwitchTiny.WIDTH, ModSwitchTiny.DEFAULT_HEIGHT); @@ -755,16 +793,17 @@ namespace osu.Game.Screens.SelectV2 new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.2f), + Colour = colourProvider.Background6, }, new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Shadow = false, - Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black), - Text = $"+{count}", - Colour = colours.Yellow, + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Bold), + Text = ". . .", + Colour = Color4.White, + UseFullGlyphHeight = false, Margin = new MarginPadding { Top = 4 @@ -773,6 +812,37 @@ namespace osu.Game.Screens.SelectV2 } }; } + + protected override bool OnClick(ClickEvent e) + { + this.ShowPopover(); + return true; + } + + protected override bool OnHover(HoverEvent e) => true; + + public Popover GetPopover() => new MoreModsPopover(mods); + } + + public partial class MoreModsPopover : OsuPopover + { + public MoreModsPopover(IReadOnlyList mods) + { + AutoSizeAxes = Axes.Both; + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + + Child = new FillFlowContainer + { + Width = 125f, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(2.5f), + ChildrenEnumerable = mods.AsOrdered().Select(m => new ColouredModSwitchTiny(m) + { + Scale = new Vector2(0.3125f), + }) + }; + } } #endregion From 60171f1bf1da81c42cda2537f20f341a774d4a52 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:31:29 -0400 Subject: [PATCH 022/152] Add new beatmap leaderboard score tooltip --- .../SelectV2/BeatmapLeaderboardScore.cs | 2 +- .../BeatmapLeaderboardScore_Tooltip.cs | 378 ++++++++++++++++++ 2 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index cefb3aec54..add5e39cf2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.SelectV2 private Container rankLabelStandalone = null!; private Container rankLabelOverlay = null!; - public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); + public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(colourProvider); public virtual ScoreInfo TooltipContent => score; public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs new file mode 100644 index 0000000000..7f1997522e --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -0,0 +1,378 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapLeaderboardScore + { + public partial class LeaderboardScoreTooltip : VisibilityContainer, ITooltip + { + private const float spacing = 20f; + + private DateAndStatisticsPanel dateAndStatistics = null!; + private ModsPanel modsPanel = null!; + private TotalScoreRankPanel totalScoreRankPanel = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider; + + public LeaderboardScoreTooltip(OverlayColourProvider colourProvider) + { + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load() + { + Width = 170; + AutoSizeAxes = Axes.Y; + + InternalChild = new ReverseChildIDFillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, -spacing), + Children = new Drawable[] + { + dateAndStatistics = new DateAndStatisticsPanel(), + modsPanel = new ModsPanel(), + totalScoreRankPanel = new TotalScoreRankPanel(), + }, + }; + } + + private ScoreInfo? lastContent; + + public void SetContent(ScoreInfo content) + { + if (lastContent != null && lastContent.Equals(content)) + return; + + dateAndStatistics.Score = content; + modsPanel.Score = content; + totalScoreRankPanel.Score = content; + lastContent = content; + } + + protected override void PopIn() => this.FadeIn(300, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(300, Easing.OutQuint); + public void Move(Vector2 pos) => Position = pos; + + private partial class DateAndStatisticsPanel : CompositeDrawable + { + private OsuSpriteText absoluteDate = null!; + private DrawableDate relativeDate = null!; + private FillFlowContainer statistics = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public ScoreInfo Score + { + set + { + absoluteDate.Text = value.Date.ToLocalisableString(@"dd MMMM yyyy h:mm tt"); + relativeDate.Date = value.Date; + + var judgementsStatistics = value.GetStatisticsForDisplay().Select(s => + { + Colour4 colour = colours.ForHitResult(s.Result); + var hsl = colour.ToHSL(); + + Colour4 lightColour = Colour4.FromHSL(hsl.X, hsl.Y, 0.8f); + return new StatisticRow(s.DisplayName.ToUpper(), lightColour, s.Count.ToLocalisableString("N0")); + }); + + double multiplier = 1.0; + + foreach (var mod in value.Mods) + multiplier *= mod.ScoreMultiplier; + + var generalStatistics = new[] + { + new StatisticRow("Score Multiplier", colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)), + new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, colourProvider.Content2, value.MaxCombo.ToLocalisableString(@"0\x")), + new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, colourProvider.Content2, value.Accuracy.FormatAccuracy()), + }; + + if (value.PP != null) + { + generalStatistics = new[] + { + new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, value.PP.ToLocalisableString("N0")) + }.Concat(generalStatistics).ToArray(); + } + + statistics.ChildrenEnumerable = judgementsStatistics + .Append(Empty().With(d => d.Height = 20)) + .Concat(generalStatistics); + } + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + CornerRadius = corner_radius; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 4f), + Margin = new MarginPadding { Top = 8f }, + Children = new Drawable[] + { + absoluteDate = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + UseFullGlyphHeight = false, + }, + relativeDate = new DrawableDate(default, OsuFont.Style.Caption1.Size) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Colour = colourProvider.Content2, + UseFullGlyphHeight = false, + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + CornerRadius = corner_radius, + Masking = true, + Margin = new MarginPadding { Top = 4f }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + statistics = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 4f), + Padding = new MarginPadding(8f), + }, + }, + }, + }, + }, + }; + } + } + + private partial class StatisticRow : CompositeDrawable + { + public StatisticRow(LocalisableString label, Color4 labelColour, LocalisableString value) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new OsuSpriteText + { + Text = label, + Colour = labelColour, + Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), + }, + new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = value, + Colour = Color4.White, + Font = OsuFont.Style.Caption2, + }, + }; + } + } + + private partial class ModsPanel : CompositeDrawable + { + private FillFlowContainer modsFlow = null!; + + public ScoreInfo Score + { + set + { + var mods = value.Mods; + + if (!mods.Any()) + Hide(); + else + { + Show(); + + modsFlow.ChildrenEnumerable = mods.AsOrdered().Select(m => new ModSwitchTiny(m) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.3125f), + Active = { Value = true }, + }); + } + } + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + CornerRadius = corner_radius; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Transparent, + }, + modsFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 6f, Top = 6f + spacing }, + Padding = new MarginPadding { Horizontal = 16f }, + Spacing = new Vector2(2.5f), + }, + }; + } + } + + public partial class TotalScoreRankPanel : CompositeDrawable + { + private Box rankBackground = null!; + private Container rankContainer = null!; + private OsuSpriteText totalScore = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + public ScoreInfo Score + { + set + { + rankBackground.Colour = ColourInfo.GradientVertical( + OsuColour.ForRank(value.Rank).Opacity(0f), + OsuColour.ForRank(value.Rank).Opacity(0.5f)); + rankContainer.Child = new DrawableRank(value.Rank); + totalScore.Current = scoreManager.GetBindableTotalScoreString(value); + } + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + CornerRadius = corner_radius; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#353535"), + }, + rankBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + rankContainer = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(25f, 14f), + Margin = new MarginPadding { Bottom = 5f }, + }, + totalScore = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding { Bottom = 25f, Top = 10f + spacing }, + Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), + Spacing = new Vector2(-1.5f), + UseFullGlyphHeight = false, + }, + }; + } + } + } + } +} From 62b96466c4ee8d5c267de6276879656eccc215c0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 05:05:35 -0400 Subject: [PATCH 023/152] Remove padding from `ShearedButton` for better sheared flow alignment --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 8 ++++---- osu.Game/Overlays/Mods/AddPresetButton.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index a059490aa8..cc57e9c75f 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -88,11 +88,11 @@ namespace osu.Game.Graphics.UserInterface public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT) { Height = height; - Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height }; - Content.CornerRadius = CORNER_RADIUS; - Content.Shear = OsuGame.SHEAR; - Content.Masking = true; + CornerRadius = CORNER_RADIUS; + Shear = OsuGame.SHEAR; + Masking = true; + Content.Anchor = Content.Origin = Anchor.Centre; Children = new Drawable[] diff --git a/osu.Game/Overlays/Mods/AddPresetButton.cs b/osu.Game/Overlays/Mods/AddPresetButton.cs index 276afd9bec..e4f7f83c11 100644 --- a/osu.Game/Overlays/Mods/AddPresetButton.cs +++ b/osu.Game/Overlays/Mods/AddPresetButton.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Mods Height = ModSelectPanel.HEIGHT; // shear will be applied at a higher level in `ModPresetColumn`. - Content.Shear = Vector2.Zero; + Shear = Vector2.Zero; Padding = new MarginPadding(); Text = "+"; From 6aab4731506a9fdedee8176368cb4a1bc5b8c94c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 05:06:08 -0400 Subject: [PATCH 024/152] Add drop shadow support in `ShearedNub` Used for range sliders --- .../TestSceneShearedSliderBar.cs | 10 ++ osu.Game/Graphics/UserInterface/ShearedNub.cs | 111 ++++++++++++------ .../UserInterface/ShearedSliderBar.cs | 6 + 3 files changed, 89 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index 28f22f1b6c..cc6b0af9a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -32,6 +32,16 @@ namespace osu.Game.Tests.Visual.UserInterface Width = 0.4f }; + [Test] + public void TestNubShadow() + { + AddToggleStep("toggle nub shadow", v => + { + if (slider.IsNotNull()) + slider.NubShadowColour = v ? Color4.Black.Opacity(0.2f) : Color4.Black.Opacity(0f); + }); + } + [Test] public void TestNubDoubleClickRevertToDefault() { diff --git a/osu.Game/Graphics/UserInterface/ShearedNub.cs b/osu.Game/Graphics/UserInterface/ShearedNub.cs index 17b50b5d58..f8a0b20e3e 100644 --- a/osu.Game/Graphics/UserInterface/ShearedNub.cs +++ b/osu.Game/Graphics/UserInterface/ShearedNub.cs @@ -21,13 +21,12 @@ namespace osu.Game.Graphics.UserInterface { public Action? OnDoubleClicked { get; init; } - protected const float BORDER_WIDTH = 3; - public const int HEIGHT = 30; public const float EXPANDED_SIZE = 50; private readonly Box fill; private readonly Container main; + private readonly Container shadow; /// /// Implements the shape for the nub, allowing for any type of container to be used. @@ -36,22 +35,43 @@ namespace osu.Game.Graphics.UserInterface public ShearedNub() { Size = new Vector2(EXPANDED_SIZE, HEIGHT); - InternalChild = main = new Container + InternalChildren = new Drawable[] { - Shear = OsuGame.SHEAR, - BorderColour = Colour4.White, - BorderThickness = BORDER_WIDTH, - Masking = true, - CornerRadius = 5, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Child = fill = new Box + shadow = new Container { + Shear = OsuGame.SHEAR, + Masking = true, + CornerRadius = 5, RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - } + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 20f, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + } + }, + main = new Container + { + Shear = OsuGame.SHEAR, + BorderColour = Colour4.White, + BorderThickness = 8f, + Masking = true, + CornerRadius = 5, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Child = fill = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + } + }, }; } @@ -76,6 +96,7 @@ namespace osu.Game.Graphics.UserInterface base.LoadComplete(); Current.BindValueChanged(onCurrentValueChanged, true); + FinishTransforms(true); } private bool glowing; @@ -89,22 +110,22 @@ namespace osu.Game.Graphics.UserInterface return; glowing = value; + updateDisplay(); + } + } - if (value) - { - main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint) - .Then() - .FadeColour(GlowingAccentColour, 800, Easing.OutQuint); + private Color4 shadowColour = Color4.Black.Opacity(0f); - main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint) - .Then() - .FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint); - } - else - { - main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint); - main.FadeColour(AccentColour, 800, Easing.OutQuint); - } + public Color4 ShadowColour + { + get => shadowColour; + set + { + if (shadowColour == value) + return; + + shadowColour = value; + shadow.FadeEdgeEffectTo(value, 800, Easing.OutQuint); } } @@ -130,8 +151,7 @@ namespace osu.Game.Graphics.UserInterface set { accentColour = value; - if (!Glowing) - main.Colour = value; + updateDisplay(); } } @@ -143,8 +163,7 @@ namespace osu.Game.Graphics.UserInterface set { glowingAccentColour = value; - if (Glowing) - main.Colour = value; + updateDisplay(); } } @@ -156,10 +175,7 @@ namespace osu.Game.Graphics.UserInterface set { glowColour = value; - - var effect = main.EdgeEffect; - effect.Colour = Glowing ? value : value.Opacity(0); - main.EdgeEffect = effect; + updateDisplay(); } } @@ -177,7 +193,26 @@ namespace osu.Game.Graphics.UserInterface else { main.ResizeWidthTo(0.75f, duration, Easing.OutQuint); - main.TransformTo(nameof(BorderThickness), BORDER_WIDTH, duration, Easing.OutQuint); + main.TransformTo(nameof(BorderThickness), 8f, duration, Easing.OutQuint); + } + } + + private void updateDisplay() + { + if (Glowing) + { + main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint) + .Then() + .FadeColour(GlowingAccentColour, 800, Easing.OutQuint); + + main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint) + .Then() + .FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint); + } + else + { + main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint); + main.FadeColour(AccentColour, 800, Easing.OutQuint); } } diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index e7b57f5c9e..e09995634f 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -56,6 +56,12 @@ namespace osu.Game.Graphics.UserInterface } } + public Color4 NubShadowColour + { + get => Nub.ShadowColour; + set => Nub.ShadowColour = value; + } + public ShearedSliderBar() { Shear = OsuGame.SHEAR; From 715396e5c476c18788169cf336e77396e8be6cc5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 05:08:09 -0400 Subject: [PATCH 025/152] Allow disabling focus indicator in `ShearedSliderBar` --- .../Graphics/UserInterface/ShearedSliderBar.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index e09995634f..10e18f139a 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -29,6 +29,8 @@ namespace osu.Game.Graphics.UserInterface private readonly Container mainContent; + protected virtual bool FocusIndicator => true; + private Color4 accentColour; public Color4 AccentColour @@ -152,13 +154,16 @@ namespace osu.Game.Graphics.UserInterface { base.OnFocus(e); - mainContent.EdgeEffect = new EdgeEffectParameters + if (FocusIndicator) { - Type = EdgeEffectType.Glow, - Colour = AccentColour.Darken(1), - Hollow = true, - Radius = 2, - }; + mainContent.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = AccentColour.Darken(1), + Hollow = true, + Radius = 2, + }; + } } protected override void OnFocusLost(FocusLostEvent e) From c75ff30c43ac167ad75e3437ce0f8d7638a7325a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 07:29:49 -0400 Subject: [PATCH 026/152] Add slider step for resizing nub width --- .../TestSceneShearedSliderBar.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index cc6b0af9a8..7a654fcb4b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -16,9 +16,9 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneShearedSliderBar : ThemeComparisonTestScene { - private ShearedSliderBar slider = null!; + private TestSliderBar slider = null!; - protected override Drawable CreateContent() => slider = new ShearedSliderBar + protected override Drawable CreateContent() => slider = new TestSliderBar { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -33,9 +33,17 @@ namespace osu.Game.Tests.Visual.UserInterface }; [Test] - public void TestNubShadow() + public void TestNubDisplay() { - AddToggleStep("toggle nub shadow", v => + AddSliderStep("nub width", 20, 80, 50, v => + { + if (slider.IsNotNull()) + { + slider.Nub.Width = v; + slider.RangePadding = v / 2f; + } + }); + AddToggleStep("nub shadow", v => { if (slider.IsNotNull()) slider.NubShadowColour = v ? Color4.Black.Opacity(0.2f) : Color4.Black.Opacity(0f); @@ -75,5 +83,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1)); AddStep("enable slider", () => slider.Current.Disabled = false); } + + public partial class TestSliderBar : ShearedSliderBar + { + public new ShearedNub Nub => base.Nub; + } } } From 4c911d3d9197546a4b8c7844a3821f5d5aed6185 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 06:45:43 -0400 Subject: [PATCH 027/152] Fix `ShearedSliderBar` left/right boxes not resized correctly --- osu.Game/Graphics/UserInterface/ShearedNub.cs | 9 +++------ osu.Game/Graphics/UserInterface/ShearedSliderBar.cs | 11 +++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedNub.cs b/osu.Game/Graphics/UserInterface/ShearedNub.cs index f8a0b20e3e..0021c1cbd2 100644 --- a/osu.Game/Graphics/UserInterface/ShearedNub.cs +++ b/osu.Game/Graphics/UserInterface/ShearedNub.cs @@ -23,15 +23,12 @@ namespace osu.Game.Graphics.UserInterface public const int HEIGHT = 30; public const float EXPANDED_SIZE = 50; + public const float CORNER_RADIUS = 5; private readonly Box fill; private readonly Container main; private readonly Container shadow; - /// - /// Implements the shape for the nub, allowing for any type of container to be used. - /// - /// public ShearedNub() { Size = new Vector2(EXPANDED_SIZE, HEIGHT); @@ -41,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface { Shear = OsuGame.SHEAR, Masking = true, - CornerRadius = 5, + CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, EdgeEffect = new EdgeEffectParameters { @@ -61,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface BorderColour = Colour4.White, BorderThickness = 8f, Masking = true, - CornerRadius = 5, + CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index 10e18f139a..4c3909eed8 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Overlays; -using static osu.Game.Graphics.UserInterface.ShearedNub; using Vector2 = osuTK.Vector2; namespace osu.Game.Graphics.UserInterface @@ -67,8 +66,8 @@ namespace osu.Game.Graphics.UserInterface public ShearedSliderBar() { Shear = OsuGame.SHEAR; - Height = HEIGHT; - RangePadding = EXPANDED_SIZE / 2; + Height = ShearedNub.HEIGHT; + RangePadding = ShearedNub.EXPANDED_SIZE / 2; Children = new Drawable[] { mainContent = new Container @@ -110,7 +109,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Both, Child = Nub = new ShearedNub { - X = -OsuGame.SHEAR.X * HEIGHT / 2f, + X = -OsuGame.SHEAR.X * ShearedNub.HEIGHT / 2f, Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, @@ -202,8 +201,8 @@ namespace osu.Game.Graphics.UserInterface protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1); - RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1); + LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1); + RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - RangePadding - Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1); } protected override void UpdateValue(float value) From af991d3b2942e68fa1a31a46618551b3719e363d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 07:37:23 -0400 Subject: [PATCH 028/152] Remove weird default for slider nub X value --- osu.Game/Graphics/UserInterface/ShearedSliderBar.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index 4c3909eed8..cdbf768b1c 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -109,7 +109,6 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Both, Child = Nub = new ShearedNub { - X = -OsuGame.SHEAR.X * ShearedNub.HEIGHT / 2f, Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, From 34e8943c74198220a626dc1fe3775e7904eb1cfb Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:42:18 -0400 Subject: [PATCH 029/152] Add beatmap leaderboard wedge --- .../TestSceneBeatmapLeaderboardWedge.cs | 352 +++++++++++++++++ .../SelectV2/BeatmapLeaderboardWedge.cs | 370 ++++++++++++++++++ 2 files changed, 722 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs new file mode 100644 index 0000000000..060f2ad956 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -0,0 +1,352 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.SongSelect; +using osu.Game.Users; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapLeaderboardWedge : SongSelectComponentsTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + private TestBeatmapLeaderboardWedge leaderboard = null!; + private ScoreManager scoreManager = null!; + private RulesetStore rulesetStore = null!; + private BeatmapManager beatmapManager = null!; + private OsuContextMenuContainer contentContainer = null!; + private DialogOverlay dialogOverlay = null!; + + private LeaderboardManager leaderboardManager = null!; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); + + Dependencies.Cache(Realm); + + return dependencies; + } + + [BackgroundDependencyLoader] + private void load() + { + LoadComponent(dialogOverlay = new DialogOverlay + { + Depth = -1 + }); + + LoadComponent(leaderboardManager); + + Child = contentContainer = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.X, + Height = 500, + Children = new Drawable[] + { + dialogOverlay, + } + }; + + AddSliderStep("change relative height", 0f, 1f, 0.65f, v => Schedule(() => + { + contentContainer.Height = v * DrawHeight; + })); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + if (leaderboard.IsNotNull()) + contentContainer.Remove(leaderboard, false); + + contentContainer.Add(leaderboard = new TestBeatmapLeaderboardWedge + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + }); + }); + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + } + + [Test] + public void TestGlobalScoresDisplay() + { + setScope(BeatmapLeaderboardScope.Global); + + AddStep(@"New Scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()).Select(s => + { + s.User.Team = new APITeam(); + return s; + }))); + } + + [Test] + public void TestPersonalBest() + { + AddStep(@"Show personal best", showPersonalBest); + } + + [Test] + public void TestPersonalBestWithNullPosition() + { + AddStep("null personal best position", showPersonalBestWithNullPosition); + } + + [Test] + public void TestPlaceholderStates() + { + AddStep("ensure no scores displayed", () => leaderboard.SetScores(Array.Empty())); + + AddStep(@"Network failure", () => leaderboard.SetState(LeaderboardState.NetworkFailure)); + AddStep(@"No team", () => leaderboard.SetState(LeaderboardState.NoTeam)); + AddStep(@"No supporter", () => leaderboard.SetState(LeaderboardState.NotSupporter)); + AddStep(@"Not logged in", () => leaderboard.SetState(LeaderboardState.NotLoggedIn)); + AddStep(@"Ruleset unavailable", () => leaderboard.SetState(LeaderboardState.RulesetUnavailable)); + AddStep(@"Beatmap unavailable", () => leaderboard.SetState(LeaderboardState.BeatmapUnavailable)); + AddStep(@"None selected", () => leaderboard.SetState(LeaderboardState.NoneSelected)); + } + + [Test] + public void TestUseTheseModsDoesNotCopySystemMods() + { + AddStep(@"set scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + { + OnlineID = 1337, + Position = 999, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + } + })); + AddUntilStep("wait for scores", () => this.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + AddStep("right click panel", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Right); + }); + AddStep("click use these mods", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("received HD", () => this.ChildrenOfType().Last().SelectedMods.Value.Any(m => m is OsuModHidden)); + AddAssert("did not receive SV2", () => !this.ChildrenOfType().Last().SelectedMods.Value.Any(m => m is ModScoreV2)); + } + + [Test] + [Ignore("Pending implementation")] + // todo: add score fetch functionality to BeatmapLeaderboardWedge + public void TestLocalScoresDisplay() + { + BeatmapInfo beatmapInfo = null!; + + setScope(BeatmapLeaderboardScope.Local); + + AddStep(@"Set beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo); + }); + + clearScores(); + checkDisplayedCount(0); + + importMoreScores(() => beatmapInfo); + checkDisplayedCount(10); + + importMoreScores(() => beatmapInfo); + checkDisplayedCount(20); + + clearScores(); + checkDisplayedCount(0); + } + + [Test] + [Ignore("Pending implementation")] + // todo: add score fetch functionality to BeatmapLeaderboardWedge + public void TestLocalScoresDisplayOnBeatmapEdit() + { + BeatmapInfo beatmapInfo = null!; + string originalHash = string.Empty; + + setScope(BeatmapLeaderboardScope.Local); + + AddStep(@"Import beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo); + }); + + clearScores(); + checkDisplayedCount(0); + + AddStep(@"Perform initial save to guarantee stable hash", () => + { + IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; + beatmapManager.Save(beatmapInfo, beatmap); + + originalHash = beatmapInfo.Hash; + }); + + importMoreScores(() => beatmapInfo); + + checkDisplayedCount(10); + checkStoredCount(10); + + AddStep(@"Save with changes", () => + { + IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; + beatmap.Difficulty.ApproachRate = 12; + beatmapManager.Save(beatmapInfo, beatmap); + }); + + AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash)); + checkDisplayedCount(0); + checkStoredCount(10); + + importMoreScores(() => beatmapInfo); + importMoreScores(() => beatmapInfo); + checkDisplayedCount(20); + checkStoredCount(30); + + AddStep(@"Revert changes", () => + { + IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; + beatmap.Difficulty.ApproachRate = 8; + beatmapManager.Save(beatmapInfo, beatmap); + }); + + AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash)); + checkDisplayedCount(10); + checkStoredCount(30); + + clearScores(); + checkDisplayedCount(0); + checkStoredCount(0); + } + + private void showPersonalBestWithNullPosition() + { + leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + { + OnlineID = 1337, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + }, + }); + } + + private void showPersonalBest() + { + leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + { + OnlineID = 1337, + Position = 999, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + } + }); + } + + private void setScope(BeatmapLeaderboardScope scope) + { + AddStep(@"Set scope", () => ((Bindable)leaderboard.Scope).Value = scope); + } + + private void importMoreScores(Func beatmapInfo) + { + AddStep(@"Import new scores", () => + { + foreach (var score in TestSceneBeatmapLeaderboard.GenerateSampleScores(beatmapInfo())) + scoreManager.Import(score); + }); + } + + private void clearScores() + { + AddStep("Clear all scores", () => scoreManager.Delete()); + } + + private void checkDisplayedCount(int expected) => + AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType().Count(), () => Is.EqualTo(expected)); + + private void checkStoredCount(int expected) => + AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All().Count(s => !s.DeletePending)), () => Is.EqualTo(expected)); + + private partial class TestBeatmapLeaderboardWedge : BeatmapLeaderboardWedge + { + public new void SetState(LeaderboardState state) => base.SetState(state); + public new void SetScores(IEnumerable scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs new file mode 100644 index 0000000000..d15927a67f --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -0,0 +1,370 @@ +// 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 System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.Leaderboards; +using osu.Game.Online.Placeholders; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapLeaderboardWedge : VisibilityContainer + { + private Container scoresContainer = null!; + + private OsuScrollContainer scoresScroll = null!; + private Container personalBestDisplay = null!; + private Container personalBestScoreContainer = null!; + private LoadingLayer loading = null!; + + private Container placeholderContainer = null!; + private Placeholder? placeholder; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public IBindable Scope { get; } = new Bindable(); + + private bool isOnlineScope => Scope.Value != BeatmapLeaderboardScope.Local; + + public IBindable FilterBySelectedMods { get; } = new BindableBool(); + + private CancellationTokenSource? cancellationTokenSource; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + scoresScroll = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Shear = OsuGame.SHEAR, + Child = scoresContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 4f, Bottom = 180f }, + }, + }, + personalBestDisplay = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = OsuGame.SHEAR, + Margin = new MarginPadding { Left = -60f }, + CornerRadius = 10f, + Masking = true, + // push the personal best 1px down to hide masking issues + Y = 1f, + X = -100f, + Alpha = 0f, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + Padding = new MarginPadding { Top = 5f, Bottom = 30f, Left = 100f, Right = 30f }, + Children = new Drawable[] + { + new OsuSpriteText + { + Colour = colourProvider.Content2, + Text = "Personal Best", + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + personalBestScoreContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 20f }, + }, + } + }, + }, + }, + placeholderContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + loading = new LoadingLayer(), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Scope.BindValueChanged(_ => refetchScores()); + FilterBySelectedMods.BindValueChanged(_ => refetchScores()); + beatmap.BindValueChanged(_ => refetchScores()); + ruleset.BindValueChanged(_ => refetchScores()); + mods.BindValueChanged(_ => refetchScoresFromMods()); + + refetchScores(); + } + + protected override void PopIn() + { + this.FadeIn(300, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(300, Easing.OutQuint); + } + + private void refetchScoresFromMods() + { + if (FilterBySelectedMods.Value) + refetchScores(); + } + + private void refetchScores() + { + SetScores(Array.Empty(), null); + SetState(LeaderboardState.Retrieving); + + if (beatmap.IsDefault) + { + SetState(LeaderboardState.NoneSelected); + return; + } + + var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; + + if (!api.IsLoggedIn) + { + SetState(LeaderboardState.NotLoggedIn); + return; + } + + if (!fetchRuleset.IsLegacyRuleset()) + { + SetState(LeaderboardState.RulesetUnavailable); + return; + } + + if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && isOnlineScope) + { + SetState(LeaderboardState.BeatmapUnavailable); + return; + } + + if (Scope.Value.RequiresSupporter(FilterBySelectedMods.Value) && !api.LocalUser.Value.IsSupporter) + { + SetState(LeaderboardState.NotSupporter); + return; + } + + if (Scope.Value == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) + { + SetState(LeaderboardState.NoTeam); + return; + } + + // todo: missing implementation + SetScores(Array.Empty(), null); + } + + protected void SetScores(IEnumerable scores, ScoreInfo? userScore) + { + cancellationTokenSource?.Cancel(); + cancellationTokenSource = new CancellationTokenSource(); + + clearScores(); + SetState(LeaderboardState.Success); + + if (!scores.Any()) + { + SetState(LeaderboardState.NoScores); + return; + } + + LoadComponentsAsync(scores.Select((s, i) => new BeatmapLeaderboardScore(s) + { + Rank = i + 1, + IsPersonalBest = s.OnlineID == userScore?.OnlineID, + SelectedMods = { BindTarget = mods }, + }), loadedScores => + { + int delay = 100; + int accumulation = 1; + int i = 0; + + foreach (var scoreDrawable in loadedScores) + { + Container scoreDrawableContainer; + + scoresContainer.Add(scoreDrawableContainer = new Container + { + Shear = -OsuGame.SHEAR, + Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0f, + Padding = new MarginPadding { Left = 80f }, + Child = scoreDrawable, + }); + + scoreDrawableContainer.Delay(delay).FadeIn(300, Easing.OutQuint); + scoreDrawableContainer.MoveToX(-100f).Delay(delay).MoveToX(0f, 300, Easing.OutQuint); + + delay += Math.Max(0, 50 - accumulation); + accumulation *= 2; + i++; + } + }, cancellation: cancellationTokenSource.Token); + + if (userScore != null) + { + personalBestDisplay.MoveToX(0, 600, Easing.OutQuint); + personalBestDisplay.FadeIn(600, Easing.OutQuint); + personalBestScoreContainer.Child = new BeatmapLeaderboardScore(userScore) + { + IsPersonalBest = true, + Rank = userScore.Position, + SelectedMods = { BindTarget = mods }, + }; + + scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding { Bottom = 100 }, 300, Easing.OutQuint); + } + } + + private void clearScores() + { + foreach (var scoreDrawable in scoresContainer) + { + scoreDrawable.MoveToX(-50f, 200, Easing.OutQuint); + scoreDrawable.FadeOut(200, Easing.OutQuint); + scoreDrawable.Expire(); + } + + personalBestDisplay.MoveToX(-100, 300, Easing.OutQuint); + personalBestDisplay.FadeOut(300, Easing.OutQuint); + scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding(), 300, Easing.OutQuint); + } + + private LeaderboardState displayedState; + + protected void SetState(LeaderboardState state) + { + if (state == displayedState) + return; + + if (state == LeaderboardState.Retrieving) + loading.Show(); + else + loading.Hide(); + + displayedState = state; + + placeholder?.FadeOut(150, Easing.OutQuint).Expire(); + placeholder = getPlaceholderFor(state); + + if (placeholder == null) + return; + + placeholderContainer.Child = placeholder; + + placeholder.ScaleTo(0.8f).Then().ScaleTo(1, 900, Easing.OutQuint); + placeholder.FadeInFromZero(300, Easing.OutQuint); + } + + private Placeholder? getPlaceholderFor(LeaderboardState state) + { + switch (state) + { + case LeaderboardState.NetworkFailure: + return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync) + { + Action = refetchScores + }; + + case LeaderboardState.NoneSelected: + return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap); + + case LeaderboardState.RulesetUnavailable: + return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset); + + case LeaderboardState.BeatmapUnavailable: + return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap); + + case LeaderboardState.NoScores: + return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet); + + case LeaderboardState.NotLoggedIn: + return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards); + + case LeaderboardState.NotSupporter: + return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard); + + case LeaderboardState.NoTeam: + return new MessagePlaceholder(LeaderboardStrings.NoTeam); + + case LeaderboardState.Retrieving: + return null; + + case LeaderboardState.Success: + return null; + + default: + throw new ArgumentOutOfRangeException(nameof(state)); + } + } + } +} From 81d54a9f32ad3f2eb04d360b6078ab78aa2f03ec Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:43:47 -0400 Subject: [PATCH 030/152] Implement score fetch functionality Copied logic from `BeatmapLeaderboard`. --- .../TestSceneBeatmapLeaderboardWedge.cs | 4 --- .../SelectV2/BeatmapLeaderboardWedge.cs | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs index 060f2ad956..f034049476 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -182,8 +182,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - [Ignore("Pending implementation")] - // todo: add score fetch functionality to BeatmapLeaderboardWedge public void TestLocalScoresDisplay() { BeatmapInfo beatmapInfo = null!; @@ -212,8 +210,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - [Ignore("Pending implementation")] - // todo: add score fetch functionality to BeatmapLeaderboardWedge public void TestLocalScoresDisplayOnBeatmapEdit() { BeatmapInfo beatmapInfo = null!; diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index d15927a67f..66e799c93e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -43,6 +42,9 @@ namespace osu.Game.Screens.SelectV2 private Container placeholderContainer = null!; private Placeholder? placeholder; + [Resolved] + private LeaderboardManager leaderboardManager { get; set; } = null!; + [Resolved] private IBindable beatmap { get; set; } = null!; @@ -66,6 +68,8 @@ namespace osu.Game.Screens.SelectV2 private CancellationTokenSource? cancellationTokenSource; + private readonly Bindable fetchedScores = new Bindable(); + [BackgroundDependencyLoader] private void load() { @@ -142,6 +146,8 @@ namespace osu.Game.Screens.SelectV2 loading = new LoadingLayer(), } }; + + ((IBindable)fetchedScores).BindTo(leaderboardManager.Scores); } protected override void LoadComplete() @@ -217,8 +223,22 @@ namespace osu.Game.Screens.SelectV2 return; } - // todo: missing implementation - SetScores(Array.Empty(), null); + leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null)) + .ContinueWith(t => + { + if (t.Exception != null && !t.IsCanceled) + { + Schedule(() => SetState(LeaderboardState.NetworkFailure)); + return; + } + + fetchedScores.UnbindEvents(); + fetchedScores.BindValueChanged(scores => + { + if (scores.NewValue != null) + Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore)); + }, true); + }); } protected void SetScores(IEnumerable scores, ScoreInfo? userScore) From 066b03646661441bdb5b541c1ca39c0306d83d5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 17:19:16 +0900 Subject: [PATCH 031/152] Add padding around footer content to avoid sheared overflow --- osu.Game/Overlays/WizardOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 2a881045fd..5ed9870aae 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -243,11 +243,12 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both; + Padding = new MarginPadding { Horizontal = 20 }; + InternalChild = NextButton = new ShearedButton(0) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 12f }, RelativeSizeAxes = Axes.X, Width = 1, Text = FirstRunSetupOverlayStrings.GetStarted, From 5e06b3d1b43e8165249578dbb1f2a5b4aa552a6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 17:30:03 +0900 Subject: [PATCH 032/152] Make mod preset shear buttons take up full width --- osu.Game/Overlays/Mods/AddPresetPopover.cs | 4 +++- osu.Game/Overlays/Mods/EditPresetPopover.cs | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index 7df7d6339c..40a1e4f7e9 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -63,10 +63,12 @@ namespace osu.Game.Overlays.Mods Label = CommonStrings.Description, TabbableContentContainer = this }, - createButton = new ShearedButton + createButton = new ShearedButton(0) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Width = 1, Text = ModSelectOverlayStrings.AddPreset, Action = createPreset } diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 8014126942..8295bdbab8 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -112,20 +112,24 @@ namespace osu.Game.Overlays.Mods Spacing = new Vector2(7), Children = new Drawable[] { - useCurrentModsButton = new ShearedButton + useCurrentModsButton = new ShearedButton(0) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Width = 1, Text = ModSelectOverlayStrings.UseCurrentMods, DarkerColour = colours.Blue1, LighterColour = colours.Blue0, TextColour = colourProvider.Background6, Action = useCurrentMods, }, - saveButton = new ShearedButton + saveButton = new ShearedButton(0) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Width = 1, Text = Resources.Localisation.Web.CommonStrings.ButtonsSave, DarkerColour = colours.Orange1, LighterColour = colours.Orange0, From 7a18a771b3e557e2f64a604636de2f70f99d4952 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 17:44:57 +0900 Subject: [PATCH 033/152] Fix regression from copy waste --- .../TestSceneBeatmapLeaderboardWedge.cs | 21 +++++++++++++++++++ .../SelectV2/BeatmapLeaderboardWedge.cs | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs index f034049476..baeb9ba5bb 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -209,6 +209,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkDisplayedCount(0); } + [Test] + public void TestLocalScoresDisplayWorksWhenStartingOffline() + { + BeatmapInfo beatmapInfo = null!; + + AddStep("Log out", () => API.Logout()); + setScope(BeatmapLeaderboardScope.Local); + + AddStep(@"Import beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo); + }); + + clearScores(); + importMoreScores(() => beatmapInfo); + checkDisplayedCount(10); + } + [Test] public void TestLocalScoresDisplayOnBeatmapEdit() { diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 66e799c93e..a6db5ec7a5 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -193,7 +193,7 @@ namespace osu.Game.Screens.SelectV2 var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - if (!api.IsLoggedIn) + if (!api.IsLoggedIn && isOnlineScope) { SetState(LeaderboardState.NotLoggedIn); return; From bec2d62a7ab6dd793c2c320aac76172dd957e95f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 18:01:12 +0900 Subject: [PATCH 034/152] Seal `BeatmapLeaderboardScore` for now We'll need to figure out what to do in multiplayer cases in the future, but the hope is that it can be done without further subclassing if possible. --- .../SelectV2/BeatmapLeaderboardScore.cs | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index add5e39cf2..197d13d30f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -45,7 +45,7 @@ using CommonStrings = osu.Game.Localisation.CommonStrings; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip + public sealed partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { public Bindable> SelectedMods = new Bindable>(); @@ -66,7 +66,6 @@ namespace osu.Game.Screens.SelectV2 private const float statistics_compact_min_width = 90; private const float rank_label_width = 60; - private readonly ScoreInfo score; private readonly bool sheared; public const int HEIGHT = 50; @@ -109,7 +108,6 @@ namespace osu.Game.Screens.SelectV2 private Container rightContent = null!; - protected Container RankContainer { get; private set; } = null!; private FillFlowContainer flagBadgeAndDateContainer = null!; private FillFlowContainer modsContainer = null!; @@ -123,11 +121,12 @@ namespace osu.Game.Screens.SelectV2 private Container rankLabelOverlay = null!; public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(colourProvider); - public virtual ScoreInfo TooltipContent => score; + + public ScoreInfo TooltipContent { get; } public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) { - this.score = score; + TooltipContent = score; this.sheared = sheared; Shear = sheared ? OsuGame.SHEAR : Vector2.Zero; @@ -138,14 +137,14 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - var user = score.User; + var user = TooltipContent.User; foregroundColour = colourProvider.Background5; backgroundColour = colourProvider.Background3; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); personalBestGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left, personal_best_gradient_right); - statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score) + statisticsLabels = getStatistics(TooltipContent).Select(s => new ScoreComponentLabel(s, TooltipContent) { // ensure statistics container is the correct width when invalidating AlwaysPresent = true, @@ -238,18 +237,18 @@ namespace osu.Game.Screens.SelectV2 { int maxMods = scoringMode.Value == ScoringMode.Standardised ? 4 : 5; - if (score.Mods.Length > 0) + if (TooltipContent.Mods.Length > 0) { modsContainer.Padding = new MarginPadding { Top = 4f }; - modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Take(Math.Min(maxMods, score.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) + modsContainer.ChildrenEnumerable = TooltipContent.Mods.AsOrdered().Take(Math.Min(maxMods, TooltipContent.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.3125f) }); - if (score.Mods.Length > maxMods) + if (TooltipContent.Mods.Length > maxMods) { modsContainer.Remove(modsContainer[^1], true); - modsContainer.Add(new MoreModSwitchTiny(score.Mods) + modsContainer.Add(new MoreModSwitchTiny(TooltipContent.Mods) { Scale = new Vector2(0.3125f), }); @@ -273,7 +272,7 @@ namespace osu.Game.Screens.SelectV2 new UserCoverBackground { RelativeSizeAxes = Axes.Both, - User = score.User, + User = TooltipContent.User, Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -364,7 +363,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.CentreLeft, Size = new Vector2(30, 15), }, - new DateLabel(score.Date) + new DateLabel(TooltipContent.Date) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -428,7 +427,7 @@ namespace osu.Game.Screens.SelectV2 Child = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(TooltipContent.Rank)), }, }, new Box @@ -437,7 +436,7 @@ namespace osu.Game.Screens.SelectV2 Width = grade_width, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Colour = OsuColour.ForRank(score.Rank), + Colour = OsuColour.ForRank(TooltipContent.Rank), }, new TrianglesV2 { @@ -446,9 +445,9 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.TopRight, SpawnRatio = 2, Velocity = 0.7f, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(TooltipContent.Rank).Darken(0.2f)), }, - RankContainer = new Container + new Container { Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.CentreRight, @@ -460,9 +459,9 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(score.Rank), + Colour = DrawableRank.GetRankNameColour(TooltipContent.Rank), Font = OsuFont.Numeric.With(size: 14), - Text = DrawableRank.GetRankName(score.Rank), + Text = DrawableRank.GetRankName(TooltipContent.Rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, @@ -492,7 +491,7 @@ namespace osu.Game.Screens.SelectV2 new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(TooltipContent.Rank).Opacity(0.5f)), }, new FillFlowContainer { @@ -509,7 +508,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.TopRight, UseFullGlyphHeight = false, Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Current = scoreManager.GetBindableTotalScoreString(score), + Current = scoreManager.GetBindableTotalScoreString(TooltipContent), Spacing = new Vector2(-1.5f), Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), }, @@ -535,7 +534,7 @@ namespace osu.Game.Screens.SelectV2 }, }; - protected (CaseTransformableString, LocalisableString DisplayAccuracy)[] GetStatistics(ScoreInfo model) => new[] + private (CaseTransformableString, LocalisableString DisplayAccuracy)[] getStatistics(ScoreInfo model) => new[] { (BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), model.MaxCombo.ToString().Insert(model.MaxCombo.ToString().Length, "x")), (BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), model.DisplayAccuracy), @@ -854,18 +853,18 @@ namespace osu.Game.Screens.SelectV2 List items = new List(); // system mods should never be copied across regardless of anything. - var copyableMods = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); + var copyableMods = TooltipContent.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); if (copyableMods.Length > 0) items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); - if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); + if (TooltipContent.OnlineID > 0) + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{TooltipContent.OnlineID}"))); - if (score.Files.Count <= 0) return items.ToArray(); + if (TooltipContent.Files.Count <= 0) return items.ToArray(); - items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); - items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); + items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(TooltipContent))); + items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(TooltipContent)))); return items.ToArray(); } From 26f2703688258f010f31699dd2052584b67a2225 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 18:04:41 +0900 Subject: [PATCH 035/152] Fix non-sheared test showing sheared drawables --- .../Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index c82f20a758..c2f1eb6b15 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var scoreInfo in getTestScores()) { - fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo, sheared: false) { Rank = scoreInfo.Position, IsPersonalBest = scoreInfo.User.Id == 2, From de821005dc5a82ec23c33c0f3d3a5a12453b65ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 18:30:05 +0900 Subject: [PATCH 036/152] Make leaderbaord animation barely bareable --- .../TestSceneBeatmapLeaderboardWedge.cs | 12 +++--- .../SelectV2/BeatmapLeaderboardWedge.cs | 38 ++++++++++++------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs index baeb9ba5bb..f03d83b5e8 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -107,6 +107,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 base.SetUpSteps(); } + [Test] + public void TestPersonalBest() + { + AddStep(@"Show personal best", showPersonalBest); + } + [Test] public void TestGlobalScoresDisplay() { @@ -120,12 +126,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }))); } - [Test] - public void TestPersonalBest() - { - AddStep(@"Show personal best", showPersonalBest); - } - [Test] public void TestPersonalBestWithNullPosition() { diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index a6db5ec7a5..774c1540c7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -262,15 +262,14 @@ namespace osu.Game.Screens.SelectV2 SelectedMods = { BindTarget = mods }, }), loadedScores => { - int delay = 100; - int accumulation = 1; + int delay = 200; int i = 0; - foreach (var scoreDrawable in loadedScores) + foreach (var d in loadedScores) { - Container scoreDrawableContainer; + Container animContainer; - scoresContainer.Add(scoreDrawableContainer = new Container + scoresContainer.Add(animContainer = new Container { Shear = -OsuGame.SHEAR, Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i, @@ -278,14 +277,16 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Y, Alpha = 0f, Padding = new MarginPadding { Left = 80f }, - Child = scoreDrawable, + Child = d, }); - scoreDrawableContainer.Delay(delay).FadeIn(300, Easing.OutQuint); - scoreDrawableContainer.MoveToX(-100f).Delay(delay).MoveToX(0f, 300, Easing.OutQuint); + animContainer + .MoveToX(-20f) + .Delay(delay) + .FadeIn(300, Easing.OutQuint) + .MoveToX(0f, 300, Easing.OutQuint); - delay += Math.Max(0, 50 - accumulation); - accumulation *= 2; + delay += 30; i++; } }, cancellation: cancellationTokenSource.Token); @@ -307,11 +308,20 @@ namespace osu.Game.Screens.SelectV2 private void clearScores() { - foreach (var scoreDrawable in scoresContainer) + float delay = 0; + + foreach (var d in scoresContainer) { - scoreDrawable.MoveToX(-50f, 200, Easing.OutQuint); - scoreDrawable.FadeOut(200, Easing.OutQuint); - scoreDrawable.Expire(); + // Avoid applying animations a second time to drawables which are already fading out. + if (d.LifetimeEnd != double.MaxValue) + continue; + + d.Delay(delay) + .MoveToX(-10f, 120, Easing.Out) + .FadeOut(120, Easing.Out) + .Expire(); + + delay += 20; } personalBestDisplay.MoveToX(-100, 300, Easing.OutQuint); From 8bf95ca3a864702e061d407620794288e4814e24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 18:41:15 +0900 Subject: [PATCH 037/152] Don't animate on initial mode display This removes a lot of movement, but honestly it didn't feel good in the first place. If anything I'll come back with a second-pass animation pass on this. --- .../Screens/SelectV2/BeatmapLeaderboardScore.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 197d13d30f..d76f2b181f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -398,8 +398,6 @@ namespace osu.Game.Screens.SelectV2 Direction = FillDirection.Horizontal, Children = statisticsLabels, Alpha = 0, - LayoutEasing = Easing.OutQuint, - LayoutDuration = transition_duration, } } } @@ -615,25 +613,26 @@ namespace osu.Game.Screens.SelectV2 if (currentMode != mode) { + double duration = currentMode == null ? 0 : transition_duration; if (mode >= DisplayMode.Full) - rankLabelStandalone.FadeIn(transition_duration, Easing.OutQuint).ResizeWidthTo(rank_label_width, transition_duration, Easing.OutQuint); + rankLabelStandalone.FadeIn(duration, Easing.OutQuint).ResizeWidthTo(rank_label_width, duration, Easing.OutQuint); else - rankLabelStandalone.FadeOut(transition_duration, Easing.OutQuint).ResizeWidthTo(0, transition_duration, Easing.OutQuint); + rankLabelStandalone.FadeOut(duration, Easing.OutQuint).ResizeWidthTo(0, duration, Easing.OutQuint); if (mode >= DisplayMode.Regular) { - statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + statisticsContainer.FadeIn(duration, Easing.OutQuint).MoveToX(0, duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Horizontal; - statisticsContainer.ScaleTo(1, transition_duration, Easing.OutQuint); + statisticsContainer.ScaleTo(1, duration, Easing.OutQuint); } else if (mode >= DisplayMode.Compact) { - statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + statisticsContainer.FadeIn(duration, Easing.OutQuint).MoveToX(0, duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Vertical; - statisticsContainer.ScaleTo(0.8f, transition_duration, Easing.OutQuint); + statisticsContainer.ScaleTo(0.8f, duration, Easing.OutQuint); } else - statisticsContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, transition_duration, Easing.OutQuint); + statisticsContainer.FadeOut(duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, duration, Easing.OutQuint); currentMode = mode; } From 7c6a1f2502d55d8a6814fdbebd8f77d1f90c440b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 18:43:12 +0900 Subject: [PATCH 038/152] Update fetch logic to match existing leaderboard Also handles some display edge cases where scores may overlap placeholder or do other weird things. --- .../TestSceneBeatmapLeaderboardWedge.cs | 1 + .../SelectV2/BeatmapLeaderboardWedge.cs | 78 +++++++------------ 2 files changed, 28 insertions(+), 51 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs index f03d83b5e8..61d23c4513 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -137,6 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("ensure no scores displayed", () => leaderboard.SetScores(Array.Empty())); + AddStep(@"Retrieving", () => leaderboard.SetState(LeaderboardState.Retrieving)); AddStep(@"Network failure", () => leaderboard.SetState(LeaderboardState.NetworkFailure)); AddStep(@"No team", () => leaderboard.SetState(LeaderboardState.NoTeam)); AddStep(@"No supporter", () => leaderboard.SetState(LeaderboardState.NotSupporter)); diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 774c1540c7..c6e110b282 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -12,14 +12,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; -using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; -using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Online.Placeholders; using osu.Game.Overlays; @@ -54,21 +52,16 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable> mods { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; public IBindable Scope { get; } = new Bindable(); - private bool isOnlineScope => Scope.Value != BeatmapLeaderboardScope.Local; - public IBindable FilterBySelectedMods { get; } = new BindableBool(); private CancellationTokenSource? cancellationTokenSource; - private readonly Bindable fetchedScores = new Bindable(); + private readonly IBindable fetchedScores = new Bindable(); [BackgroundDependencyLoader] private void load() @@ -147,7 +140,7 @@ namespace osu.Game.Screens.SelectV2 } }; - ((IBindable)fetchedScores).BindTo(leaderboardManager.Scores); + fetchedScores.BindTo(leaderboardManager.Scores); } protected override void LoadComplete() @@ -179,10 +172,11 @@ namespace osu.Game.Screens.SelectV2 refetchScores(); } + private bool initialFetchComplete; + private void refetchScores() { SetScores(Array.Empty(), null); - SetState(LeaderboardState.Retrieving); if (beatmap.IsDefault) { @@ -190,55 +184,35 @@ namespace osu.Game.Screens.SelectV2 return; } + SetState(LeaderboardState.Retrieving); + var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - if (!api.IsLoggedIn && isOnlineScope) + // For now, we forcefully refresh to keep things simple. + // In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios + // (like returning from gameplay after setting a new score, returning to song select after main menu). + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), forceRefresh: true); + + if (!initialFetchComplete) { - SetState(LeaderboardState.NotLoggedIn); - return; + // only bind this after the first fetch to avoid reading stale scores. + fetchedScores.BindTo(leaderboardManager.Scores); + fetchedScores.BindValueChanged(_ => updateScores(), true); + initialFetchComplete = true; } + } - if (!fetchRuleset.IsLegacyRuleset()) - { - SetState(LeaderboardState.RulesetUnavailable); - return; - } + private void updateScores() + { + var scores = fetchedScores.Value; - if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && isOnlineScope) - { - SetState(LeaderboardState.BeatmapUnavailable); - return; - } + if (scores == null) return; - if (Scope.Value.RequiresSupporter(FilterBySelectedMods.Value) && !api.LocalUser.Value.IsSupporter) - { - SetState(LeaderboardState.NotSupporter); - return; - } - - if (Scope.Value == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) - { - SetState(LeaderboardState.NoTeam); - return; - } - - leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null)) - .ContinueWith(t => - { - if (t.Exception != null && !t.IsCanceled) - { - Schedule(() => SetState(LeaderboardState.NetworkFailure)); - return; - } - - fetchedScores.UnbindEvents(); - fetchedScores.BindValueChanged(scores => - { - if (scores.NewValue != null) - Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore)); - }, true); - }); + if (scores.FailState != null) + SetState((LeaderboardState)scores.FailState); + else + SetScores(scores.TopScores, scores.UserScore); } protected void SetScores(IEnumerable scores, ScoreInfo? userScore) @@ -349,6 +323,8 @@ namespace osu.Game.Screens.SelectV2 if (placeholder == null) return; + clearScores(); + placeholderContainer.Child = placeholder; placeholder.ScaleTo(0.8f).Then().ScaleTo(1, 900, Easing.OutQuint); From 698f9bd669f4537832a1e6f193b47199d0a0efb5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 18:58:49 +0900 Subject: [PATCH 039/152] Begin to fix eyesore code in `BeatmapLeaderboardScore` --- .../SelectV2/BeatmapLeaderboardScore.cs | 123 +++++++++--------- 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index d76f2b181f..4f6a9df34a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -122,11 +122,11 @@ namespace osu.Game.Screens.SelectV2 public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(colourProvider); - public ScoreInfo TooltipContent { get; } + private readonly ScoreInfo score; public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) { - TooltipContent = score; + this.score = score; this.sheared = sheared; Shear = sheared ? OsuGame.SHEAR : Vector2.Zero; @@ -137,14 +137,14 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - var user = TooltipContent.User; + var user = score.User; foregroundColour = colourProvider.Background5; backgroundColour = colourProvider.Background3; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); personalBestGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left, personal_best_gradient_right); - statisticsLabels = getStatistics(TooltipContent).Select(s => new ScoreComponentLabel(s, TooltipContent) + statisticsLabels = getStatistics(score).Select(s => new ScoreComponentLabel(s, score) { // ensure statistics container is the correct width when invalidating AlwaysPresent = true, @@ -237,18 +237,18 @@ namespace osu.Game.Screens.SelectV2 { int maxMods = scoringMode.Value == ScoringMode.Standardised ? 4 : 5; - if (TooltipContent.Mods.Length > 0) + if (score.Mods.Length > 0) { modsContainer.Padding = new MarginPadding { Top = 4f }; - modsContainer.ChildrenEnumerable = TooltipContent.Mods.AsOrdered().Take(Math.Min(maxMods, TooltipContent.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) + modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Take(Math.Min(maxMods, score.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.3125f) }); - if (TooltipContent.Mods.Length > maxMods) + if (score.Mods.Length > maxMods) { modsContainer.Remove(modsContainer[^1], true); - modsContainer.Add(new MoreModSwitchTiny(TooltipContent.Mods) + modsContainer.Add(new MoreModSwitchTiny(score.Mods) { Scale = new Vector2(0.3125f), }); @@ -272,7 +272,7 @@ namespace osu.Game.Screens.SelectV2 new UserCoverBackground { RelativeSizeAxes = Axes.Both, - User = TooltipContent.User, + User = score.User, Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -363,7 +363,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.CentreLeft, Size = new Vector2(30, 15), }, - new DateLabel(TooltipContent.Date) + new DateLabel(score.Date) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -425,7 +425,7 @@ namespace osu.Game.Screens.SelectV2 Child = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(TooltipContent.Rank)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), }, }, new Box @@ -434,7 +434,7 @@ namespace osu.Game.Screens.SelectV2 Width = grade_width, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Colour = OsuColour.ForRank(TooltipContent.Rank), + Colour = OsuColour.ForRank(score.Rank), }, new TrianglesV2 { @@ -443,7 +443,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.TopRight, SpawnRatio = 2, Velocity = 0.7f, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(TooltipContent.Rank).Darken(0.2f)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), }, new Container { @@ -457,9 +457,9 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(TooltipContent.Rank), + Colour = DrawableRank.GetRankNameColour(score.Rank), Font = OsuFont.Numeric.With(size: 14), - Text = DrawableRank.GetRankName(TooltipContent.Rank), + Text = DrawableRank.GetRankName(score.Rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, @@ -489,7 +489,7 @@ namespace osu.Game.Screens.SelectV2 new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(TooltipContent.Rank).Opacity(0.5f)), + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), }, new FillFlowContainer { @@ -506,7 +506,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.TopRight, UseFullGlyphHeight = false, Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Current = scoreManager.GetBindableTotalScoreString(TooltipContent), + Current = scoreManager.GetBindableTotalScoreString(score), Spacing = new Vector2(-1.5f), Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), }, @@ -652,7 +652,31 @@ namespace osu.Game.Screens.SelectV2 return DisplayMode.Minimal; } - #region Subclasses + ScoreInfo IHasCustomTooltip.TooltipContent => score; + + MenuItem[] IHasContextMenu.ContextMenuItems + { + get + { + List items = new List(); + + // system mods should never be copied across regardless of anything. + var copyableMods = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); + + if (copyableMods.Length > 0) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); + + if (score.OnlineID > 0) + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); + + if (score.Files.Count <= 0) return items.ToArray(); + + items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); + items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); + + return items.ToArray(); + } + } private enum DisplayMode { @@ -753,19 +777,18 @@ namespace osu.Game.Screens.SelectV2 private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasCustomTooltip { - public Mod? TooltipContent { get; } - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; public ColouredModSwitchTiny(Mod mod) : base(mod) { - TooltipContent = mod; Active.Value = true; } public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); + + Mod? IHasCustomTooltip.TooltipContent => (Mod)Mod; } private sealed partial class MoreModSwitchTiny : CompositeDrawable, IHasPopover @@ -820,52 +843,26 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnHover(HoverEvent e) => true; public Popover GetPopover() => new MoreModsPopover(mods); - } - public partial class MoreModsPopover : OsuPopover - { - public MoreModsPopover(IReadOnlyList mods) + public partial class MoreModsPopover : OsuPopover { - AutoSizeAxes = Axes.Both; - AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; - - Child = new FillFlowContainer + public MoreModsPopover(IReadOnlyList mods) { - Width = 125f, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, - Spacing = new Vector2(2.5f), - ChildrenEnumerable = mods.AsOrdered().Select(m => new ColouredModSwitchTiny(m) + AutoSizeAxes = Axes.Both; + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + + Child = new FillFlowContainer { - Scale = new Vector2(0.3125f), - }) - }; - } - } - - #endregion - - public MenuItem[] ContextMenuItems - { - get - { - List items = new List(); - - // system mods should never be copied across regardless of anything. - var copyableMods = TooltipContent.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); - - if (copyableMods.Length > 0) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); - - if (TooltipContent.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{TooltipContent.OnlineID}"))); - - if (TooltipContent.Files.Count <= 0) return items.ToArray(); - - items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(TooltipContent))); - items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(TooltipContent)))); - - return items.ToArray(); + Width = 125f, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(2.5f), + ChildrenEnumerable = mods.AsOrdered().Select(m => new ColouredModSwitchTiny(m) + { + Scale = new Vector2(0.3125f), + }) + }; + } } } } From d13c7e69955086485c14f4df1aa5cc40c51640c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 19:31:16 +0900 Subject: [PATCH 040/152] Remove all animations from `BeatmapLeaderboardScore` and fix more eyesore code --- .../SelectV2/BeatmapLeaderboardScore.cs | 668 ++++++++---------- 1 file changed, 305 insertions(+), 363 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 4f6a9df34a..c573239623 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -27,7 +27,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; @@ -47,6 +46,8 @@ namespace osu.Game.Screens.SelectV2 { public sealed partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { + public const int HEIGHT = 50; + public Bindable> SelectedMods = new Bindable>(); /// @@ -59,28 +60,6 @@ namespace osu.Game.Screens.SelectV2 public int? Rank { get; init; } public bool IsPersonalBest { get; init; } - private const float expanded_right_content_width = 200; - private const float grade_width = 35; - private const float username_min_width = 120; - private const float statistics_regular_min_width = 165; - private const float statistics_compact_min_width = 90; - private const float rank_label_width = 60; - - private readonly bool sheared; - - public const int HEIGHT = 50; - - private const int corner_radius = 10; - private const int transition_duration = 200; - - private Colour4 foregroundColour; - private Colour4 backgroundColour; - private ColourInfo totalScoreBackgroundGradient; - - private static readonly Color4 personal_best_gradient_left = Color4Extensions.FromHex("#66FFCC"); - private static readonly Color4 personal_best_gradient_right = Color4Extensions.FromHex("#51A388"); - private ColourInfo personalBestGradient; - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -90,29 +69,45 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private ScoreManager scoreManager { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + [Resolved] private Clipboard? clipboard { get; set; } [Resolved] private IAPIProvider api { get; set; } = null!; - private Container content = null!; + private const float expanded_right_content_width = 200; + private const float grade_width = 35; + private const float username_min_width = 120; + private const float statistics_regular_min_width = 165; + private const float statistics_compact_min_width = 90; + private const float rank_label_width = 60; + + private const int corner_radius = 10; + private const int transition_duration = 200; + + private static readonly Color4 personal_best_gradient_left = Color4Extensions.FromHex("#66FFCC"); + private static readonly Color4 personal_best_gradient_right = Color4Extensions.FromHex("#51A388"); + + private Colour4 foregroundColour; + private Colour4 backgroundColour; + private ColourInfo totalScoreBackgroundGradient; + + private ColourInfo personalBestGradient; + + private IBindable scoringMode { get; set; } = null!; + private Box background = null!; private Box foreground = null!; - private Drawable avatar = null!; private ClickableAvatar innerAvatar = null!; - private OsuSpriteText nameLabel = null!; - private List statisticsLabels = null!; - private Container rightContent = null!; - private FillFlowContainer flagBadgeAndDateContainer = null!; private FillFlowContainer modsContainer = null!; - private OsuSpriteText scoreText = null!; - private Drawable scoreRank = null!; private Box totalScoreBackground = null!; private FillFlowContainer statisticsContainer = null!; @@ -120,10 +115,10 @@ namespace osu.Game.Screens.SelectV2 private Container rankLabelStandalone = null!; private Container rankLabelOverlay = null!; - public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(colourProvider); - private readonly ScoreInfo score; + private readonly bool sheared; + public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) { this.score = score; @@ -137,20 +132,12 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - var user = score.User; - foregroundColour = colourProvider.Background5; backgroundColour = colourProvider.Background3; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); personalBestGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left, personal_best_gradient_right); - statisticsLabels = getStatistics(score).Select(s => new ScoreComponentLabel(s, score) - { - // ensure statistics container is the correct width when invalidating - AlwaysPresent = true, - }).ToList(); - - Child = content = new Container + Child = new Container { Masking = true, CornerRadius = corner_radius, @@ -195,8 +182,279 @@ namespace osu.Game.Screens.SelectV2 } }, }, - createCentreContent(user), - createRightContent() + new Container + { + Name = @"Centre container", + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + foreground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = foregroundColour + }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = score.User, + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = corner_radius, + Masking = true, + Children = new Drawable[] + { + new DelayedLoadWrapper(innerAvatar = new ClickableAvatar(score.User) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(HEIGHT) + }, + rankLabelOverlay = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Black.Opacity(0.5f), + }, + new RankLabel(Rank, sheared, false) + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + } + }, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Children = new Drawable[] + { + new FillFlowContainer + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new UpdateableFlag(score.User.CountryCode) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20, 14), + }, + new UpdateableTeamFlag(score.User.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 15), + }, + new DateLabel(score.Date) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = colourProvider.Content2, + UseFullGlyphHeight = false, + } + } + }, + new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Text = score.User.Username, + Font = OsuFont.Style.Heading2, + } + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Child = statisticsContainer = new FillFlowContainer + { + Name = @"Statistics container", + Padding = new MarginPadding { Right = 40 }, + Spacing = new Vector2(25, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = getStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(), + Alpha = 0, + } + } + } + }, + }, + }, + }, + rightContent = new Container + { + Name = @"Right content", + RelativeSizeAxes = Axes.Y, + Child = new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = grade_width }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), + }, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = OsuColour.ForRank(score.Rank), + }, + new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + SpawnRatio = 2, + Velocity = 0.7f, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), + }, + new Container + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Child = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-2), + Colour = DrawableRank.GetRankNameColour(score.Rank), + Font = OsuFont.Numeric.With(size: 14), + Text = DrawableRank.GetRankName(score.Rank), + ShadowColour = Color4.Black.Opacity(0.3f), + ShadowOffset = new Vector2(0, 0.08f), + Shadow = true, + UseFullGlyphHeight = false, + }, + }, + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = grade_width }, + Child = new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] + { + totalScoreBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = totalScoreBackgroundGradient, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + UseFullGlyphHeight = false, + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Current = scoreManager.GetBindableTotalScoreString(score), + Spacing = new Vector2(-1.5f), + Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), + }, + new InputBlockingContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Child = modsContainer = new FillFlowContainer + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f, 0f), + }, + }, + } + } + } + } + } + } + }, + } } } } @@ -206,11 +464,6 @@ namespace osu.Game.Screens.SelectV2 innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); } - [Resolved] - private OsuConfigManager config { get; set; } = null!; - - private IBindable scoringMode { get; set; } = null!; - protected override void LoadComplete() { base.LoadComplete(); @@ -256,325 +509,12 @@ namespace osu.Game.Screens.SelectV2 } } - private Container createCentreContent(APIUser user) => new Container - { - Name = @"Centre container", - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - foreground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = foregroundColour - }, - new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - User = score.User, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - Children = new[] - { - avatar = new DelayedLoadWrapper( - innerAvatar = new ClickableAvatar(user) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - RelativeSizeAxes = Axes.Both, - }) - { - RelativeSizeAxes = Axes.None, - Size = new Vector2(HEIGHT) - }, - rankLabelOverlay = new Container - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.Black.Opacity(0.5f), - }, - new RankLabel(Rank, sheared, false) - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - } - }, - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Children = new Drawable[] - { - flagBadgeAndDateContainer = new FillFlowContainer - { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new UpdateableFlag(user.CountryCode) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(20, 14), - }, - new UpdateableTeamFlag(user.Team) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(30, 15), - }, - new DateLabel(score.Date) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = colourProvider.Content2, - UseFullGlyphHeight = false, - } - } - }, - nameLabel = new TruncatingSpriteText - { - RelativeSizeAxes = Axes.X, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Text = user.Username, - Font = OsuFont.Style.Heading2, - } - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Child = statisticsContainer = new FillFlowContainer - { - Name = @"Statistics container", - Padding = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25, 0), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = statisticsLabels, - Alpha = 0, - } - } - } - }, - }, - }, - }; - - private Container createRightContent() => rightContent = new Container - { - Name = @"Right content", - RelativeSizeAxes = Axes.Y, - Child = new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = grade_width }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), - }, - }, - new Box - { - RelativeSizeAxes = Axes.Y, - Width = grade_width, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Colour = OsuColour.ForRank(score.Rank), - }, - new TrianglesV2 - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - SpawnRatio = 2, - Velocity = 0.7f, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), - }, - new Container - { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Y, - Width = grade_width, - Child = scoreRank = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(score.Rank), - Font = OsuFont.Numeric.With(size: 14), - Text = DrawableRank.GetRankName(score.Rank), - ShadowColour = Color4.Black.Opacity(0.3f), - ShadowOffset = new Vector2(0, 0.08f), - Shadow = true, - UseFullGlyphHeight = false, - }, - }, - new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Right = grade_width }, - Child = new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Masking = true, - CornerRadius = corner_radius, - Children = new Drawable[] - { - totalScoreBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = totalScoreBackgroundGradient, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Children = new Drawable[] - { - scoreText = new OsuSpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - UseFullGlyphHeight = false, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Current = scoreManager.GetBindableTotalScoreString(score), - Spacing = new Vector2(-1.5f), - Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), - }, - new InputBlockingContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Child = modsContainer = new FillFlowContainer - { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(2f, 0f), - }, - }, - } - } - } - } - } - } - }, - }; - private (CaseTransformableString, LocalisableString DisplayAccuracy)[] getStatistics(ScoreInfo model) => new[] { (BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), model.MaxCombo.ToString().Insert(model.MaxCombo.ToString().Length, "x")), (BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), model.DisplayAccuracy), }; - public override void Show() - { - foreach (var d in new[] { avatar, nameLabel, scoreText, scoreRank, flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels)) - d.FadeOut(); - - Alpha = 0; - - content.MoveToY(60); - avatar.MoveToX(60); - nameLabel.MoveToX(125); - - this.FadeIn(200); - content.MoveToY(0, 800, Easing.OutQuint); - - using (BeginDelayedSequence(100)) - { - avatar.FadeIn(300, Easing.OutQuint); - nameLabel.FadeIn(350, Easing.OutQuint); - - avatar.MoveToX(0, 300, Easing.OutQuint); - nameLabel.MoveToX(0, 350, Easing.OutQuint); - - using (BeginDelayedSequence(250)) - { - scoreText.FadeIn(200); - scoreRank.FadeIn(200); - - using (BeginDelayedSequence(50)) - { - var drawables = new Drawable[] { flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels).ToArray(); - for (int i = 0; i < drawables.Length; i++) - drawables[i].FadeIn(100 + i * 50); - } - } - } - } - protected override bool OnHover(HoverEvent e) { updateState(); @@ -652,6 +592,8 @@ namespace osu.Game.Screens.SelectV2 return DisplayMode.Minimal; } + ITooltip IHasCustomTooltip.GetCustomTooltip() => new LeaderboardScoreTooltip(colourProvider); + ScoreInfo IHasCustomTooltip.TooltipContent => score; MenuItem[] IHasContextMenu.ContextMenuItems @@ -788,7 +730,7 @@ namespace osu.Game.Screens.SelectV2 public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); - Mod? IHasCustomTooltip.TooltipContent => (Mod)Mod; + Mod IHasCustomTooltip.TooltipContent => (Mod)Mod; } private sealed partial class MoreModSwitchTiny : CompositeDrawable, IHasPopover From 3b2e8281b4e3f2ecdf5d6821427973d9004bb01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Apr 2025 13:22:25 +0200 Subject: [PATCH 041/152] Remove double binding --- osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index c6e110b282..e4df89c1f5 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -139,8 +139,6 @@ namespace osu.Game.Screens.SelectV2 loading = new LoadingLayer(), } }; - - fetchedScores.BindTo(leaderboardManager.Scores); } protected override void LoadComplete() From bd58aac9cca8b410b550ce6fa8afcdfb68a14b5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Apr 2025 20:59:07 +0900 Subject: [PATCH 042/152] Begin to fix `BeatmapLeaderboardWedge` code quality --- .../SelectV2/BeatmapLeaderboardWedge.cs | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index e4df89c1f5..b8c4d07d04 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -25,20 +25,15 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; +using osuTK; namespace osu.Game.Screens.SelectV2 { public partial class BeatmapLeaderboardWedge : VisibilityContainer { - private Container scoresContainer = null!; + public IBindable Scope { get; } = new Bindable(); - private OsuScrollContainer scoresScroll = null!; - private Container personalBestDisplay = null!; - private Container personalBestScoreContainer = null!; - private LoadingLayer loading = null!; - - private Container placeholderContainer = null!; - private Placeholder? placeholder; + public IBindable FilterBySelectedMods { get; } = new BindableBool(); [Resolved] private LeaderboardManager leaderboardManager { get; set; } = null!; @@ -55,14 +50,23 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public IBindable Scope { get; } = new Bindable(); + private Container placeholderContainer = null!; + private Placeholder? placeholder; - public IBindable FilterBySelectedMods { get; } = new BindableBool(); + private Container scoresContainer = null!; + + private OsuScrollContainer scoresScroll = null!; + private Container personalBestDisplay = null!; + + private Container personalBestScoreContainer = null!; + private LoadingLayer loading = null!; private CancellationTokenSource? cancellationTokenSource; private readonly IBindable fetchedScores = new Bindable(); + private const float personal_best_height = 80; + [BackgroundDependencyLoader] private void load() { @@ -82,7 +86,15 @@ namespace osu.Game.Screens.SelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 4f, Bottom = 180f }, + Padding = new MarginPadding + { + Top = 5, + // Left padding offsets the shear to create a visually appealing list display. + Left = 80f, + // Bottom padding ensures the last entry's full width is displayed + // (ie it is fully on screen after shear is considered). + Bottom = BeatmapLeaderboardScore.HEIGHT * 3 + }, }, }, personalBestDisplay = new Container @@ -90,9 +102,9 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + Height = personal_best_height, Shear = OsuGame.SHEAR, - Margin = new MarginPadding { Left = -60f }, + Margin = new MarginPadding { Left = -40f }, CornerRadius = 10f, Masking = true, // push the personal best 1px down to hide masking issues @@ -111,7 +123,7 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Shear = -OsuGame.SHEAR, - Padding = new MarginPadding { Top = 5f, Bottom = 30f, Left = 100f, Right = 30f }, + Padding = new MarginPadding { Top = 5f, Bottom = 5f, Left = 70f, Right = 10f }, Children = new Drawable[] { new OsuSpriteText @@ -239,24 +251,19 @@ namespace osu.Game.Screens.SelectV2 foreach (var d in loadedScores) { - Container animContainer; + d.Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i; - scoresContainer.Add(animContainer = new Container - { - Shear = -OsuGame.SHEAR, - Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0f, - Padding = new MarginPadding { Left = 80f }, - Child = d, - }); + // This is a bit of a weird one. We're already in a sheared state and don't want top-level + // shear applied, but still need the `BeatmapLeadeboardScore` to be in "sheared" mode (see ctor). + d.Shear = Vector2.Zero; - animContainer - .MoveToX(-20f) - .Delay(delay) - .FadeIn(300, Easing.OutQuint) - .MoveToX(0f, 300, Easing.OutQuint); + scoresContainer.Add(d); + + d.FadeOut() + .MoveToX(-20f) + .Delay(delay) + .FadeIn(300, Easing.OutQuint) + .MoveToX(0f, 300, Easing.OutQuint); delay += 30; i++; @@ -274,7 +281,7 @@ namespace osu.Game.Screens.SelectV2 SelectedMods = { BindTarget = mods }, }; - scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding { Bottom = 100 }, 300, Easing.OutQuint); + scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding { Bottom = personal_best_height }, 300, Easing.OutQuint); } } From 6cdbfe064799b46818ce49be3926e6f70d9191c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Apr 2025 21:22:01 +0900 Subject: [PATCH 043/152] Update 404ing cover image URLs --- osu.Game.Tests/Resources/TestResources.cs | 7 +++++- .../TestSceneDailyChallengeCarousel.cs | 5 ++-- .../TestSceneDailyChallengeEventFeed.cs | 8 +++---- .../TestSceneDailyChallengeScoreBreakdown.cs | 5 ++-- .../TestSceneDailyChallengeTotalsDisplay.cs | 4 ++-- .../TestSceneMultiplayerParticipantsList.cs | 23 ++++++++++--------- .../Online/TestSceneDashboardOverlay.cs | 3 ++- .../Visual/Online/TestSceneFriendDisplay.cs | 5 ++-- .../Online/TestSceneUserClickableAvatar.cs | 7 +++--- .../Online/TestSceneUserProfileHeader.cs | 5 ++-- .../Online/TestSceneUserProfileOverlay.cs | 11 +++++---- .../TestScenePlaylistsResultsScreen.cs | 6 ++--- .../TestSceneBeatmapLeaderboardScore.cs | 6 ++--- 13 files changed, 54 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index e0572e604c..54204d412a 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -29,6 +29,11 @@ namespace osu.Game.Tests.Resources { public const double QUICK_BEATMAP_LENGTH = 10000; + public const string COVER_IMAGE_1 = "https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg"; + public const string COVER_IMAGE_2 = "https://assets.ppy.sh/user-cover-presets/7/4a0ccb7b7fdd5c4238b11f0e7c686760fe2c99c6472b19400e82d1a8ff503e31.jpeg"; + public const string COVER_IMAGE_3 = "https://assets.ppy.sh/user-cover-presets/12/6e8d3402c8080c2d9549a98321e1bff111dd9c94603ccdb237597479cab6e8a7.jpeg"; + public const string COVER_IMAGE_4 = "https://assets.ppy.sh/user-cover-presets/17/80f82e4c2b27d8d6eed3ce89708ec27343e5ac63389cba6b5fb4550776562d08.jpeg"; + private static readonly TemporaryNativeStorage temp_storage = new TemporaryNativeStorage("TestResources"); public static DllResourceStore GetStore() => new DllResourceStore(typeof(TestResources).Assembly); @@ -178,7 +183,7 @@ namespace osu.Game.Tests.Resources { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = COVER_IMAGE_3, }, BeatmapInfo = beatmap, BeatmapHash = beatmap.Hash, diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs index b9470f3be4..becce7b22a 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs @@ -16,6 +16,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.DailyChallenge { @@ -129,7 +130,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), null); feed.AddNewScore(ev); @@ -141,7 +142,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), RNG.Next(1, 1000)); feed.AddNewScore(ev); diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs index 4b784f661d..eda596effb 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), null); feed.AddNewScore(ev); @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), RNG.Next(11, 1000)); var testScore = TestResources.CreateTestScoreInfo(); @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), RNG.Next(1, 10)); feed.AddNewScore(ev); @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), null); feed.AddNewScore(ev); diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs index b04696aded..b4e1ffffdb 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs @@ -13,6 +13,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.DailyChallenge { @@ -65,7 +66,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), null); breakdown.AddNewScore(ev); @@ -85,7 +86,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), null); breakdown.AddNewScore(ev); diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs index ae212f5212..4619fad938 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), null); totals.AddNewScore(ev); @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { Id = 2, Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), RNG.Next(11, 1000)); var testScore = TestResources.CreateTestScoreInfo(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index ed3fd4a6f8..158a1f46a0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; +using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -46,7 +47,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = 3, Username = "Second", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, })); AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); @@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = 3, Username = "Second", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }); }); @@ -159,7 +160,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = 3, Username = "Second", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, })); AddUntilStep("first user crown visible", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 1); @@ -178,7 +179,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = 3, Username = "Second", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, })); AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); @@ -197,7 +198,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = 3, Username = "Second", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, })); AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); @@ -218,7 +219,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = 3, Username = "Second", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, })); AddStep("kick second user", () => this.ChildrenOfType().Single(d => d.IsPresent).TriggerClick()); @@ -246,7 +247,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new UserStatistics { GlobalRank = RNG.Next(1, 100000), } } }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }); MultiplayerClient.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); @@ -293,7 +294,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new UserStatistics { GlobalRank = RNG.Next(1, 100000), } } }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }); MultiplayerClient.ChangeUserMods(0, new Mod[] @@ -330,7 +331,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new UserStatistics { GlobalRank = RNG.Next(1, 100000), } } }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }); MultiplayerClient.ChangeUserStyle(0, 259, 2); @@ -366,7 +367,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new UserStatistics { GlobalRank = RNG.Next(1, 100000), } } }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }); MultiplayerClient.ChangeUserMods(0, new Mod[] { @@ -415,7 +416,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new UserStatistics { GlobalRank = RNG.Next(1, 100000), } } }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }); MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()); diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs index fb54e936bc..13b7e6e18c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Online { @@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online Username = @"peppy", Id = 2, Colour = "99EB47", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, IsSupporter = supportLevel > 0, SupportLevel = supportLevel } diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 52905fe5da..805ac44829 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; +using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Users; @@ -237,7 +238,7 @@ namespace osu.Game.Tests.Visual.Online WasRecentlyOnline = true, Statistics = new UserStatistics { GlobalRank = 1111 }, CountryCode = CountryCode.JP, - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + CoverUrl = TestResources.COVER_IMAGE_4 }, new APIUser { @@ -246,7 +247,7 @@ namespace osu.Game.Tests.Visual.Online WasRecentlyOnline = false, Statistics = new UserStatistics { GlobalRank = 2222 }, CountryCode = CountryCode.AU, - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, IsSupporter = true, SupportLevel = 3, }, diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 29272f7336..3333eae567 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Tests.Resources; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; @@ -30,9 +31,9 @@ namespace osu.Game.Tests.Visual.Online Spacing = new Vector2(10f), Children = new[] { - generateUser(@"peppy", 2, CountryCode.AU, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false, "99EB47"), - generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", true), - generateUser(@"joshika39", 17032217, CountryCode.RS, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false), + generateUser(@"peppy", 2, CountryCode.AU, TestResources.COVER_IMAGE_3, false, "99EB47"), + generateUser(@"flyte", 3103765, CountryCode.JP, TestResources.COVER_IMAGE_4, true), + generateUser(@"joshika39", 17032217, CountryCode.RS, TestResources.COVER_IMAGE_3, false), new UpdateableAvatar(), new UpdateableAvatar() }, diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 193b356d71..d3be8d3b98 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -18,6 +18,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Profile; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Resources; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -136,7 +137,7 @@ namespace osu.Game.Tests.Visual.Online { Id = 727, Username = "SomeoneIndecisive", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + CoverUrl = TestResources.COVER_IMAGE_1, Groups = new[] { new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" }, @@ -162,7 +163,7 @@ namespace osu.Game.Tests.Visual.Online { Id = 728, Username = "Certain Guy", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + CoverUrl = TestResources.COVER_IMAGE_2, Statistics = new UserStatistics { IsRanked = false, diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 2972f69cba..1c2fdc7860 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -13,6 +13,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets.Taiko; +using osu.Game.Tests.Resources; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -152,7 +153,7 @@ namespace osu.Game.Tests.Visual.Online Username = $"Colorful #{hue}", Id = 1, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + CoverUrl = TestResources.COVER_IMAGE_2, ProfileHue = hue, PlayMode = "osu", }); @@ -196,7 +197,7 @@ namespace osu.Game.Tests.Visual.Online Username = $"Colorful #{hue}", Id = 1, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + CoverUrl = TestResources.COVER_IMAGE_2, ProfileHue = hue, PlayMode = "osu", })); @@ -212,7 +213,7 @@ namespace osu.Game.Tests.Visual.Online Username = $"Colorful #{hue2}", Id = 2, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + CoverUrl = TestResources.COVER_IMAGE_2, ProfileHue = hue2, PlayMode = "osu", })); @@ -225,7 +226,7 @@ namespace osu.Game.Tests.Visual.Online Username = $"Colorful #{hue2}", Id = 2, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + CoverUrl = TestResources.COVER_IMAGE_2, ProfileHue = hue2, PlayMode = "osu", })); @@ -236,7 +237,7 @@ namespace osu.Game.Tests.Visual.Online Username = @"Somebody", Id = 1, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + CoverUrl = TestResources.COVER_IMAGE_1, JoinDate = DateTimeOffset.Now.AddDays(-1), LastVisit = DateTimeOffset.Now, Groups = new[] diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 6b73f1a5f4..61269a7bf4 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -416,7 +416,7 @@ namespace osu.Game.Tests.Visual.Playlists { Id = 2 + i, Username = $"peppy{i}", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, }); @@ -432,7 +432,7 @@ namespace osu.Game.Tests.Visual.Playlists { Id = 2 + i, Username = $"peppy{i}", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, }); @@ -497,7 +497,7 @@ namespace osu.Game.Tests.Visual.Playlists { Id = 2 + i, Username = $"peppy{i}", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = TestResources.COVER_IMAGE_3, }, }); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index c2f1eb6b15..59bc17d75b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Id = 6602580, Username = @"waaiiru", CountryCode = CountryCode.ES, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + CoverUrl = TestResources.COVER_IMAGE_1, }, Date = DateTimeOffset.Now.AddYears(-2), }; @@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Id = 6602580, Username = @"waaiiru", CountryCode = CountryCode.ES, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + CoverUrl = TestResources.COVER_IMAGE_1, }, Date = DateTimeOffset.Now.AddYears(-2), }, @@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Id = 1541390, Username = @"Toukai", CountryCode = CountryCode.CA, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + CoverUrl = TestResources.COVER_IMAGE_2, }, Date = DateTimeOffset.Now.AddMonths(-6), }, From 18060d30afbad9725e7cafe5c783c541cec6364d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Apr 2025 21:35:54 +0900 Subject: [PATCH 044/152] Fix user covers not loading if one corner is off-screen --- osu.Game/Users/UserCoverBackground.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/UserCoverBackground.cs b/osu.Game/Users/UserCoverBackground.cs index de6a306b2a..4d248d450b 100644 --- a/osu.Game/Users/UserCoverBackground.cs +++ b/osu.Game/Users/UserCoverBackground.cs @@ -33,7 +33,10 @@ namespace osu.Game.Users protected virtual double UnloadDelay => 5000; protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) - => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay); + => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay) + { + RelativeSizeAxes = Axes.Both, + }; [LongRunningLoad] private partial class Cover : CompositeDrawable From 395510c2c65f5fe798ab0c124963a093529c298a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 27 Apr 2025 02:55:33 +0300 Subject: [PATCH 045/152] Add test coverage --- .../TestSceneBeatmapTitleWedge.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index 8b89de5fce..ea90828f45 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -10,6 +11,9 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Drawables; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -26,6 +30,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private BeatmapTitleWedge titleWedge = null!; private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType().Single(); + private APIBeatmapSet? currentOnlineSet; + [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { @@ -36,6 +42,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { base.LoadComplete(); + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest set: + if (set.ID == currentOnlineSet?.OnlineID) + { + set.TriggerSuccess(currentOnlineSet); + return true; + } + + return false; + + default: + return false; + } + }; + AddRange(new Drawable[] { new Container @@ -115,6 +139,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("check visibility", () => titleWedge.Alpha > 0); } + [Test] + public void TestOnlineAvailability() + { + AddStep("online beatmapset", () => + { + var (working, onlineSet) = createTestBeatmap(); + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddAssert("play count = 10000", () => this.ChildrenOfType().ElementAt(0).Text.ToString() == "10,000"); + AddAssert("favourites count = 2345", () => this.ChildrenOfType().ElementAt(1).Text.ToString() == "2,345"); + AddStep("online beatmapset with local diff", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Beatmaps = Array.Empty(); + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddAssert("play count = -", () => this.ChildrenOfType().ElementAt(0).Text.ToString() == "-"); + AddAssert("favourites count = 2345", () => this.ChildrenOfType().ElementAt(1).Text.ToString() == "2,345"); + AddStep("local beatmapset", () => + { + var (working, _) = createTestBeatmap(); + + currentOnlineSet = null; + Beatmap.Value = working; + }); + AddAssert("play count = -", () => this.ChildrenOfType().ElementAt(0).Text.ToString() == "-"); + AddAssert("favourites count = -", () => this.ChildrenOfType().ElementAt(1).Text.ToString() == "-"); + } + [TestCase(120, 125, null, "120-125 (mostly 120)")] [TestCase(120, 120.6, null, "120-121 (mostly 120)")] [TestCase(120, 120.4, null, "120")] @@ -155,5 +213,29 @@ namespace osu.Game.Tests.Visual.SongSelectV2 return label.Text == target; }); } + + private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap() + { + var working = CreateWorkingBeatmap(Ruleset.Value); + var onlineSet = new APIBeatmapSet + { + OnlineID = working.BeatmapSetInfo.OnlineID, + FavouriteCount = 2345, + Beatmaps = new[] + { + new APIBeatmap + { + OnlineID = working.BeatmapInfo.OnlineID, + PlayCount = 10000, + PassCount = 4567, + UserPlayCount = 123, + }, + } + }; + + working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now; + working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now; + return (working, onlineSet); + } } } From 5ea1654f1d58819e2ada7c26ab281e32dc685aef Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 27 Apr 2025 02:55:30 +0300 Subject: [PATCH 046/152] Fix playcount statistic hiding on local diff instead of showing `-` --- osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index d892fcb485..26294140a8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -308,18 +308,7 @@ namespace osu.Game.Screens.SelectV2 var onlineBeatmapSet = currentOnlineBeatmapSet; var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID); - if (onlineBeatmap != null) - { - playCount.FadeIn(300, Easing.OutQuint); - playCount.Value = new StatisticPlayCount.Data(onlineBeatmap.PlayCount, onlineBeatmap.UserPlayCount); - } - else - { - playCount.FadeOut(300, Easing.OutQuint); - playCount.Value = null; - } - - favouritesStatistic.FadeIn(300, Easing.OutQuint); + playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1); favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0"); } } From d907719aa8ad676b14e17134ba86f95651ece89f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Apr 2025 16:14:12 +0900 Subject: [PATCH 047/152] Add tests with mods with adjusted settings --- .../Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index 59bc17d75b..90a9310aeb 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -276,9 +276,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 scores[2].TotalScore = RNG.Next(120_000, 400_000); scores[2].MaximumStatistics[HitResult.Great] = 3000; - scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight() }; + scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 2 } }, new OsuModHardRock(), new OsuModFlashlight() }; scores[2].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic() }; - scores[3].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic(), new OsuModDifficultyAdjust() }; + scores[3].Mods = new Mod[] + { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight { ComboBasedSize = { Value = false } }, new OsuModClassic(), new OsuModDifficultyAdjust() }; scores[4].Mods = new ManiaRuleset().CreateAllMods().ToArray(); return scores; From da827f0cd61806f8a33760c8c247e0e8139f7cc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Apr 2025 16:37:55 +0900 Subject: [PATCH 048/152] Adjust mod icon test scene to show overlapping versions too --- .../Visual/UserInterface/TestSceneModIcon.cs | 106 +++++++++++++----- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs index 11cd122c99..b6d4836316 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs @@ -12,22 +12,66 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneModIcon : OsuTestScene { + private FillFlowContainer spreadOutFlow = null!; + private ModDisplay modDisplay = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create flows", () => + { + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.5f), + new Dimension(GridSizeMode.Relative, 0.5f), + }, + Content = new[] + { + new Drawable[] + { + modDisplay = new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }, + new Drawable[] + { + spreadOutFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + } + } + } + }; + }); + } + + private void addRange(IEnumerable mods) + { + spreadOutFlow.AddRange(mods.Select(m => new ModIcon(m))); + modDisplay.Current.Value = modDisplay.Current.Value.Concat(mods.OfType()).ToList(); + } + [Test] public void TestShowAllMods() { AddStep("create mod icons", () => { - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Full, - ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)), - }; + addRange(Ruleset.Value.CreateInstance().CreateAllMods()); }); AddStep("toggle selected", () => @@ -42,26 +86,22 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("create mod icons", () => { - Child = new FillFlowContainer + var rateAdjustMods = Ruleset.Value.CreateInstance().CreateAllMods() + .OfType(); + + addRange(rateAdjustMods.SelectMany(m => { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Full, - ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods() - .OfType() - .SelectMany(m => - { - List icons = new List { new ModIcon(m) }; + List mods = new List { m }; - for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10) - { - m = (ModRateAdjust)m.DeepClone(); - m.SpeedChange.Value = i; - icons.Add(new ModIcon(m)); - } + for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10) + { + m = (ModRateAdjust)m.DeepClone(); + m.SpeedChange.Value = i; + mods.Add(m); + } - return icons; - }), - }; + return mods; + })); }); AddStep("adjust rates", () => @@ -81,21 +121,25 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestChangeModType() { - ModIcon icon = null!; - - AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime())); - AddStep("change mod", () => icon.Mod = new OsuModEasy()); + AddStep("create mod icon", () => addRange([new OsuModDoubleTime()])); + AddStep("change mod", () => + { + foreach (var modIcon in this.ChildrenOfType()) + modIcon.Mod = new OsuModEasy(); + }); } [Test] public void TestInterfaceModType() { - ModIcon icon = null!; - var ruleset = new OsuRuleset(); - AddStep("create mod icon", () => Child = icon = new ModIcon(ruleset.AllMods.First(m => m.Acronym == "DT"))); - AddStep("change mod", () => icon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ")); + AddStep("create mod icon", () => addRange([ruleset.AllMods.First(m => m.Acronym == "DT")])); + AddStep("change mod", () => + { + foreach (var modIcon in this.ChildrenOfType()) + modIcon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ"); + }); } } } From c16514274d7f3bcfb02d79793280291613539e70 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Apr 2025 16:58:16 +0900 Subject: [PATCH 049/152] Show singular difficulty adjust modifications inline in mod icons --- .../Mods/CatchModDifficultyAdjust.cs | 30 +++++++++++++++++++ .../Mods/OsuModDifficultyAdjust.cs | 30 +++++++++++++++++++ .../Mods/TaikoModDifficultyAdjust.cs | 28 +++++++++++++++++ .../Visual/UserInterface/TestSceneModIcon.cs | 25 ++++++++++++++++ .../Extensions/NumberFormattingExtensions.cs | 2 +- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 28 +++++++++++++++++ 6 files changed, 142 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 1312f45cdc..856989a685 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Mods; @@ -36,6 +37,35 @@ namespace osu.Game.Rulesets.Catch.Mods [SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")] public BindableBool HardRockOffsets { get; } = new BindableBool(); + public override int AdjustedSettingsCount + { + get + { + int count = base.AdjustedSettingsCount; + if (!ApproachRate.IsDefault) count++; + if (!CircleSize.IsDefault) count++; + return count; + } + } + + public override string ExtendedIconInformation + { + get + { + if (AdjustedSettingsCount != 1) + return string.Empty; + + if (!CircleSize.IsDefault) return format("CS", CircleSize); + if (!ApproachRate.IsDefault) return format("AR", ApproachRate); + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); + if (!DrainRate.IsDefault) return format("HP", DrainRate); + + return string.Empty; + + string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + } + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 77e9aeb123..357a971c0f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -7,6 +7,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -36,6 +37,35 @@ namespace osu.Game.Rulesets.Osu.Mods ReadCurrentFromDifficulty = diff => diff.ApproachRate, }; + public override int AdjustedSettingsCount + { + get + { + int count = base.AdjustedSettingsCount; + if (!ApproachRate.IsDefault) count++; + if (!CircleSize.IsDefault) count++; + return count; + } + } + + public override string ExtendedIconInformation + { + get + { + if (AdjustedSettingsCount != 1) + return string.Empty; + + if (!CircleSize.IsDefault) return format("CS", CircleSize); + if (!ApproachRate.IsDefault) return format("AR", ApproachRate); + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); + if (!DrainRate.IsDefault) return format("HP", DrainRate); + + return string.Empty; + + string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + } + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 000736e9f7..628592fe51 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Taiko.Mods @@ -20,6 +21,33 @@ namespace osu.Game.Rulesets.Taiko.Mods ReadCurrentFromDifficulty = _ => 1, }; + public override int AdjustedSettingsCount + { + get + { + int count = base.AdjustedSettingsCount; + if (!ScrollSpeed.IsDefault) count++; + return count; + } + } + + public override string ExtendedIconInformation + { + get + { + if (AdjustedSettingsCount != 1) + return string.Empty; + + if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed); + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); + if (!DrainRate.IsDefault) return format("HP", DrainRate); + + return string.Empty; + + string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + } + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs index b6d4836316..c47a6fd610 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs @@ -141,5 +141,30 @@ namespace osu.Game.Tests.Visual.UserInterface modIcon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ"); }); } + + [Test] + public void TestDifficultyAdjust() + { + AddStep("create icons", () => + { + addRange([ + new OsuModDifficultyAdjust + { + CircleSize = { Value = 8 } + }, + new OsuModDifficultyAdjust + { + CircleSize = { Value = 5.5f } + }, + new OsuModDifficultyAdjust + { + CircleSize = { Value = 8 }, + ApproachRate = { Value = 8 }, + OverallDifficulty = { Value = 8 }, + DrainRate = { Value = 8 }, + } + ]); + }); + } } } diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs index 618b086a5b..33252448fc 100644 --- a/osu.Game/Extensions/NumberFormattingExtensions.cs +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -17,7 +17,7 @@ namespace osu.Game.Extensions /// The maximum number of decimals to be considered in the original value. /// Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%. /// The formatted output. - public static string ToStandardFormattedString(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber, IMinMaxValue + public static string ToStandardFormattedString(this T value, int maxDecimalDigits, bool asPercentage = false) where T : struct, INumber, IMinMaxValue { double floatValue = double.CreateTruncating(value); diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 79fc918487..857527062f 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; namespace osu.Game.Rulesets.Mods { @@ -67,6 +68,33 @@ namespace osu.Game.Rulesets.Mods } } + public virtual int AdjustedSettingsCount + { + get + { + int count = 0; + if (!DrainRate.IsDefault) count++; + if (!OverallDifficulty.IsDefault) count++; + return count; + } + } + + public override string ExtendedIconInformation + { + get + { + if (AdjustedSettingsCount != 1) + return string.Empty; + + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); + if (!DrainRate.IsDefault) return format("HP", DrainRate); + + return string.Empty; + + string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + } + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get From a0d32c66055ff0a206cc0e4d2b400e185454c06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Apr 2025 10:35:09 +0200 Subject: [PATCH 050/152] Add raw prefixes in link handling code --- osu.Game/Online/Chat/MessageFormatter.cs | 44 ++++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index f354eea027..8900ce6710 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -129,8 +129,8 @@ namespace osu.Game.Online.Chat switch (args[0]) { - case "http": - case "https": + case @"http": + case @"https": // length > 3 since all these links need another argument to work if (args.Length > 3 && args[1].EndsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase)) { @@ -139,8 +139,8 @@ namespace osu.Game.Online.Chat switch (args[2]) { // old site only - case "b": - case "beatmaps": + case @"b": + case @"beatmaps": { string trimmed = mainArg.Split('?').First(); if (int.TryParse(trimmed, out int id)) @@ -149,11 +149,11 @@ namespace osu.Game.Online.Chat break; } - case "s": - case "beatmapsets": - case "d": + case @"s": + case @"beatmapsets": + case @"d": { - if (mainArg == "discussions") + if (mainArg == @"discussions") // handle discussion links externally for now return new LinkDetails(LinkAction.External, url); @@ -169,15 +169,15 @@ namespace osu.Game.Online.Chat break; } - case "u": - case "users": + case @"u": + case @"users": return getUserLink(mainArg); - case "wiki": + case @"wiki": return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3))); - case "home": - if (mainArg != "changelog") + case @"home": + if (mainArg != @"changelog") // handle link other than changelog as external for now return new LinkDetails(LinkAction.External, url); @@ -198,7 +198,7 @@ namespace osu.Game.Online.Chat break; - case "osu": + case @"osu": // every internal link also needs some kind of argument if (args.Length < 3) break; @@ -207,28 +207,28 @@ namespace osu.Game.Online.Chat switch (args[1]) { - case "chan": + case @"chan": linkType = LinkAction.OpenChannel; break; - case "edit": + case @"edit": linkType = LinkAction.OpenEditorTimestamp; break; - case "b": + case @"b": linkType = LinkAction.OpenBeatmap; break; - case "s": - case "dl": + case @"s": + case @"dl": linkType = LinkAction.OpenBeatmapSet; break; - case "spectate": + case @"spectate": linkType = LinkAction.Spectate; break; - case "u": + case @"u": return getUserLink(args[2]); default: @@ -237,7 +237,7 @@ namespace osu.Game.Online.Chat return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2])); - case "osump": + case @"osump": return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]); } From 1e8d9b3482e585207d2d5806bdf12f7f2f5892c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Apr 2025 17:24:24 +0900 Subject: [PATCH 051/152] Show marker when settings are adjusted --- .../Visual/UserInterface/TestSceneModIcon.cs | 17 ++++++++- osu.Game/Rulesets/Mods/IMod.cs | 26 ++++++++++++++ osu.Game/Rulesets/UI/ModIcon.cs | 35 ++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs index c47a6fd610..c8283d0956 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs @@ -71,7 +71,22 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("create mod icons", () => { - addRange(Ruleset.Value.CreateInstance().CreateAllMods()); + addRange(Ruleset.Value.CreateInstance().CreateAllMods().Select(m => + { + if (m is OsuModFlashlight fl) + fl.FollowDelay.Value = 1245; + + if (m is OsuModDaycore dc) + dc.SpeedChange.Value = 0.74f; + + if (m is OsuModDifficultyAdjust da) + da.CircleSize.Value = 8.2f; + + if (m is ModAdaptiveSpeed ad) + ad.AdjustPitch.Value = false; + + return m; + })); }); AddStep("toggle selected", () => diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index 5d4cc5fd12..d4c51b1dfb 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods { @@ -81,5 +83,29 @@ namespace osu.Game.Rulesets.Mods /// Create a fresh instance based on this mod. /// Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType())!; + + /// + /// Whether any user adjustable setting attached to this mod has a non-default value. + /// + bool HasNonDefaultSettings + { + get + { + bool hasAdjustments = false; + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var bindable = (IBindable)property.GetValue(this)!; + + if (!bindable.IsDefault) + { + hasAdjustments = true; + break; + } + } + + return hasAdjustments; + } + } } } diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index ee0103a8e5..d42e185784 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; @@ -81,6 +82,8 @@ namespace osu.Game.Rulesets.UI private Container extendedContent = null!; + private Drawable adjustmentMarker = null!; + private ModSettingChangeTracker? modSettingsChangeTracker; /// @@ -139,7 +142,7 @@ namespace osu.Game.Rulesets.UI Origin = Anchor.CentreLeft, Name = "main content", Size = MOD_ICON_SIZE, - Children = new Drawable[] + Children = new[] { background = new Sprite { @@ -165,6 +168,31 @@ namespace osu.Game.Rulesets.UI Size = new Vector2(45), Icon = FontAwesome.Solid.Question }, + adjustmentMarker = new Container + { + Size = new Vector2(20), + Origin = Anchor.Centre, + Position = new Vector2(64, 14), + Children = new Drawable[] + { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.YellowLight, + RelativeSizeAxes = Axes.Both, + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Cog, + Colour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.7f), + } + } + }, } }, }; @@ -207,6 +235,11 @@ namespace osu.Game.Rulesets.UI backgroundColour = colours.ForModType(value.Type); updateColour(); + if (mod.HasNonDefaultSettings) + adjustmentMarker.Show(); + else + adjustmentMarker.Hide(); + updateExtendedInformation(); } From 09602097bd071cbeed69f3f920a843290d2ffcad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Apr 2025 11:40:00 +0200 Subject: [PATCH 052/152] Add support for opening multiplayer / playlist room links directly As in, `https://osu.ppy.sh/multiplayer/rooms/{id}` links, clicked from the chat overlay, now directly open in the client. Additionally, `osu://room/{id}` can be used in the same way to open a room from a third-party application or a browser. --- .../Visual/Online/TestSceneChatLink.cs | 6 +-- osu.Game/Online/Chat/MessageFormatter.cs | 22 ++++++-- osu.Game/OsuGame.cs | 53 ++++++++++++++++++- 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index c793535255..07657c53e5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -75,9 +75,9 @@ namespace osu.Game.Tests.Visual.Online [TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)] [TestCase("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", LinkAction.External, LinkAction.OpenWiki)] [TestCase("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).")] // note that there's 0 links here (they get removed if a channel is not found) - [TestCase("Join my multiplayer game osump://12346.", LinkAction.JoinMultiplayerMatch)] - [TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinMultiplayerMatch)] - [TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinMultiplayerMatch)] + [TestCase("Join my multiplayer game osump://12346.", LinkAction.JoinRoom)] + [TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinRoom)] + [TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinRoom)] [TestCase($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", LinkAction.OpenChannel)] [TestCase($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)] [TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)] diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 8900ce6710..9478f13074 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -192,6 +192,19 @@ namespace osu.Game.Online.Chat return new LinkDetails(LinkAction.OpenChangelog, $"{args[4]}/{args[5]}"); } + break; + + case @"multiplayer": + if (mainArg != @"rooms") + return new LinkDetails(LinkAction.External, url); + + if (args.Length == 5) + { + // https://osu.ppy.sh/multiplayer/rooms/{id} + // route used for both multiplayer and playlists + return new LinkDetails(LinkAction.JoinRoom, args[4]); + } + break; } } @@ -231,14 +244,15 @@ namespace osu.Game.Online.Chat case @"u": return getUserLink(args[2]); + case @"room": + linkType = LinkAction.JoinRoom; + break; + default: return new LinkDetails(LinkAction.External, url); } return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2])); - - case @"osump": - return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]); } return new LinkDetails(LinkAction.External, url); @@ -337,7 +351,7 @@ namespace osu.Game.Online.Chat OpenBeatmapSet, OpenChannel, OpenEditorTimestamp, - JoinMultiplayerMatch, + JoinRoom, Spectate, OpenUserProfile, SearchBeatmapSet, diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 962718b564..9d3af413dd 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -67,6 +67,7 @@ using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; @@ -79,6 +80,7 @@ using osu.Game.Utils; using osuTK; using osuTK.Graphics; using Sentry; +using MatchType = osu.Game.Online.Rooms.MatchType; namespace osu.Game { @@ -491,7 +493,6 @@ namespace osu.Game HandleTimestamp(argString); break; - case LinkAction.JoinMultiplayerMatch: case LinkAction.Spectate: waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification { @@ -523,6 +524,11 @@ namespace osu.Game break; + case LinkAction.JoinRoom: + if (long.TryParse(argString, out long roomId)) + JoinRoom(roomId); + break; + default: throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action."); } @@ -598,6 +604,28 @@ namespace osu.Game /// The build version of the update stream public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version)); + /// + /// Joins a multiplayer or playlists room with the given . + /// + public void JoinRoom(long id) + { + var request = new GetRoomRequest(id); + request.Success += room => + { + switch (room.Type) + { + case MatchType.Playlists: + PresentPlaylist(room); + break; + + default: + PresentMultiplayerMatch(room, string.Empty); + break; + } + }; + API.Queue(request); + } + /// /// Seeks to the provided if the editor is currently open. /// Can also select objects as indicated by the (depends on ruleset implementation). @@ -725,6 +753,12 @@ namespace osu.Game /// The password to join the room, if any is given. public void PresentMultiplayerMatch(Room room, string password) { + if (room.HasEnded) + { + Notifications.Post(new SimpleNotification { Text = "This multiplayer room has ended." }); + return; + } + PerformFromScreen(screen => { if (!(screen is Multiplayer multiplayer)) @@ -736,6 +770,23 @@ namespace osu.Game // but `PerformFromScreen` doesn't understand nested stacks. } + /// + /// Join a playlist immediately. + /// + /// The playlist to join. + public void PresentPlaylist(Room room) + { + PerformFromScreen(screen => + { + if (!(screen is Playlists playlists)) + screen.Push(playlists = new Playlists()); + + playlists.Join(room); + }); + // TODO: We should really be able to use `validScreens: new[] { typeof(Playlists) }` here + // but `PerformFromScreen` doesn't understand nested stacks. + } + /// /// Present a score's replay immediately. /// The user should have already requested this interactively. From 38af2d6ad7d241a02f4f5ce1de587c9b126dd2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Apr 2025 13:56:45 +0200 Subject: [PATCH 053/152] Fix and expand test coverage --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 07657c53e5..e7337769fd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -75,9 +75,11 @@ namespace osu.Game.Tests.Visual.Online [TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)] [TestCase("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", LinkAction.External, LinkAction.OpenWiki)] [TestCase("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).")] // note that there's 0 links here (they get removed if a channel is not found) - [TestCase("Join my multiplayer game osump://12346.", LinkAction.JoinRoom)] - [TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinRoom)] - [TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinRoom)] + [TestCase("Join my multiplayer game osu://room/12346.", LinkAction.JoinRoom)] + [TestCase("Join my multiplayer gameosu://room/12346.", LinkAction.JoinRoom)] + [TestCase("Join my [multiplayer game](osu://room/12346).", LinkAction.JoinRoom)] + [TestCase("Join my multiplayer game http://dev.ppy.sh/multiplayer/rooms/12346", LinkAction.JoinRoom)] + [TestCase("Join my [multiplayer game](http://dev.ppy.sh/multiplayer/rooms/12346).", LinkAction.JoinRoom)] [TestCase($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", LinkAction.OpenChannel)] [TestCase($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)] [TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)] From c1bc3d7ff43efbcec5e4a24a289f79a1d4bd982c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 03:33:29 +0300 Subject: [PATCH 054/152] Fix overlay buttons in screen footer not correctly aligned with back button --- osu.Game/Screens/Footer/ScreenFooter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 94f4ceeb1a..ea9cc443ce 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Footer footerContentContainer = new Container { RelativeSizeAxes = Axes.Both, - Y = -15f, + Y = -OsuGame.SCREEN_EDGE_MARGIN, }, }, } From 39bb3105ec9782f9453a8f870b77d3dc9222f02f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 03:39:19 +0300 Subject: [PATCH 055/152] Cull out magic numbers in specs --- osu.Game/Screens/Footer/ScreenFooter.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index ea9cc443ce..af2496f97a 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -65,6 +65,8 @@ namespace osu.Game.Screens.Footer [BackgroundDependencyLoader] private void load() { + const float footer_button_y_offset = 10; + InternalChildren = new Drawable[] { background = new Box @@ -75,7 +77,7 @@ namespace osu.Game.Screens.Footer new GridContainer { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, + Padding = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), @@ -89,7 +91,7 @@ namespace osu.Game.Screens.Footer { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Y = 10f, + Y = footer_button_y_offset, Direction = FillDirection.Horizontal, Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both, @@ -112,7 +114,7 @@ namespace osu.Game.Screens.Footer hiddenButtonsContainer = new Container { Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, - Y = 10f, + Y = footer_button_y_offset, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, From 72987aa166fe0ffa4908032bb1d0138eda696eea Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 03:44:42 +0300 Subject: [PATCH 056/152] Fix sheared button alignment in preset popovers --- osu.Game/Overlays/Mods/AddPresetPopover.cs | 24 ++++++++++++++----- osu.Game/Overlays/Mods/EditPresetPopover.cs | 26 ++++++++++----------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index 40a1e4f7e9..817a61f7ac 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -40,11 +40,13 @@ namespace osu.Game.Overlays.Mods public AddPresetPopover(AddPresetButton addPresetButton) { + const float content_width = 300; + button = addPresetButton; Child = new FillFlowContainer { - Width = 300, + Width = content_width, AutoSizeAxes = Axes.Y, Spacing = new Vector2(7), Children = new Drawable[] @@ -63,14 +65,24 @@ namespace osu.Game.Overlays.Mods Label = CommonStrings.Description, TabbableContentContainer = this }, - createButton = new ShearedButton(0) + new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Width = 1, - Text = ModSelectOverlayStrings.AddPreset, - Action = createPreset + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(7), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + createButton = new ShearedButton(content_width) + { + // todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = ModSelectOverlayStrings.AddPreset, + Action = createPreset + } + } } } }; diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 8295bdbab8..eb128c7792 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -52,9 +52,11 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { + const float content_width = 300; + Child = new FillFlowContainer { - Width = 300, + Width = content_width, AutoSizeAxes = Axes.Y, Spacing = new Vector2(7), Direction = FillDirection.Vertical, @@ -107,29 +109,27 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, Spacing = new Vector2(7), + Direction = FillDirection.Vertical, Children = new Drawable[] { - useCurrentModsButton = new ShearedButton(0) + useCurrentModsButton = new ShearedButton(content_width) { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Width = 1, + // todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, Text = ModSelectOverlayStrings.UseCurrentMods, DarkerColour = colours.Blue1, LighterColour = colours.Blue0, TextColour = colourProvider.Background6, Action = useCurrentMods, }, - saveButton = new ShearedButton(0) + saveButton = new ShearedButton(content_width) { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Width = 1, + // todo: for some very odd reason, this needs to be anchored to topright for the fill flow to be correctly sized to the AABB of the sheared button + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, Text = Resources.Localisation.Web.CommonStrings.ButtonsSave, DarkerColour = colours.Orange1, LighterColour = colours.Orange0, From b4cf9746625c5d52cb0adaad86e2b048d147ef58 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 04:16:28 +0300 Subject: [PATCH 057/152] Fix sheared button getting cut when scaled beyond 100% Keep masking back in `Content`, since the scaling animation is happening on `Content` instead of `this`. This doesn't regress the intended behaviour in this PR (which is to just to make the button class itself sheared instead of its content). --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index cc57e9c75f..16891babf3 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -89,11 +89,11 @@ namespace osu.Game.Graphics.UserInterface { Height = height; - CornerRadius = CORNER_RADIUS; Shear = OsuGame.SHEAR; - Masking = true; Content.Anchor = Content.Origin = Anchor.Centre; + Content.CornerRadius = CORNER_RADIUS; + Content.Masking = true; Children = new Drawable[] { From d1c4c65e6d130232da70ee55731296f9838dcd8b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 03:59:03 +0300 Subject: [PATCH 058/152] Fix weird alignment code in wizard overlay footer content --- osu.Game/Overlays/WizardOverlay.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 5ed9870aae..3cc403dbff 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -243,12 +243,10 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding { Horizontal = 20 }; + Padding = new MarginPadding { Right = OsuGame.SCREEN_EDGE_MARGIN }; InternalChild = NextButton = new ShearedButton(0) { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, Width = 1, Text = FirstRunSetupOverlayStrings.GetStarted, From dd86620ae37af914eb678b6a603b3fdbbdeb66a4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 06:17:27 +0300 Subject: [PATCH 059/152] Add hover click sounds to tag overflow button --- osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs index 56b83a2578..185b1ac451 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs @@ -17,6 +17,7 @@ using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osuTK; @@ -163,7 +164,8 @@ namespace osu.Game.Screens.SelectV2 Text = "...", Colour = colourProvider.Background4, Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), - } + }, + new HoverClickSounds(HoverSampleSet.Button), }; } From 71620bfe267273ac37bb39eed5ca6629341cfc32 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 06:03:28 +0300 Subject: [PATCH 060/152] Bring back full mod icons --- .../SelectV2/BeatmapLeaderboardScore.cs | 121 ++---------------- .../BeatmapLeaderboardScore_Tooltip.cs | 7 +- 2 files changed, 13 insertions(+), 115 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index c573239623..699a5216eb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -25,7 +24,6 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -106,7 +104,7 @@ namespace osu.Game.Screens.SelectV2 private Container rightContent = null!; - private FillFlowContainer modsContainer = null!; + private FillFlowContainer modsContainer = null!; private Box totalScoreBackground = null!; @@ -422,6 +420,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.CentreLeft, Direction = FillDirection.Vertical, Padding = new MarginPadding { Horizontal = corner_radius }, + Spacing = new Vector2(0f, -2f), Children = new Drawable[] { new OsuSpriteText @@ -429,22 +428,22 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Current = scoreManager.GetBindableTotalScoreString(score), Spacing = new Vector2(-1.5f), Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, }, new InputBlockingContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, - Child = modsContainer = new FillFlowContainer + Child = modsContainer = new FillFlowContainer { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(2f, 0f), + Spacing = new Vector2(-10, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, }, }, } @@ -488,24 +487,15 @@ namespace osu.Game.Screens.SelectV2 private void updateModDisplay() { - int maxMods = scoringMode.Value == ScoringMode.Standardised ? 4 : 5; - if (score.Mods.Length > 0) { modsContainer.Padding = new MarginPadding { Top = 4f }; - modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Take(Math.Min(maxMods, score.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) + modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { - Scale = new Vector2(0.3125f) + Scale = new Vector2(0.3f), + // trim mod icon height down to its true height for alignment purposes. + Height = ModIcon.MOD_ICON_SIZE.Y * 3 / 4f, }); - - if (score.Mods.Length > maxMods) - { - modsContainer.Remove(modsContainer[^1], true); - modsContainer.Add(new MoreModSwitchTiny(score.Mods) - { - Scale = new Vector2(0.3125f), - }); - } } } @@ -716,96 +706,5 @@ namespace osu.Game.Screens.SelectV2 public LocalisableString TooltipText { get; } } - - private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasCustomTooltip - { - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - public ColouredModSwitchTiny(Mod mod) - : base(mod) - { - Active.Value = true; - } - - public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); - - Mod IHasCustomTooltip.TooltipContent => (Mod)Mod; - } - - private sealed partial class MoreModSwitchTiny : CompositeDrawable, IHasPopover - { - private readonly IReadOnlyList mods; - - public MoreModSwitchTiny(IReadOnlyList mods) - { - this.mods = mods; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Size = new Vector2(ModSwitchTiny.WIDTH, ModSwitchTiny.DEFAULT_HEIGHT); - - InternalChild = new CircularContainer - { - Masking = true, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Bold), - Text = ". . .", - Colour = Color4.White, - UseFullGlyphHeight = false, - Margin = new MarginPadding - { - Top = 4 - } - } - } - }; - } - - protected override bool OnClick(ClickEvent e) - { - this.ShowPopover(); - return true; - } - - protected override bool OnHover(HoverEvent e) => true; - - public Popover GetPopover() => new MoreModsPopover(mods); - - public partial class MoreModsPopover : OsuPopover - { - public MoreModsPopover(IReadOnlyList mods) - { - AutoSizeAxes = Axes.Both; - AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; - - Child = new FillFlowContainer - { - Width = 125f, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, - Spacing = new Vector2(2.5f), - ChildrenEnumerable = mods.AsOrdered().Select(m => new ColouredModSwitchTiny(m) - { - Scale = new Vector2(0.3125f), - }) - }; - } - } - } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index 7f1997522e..5813864a82 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -257,12 +257,11 @@ namespace osu.Game.Screens.SelectV2 { Show(); - modsFlow.ChildrenEnumerable = mods.AsOrdered().Select(m => new ModSwitchTiny(m) + modsFlow.ChildrenEnumerable = mods.AsOrdered().Select(m => new ModIcon(m) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = new Vector2(0.3125f), - Active = { Value = true }, + Scale = new Vector2(0.3f), }); } } @@ -301,7 +300,7 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Bottom = 6f, Top = 6f + spacing }, Padding = new MarginPadding { Horizontal = 16f }, - Spacing = new Vector2(2.5f), + Spacing = new Vector2(2f, -4f), }, }; } From ca11f3348d53df325cde99c29ae6c1d8dc009a5c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 06:03:59 +0300 Subject: [PATCH 061/152] Add DA mod with custom adjustment in new score test scene --- .../Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index 90a9310aeb..1b6d56df16 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -279,7 +279,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 2 } }, new OsuModHardRock(), new OsuModFlashlight() }; scores[2].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic() }; scores[3].Mods = new Mod[] - { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight { ComboBasedSize = { Value = false } }, new OsuModClassic(), new OsuModDifficultyAdjust() }; + { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight { ComboBasedSize = { Value = false } }, new OsuModClassic(), new OsuModDifficultyAdjust { CircleSize = { Value = 3.2f } } }; scores[4].Mods = new ManiaRuleset().CreateAllMods().ToArray(); return scores; From 6ee282dadc047645ac8f5fdc3513b454c84db45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Apr 2025 10:43:07 +0200 Subject: [PATCH 062/152] Use leaderboard criteria set in song select on results screen too --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 3486d81e8a..d1ee0cd197 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -1,37 +1,39 @@ // 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.Diagnostics; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Extensions; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; +using osu.Game.Online.Leaderboards; using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Screens.Ranking { public partial class SoloResultsScreen : ResultsScreen { - private GetScoresRequest? getScoreRequest; + private readonly IBindable globalScores = new Bindable(); [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - [Resolved] - private IAPIProvider api { get; set; } = null!; + private LeaderboardManager leaderboardManager { get; set; } = null!; public SoloResultsScreen(ScoreInfo score) : base(score) { } + protected override void LoadComplete() + { + base.LoadComplete(); + globalScores.BindTo(leaderboardManager.Scores); + } + protected override async Task FetchScores() { Debug.Assert(Score != null); @@ -39,52 +41,52 @@ namespace osu.Game.Screens.Ranking if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) return []; - var requestTaskSource = new TaskCompletionSource(); - - getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += requestTaskSource.SetResult; - getScoreRequest.Failure += requestTaskSource.SetException; - api.Queue(getScoreRequest); - - try + var criteria = new LeaderboardCriteria( + Score.BeatmapInfo!, + Score.Ruleset, + leaderboardManager.CurrentCriteria?.Scope ?? BeatmapLeaderboardScope.Global, + leaderboardManager.CurrentCriteria?.ExactMods + ); + var requestTaskSource = new TaskCompletionSource(); + globalScores.BindValueChanged(_ => { - var scores = await requestTaskSource.Task.ConfigureAwait(false); - var toDisplay = new List(); + if (globalScores.Value != null && leaderboardManager.CurrentCriteria?.Equals(criteria) == true) + requestTaskSource.TrySetResult(globalScores.Value); + }); + leaderboardManager.FetchWithCriteria(criteria, forceRefresh: true); - for (int i = 0; i < scores.Scores.Count; ++i) - { - var score = scores.Scores[i]; - int position = i + 1; + var result = await requestTaskSource.Task.ConfigureAwait(false); - if (score.MatchesOnlineID(Score)) - { - // we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect, - // so we have to fish out the actual drawable panel and set the position to it directly. - var panel = ScorePanelList.GetPanelForScore(Score); - Score.Position = panel.ScorePosition.Value = position; - } - else - { - var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo); - converted.Position = position; - toDisplay.Add(converted); - } - } - - return toDisplay.ToArray(); - } - catch (Exception ex) + if (result.FailState != null) { - Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {ex}"); + Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {result.FailState}"); return []; } - } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); + var toDisplay = new List(); - getScoreRequest?.Cancel(); + var scores = result.AllScores.Select(s => s.DeepClone()).ToList(); + + for (int i = 0; i < scores.Count; ++i) + { + var score = scores[i]; + int position = i + 1; + + if (score.MatchesOnlineID(Score)) + { + // we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect, + // so we have to fish out the actual drawable panel and set the position to it directly. + var panel = ScorePanelList.GetPanelForScore(Score); + Score.Position = panel.ScorePosition.Value = position; + } + else + { + score.Position = position; + toDisplay.Add(score); + } + } + + return toDisplay.ToArray(); } } } From be34331f176cdbf3e907d60b684dfe51d68a0695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Apr 2025 11:59:37 +0200 Subject: [PATCH 063/152] Add test coverage for position accounting on results screen --- .../Ranking/TestSceneSoloResultsScreen.cs | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs new file mode 100644 index 0000000000..b3f01d093f --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs @@ -0,0 +1,362 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Ranking +{ + public partial class TestSceneSoloResultsScreen : ScreenTestScene + { + private ScoreManager scoreManager = null!; + private RulesetStore rulesetStore = null!; + private BeatmapManager beatmapManager = null!; + + private LeaderboardManager leaderboardManager = null!; + private BeatmapInfo importedBeatmap = null!; + + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); + + Dependencies.Cache(Realm); + + return dependencies; + } + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("load leaderboard manager", () => LoadComponent(leaderboardManager)); + + AddStep(@"set beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + Realm.Write(r => + { + foreach (var set in r.All()) + set.Status = BeatmapOnlineStatus.Ranked; + + foreach (var b in r.All()) + b.Status = BeatmapOnlineStatus.Ranked; + }); + importedBeatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + }); + AddStep("clear all scores", () => Realm.Write(r => r.RemoveAll())); + } + + [Test] + public void TestLocalLeaderboardWithOfflineScore() + { + ScoreInfo localScore = null!; + + AddStep("set leaderboard to local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Local, null))); + AddStep("import some local scores", () => + { + for (int i = 0; i < 30; ++i) + { + var score = TestResources.CreateTestScoreInfo(importedBeatmap); + score.TotalScore = 10_000 * (30 - i); + scoreManager.Import(score); + } + + localScore = TestResources.CreateTestScoreInfo(importedBeatmap); + localScore.TotalScore = 151_000; + localScore.Position = null; + scoreManager.Import(localScore); + localScore = localScore.Detach(); + }); + + AddStep("show results", () => LoadScreen(new SoloResultsScreen(localScore))); + AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); + AddAssert("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); + } + + [Test] + public void TestLocalLeaderboardWithOnlineScore() + { + ScoreInfo localScore = null!; + + AddStep("set leaderboard to local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Local, null))); + AddStep("import some local scores", () => + { + for (int i = 0; i < 30; ++i) + { + var score = TestResources.CreateTestScoreInfo(importedBeatmap); + score.OnlineID = i; + score.TotalScore = 10_000 * (30 - i); + scoreManager.Import(score); + } + + localScore = TestResources.CreateTestScoreInfo(importedBeatmap); + localScore.TotalScore = 151_000; + localScore.OnlineID = 30; + localScore.Position = null; + scoreManager.Import(localScore); + localScore = localScore.Detach(); + }); + + AddStep("show results", () => LoadScreen(new SoloResultsScreen(localScore))); + AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); + AddAssert("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); + } + + [Test] + public void TestOnlineLeaderboardWithLessThan50Scores() + { + ScoreInfo localScore = null!; + + AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null))); + AddStep("set up request handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetScoresRequest getScoresRequest: + var scores = new List(); + + for (int i = 0; i < 30; ++i) + { + var score = TestResources.CreateTestScoreInfo(importedBeatmap); + score.TotalScore = 10_000 * (30 - i); + score.Position = i + 1; + scores.Add(SoloScoreInfo.ForSubmission(score)); + } + + getScoresRequest.TriggerSuccess(new APIScoresCollection { Scores = scores }); + return true; + } + + return false; + }); + + AddStep("show results", () => + { + localScore = TestResources.CreateTestScoreInfo(importedBeatmap); + localScore.TotalScore = 151_000; + localScore.Position = null; + LoadScreen(new SoloResultsScreen(localScore)); + }); + AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); + AddAssert("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); + } + + [Test] + public void TestOnlineLeaderboardWithLessThan50Scores_UserIsLast() + { + ScoreInfo localScore = null!; + + AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null))); + AddStep("set up request handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetScoresRequest getScoresRequest: + var scores = new List(); + + for (int i = 0; i < 30; ++i) + { + var score = TestResources.CreateTestScoreInfo(importedBeatmap); + score.TotalScore = 300_000 + 10_000 * (30 - i); + score.Position = i + 1; + scores.Add(SoloScoreInfo.ForSubmission(score)); + } + + getScoresRequest.TriggerSuccess(new APIScoresCollection { Scores = scores }); + return true; + } + + return false; + }); + + AddStep("show results", () => + { + localScore = TestResources.CreateTestScoreInfo(importedBeatmap); + localScore.TotalScore = 151_000; + localScore.Position = null; + LoadScreen(new SoloResultsScreen(localScore)); + }); + AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); + AddAssert("local score is #31", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(31)); + } + + [Test] + public void TestOnlineLeaderboardWithMoreThan50Scores_UserOutsideOfTop50() + { + ScoreInfo localScore = null!; + + AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null))); + AddStep("set up request handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetScoresRequest getScoresRequest: + var scores = new List(); + + for (int i = 0; i < 50; ++i) + { + var score = TestResources.CreateTestScoreInfo(importedBeatmap); + score.TotalScore = 500_000 + 10_000 * (50 - i); + score.Position = i + 1; + scores.Add(SoloScoreInfo.ForSubmission(score)); + } + + var userBest = TestResources.CreateTestScoreInfo(importedBeatmap); + userBest.TotalScore = 50_000; + + getScoresRequest.TriggerSuccess(new APIScoresCollection + { + Scores = scores, + UserScore = new APIScoreWithPosition + { + Score = SoloScoreInfo.ForSubmission(userBest), + Position = 133_337, + } + }); + return true; + } + + return false; + }); + + AddStep("show results", () => + { + localScore = TestResources.CreateTestScoreInfo(importedBeatmap); + localScore.TotalScore = 151_000; + localScore.Position = null; + LoadScreen(new SoloResultsScreen(localScore)); + }); + AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); + AddAssert("local score has no position", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.Null); + AddAssert("user best position preserved", () => this.ChildrenOfType().Any(p => p.ScorePosition.Value == 133_337)); + } + + [Test] + public void TestOnlineLeaderboardWithMoreThan50Scores_UserInTop50() + { + ScoreInfo localScore = null!; + + AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null))); + AddStep("set up request handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetScoresRequest getScoresRequest: + var scores = new List(); + + for (int i = 0; i < 50; ++i) + { + var score = TestResources.CreateTestScoreInfo(importedBeatmap); + score.TotalScore = 500_000 + 10_000 * (50 - i); + score.Position = i + 1; + scores.Add(SoloScoreInfo.ForSubmission(score)); + } + + var userBest = TestResources.CreateTestScoreInfo(importedBeatmap); + userBest.TotalScore = 50_000; + + getScoresRequest.TriggerSuccess(new APIScoresCollection + { + Scores = scores, + UserScore = new APIScoreWithPosition + { + Score = SoloScoreInfo.ForSubmission(userBest), + Position = 133_337, + } + }); + return true; + } + + return false; + }); + + AddStep("show results", () => + { + localScore = TestResources.CreateTestScoreInfo(importedBeatmap); + localScore.TotalScore = 651_000; + localScore.Position = null; + LoadScreen(new SoloResultsScreen(localScore)); + }); + AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); + AddAssert("local score is #36", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(36)); + AddAssert("user best position incremented by 1", () => this.ChildrenOfType().Any(p => p.ScorePosition.Value == 133_338)); + } + + [Test] + public void TestOnlineLeaderboardDeduplication() + { + AddStep("set leaderboard to global", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(importedBeatmap, importedBeatmap.Ruleset, BeatmapLeaderboardScope.Global, null))); + AddStep("set up request handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetScoresRequest getScoresRequest: + var scores = new List(); + + for (int i = 0; i < 50; ++i) + { + var score = TestResources.CreateTestScoreInfo(importedBeatmap); + score.TotalScore = 500_000 + 10_000 * (50 - i); + score.Position = i + 1; + scores.Add(SoloScoreInfo.ForSubmission(score)); + } + + var userBest = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo(importedBeatmap)); + userBest.TotalScore = 151_000; + userBest.ID = 12345; + + getScoresRequest.TriggerSuccess(new APIScoresCollection + { + Scores = scores, + UserScore = new APIScoreWithPosition + { + Score = userBest, + Position = 133_337, + } + }); + return true; + } + + return false; + }); + + AddStep("show results", () => + { + var localScore = TestResources.CreateTestScoreInfo(importedBeatmap); + localScore.TotalScore = 151_000; + localScore.OnlineID = 12345; + localScore.Position = null; + LoadScreen(new SoloResultsScreen(localScore)); + }); + AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); + AddAssert("only one score with ID 12345", () => this.ChildrenOfType().Count(s => s.Score.OnlineID == 12345), () => Is.EqualTo(1)); + AddAssert("user best position preserved", () => this.ChildrenOfType().Any(p => p.ScorePosition.Value == 133_337)); + } + } +} From 82a866e475b102af721c1de4670b308857b1aee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Apr 2025 12:30:16 +0200 Subject: [PATCH 064/152] Use more correct accounting of positions on results screen --- .../Online/Leaderboards/LeaderboardManager.cs | 9 ++- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 66 +++++++++++++++---- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index dd68085103..4aca3b1a4a 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -125,7 +125,14 @@ namespace osu.Game.Online.Leaderboards var result = LeaderboardScores.Success ( - response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore().ToArray(), + response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)) + .OrderByTotalScore() + .Select((s, idx) => + { + s.Position = idx + 1; + return s; + }) + .ToArray(), response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) ); inFlightOnlineRequest = null; diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index d1ee0cd197..c09986f508 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -63,30 +63,68 @@ namespace osu.Game.Screens.Ranking return []; } - var toDisplay = new List(); + var clonedScores = result.AllScores.Select(s => s.DeepClone()).ToArray(); - var scores = result.AllScores.Select(s => s.DeepClone()).ToList(); + List sortedScores = []; - for (int i = 0; i < scores.Count; ++i) + foreach (var clonedScore in clonedScores) { - var score = scores[i]; - int position = i + 1; - - if (score.MatchesOnlineID(Score)) + // ensure that we do not double up on the score being presented here. + // additionally, ensure that the reference that ends up in `sortedScores` is the `Score` reference specifically. + // this simplifies handling later. + if (clonedScore.Equals(Score) || clonedScore.MatchesOnlineID(Score)) { - // we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect, - // so we have to fish out the actual drawable panel and set the position to it directly. - var panel = ScorePanelList.GetPanelForScore(Score); - Score.Position = panel.ScorePosition.Value = position; + Score.Position = clonedScore.Position; + sortedScores.Add(Score); } + else + sortedScores.Add(clonedScore); + } + + // if we haven't encountered a match for the presented score, we still need to attach it. + // note that the above block ensuring that the `Score` reference makes it in here makes this valid to write in this way. + if (!sortedScores.Contains(Score)) + sortedScores.Add(Score); + + sortedScores = sortedScores.OrderByTotalScore().ToList(); + + int delta = 0; + bool isPartialLeaderboard = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && result.TopScores.Count >= 50; + + for (int i = 0; i < sortedScores.Count; i++) + { + var sortedScore = sortedScores[i]; + + if (!isPartialLeaderboard) + sortedScore.Position = i + 1; else { - score.Position = position; - toDisplay.Add(score); + if (ReferenceEquals(sortedScore, Score) && sortedScore.Position == null) + { + int? previousScorePosition = i > 0 ? sortedScores[i - 1].Position : 0; + int? nextScorePosition = i < result.TopScores.Count - 1 ? sortedScores[i + 1].Position : null; + + if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition) + { + sortedScore.Position = previousScorePosition + 1; + delta += 1; + } + else + sortedScore.Position = null; + } + else + sortedScore.Position += delta; } } - return toDisplay.ToArray(); + // there's a non-zero chance that the `Score`'s `ScorePosition` was mutated above, + // but the two are not actually coupled together in any way, + // so ensure that the drawable panel also receives the updated position. + // note that this is valid to do precisely because we ensured `Score` was in `sortedScores` earlier. + ScorePanelList.GetPanelForScore(Score).ScorePosition.Value = Score.Position; + + sortedScores.Remove(Score); + return sortedScores.ToArray(); } } } From c01ff9f845c95955db97d58c7aa492d62a6aa7be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Apr 2025 23:02:30 +0900 Subject: [PATCH 065/152] Share constant more correctly --- osu.Game/Screens/Footer/ScreenFooter.cs | 6 ++---- osu.Game/Screens/Footer/ScreenFooterButton.cs | 7 ++++--- osu.Game/Screens/SelectV2/FooterButtonMods.cs | 8 ++++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index af2496f97a..b2f2903d41 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -65,8 +65,6 @@ namespace osu.Game.Screens.Footer [BackgroundDependencyLoader] private void load() { - const float footer_button_y_offset = 10; - InternalChildren = new Drawable[] { background = new Box @@ -91,7 +89,7 @@ namespace osu.Game.Screens.Footer { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Y = footer_button_y_offset, + Y = ScreenFooterButton.Y_OFFSET, Direction = FillDirection.Horizontal, Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both, @@ -114,7 +112,7 @@ namespace osu.Game.Screens.Footer hiddenButtonsContainer = new Container { Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, - Y = footer_button_y_offset, + Y = ScreenFooterButton.Y_OFFSET, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 5e96eadfea..6385901db7 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -25,7 +25,8 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - protected const int CORNER_RADIUS = 10; + public const int Y_OFFSET = 10; + protected const int BUTTON_HEIGHT = 75; protected const int BUTTON_WIDTH = 116; @@ -87,7 +88,7 @@ namespace osu.Game.Screens.Footer }, Shear = OsuGame.SHEAR, Masking = true, - CornerRadius = CORNER_RADIUS, + CornerRadius = 10, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { @@ -134,7 +135,7 @@ namespace osu.Game.Screens.Footer Shear = -OsuGame.SHEAR, Anchor = Anchor.BottomCentre, Origin = Anchor.Centre, - Y = -CORNER_RADIUS, + Y = -Y_OFFSET, Size = new Vector2(100, 5), Masking = true, CornerRadius = 3, diff --git a/osu.Game/Screens/SelectV2/FooterButtonMods.cs b/osu.Game/Screens/SelectV2/FooterButtonMods.cs index 833ea96139..3a270d8a68 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonMods.cs @@ -79,7 +79,7 @@ namespace osu.Game.Screens.SelectV2 Depth = float.MaxValue, Origin = Anchor.BottomLeft, Shear = OsuGame.SHEAR, - CornerRadius = CORNER_RADIUS, + CornerRadius = Y_OFFSET, Size = new Vector2(BUTTON_WIDTH, bar_height), Masking = true, EdgeEffect = new EdgeEffectParameters @@ -115,7 +115,7 @@ namespace osu.Game.Screens.SelectV2 }, new Container { - CornerRadius = CORNER_RADIUS, + CornerRadius = Y_OFFSET, RelativeSizeAxes = Axes.Both, Width = mod_display_portion, Masking = true, @@ -264,7 +264,7 @@ namespace osu.Game.Screens.SelectV2 private void load() { AutoSizeAxes = Axes.Both; - CornerRadius = CORNER_RADIUS; + CornerRadius = Y_OFFSET; Masking = true; InternalChildren = new Drawable[] @@ -306,7 +306,7 @@ namespace osu.Game.Screens.SelectV2 Depth = float.MaxValue; Origin = Anchor.BottomLeft; Shear = OsuGame.SHEAR; - CornerRadius = CORNER_RADIUS; + CornerRadius = Y_OFFSET; AutoSizeAxes = Axes.X; Height = bar_height; Masking = true; From 901c1b26506d5f0a41be8e46b9620b0e3c661e28 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 02:23:28 +0300 Subject: [PATCH 066/152] Remove pre-rate rounding in BPM display --- osu.Game/Utils/FormatUtils.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index e93a494b65..f7250c6833 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -59,11 +59,8 @@ namespace osu.Game.Utils /// /// Applies rounding to the given BPM value. /// - /// - /// Double-rounding is applied intentionally (see https://github.com/ppy/osu/pull/18345#issue-1243311382 for rationale). - /// /// The base BPM to round. /// Rate adjustment, if applicable. - public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(Math.Round(baseBpm) * rate); + public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(baseBpm * rate); } } From dcbb7209dfd47241efff170099ebd69c9ebb3743 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 02:23:33 +0300 Subject: [PATCH 067/152] Update existing test cases --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 4 ++-- .../Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 8132f8a841..0e0f3c554a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -163,8 +163,8 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase(120, 125, null, "120-125 (mostly 120)")] [TestCase(120, 120.6, null, "120-121 (mostly 120)")] [TestCase(120, 120.4, null, "120")] - [TestCase(120, 120.6, "DT", "180-182 (mostly 180)")] - [TestCase(120, 120.4, "DT", "180")] + [TestCase(120, 120.6, "DT", "180-181 (mostly 180)")] + [TestCase(120, 120.4, "DT", "180-181 (mostly 180)")] public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) { IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index 8b89de5fce..b6fa7cd798 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -118,8 +118,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [TestCase(120, 125, null, "120-125 (mostly 120)")] [TestCase(120, 120.6, null, "120-121 (mostly 120)")] [TestCase(120, 120.4, null, "120")] - [TestCase(120, 120.6, "DT", "180-182 (mostly 180)")] - [TestCase(120, 120.4, "DT", "180")] + [TestCase(120, 120.6, "DT", "180-181 (mostly 180)")] + [TestCase(120, 120.4, "DT", "180-181 (mostly 180)")] public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) { IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo); From a3a4881432864de5471dd3f837281aefd3812f00 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 02:43:14 +0300 Subject: [PATCH 068/152] Add failing test case --- .../TestSceneManiaTouchInput.cs | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs index fc495a5ab0..3e83f4a5e8 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -18,7 +18,12 @@ namespace osu.Game.Rulesets.Mania.Tests protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); [SetUp] - public void SetUp() => Schedule(() => toggleTouchControls(false)); + public void SetUp() => Schedule(() => + { + InputManager.EndTouch(new Touch(TouchSource.Touch1, Vector2.Zero)); + InputManager.EndTouch(new Touch(TouchSource.Touch2, Vector2.Zero)); + toggleTouchControls(false); + }); #region Without touch controls @@ -71,6 +76,35 @@ namespace osu.Game.Rulesets.Mania.Tests () => Does.Not.Contain(getColumn(0).Action.Value)); } + [Test] + public void TestBetweenTwoColumns() + { + AddStep("touch after column 0", () => + { + var column = getColumn(0); + InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(column.LayoutSize.X + 0.5f, column.LayoutSize.Y / 2)))); + }); + AddAssert("column 0 pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre))); + AddAssert("column 0 released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(0).Action.Value)); + AddStep("touch before column 1", () => + { + var column = getColumn(1); + InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(-0.5f, column.LayoutSize.Y / 2)))); + }); + AddAssert("column 1 pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(1).Action.Value)); + AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre))); + AddAssert("column 1 released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(1).Action.Value)); + } + #endregion #region With touch controls @@ -132,6 +166,38 @@ namespace osu.Game.Rulesets.Mania.Tests () => Does.Not.Contain(getColumn(0).Action.Value)); } + [Test] + public void TestTouchControlBetweenTwoColumns() + { + AddStep("enable touch controls", () => toggleTouchControls(true)); + + AddStep("touch after receptor 0", () => + { + var column = getReceptor(0); + InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(column.LayoutSize.X + 1f, column.LayoutSize.Y / 2)))); + }); + + AddAssert("column 0 pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getReceptor(0).Action.Value)); + AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre))); + AddAssert("column 0 released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getReceptor(0).Action.Value)); + AddStep("touch before receptor 1", () => + { + var column = getReceptor(1); + InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(-1f, column.LayoutSize.Y / 2)))); + }); + AddAssert("column 1 pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getReceptor(1).Action.Value)); + AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre))); + AddAssert("column 1 released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getReceptor(1).Action.Value)); + } + #endregion private void toggleTouchControls(bool enabled) From d63f9533b1bc69e6fb17bc127aaa8adf132cc396 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 03:50:42 +0300 Subject: [PATCH 069/152] Make column spacing lookups easy to use --- .../Argon/ManiaArgonSkinTransformer.cs | 6 +++--- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 13 +++++++------ .../LegacyManiaSkinConfigurationLookup.cs | 5 +++-- osu.Game/Skinning/LegacySkin.cs | 18 ++++++++++++++---- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 6f010ffe48..f5bbd0fae8 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -131,8 +131,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon switch (maniaLookup.Lookup) { - case LegacyManiaSkinConfigurationLookups.ColumnSpacing: - return SkinUtils.As(new Bindable(2)); + case LegacyManiaSkinConfigurationLookups.LeftColumnSpacing: + case LegacyManiaSkinConfigurationLookups.RightColumnSpacing: + return SkinUtils.As(new Bindable(1)); case LegacyManiaSkinConfigurationLookups.StagePaddingBottom: case LegacyManiaSkinConfigurationLookups.StagePaddingTop: @@ -146,7 +147,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return SkinUtils.As(new Bindable(width)); case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: - var colour = getColourForLayout(columnIndex, stage); return SkinUtils.As(new Bindable(colour)); diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index cee43b300a..953be8d507 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -124,14 +124,15 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < stageDefinition.Columns; i++) { - if (i > 0) - { - float spacing = skin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1)) + float leftSpacing = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.LeftColumnSpacing, i)) ?.Value ?? Stage.COLUMN_SPACING; - columns[i].Margin = new MarginPadding { Left = spacing }; - } + float rightSpacing = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.RightColumnSpacing, i)) + ?.Value ?? Stage.COLUMN_SPACING; + + columns[i].Margin = new MarginPadding { Left = leftSpacing, Right = rightSpacing }; float? width = skin.GetConfig( new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index e94fb23681..c4f5d6a53c 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -37,7 +37,6 @@ namespace osu.Game.Skinning public enum LegacyManiaSkinConfigurationLookups { ColumnWidth, - ColumnSpacing, LightImage, LeftLineWidth, RightLineWidth, @@ -83,6 +82,8 @@ namespace osu.Game.Skinning Hit0, KeysUnderNotes, NoteBodyStyle, - LightFramePerSecond + LightFramePerSecond, + LeftColumnSpacing, + RightColumnSpacing, } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 51c1473303..210050fddb 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -148,10 +148,6 @@ namespace osu.Game.Skinning Debug.Assert(maniaLookup.ColumnIndex != null); return SkinUtils.As(new Bindable(existing.WidthForNoteHeightScale)); - case LegacyManiaSkinConfigurationLookups.ColumnSpacing: - Debug.Assert(maniaLookup.ColumnIndex != null); - return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value])); - case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); @@ -278,6 +274,20 @@ namespace osu.Game.Skinning Debug.Assert(maniaLookup.ColumnIndex != null); return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value + 1])); + case LegacyManiaSkinConfigurationLookups.LeftColumnSpacing: + Debug.Assert(maniaLookup.ColumnIndex != null); + if (maniaLookup.ColumnIndex == 0) + return SkinUtils.As(new Bindable()); + + return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value - 1] / 2)); + + case LegacyManiaSkinConfigurationLookups.RightColumnSpacing: + Debug.Assert(maniaLookup.ColumnIndex != null); + if (maniaLookup.ColumnIndex == existing.ColumnSpacing.Length) + return SkinUtils.As(new Bindable()); + + return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value] / 2)); + case LegacyManiaSkinConfigurationLookups.Hit0: case LegacyManiaSkinConfigurationLookups.Hit50: case LegacyManiaSkinConfigurationLookups.Hit100: From 1579543bbea45c2c75a616b1c9b1c7b9e6e68a46 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 03:07:56 +0300 Subject: [PATCH 070/152] Fix column not handling input in gaps correctly --- osu.Game.Rulesets.Mania/UI/Column.cs | 18 ++++++++++++++++-- .../UI/ManiaTouchInputArea.cs | 6 ++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index cb825761d1..eccececd22 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Mania.UI private IBindable mobilePlayStyle = null!; + private float leftColumnSpacing; + private float rightColumnSpacing; + public Column(int index, bool isSpecial) { Index = index; @@ -126,6 +129,14 @@ namespace osu.Game.Rulesets.Mania.UI private void onSourceChanged() { AccentColour.Value = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, Index)?.Value ?? Color4.Black; + + leftColumnSpacing = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.LeftColumnSpacing, Index)) + ?.Value ?? Stage.COLUMN_SPACING; + + rightColumnSpacing = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.RightColumnSpacing, Index)) + ?.Value ?? Stage.COLUMN_SPACING; } protected override void LoadComplete() @@ -187,8 +198,11 @@ namespace osu.Game.Rulesets.Mania.UI } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border - => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); + { + // Extend input coverage to the gaps close to this column. + var spacingInflation = new MarginPadding { Left = leftColumnSpacing, Right = rightColumnSpacing }; + return DrawRectangle.Inflate(spacingInflation).Contains(ToLocalSpace(screenSpacePos)); + } #region Touch Input diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 2a2faf0cf7..7c5f759833 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.UI receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action }, + Spacing = { BindTarget = Spacing }, }); receptorGridDimensions.Add(new Dimension()); @@ -122,6 +123,7 @@ namespace osu.Game.Rulesets.Mania.UI public partial class ColumnInputReceptor : CompositeDrawable { public readonly IBindable Action = new Bindable(); + public readonly IBindable Spacing = new BindableFloat(); private readonly Box highlightOverlay; @@ -159,6 +161,10 @@ namespace osu.Game.Rulesets.Mania.UI }; } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + // Extend input coverage to the gaps close to this receptor. + => DrawRectangle.Inflate(new Vector2(Spacing.Value / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); + protected override bool OnTouchDown(TouchDownEvent e) { updateButton(true); From a8ca60497c4693d1c463e8a61ac59e41d56ad9c0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 05:03:51 +0300 Subject: [PATCH 071/152] Fix osu!catch getting upscaled on portrait orientation --- .../UI/CatchPlayfieldAdjustmentContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index 3b9cca8ef0..bbf065f388 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -68,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI // needs to be scaled down to remain playable. const float base_aspect_ratio = 1024f / 768f; float aspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y; - scaleContainer.Scale = new Vector2(base_aspect_ratio / aspectRatio); + scaleContainer.Scale = new Vector2(Math.Min(1, base_aspect_ratio / aspectRatio)); } } From 713fbfb2c89cb6919d72d9ed9dc8768ef117c4fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 12:48:07 +0900 Subject: [PATCH 072/152] Adjust colours to match icon better --- osu.Game/Rulesets/UI/ModIcon.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index d42e185784..bfd5d63268 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -84,6 +84,9 @@ namespace osu.Game.Rulesets.UI private Drawable adjustmentMarker = null!; + private Circle cogBackground = null!; + private SpriteIcon cog = null!; + private ModSettingChangeTracker? modSettingsChangeTracker; /// @@ -175,21 +178,19 @@ namespace osu.Game.Rulesets.UI Position = new Vector2(64, 14), Children = new Drawable[] { - new Circle + cogBackground = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Colour = colours.YellowLight, RelativeSizeAxes = Axes.Both, }, - new SpriteIcon + cog = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.Cog, - Colour = colours.YellowDark, RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.7f), + Size = new Vector2(0.6f), } } }, @@ -254,6 +255,8 @@ namespace osu.Game.Rulesets.UI private void updateColour() { modAcronym.Colour = modIcon.Colour = Interpolation.ValueAt(0.1f, Colour4.Black, backgroundColour, 0, 1); + cogBackground.Colour = Interpolation.ValueAt(0.1f, Colour4.Black, backgroundColour, 0, 1); + cog.Colour = backgroundColour; extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour; extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f); From 14d0565194003999e1a40bc135f868c9727765b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 12:49:39 +0900 Subject: [PATCH 073/152] Add xmldoc note explaining new flag is instantaneous state --- osu.Game/Rulesets/Mods/IMod.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index d4c51b1dfb..08e64c4aa9 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -87,6 +87,10 @@ namespace osu.Game.Rulesets.Mods /// /// Whether any user adjustable setting attached to this mod has a non-default value. /// + /// + /// This returns the instantaneous state of this mod. It may change over time. + /// For tracking changes on a dynamic display, make sure to setup a . + /// bool HasNonDefaultSettings { get From babccca2bbe102328142349268db3777ee9df6d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 12:58:40 +0900 Subject: [PATCH 074/152] Don't bother with localised implementation of `AdjustedSettingsCount` I was avoiding using reflection to save on overheads, but it's probably not worth it. --- .../Mods/CatchModDifficultyAdjust.cs | 13 +----------- .../Mods/OsuModDifficultyAdjust.cs | 13 +----------- .../Mods/TaikoModDifficultyAdjust.cs | 12 +---------- osu.Game/Rulesets/Mods/Mod.cs | 21 +++++++++++++++++++ osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 13 +----------- 5 files changed, 25 insertions(+), 47 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 856989a685..c300afa79f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -37,22 +37,11 @@ namespace osu.Game.Rulesets.Catch.Mods [SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")] public BindableBool HardRockOffsets { get; } = new BindableBool(); - public override int AdjustedSettingsCount - { - get - { - int count = base.AdjustedSettingsCount; - if (!ApproachRate.IsDefault) count++; - if (!CircleSize.IsDefault) count++; - return count; - } - } - public override string ExtendedIconInformation { get { - if (AdjustedSettingsCount != 1) + if (UserAdjustedSettingsCount != 1) return string.Empty; if (!CircleSize.IsDefault) return format("CS", CircleSize); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 357a971c0f..1d94ac6335 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -37,22 +37,11 @@ namespace osu.Game.Rulesets.Osu.Mods ReadCurrentFromDifficulty = diff => diff.ApproachRate, }; - public override int AdjustedSettingsCount - { - get - { - int count = base.AdjustedSettingsCount; - if (!ApproachRate.IsDefault) count++; - if (!CircleSize.IsDefault) count++; - return count; - } - } - public override string ExtendedIconInformation { get { - if (AdjustedSettingsCount != 1) + if (UserAdjustedSettingsCount != 1) return string.Empty; if (!CircleSize.IsDefault) return format("CS", CircleSize); diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 628592fe51..57b57555c2 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -21,21 +21,11 @@ namespace osu.Game.Rulesets.Taiko.Mods ReadCurrentFromDifficulty = _ => 1, }; - public override int AdjustedSettingsCount - { - get - { - int count = base.AdjustedSettingsCount; - if (!ScrollSpeed.IsDefault) count++; - return count; - } - } - public override string ExtendedIconInformation { get { - if (AdjustedSettingsCount != 1) + if (UserAdjustedSettingsCount != 1) return string.Empty; if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 727db913e2..56a4aa7a50 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -75,6 +75,27 @@ namespace osu.Game.Rulesets.Mods } } + /// + /// The number of settings on this mod instance which have been adjusted by the user from their default values. + /// + public int UserAdjustedSettingsCount + { + get + { + int count = 0; + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var bindable = (IBindable)property.GetValue(this)!; + + if (!bindable.IsDefault) + count++; + } + + return count; + } + } + /// /// The score multiplier of this mod. /// diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 857527062f..0c1a4ab589 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -68,22 +68,11 @@ namespace osu.Game.Rulesets.Mods } } - public virtual int AdjustedSettingsCount - { - get - { - int count = 0; - if (!DrainRate.IsDefault) count++; - if (!OverallDifficulty.IsDefault) count++; - return count; - } - } - public override string ExtendedIconInformation { get { - if (AdjustedSettingsCount != 1) + if (UserAdjustedSettingsCount != 1) return string.Empty; if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); From 0c5193ba59fda2099f9b30908bc756aaa5747168 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 13:04:50 +0900 Subject: [PATCH 075/152] Fix adjustment marker not updating when settings' states change --- osu.Game/Rulesets/UI/ModIcon.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index bfd5d63268..d3f04e7e74 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -236,11 +236,6 @@ namespace osu.Game.Rulesets.UI backgroundColour = colours.ForModType(value.Type); updateColour(); - if (mod.HasNonDefaultSettings) - adjustmentMarker.Show(); - else - adjustmentMarker.Hide(); - updateExtendedInformation(); } @@ -250,6 +245,11 @@ namespace osu.Game.Rulesets.UI extendedContent.Alpha = showExtended ? 1 : 0; extendedText.Text = mod.ExtendedIconInformation; + + if (mod.HasNonDefaultSettings) + adjustmentMarker.Show(); + else + adjustmentMarker.Hide(); } private void updateColour() From fc0a233ba42b10f099434fd037f20dd45012d7e0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 07:20:15 +0300 Subject: [PATCH 076/152] Adjust right-side content layout to mask mods --- .../Screens/SelectV2/BeatmapLeaderboardScore.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 699a5216eb..b422a6474e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -334,8 +334,7 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Y, Child = new Container { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Children = new Drawable[] @@ -390,15 +389,13 @@ namespace osu.Game.Screens.SelectV2 }, new Container { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding { Right = grade_width }, Child = new Container { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = corner_radius, Children = new Drawable[] @@ -416,8 +413,8 @@ namespace osu.Game.Screens.SelectV2 new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, Direction = FillDirection.Vertical, Padding = new MarginPadding { Horizontal = corner_radius }, Spacing = new Vector2(0f, -2f), From 92b01d68b641e23ce57d730a623dfb71f1bc2938 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 04:34:49 +0300 Subject: [PATCH 077/152] Use more clear method to showcase locally created difficulty Use `ResetOnlineInfo` --- .../Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index ea90828f45..517133a9a9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { var (working, onlineSet) = createTestBeatmap(); - onlineSet.Beatmaps = Array.Empty(); + working.BeatmapInfo.ResetOnlineInfo(); currentOnlineSet = onlineSet; Beatmap.Value = working; From 72f28e5fe4dfd957f11a3f0d3a7ae6223ed33fc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Apr 2025 18:53:02 +0900 Subject: [PATCH 078/152] Simplify code --- .../Mods/TaikoModSimplifiedRhythm.cs | 115 ++++++++---------- 1 file changed, 49 insertions(+), 66 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index fc162d4b7b..661e932300 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; @@ -23,113 +24,95 @@ namespace osu.Game.Rulesets.Taiko.Mods public override ModType Type => ModType.DifficultyReduction; [SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")] - public Bindable OneThirdConversion { get; } = new BindableBool(false); + public Bindable OneThirdConversion { get; } = new BindableBool(); [SettingSource("1/6 to 1/4 conversion", "Converts 1/6 patterns to 1/4 rhythm.")] public Bindable OneSixthConversion { get; } = new BindableBool(true); [SettingSource("1/8 to 1/4 conversion", "Converts 1/8 patterns to 1/4 rhythm.")] - public Bindable OneEighthConversion { get; } = new BindableBool(false); + public Bindable OneEighthConversion { get; } = new BindableBool(); public void ApplyToBeatmap(IBeatmap beatmap) { var taikoBeatmap = (TaikoBeatmap)beatmap; var controlPointInfo = taikoBeatmap.ControlPointInfo; - List hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast().ToList(); - List toRemove = new List(); - // Snap conversions for rhythms - var snapConversions = new Dictionary - { - { 8, 4.0 }, // 1/8 snap to 1/4 snap - { 6, 4.0 }, // 1/6 snap to 1/4 snap - { 3, 2.0 }, // 1/3 snap to 1/2 snap - }; + Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast().ToArray(); + + var conversions = new List<(int, int)>(); + + if (OneEighthConversion.Value) conversions.Add((8, 4)); + if (OneSixthConversion.Value) conversions.Add((6, 4)); + if (OneThirdConversion.Value) conversions.Add((3, 2)); bool inPattern = false; - foreach (var snapConversion in snapConversions) + foreach ((int baseRhythm, int adjustedRhythm) in conversions) { int patternStartIndex = 0; - // Skip processing if the corresponding conversion is disabled - if (!shouldProcessRhythm(snapConversion.Key)) - continue; - - for (int i = 0; i < hits.Count; i++) + for (int i = 1; i < hits.Length; i++) { - double snapValue = i < hits.Count - 1 - ? getSnapBetweenNotes(controlPointInfo, hits[i], hits[i + 1]) - : 1; // No next note, default to a safe 1/1 snap + double snapValue = getSnapBetweenNotes(controlPointInfo, hits[i - 1], hits[i]); - if (snapValue == snapConversion.Key) + if (inPattern) { - if (!inPattern) - { - patternStartIndex = i; - } + // pattern continues + if (snapValue == baseRhythm) continue; - inPattern = true; - } - - // Check if end of pattern - if (inPattern && snapValue != snapConversion.Key) - { - // End pattern inPattern = false; // Iterate through the pattern - for (int j = patternStartIndex; j <= i; j++) + for (int j = patternStartIndex; j < i; j++) { - int currentHitPosition = j - patternStartIndex; + int indexInPattern = j - patternStartIndex; - if (snapConversion.Key == 8) + switch (baseRhythm) { - // 1/8: Remove the second note - if (currentHitPosition % 2 == 1) + // 1/8: Remove every second note + case 8: { - toRemove.Add(hits[j]); + if (indexInPattern % 2 == 1) + { + taikoBeatmap.HitObjects.Remove(hits[j]); + } + + break; } - } - else - { - // 1/6 and 1/3: Remove the second note and adjust the third - if (currentHitPosition % 3 == 1) + + // 1/6 and 1/3: Remove every second note and adjust time of every third + case 6: + case 3: { - toRemove.Add(hits[j]); - } - else if (currentHitPosition % 3 == 2 && j < hits.Count - 1) - { - double offset = controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / snapConversion.Value; - hits[j].StartTime = hits[j + 1].StartTime - offset; + if (indexInPattern % 3 == 1) + taikoBeatmap.HitObjects.Remove(hits[j]); + else if (indexInPattern % 3 == 2) + hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm; + + break; } + + default: + throw new ArgumentOutOfRangeException(nameof(baseRhythm)); } } } + else + { + if (snapValue == baseRhythm) + { + patternStartIndex = i - 1; + inPattern = true; + } + } } - - // Remove queued notes - taikoBeatmap.HitObjects.RemoveAll(obj => toRemove.Contains(obj)); } } private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote) { - double gapMs = nextNote.StartTime - currentNote.StartTime; var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime); - - return controlPointInfo.GetClosestBeatDivisor(gapMs + currentTimingPoint.Time); - } - - private bool shouldProcessRhythm(int snap) - { - return snap switch - { - 3 => OneThirdConversion.Value, - 6 => OneSixthConversion.Value, - 8 => OneEighthConversion.Value, - _ => false, - }; + return controlPointInfo.GetClosestBeatDivisor(currentTimingPoint.Time + (nextNote.StartTime - currentNote.StartTime)); } } } From cb3f8d7d835fa132b9603797a929f5197f8d6162 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 14:36:41 +0900 Subject: [PATCH 079/152] Remove colour lightening of judgement colours I'm not sure why this is a thing but let's not do it without proper rationale. --- .../Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs index 5813864a82..c6fe1e5f25 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -102,13 +102,7 @@ namespace osu.Game.Screens.SelectV2 relativeDate.Date = value.Date; var judgementsStatistics = value.GetStatisticsForDisplay().Select(s => - { - Colour4 colour = colours.ForHitResult(s.Result); - var hsl = colour.ToHSL(); - - Colour4 lightColour = Colour4.FromHSL(hsl.X, hsl.Y, 0.8f); - return new StatisticRow(s.DisplayName.ToUpper(), lightColour, s.Count.ToLocalisableString("N0")); - }); + new StatisticRow(s.DisplayName.ToUpper(), colours.ForHitResult(s.Result), s.Count.ToLocalisableString("N0"))); double multiplier = 1.0; From f3e23def9026dad6fcc39d66914999ed83db21d4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 06:56:13 -0400 Subject: [PATCH 080/152] Introduce sheared range slider --- .../TestSceneShearedRangeSlider.cs | 97 +++++++ .../UserInterface/ShearedRangeSlider.cs | 253 ++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs create mode 100644 osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs new file mode 100644 index 0000000000..551a471718 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs @@ -0,0 +1,97 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneShearedRangeSlider : ThemeComparisonTestScene + { + private readonly BindableNumber customStart = new BindableNumber + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f + }; + + private readonly BindableNumber customEnd = new BindableNumber(10) + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f + }; + + private ShearedRangeSlider shearedRangeSlider = null!; + + public TestSceneShearedRangeSlider() + : base(false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + shearedRangeSlider = new ShearedRangeSlider("Test") + { + Width = 600, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1), + LowerBound = customStart, + UpperBound = customEnd, + TooltipSuffix = "suffix", + NubWidth = 32, + DefaultStringLowerBound = "0.0", + DefaultStringUpperBound = "∞", + MinRange = 0.1f, + } + } + }; + + [Test] + public void TestAdjustRange() + { + AddAssert("Initial lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.1f)); + AddAssert("Initial upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(10).Within(0.1f)); + + AddStep("Adjust range", () => + { + customStart.Value = 5; + customEnd.Value = 7.5; + }); + + AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(5).Within(0.1f)); + AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.1f)); + + AddStep("Test nub pushing", () => + { + customStart.Value = 9; + }); + + AddAssert("Pushed lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(9).Within(0.1f)); + AddAssert("Pushed upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(9.1).Within(0.1f)); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs new file mode 100644 index 0000000000..b0e54337f1 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs @@ -0,0 +1,253 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Graphics.UserInterface +{ + public partial class ShearedRangeSlider : CompositeDrawable + { + private readonly LocalisableString label; + + private readonly BindableNumberWithCurrent lowerBound = new BindableNumberWithCurrent(); + + /// + /// The lower limiting value. + /// + public Bindable LowerBound + { + get => lowerBound.Current; + set => lowerBound.Current = value; + } + + private readonly BindableNumberWithCurrent upperBound = new BindableNumberWithCurrent(); + + /// + /// The upper limiting value. + /// + public Bindable UpperBound + { + get => upperBound.Current; + set => upperBound.Current = value; + } + + public float NubWidth { get; init; } + + /// + /// Minimum difference between the lower bound and higher bound + /// + public float MinRange + { + set => minRange = value; + } + + /// + /// Lower bound display for when it is set to its default value. + /// + public string DefaultStringLowerBound { get; init; } = string.Empty; + + /// + /// Upper bound display for when it is set to its default value. + /// + public string DefaultStringUpperBound { get; init; } = string.Empty; + + public LocalisableString DefaultTooltipLowerBound { get; init; } = string.Empty; + + public LocalisableString DefaultTooltipUpperBound { get; init; } = string.Empty; + + public string TooltipSuffix { get; init; } = string.Empty; + + private float minRange = 0.1f; + + protected Container SliderContainer { get; private set; } = null!; + protected BoundSliderBar LowerBoundSlider { get; private set; } = null!; + protected BoundSliderBar UpperBoundSlider { get; private set; } = null!; + + public ShearedRangeSlider(LocalisableString label) + { + this.label = label; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Height = ShearedNub.HEIGHT; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new[] + { + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Masking = true, + CornerRadius = 5f, + Shear = OsuGame.SHEAR, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = label, + Shear = -OsuGame.SHEAR, + Margin = new MarginPadding { Horizontal = 12, Vertical = 5 }, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), + }, + }, + }, + SliderContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = -10 }, + Children = new[] + { + UpperBoundSlider = CreateBoundSlider(true).With(d => + { + d.KeyboardStep = 0.1f; + d.RelativeSizeAxes = Axes.X; + d.TooltipSuffix = TooltipSuffix; + d.DefaultString = DefaultStringUpperBound; + d.DefaultTooltip = DefaultTooltipUpperBound; + d.NubWidth = NubWidth; + d.Current = upperBound; + }), + LowerBoundSlider = CreateBoundSlider(false).With(d => + { + d.KeyboardStep = 0.1f; + d.RelativeSizeAxes = Axes.X; + d.TooltipSuffix = TooltipSuffix; + d.DefaultString = DefaultStringLowerBound; + d.DefaultTooltip = DefaultTooltipLowerBound; + d.NubWidth = NubWidth; + d.Current = lowerBound; + }), + UpperBoundSlider.Nub.CreateProxy(), + LowerBoundSlider.Nub.CreateProxy(), + }, + }, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LowerBoundSlider.Current.ValueChanged += min => UpperBoundSlider.Current.Value = Math.Max(min.NewValue + minRange, UpperBoundSlider.Current.Value); + UpperBoundSlider.Current.ValueChanged += max => LowerBoundSlider.Current.Value = Math.Min(max.NewValue - minRange, LowerBoundSlider.Current.Value); + } + + protected virtual BoundSliderBar CreateBoundSlider(bool isUpper) => new BoundSliderBar(isUpper); + + protected partial class BoundSliderBar : ShearedSliderBar + { + private readonly bool isUpper; + + public new ShearedNub Nub => base.Nub; + + public string? DefaultString; + public LocalisableString? DefaultTooltip; + public string? TooltipSuffix; + + public float NubWidth { get; set; } = ShearedNub.HEIGHT; + + public new float NormalizedValue => base.NormalizedValue; + + public override LocalisableString TooltipText => + (Current.IsDefault ? DefaultTooltip : Current.Value.ToString($@"0.## {TooltipSuffix}")) ?? Current.Value.ToString($@"0.## {TooltipSuffix}"); + + protected OsuSpriteText NubText { get; private set; } = null!; + + public override bool AcceptsFocus => false; + + public BoundSliderBar(bool isUpper) + { + this.isUpper = isUpper; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Nub.Width = NubWidth; + RangePadding = Nub.Width / 2; + + Nub.Add(NubText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + X = -3, + UseFullGlyphHeight = false, + Colour = OsuColour.ForegroundTextColourFor(colourProvider.Light1), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), + }); + + AccentColour = colourProvider.Highlight1.Darken(0.1f); + Nub.AccentColour = colourProvider.Highlight1; + Nub.GlowingAccentColour = colourProvider.Highlight1; + Nub.GlowColour = colourProvider.Highlight1; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (!isUpper) + { + AccentColour = BackgroundColour; + BackgroundColour = Color4.Transparent; + } + + Current.BindValueChanged(current => UpdateDisplay(current.NewValue), true); + FinishTransforms(true); + } + + protected virtual void UpdateDisplay(double value) + { + string defaultString = DefaultString ?? value.ToString("N1"); + NubText.Text = Current.IsDefault ? defaultString : value.ToString("N1"); + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + if (isUpper) + return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X >= Nub.ScreenSpaceDrawQuad.TopLeft.X; + + return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X <= Nub.ScreenSpaceDrawQuad.TopRight.X; + } + + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + return true; // Make sure only one nub shows hover effect at once. + } + } + } +} From 9871acd618777f255d8f93e8da1617ebc2ad8b62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 15:32:58 +0900 Subject: [PATCH 081/152] Add ability to click/drag in between nubs for better control --- .../TestSceneShearedRangeSlider.cs | 57 ++++++++++++++++++- .../UserInterface/ShearedRangeSlider.cs | 26 +++++++-- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs index 551a471718..21fa82eda8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedRangeSlider.cs @@ -1,16 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { @@ -70,12 +73,22 @@ namespace osu.Game.Tests.Visual.UserInterface } }; + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset range", () => + { + customStart.SetDefault(); + customEnd.SetDefault(); + }); + + AddAssert("Initial lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.1f)); + AddAssert("Initial upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(10).Within(0.1f)); + } + [Test] public void TestAdjustRange() { - AddAssert("Initial lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.1f)); - AddAssert("Initial upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(10).Within(0.1f)); - AddStep("Adjust range", () => { customStart.Value = 5; @@ -93,5 +106,43 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("Pushed lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(9).Within(0.1f)); AddAssert("Pushed upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(9.1).Within(0.1f)); } + + [Test] + public void TestAdjustRangeClickOutsideNub() + { + Vector2 lowerBoundNub = Vector2.Zero; + Vector2 upperBoundNub = Vector2.Zero; + + AddStep("click 75%", () => + { + // save out original positions so we can use as absolute selection range. + lowerBoundNub = shearedRangeSlider.ChildrenOfType().Last().ScreenSpaceDrawQuad.Centre - OsuGame.SHEAR * 2; + upperBoundNub = shearedRangeSlider.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre - OsuGame.SHEAR * 2; + + InputManager.MoveMouseTo(lowerBoundNub + new Vector2((upperBoundNub.X - lowerBoundNub.X) * 0.75f, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.11f)); + AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f)); + + AddStep("click 30%", () => + { + InputManager.MoveMouseTo(lowerBoundNub + new Vector2((upperBoundNub.X - lowerBoundNub.X) * 0.3f, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(3.0).Within(0.11f)); + AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f)); + + AddStep("click 0%", () => + { + InputManager.MoveMouseTo(lowerBoundNub); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("Adjusted lower bound is correct", () => shearedRangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.11f)); + AddAssert("Adjusted upper bound is correct", () => shearedRangeSlider.UpperBound.Value, () => Is.EqualTo(7.5).Within(0.11f)); + } } } diff --git a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs index b0e54337f1..a4c1b93810 100644 --- a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs @@ -72,14 +72,30 @@ namespace osu.Game.Graphics.UserInterface private float minRange = 0.1f; protected Container SliderContainer { get; private set; } = null!; + protected BoundSliderBar LowerBoundSlider { get; private set; } = null!; protected BoundSliderBar UpperBoundSlider { get; private set; } = null!; + protected Vector2 ScreenSpaceHalfwayPoint + { + get + { + var lowerSS = LowerBoundSlider.Nub.ScreenSpaceDrawQuad.TopLeft; + var upperSS = UpperBoundSlider.Nub.ScreenSpaceDrawQuad.TopLeft; + + return lowerSS + (upperSS - lowerSS) / 2; + } + } + public ShearedRangeSlider(LocalisableString label) { this.label = label; } + // Special case: we want to limit input to the bounds of this control but not enable masking (which would break with shear). + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) + => ReceivePositionalInputAt(screenSpacePos); + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -165,10 +181,11 @@ namespace osu.Game.Graphics.UserInterface UpperBoundSlider.Current.ValueChanged += max => LowerBoundSlider.Current.Value = Math.Min(max.NewValue - minRange, LowerBoundSlider.Current.Value); } - protected virtual BoundSliderBar CreateBoundSlider(bool isUpper) => new BoundSliderBar(isUpper); + protected virtual BoundSliderBar CreateBoundSlider(bool isUpper) => new BoundSliderBar(this, isUpper); protected partial class BoundSliderBar : ShearedSliderBar { + private readonly ShearedRangeSlider rangeSlider; private readonly bool isUpper; public new ShearedNub Nub => base.Nub; @@ -188,8 +205,9 @@ namespace osu.Game.Graphics.UserInterface public override bool AcceptsFocus => false; - public BoundSliderBar(bool isUpper) + public BoundSliderBar(ShearedRangeSlider rangeSlider, bool isUpper) { + this.rangeSlider = rangeSlider; this.isUpper = isUpper; } @@ -238,9 +256,9 @@ namespace osu.Game.Graphics.UserInterface public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { if (isUpper) - return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X >= Nub.ScreenSpaceDrawQuad.TopLeft.X; + return screenSpacePos.X > rangeSlider.ScreenSpaceHalfwayPoint.X; - return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X <= Nub.ScreenSpaceDrawQuad.TopRight.X; + return screenSpacePos.X <= rangeSlider.ScreenSpaceHalfwayPoint.X; } protected override bool OnHover(HoverEvent e) From 10c546dcedafc317238956b88dd369bec4de0eeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 16:00:00 +0900 Subject: [PATCH 082/152] Fix masking bleed --- .../UserInterface/ShearedRangeSlider.cs | 14 +++++- .../UserInterface/ShearedSliderBar.cs | 43 ++++++++----------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs index a4c1b93810..45c8063f4c 100644 --- a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs @@ -196,8 +196,6 @@ namespace osu.Game.Graphics.UserInterface public float NubWidth { get; set; } = ShearedNub.HEIGHT; - public new float NormalizedValue => base.NormalizedValue; - public override LocalisableString TooltipText => (Current.IsDefault ? DefaultTooltip : Current.Value.ToString($@"0.## {TooltipSuffix}")) ?? Current.Value.ToString($@"0.## {TooltipSuffix}"); @@ -261,6 +259,18 @@ namespace osu.Game.Graphics.UserInterface return screenSpacePos.X <= rangeSlider.ScreenSpaceHalfwayPoint.X; } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (isUpper) + { + // Only draw left box where required to avoid masking bleed issues. + LeftBox.X = ToParentSpace(ToLocalSpace(rangeSlider.LowerBoundSlider.Nub.ScreenSpaceDrawQuad.Centre)).X; + LeftBox.Size -= new Vector2(LeftBox.X, 0); + } + } + protected override bool OnHover(HoverEvent e) { base.OnHover(e); diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index cdbf768b1c..9404b813f9 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -73,33 +73,25 @@ namespace osu.Game.Graphics.UserInterface mainContent = new Container { RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 5, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Child = new Container + Masking = true, + CornerRadius = 5, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Masking = true, - CornerRadius = 5, - Children = new Drawable[] + LeftBox = new Box { - LeftBox = new Box - { - EdgeSmoothness = new Vector2(0, 0.5f), - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - RightBox = new Box - { - EdgeSmoothness = new Vector2(0, 0.5f), - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }, + EdgeSmoothness = new Vector2(0, 0.5f), + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + RightBox = new Box + { + EdgeSmoothness = new Vector2(0, 0.5f), + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, }, }, }, @@ -200,8 +192,9 @@ namespace osu.Game.Graphics.UserInterface protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1); - RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - RangePadding - Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1); + + LeftBox.Size = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1); + RightBox.Size = new Vector2(Math.Clamp(DrawWidth - RangePadding - Nub.DrawPosition.X - Nub.DrawWidth / 2f + ShearedNub.CORNER_RADIUS - 0.5f, 0, Math.Max(0, DrawWidth)), 1); } protected override void UpdateValue(float value) From a39773747653936aad2e8d441de96871e8aed268 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 16:04:26 +0900 Subject: [PATCH 083/152] Move `UserAdjustedSettingsCount` local to `ModDifficultyAdjust` in absence of other usages --- osu.Game/Rulesets/Mods/Mod.cs | 21 ------------------- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 56a4aa7a50..727db913e2 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -75,27 +75,6 @@ namespace osu.Game.Rulesets.Mods } } - /// - /// The number of settings on this mod instance which have been adjusted by the user from their default values. - /// - public int UserAdjustedSettingsCount - { - get - { - int count = 0; - - foreach (var (_, property) in this.GetSettingsSourceProperties()) - { - var bindable = (IBindable)property.GetValue(this)!; - - if (!bindable.IsDefault) - count++; - } - - return count; - } - } - /// /// The score multiplier of this mod. /// diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 0c1a4ab589..15ce583413 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -111,5 +111,26 @@ namespace osu.Game.Rulesets.Mods if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value; if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value; } + + /// + /// The number of settings on this mod instance which have been adjusted by the user from their default values. + /// + protected int UserAdjustedSettingsCount + { + get + { + int count = 0; + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var bindable = (IBindable)property.GetValue(this)!; + + if (!bindable.IsDefault) + count++; + } + + return count; + } + } } } From d491b6872e89711fc89c995c6693a5c255cd0145 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 07:46:17 +0300 Subject: [PATCH 084/152] Fix song select test scenes not given a width when running tests individually --- .../Visual/SongSelectV2/SongSelectComponentsTestScene.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index f86ca869e1..843d65b7f8 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -23,7 +23,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }; private Container? resizeContainer; - private float relativeWidth; protected virtual Anchor ComponentAnchor => Anchor.TopLeft; protected virtual float InitialRelativeWidth => 0.5f; @@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Origin = ComponentAnchor, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Width = relativeWidth, + Width = InitialRelativeWidth, Child = Content } }; @@ -49,8 +48,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { if (resizeContainer != null) resizeContainer.Width = v; - - relativeWidth = v; }); } From 959ab11862175d4c7f727f8795dbd831c5ef40f1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 26 Apr 2025 18:34:12 +0300 Subject: [PATCH 085/152] Fix incorrect handling of beatmap with local diffs in metadata wedge Also hides ranking/failtime wedges on locally created difficulties --- .../TestSceneBeatmapMetadataWedge.cs | 34 +++++++++++++++++++ .../Screens/SelectV2/BeatmapMetadataWedge.cs | 8 ++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs index be2e6eb9bf..f2d4fad69e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs @@ -148,6 +148,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + [Test] + public void TestOnlineAvailability() + { + AddStep("online beatmapset", () => + { + var (working, onlineSet) = createTestBeatmap(); + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddUntilStep("rating wedge visible", () => wedge.RatingsVisible); + AddUntilStep("fail time wedge visible", () => wedge.FailRetryVisible); + AddStep("online beatmapset with local diff", () => + { + var (working, onlineSet) = createTestBeatmap(); + + working.BeatmapInfo.ResetOnlineInfo(); + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddUntilStep("rating wedge hidden", () => !wedge.RatingsVisible); + AddUntilStep("fail time wedge hidden", () => !wedge.FailRetryVisible); + AddStep("local beatmap", () => + { + var (working, _) = createTestBeatmap(); + + currentOnlineSet = null; + Beatmap.Value = working; + }); + AddAssert("rating wedge still hidden", () => !wedge.RatingsVisible); + AddAssert("fail time wedge still hidden", () => !wedge.FailRetryVisible); + } + private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap() { var working = CreateWorkingBeatmap(Ruleset.Value); diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index 816dfc3f95..69c24aa5df 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -35,6 +35,9 @@ namespace osu.Game.Screens.SelectV2 private Drawable failRetryWedge = null!; private FailRetryDisplay failRetryDisplay = null!; + public bool RatingsVisible => ratingsWedge.Alpha > 0; + public bool FailRetryVisible => failRetryWedge.Alpha > 0; + protected override bool StartHidden => true; [Resolved] @@ -250,7 +253,10 @@ namespace osu.Game.Screens.SelectV2 // We could consider hiding individual wedges based on zero data in the future. // Needs some experimentation on what looks good. - if (State.Value == Visibility.Visible && currentOnlineBeatmapSet != null) + var beatmapInfo = beatmap.Value.BeatmapInfo; + var currentOnlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID); + + if (State.Value == Visibility.Visible && currentOnlineBeatmap != null) { ratingsWedge.FadeIn(transition_duration, Easing.OutQuint) .MoveToX(0, transition_duration, Easing.OutQuint); From be913927602f0e96f3e4349aa2c119fb6e7c89ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Apr 2025 09:57:30 +0200 Subject: [PATCH 086/152] Fix badly phrased comment --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index c09986f508..15b6d3a0bb 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -117,8 +117,8 @@ namespace osu.Game.Screens.Ranking } } - // there's a non-zero chance that the `Score`'s `ScorePosition` was mutated above, - // but the two are not actually coupled together in any way, + // there's a non-zero chance that the `Score.Position` was mutated above, + // but that is not actually coupled to `ScorePosition` of the relevant score panel in any way, // so ensure that the drawable panel also receives the updated position. // note that this is valid to do precisely because we ensured `Score` was in `sortedScores` earlier. ScorePanelList.GetPanelForScore(Score).ScorePosition.Value = Score.Position; From 84cb4da1ecc278e8708dba59da2ad6c6bc5dd0e0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 11:08:30 +0300 Subject: [PATCH 087/152] Limit input inside slider bar pieces instead --- osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs index 45c8063f4c..7b90f35c56 100644 --- a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs @@ -92,10 +92,6 @@ namespace osu.Game.Graphics.UserInterface this.label = label; } - // Special case: we want to limit input to the bounds of this control but not enable masking (which would break with shear). - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - => ReceivePositionalInputAt(screenSpacePos); - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -254,9 +250,9 @@ namespace osu.Game.Graphics.UserInterface public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { if (isUpper) - return screenSpacePos.X > rangeSlider.ScreenSpaceHalfwayPoint.X; + return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X > rangeSlider.ScreenSpaceHalfwayPoint.X; - return screenSpacePos.X <= rangeSlider.ScreenSpaceHalfwayPoint.X; + return base.ReceivePositionalInputAt(screenSpacePos) && screenSpacePos.X <= rangeSlider.ScreenSpaceHalfwayPoint.X; } protected override void UpdateAfterChildren() From b2032f95ff9e86a8cbd85f93ab1b75af28969b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Apr 2025 10:25:05 +0200 Subject: [PATCH 088/152] Cross-reference copies of similar logic --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 3 +++ .../Select/Leaderboards/SoloGameplayLeaderboardProvider.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 15b6d3a0bb..8ef083d287 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -95,6 +95,9 @@ namespace osu.Game.Screens.Ranking { var sortedScore = sortedScores[i]; + // see `SoloGameplayLeaderboardProvider.sort()` for another place that does the same thing with slight deviations + // if this code is changed, that code should probably be changed as well + if (!isPartialLeaderboard) sortedScore.Position = i + 1; else diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 41d57f7d24..d17d55e4dd 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -70,6 +70,9 @@ namespace osu.Game.Screens.Select.Leaderboards { var score = orderedByScore[i]; + // see `SoloResultsScreen.FetchScores()` for another place that does the same thing with slight deviations + // if this code is changed, that code should probably be changed as well + score.DisplayOrder.Value = i + 1; // if we know we have all scores there can ever be, we can do the simple and obvious thing. From 4d0925b85d62415e4fea66d5691c72730fee7574 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 26 Apr 2025 18:50:04 +0300 Subject: [PATCH 089/152] Add user tags support --- .../Screens/SelectV2/BeatmapMetadataWedge.cs | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index ae9222033e..da9d5fe89b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.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 System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -23,7 +24,8 @@ namespace osu.Game.Screens.SelectV2 private MetadataDisplay source = null!; private MetadataDisplay genre = null!; private MetadataDisplay language = null!; - private MetadataDisplay tag = null!; + private MetadataDisplay userTags = null!; + private MetadataDisplay mapperTags = null!; private MetadataDisplay submitted = null!; private MetadataDisplay ranked = null!; @@ -95,6 +97,8 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0f, 10f), + AutoSizeDuration = (float)transition_duration / 3, + AutoSizeEasing = Easing.OutQuint, Children = new Drawable[] { new GridContainer @@ -151,7 +155,11 @@ namespace osu.Game.Screens.SelectV2 }, }, }, - tag = new MetadataDisplay("Tags"), + userTags = new MetadataDisplay("User Tags") + { + Alpha = 0, + }, + mapperTags = new MetadataDisplay("Mapper Tags"), }, }, }, @@ -288,7 +296,7 @@ namespace osu.Game.Screens.SelectV2 else source.Data = ("-", null); - tag.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t)); + mapperTags.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t)); submitted.Date = beatmapSetInfo.DateSubmitted; ranked.Date = beatmapSetInfo.DateRanked; @@ -357,7 +365,34 @@ namespace osu.Game.Screens.SelectV2 } } + updateUserTags(); updateSubWedgeVisibility(); } + + private void updateUserTags() + { + var beatmapInfo = beatmap.Value.BeatmapInfo; + var onlineBeatmapSet = currentOnlineBeatmapSet; + var onlineBeatmap = onlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID); + + if (onlineBeatmap?.TopTags == null || onlineBeatmap.TopTags.Length == 0 || onlineBeatmapSet?.RelatedTags == null) + { + userTags.FadeOut(transition_duration, Easing.OutQuint); + return; + } + + var tagsById = onlineBeatmapSet.RelatedTags.ToDictionary(t => t.Id); + string[] userTagsArray = onlineBeatmap.TopTags + .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId))) + .Where(t => t.relatedTag != null) + // see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria + .OrderByDescending(t => t.topTag.VoteCount) + .ThenBy(t => t.relatedTag!.Name) + .Select(t => t.relatedTag!.Name) + .ToArray(); + + userTags.FadeIn(transition_duration, Easing.OutQuint); + userTags.Tags = (userTagsArray, t => songSelect?.Search(t)); + } } } From e54b7962f4cb542948eb23fd88afe44c27c6e124 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 26 Apr 2025 18:50:10 +0300 Subject: [PATCH 090/152] Add test coverage --- .../TestSceneBeatmapMetadataWedge.cs | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs index f2d4fad69e..3cdb513b38 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs @@ -142,6 +142,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3)); onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" }; onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" }; + onlineSet.Beatmaps.Single().TopTags = Enumerable.Repeat(onlineSet.Beatmaps.Single().TopTags, 3).SelectMany(t => t!).ToArray(); currentOnlineSet = onlineSet; Beatmap.Value = working; @@ -182,6 +183,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("fail time wedge still hidden", () => !wedge.FailRetryVisible); } + [Test] + public void TestUserTags() + { + AddStep("user tags", () => + { + var (working, onlineSet) = createTestBeatmap(); + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no user tags", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Beatmaps.Single().TopTags = null; + onlineSet.RelatedTags = null; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + } + private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap() { var working = CreateWorkingBeatmap(Ruleset.Value); @@ -198,13 +221,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2 OnlineID = working.BeatmapInfo.OnlineID, PlayCount = 10000, PassCount = 4567, + TopTags = + [ + new APIBeatmapTag { TagId = 4, VoteCount = 1 }, + new APIBeatmapTag { TagId = 2, VoteCount = 1 }, + new APIBeatmapTag { TagId = 23, VoteCount = 5 }, + ], FailTimes = new APIFailTimes { Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), }, }, - } + }, + RelatedTags = + [ + new APITag + { + Id = 2, + Name = "song representation/simple", + Description = "Accessible and straightforward map design." + }, + new APITag + { + Id = 4, + Name = "style/clean", + Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects." + }, + new APITag + { + Id = 23, + Name = "aim/aim control", + Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern." + } + ] }; working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now; From 8576ef247f7fea70d732214cc0ca85bc53dbd3f4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:48:49 -0400 Subject: [PATCH 091/152] Add `ShearedSearchTextBox` variant with "N matches" note --- .../TestSceneShearedSearchTextBox.cs | 32 ++++++++--- .../UserInterface/ShearedFilterTextBox.cs | 54 +++++++++++++++++++ .../UserInterface/ShearedSearchTextBox.cs | 44 ++++++++------- 3 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/ShearedFilterTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs index f3a7f1481a..d4141f2b64 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs @@ -5,8 +5,10 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { @@ -30,16 +32,32 @@ namespace osu.Game.Tests.Visual.UserInterface { (typeof(OverlayColourProvider), colourProvider) }, - Children = new Drawable[] + Child = new FillFlowContainer { - new ShearedSearchTextBox + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Width = 0.5f + new ShearedSearchTextBox + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Width = 0.5f + }, + new ShearedFilterTextBox + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Width = 0.5f, + FilterText = "12345 matches", + }, } - } + }, }; } } diff --git a/osu.Game/Graphics/UserInterface/ShearedFilterTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedFilterTextBox.cs new file mode 100644 index 0000000000..cffe34650c --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ShearedFilterTextBox.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics.UserInterface +{ + public partial class ShearedFilterTextBox : ShearedSearchTextBox + { + private const float filter_text_size = 12; + + public LocalisableString FilterText + { + get => ((InnerFilterTextBox)TextBox).FilterText.Text; + set => Schedule(() => ((InnerFilterTextBox)TextBox).FilterText.Text = value); + } + + public ShearedFilterTextBox() + { + Height += filter_text_size; + } + + protected override InnerSearchTextBox CreateInnerTextBox() => new InnerFilterTextBox(); + + protected partial class InnerFilterTextBox : InnerSearchTextBox + { + public OsuSpriteText FilterText { get; private set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + TextContainer.Add(FilterText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + Font = OsuFont.Torus.With(size: filter_text_size, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Top = 2, Left = -1 }, + Colour = colours.Yellow + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + TextContainer.Height *= (DrawHeight - filter_text_size) / DrawHeight; + TextContainer.Margin = new MarginPadding { Bottom = filter_text_size }; + } + } + } +} diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index f5fbb3411f..b1b93dcbca 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -21,33 +21,33 @@ namespace osu.Game.Graphics.UserInterface private const float corner_radius = 7; private readonly Box background; - private readonly SearchTextBox textBox; + protected readonly InnerSearchTextBox TextBox; public Bindable Current { - get => textBox.Current; - set => textBox.Current = value; + get => TextBox.Current; + set => TextBox.Current = value; } public bool HoldFocus { - get => textBox.HoldFocus; - set => textBox.HoldFocus = value; + get => TextBox.HoldFocus; + set => TextBox.HoldFocus = value; } public LocalisableString PlaceholderText { - get => textBox.PlaceholderText; - set => textBox.PlaceholderText = value; + get => TextBox.PlaceholderText; + set => TextBox.PlaceholderText = value; } - public new bool HasFocus => textBox.HasFocus; + public new bool HasFocus => TextBox.HasFocus; - public void TakeFocus() => textBox.TakeFocus(); + public void TakeFocus() => TextBox.TakeFocus(); - public void KillFocus() => textBox.KillFocus(); + public void KillFocus() => TextBox.KillFocus(); - public bool SelectAll() => textBox.SelectAll(); + public bool SelectAll() => TextBox.SelectAll(); public ShearedSearchTextBox() { @@ -69,13 +69,7 @@ namespace osu.Game.Graphics.UserInterface { new Drawable[] { - textBox = new InnerSearchTextBox - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Size = Vector2.One - }, + TextBox = CreateInnerTextBox(), new SpriteIcon { Icon = FontAwesome.Solid.Search, @@ -101,10 +95,20 @@ namespace osu.Game.Graphics.UserInterface background.Colour = colourProvider.Background3; } - public override bool HandleNonPositionalInput => textBox.HandleNonPositionalInput; + public override bool HandleNonPositionalInput => TextBox.HandleNonPositionalInput; - private partial class InnerSearchTextBox : SearchTextBox + protected virtual InnerSearchTextBox CreateInnerTextBox() => new InnerSearchTextBox(); + + protected partial class InnerSearchTextBox : SearchTextBox { + public InnerSearchTextBox() + { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + RelativeSizeAxes = Axes.Both; + Size = Vector2.One; + } + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { From 9d3ee2a57340e6935ce6cd278f5cabbcd245b19f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:50:15 -0400 Subject: [PATCH 092/152] Add song select filter control --- .../TestSceneBeatmapFilterControl.cs | 31 +++ .../TestSceneDifficultyRangeSlider.cs | 69 +++++++ .../UserInterface/ShearedRangeSlider.cs | 2 + osu.Game/Localisation/UserInterfaceStrings.cs | 5 + osu.Game/Screens/SelectV2/FilterControl.cs | 182 ++++++++++++++++++ .../FilterControl_DifficultyRangeSlider.cs | 171 ++++++++++++++++ 6 files changed, 460 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs create mode 100644 osu.Game/Screens/SelectV2/FilterControl.cs create mode 100644 osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs new file mode 100644 index 0000000000..df7e5ee645 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapFilterControl : SongSelectComponentsTestScene + { + protected override Anchor ComponentAnchor => Anchor.TopRight; + protected override float InitialRelativeWidth => 0.7f; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new FilterControl + { + State = { Value = Visibility.Visible }, + RelativeSizeAxes = Axes.X, + }, + }; + }); + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs new file mode 100644 index 0000000000..3cadbeb1e3 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneDifficultyRangeSlider : ThemeComparisonTestScene + { + private readonly BindableNumber customStart = new BindableNumber + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f + }; + + private readonly BindableNumber customEnd = new BindableNumber(10) + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f + }; + + public TestSceneDifficultyRangeSlider() + : base(false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + new FilterControl.DifficultyRangeSlider + { + Width = 600, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1), + LowerBound = customStart, + UpperBound = customEnd, + TooltipSuffix = "suffix", + NubWidth = 32, + MinRange = 0.1f, + } + } + }; + } +} diff --git a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs index 7b90f35c56..3aaa143987 100644 --- a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs @@ -184,6 +184,8 @@ namespace osu.Game.Graphics.UserInterface private readonly ShearedRangeSlider rangeSlider; private readonly bool isUpper; + public new float NormalizedValue => base.NormalizedValue; + public new ShearedNub Nub => base.Nub; public string? DefaultString; diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs index dceedca05c..95d0a4a9ec 100644 --- a/osu.Game/Localisation/UserInterfaceStrings.cs +++ b/osu.Game/Localisation/UserInterfaceStrings.cs @@ -84,6 +84,11 @@ namespace osu.Game.Localisation /// public static LocalisableString RightMouseScroll => new TranslatableString(getKey(@"right_mouse_scroll"), @"Right mouse drag to absolute scroll"); + /// + /// "Show converts" + /// + public static LocalisableString ShowConverts => new TranslatableString(getKey(@"show_converts"), @"Show converts"); + /// /// "Show converted beatmaps" /// diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs new file mode 100644 index 0000000000..bb795e5717 --- /dev/null +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -0,0 +1,182 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.Select.Filter; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class FilterControl : OverlayContainer + { + // taken from draw visualiser. used for carousel alignment purposes. + public const float HEIGHT_FROM_SCREEN_TOP = 141 - corner_radius; + + private const float corner_radius = 8; + + private ShearedToggleButton showConvertedBeatmapsButton = null!; + private DifficultyRangeSlider difficultyRangeSlider = null!; + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Shear = OsuGame.SHEAR; + Margin = new MarginPadding { Top = -corner_radius, Right = -40 }; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = corner_radius, + Masking = true, + Child = new WedgeBackground + { + Anchor = Anchor.TopRight, + Scale = new Vector2(-1, 1), + } + }, + new ReverseChildIDFillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Padding = new MarginPadding { Top = corner_radius + 5, Bottom = 2, Right = 40f, Left = 2f }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + Child = new SongSelectSearchTextBox + { + RelativeSizeAxes = Axes.X, + HoldFocus = true, + // TODO: pending implementation + FilterText = "12345 matches", + }, + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute), // can probably be removed? + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + difficultyRangeSlider = new DifficultyRangeSlider + { + RelativeSizeAxes = Axes.X, + MinRange = 0.1f, + }, + Empty(), + showConvertedBeatmapsButton = new ShearedToggleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = UserInterfaceStrings.ShowConverts, + Height = 30f, + }, + }, + } + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + Height = 30, + Shear = -OsuGame.SHEAR, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(maxSize: 210), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: 230), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(), + }, + Content = new[] + { + new[] + { + new ShearedDropdown(SortStrings.Default) + { + RelativeSizeAxes = Axes.X, + Items = Enum.GetValues(), + }, + Empty(), + // todo: pending localisation + new ShearedDropdown("Group by") + { + RelativeSizeAxes = Axes.X, + Items = Enum.GetValues(), + }, + Empty(), + new CollectionDropdown + { + RelativeSizeAxes = Axes.X, + }, + } + } + }, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + difficultyRangeSlider.LowerBound = config.GetBindable(OsuSetting.DisplayStarsMinimum); + difficultyRangeSlider.UpperBound = config.GetBindable(OsuSetting.DisplayStarsMaximum); + config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmapsButton.Active); + } + + protected override void PopIn() + { + this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + protected override void PopOut() + { + this.MoveToX(150, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + private partial class SongSelectSearchTextBox : ShearedFilterTextBox + { + protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox(); + + private partial class InnerTextBox : InnerFilterTextBox + { + public override bool HandleLeftRightArrows => false; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs b/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs new file mode 100644 index 0000000000..58c9c60460 --- /dev/null +++ b/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs @@ -0,0 +1,171 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Utils; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class FilterControl + { + public partial class DifficultyRangeSlider : ShearedRangeSlider + { + private Container borderContainer = null!; + + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + + private static readonly (float, Color4)[] spectrum = OsuColour.STAR_DIFFICULTY_SPECTRUM + .Skip(1) + .Prepend((0.0f, OsuColour.STAR_DIFFICULTY_SPECTRUM.ElementAt(1).Item2)).ToArray(); + + public DifficultyRangeSlider() + : base("Star Rating") + { + NubWidth = ShearedNub.HEIGHT * 1.16f; + TooltipSuffix = "stars"; + DefaultStringLowerBound = "0.0"; + DefaultStringUpperBound = "∞"; + DefaultTooltipUpperBound = UserInterfaceStrings.NoLimit; + + AddLayout(drawSizeLayout); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + SliderContainer.AddRange(new Drawable[] + { + new Container + { + Depth = 1, + RelativeSizeAxes = Axes.Both, + Shear = OsuGame.SHEAR, + CornerRadius = 5f, + Masking = true, + ChildrenEnumerable = spectrum.Zip(spectrum.Skip(1)) + .Select(p => new Box + { + RelativePositionAxes = Axes.X, + X = p.First.Item1 / 10f, + RelativeSizeAxes = Axes.Both, + Width = (p.Second.Item1 - p.First.Item1) / 10f, + Colour = ColourInfo.GradientHorizontal(p.First.Item2, p.Second.Item2), + }), + }, + borderContainer = new Container + { + Depth = -1, + RelativePositionAxes = Axes.X, + RelativeSizeAxes = Axes.Both, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + BorderColour = colourProvider.Highlight1, + BorderThickness = 2, + Masking = true, + Shear = OsuGame.SHEAR, + CornerRadius = 5f, + Child = new Box + { + Colour = Color4.Transparent, + RelativeSizeAxes = Axes.Both, + } + }, + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LowerBoundSlider.Current.ValueChanged += _ => updateBorderDisplay(false); + UpperBoundSlider.Current.ValueChanged += _ => updateBorderDisplay(false); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (!drawSizeLayout.IsValid) + { + updateBorderDisplay(true); + drawSizeLayout.Validate(); + } + } + + private void updateBorderDisplay(bool instant) + { + float borderStart = LowerBoundSlider.NormalizedValue * LowerBoundSlider.UsableWidth / LowerBoundSlider.DrawWidth; + float borderEnd = UpperBoundSlider.NormalizedValue * UpperBoundSlider.UsableWidth / UpperBoundSlider.DrawWidth; + borderEnd += UpperBoundSlider.NubWidth / UpperBoundSlider.DrawWidth; + + borderContainer.MoveToX(borderStart, instant ? 0 : 250, Easing.OutQuint); + borderContainer.ResizeWidthTo(borderEnd - borderStart, instant ? 0 : 250, Easing.OutQuint); + } + + protected override BoundSliderBar CreateBoundSlider(bool isUpper) => new DifficultyBoundSliderBar(this, isUpper); + + private partial class DifficultyBoundSliderBar : BoundSliderBar + { + private readonly bool isUpper; + + protected override bool FocusIndicator => false; + + public DifficultyBoundSliderBar(ShearedRangeSlider slider, bool isUpper) + : base(slider, isUpper) + { + this.isUpper = isUpper; + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (isUpper) + { + LeftBox.Colour = OsuColour.Gray(0.4f).Opacity(0.2f); + RightBox.Colour = OsuColour.Gray(0.05f).Opacity(0.7f); + } + else + { + LeftBox.Colour = OsuColour.Gray(0.05f).Opacity(0.7f); + RightBox.Colour = OsuColour.Gray(0.4f).Opacity(0.2f); + } + } + + protected override void UpdateDisplay(double value) + { + Colour4 nubColour = ColourUtils.SampleFromLinearGradient(spectrum, (float)Math.Round(value, 2, MidpointRounding.AwayFromZero)); + nubColour = nubColour.Lighten(0.4f); + + if (value >= 8.0) + nubColour = colours.Gray4; + + Nub.AccentColour = nubColour; + Nub.GlowingAccentColour = nubColour.Lighten(0.2f); + Nub.ShadowColour = Color4.Black.Opacity(0.2f); + NubText.Colour = OsuColour.ForegroundTextColourFor(nubColour); + + base.UpdateDisplay(value); + } + } + } + } +} From 437b1fa70fb676e4aec68ad857e94df573220767 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:47:11 -0400 Subject: [PATCH 093/152] Add beatmap details/rankings area drawable This intentionally removes shear specification from root level of `BeatmapTitleWedge` since shearing is moved one level higher (see fill flow containing `BeatmapTitleWedge` in `SongSelect`). --- .../Screens/SelectV2/BeatmapDetailsArea.cs | 100 +++++++++++++ .../SelectV2/BeatmapDetailsArea_Header.cs | 139 ++++++++++++++++++ .../BeatmapDetailsArea_WedgeSelector.cs | 123 ++++++++++++++++ .../Screens/SelectV2/BeatmapTitleWedge.cs | 1 - 4 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/SelectV2/BeatmapDetailsArea.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapDetailsArea_WedgeSelector.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea.cs new file mode 100644 index 0000000000..99e3155a7a --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapDetailsArea.cs @@ -0,0 +1,100 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// The left portion of the song select screen which houses the metadata or leaderboards wedge, along with controls + /// to switch between them and adjust specifics. + /// + public partial class BeatmapDetailsArea : VisibilityContainer + { + private Header header = null!; + private Container contentContainer = null!; + + public BeatmapDetailsArea() + { + RelativeSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load() + { + const float header_height = 35f; + + InternalChildren = new Drawable[] + { + new ShearAligningWrapper(header = new Header + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + Height = header_height, + }), + new ShearAligningWrapper(contentContainer = new Container + { + Shear = -OsuGame.SHEAR, + Padding = new MarginPadding { Top = header_height }, + RelativeSizeAxes = Axes.Both, + }) + { + Depth = 1f, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + header.Type.BindValueChanged(_ => updateDisplay(), true); + } + + protected override void PopIn() + { + this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + protected override void PopOut() + { + this.MoveToX(-150, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + private Drawable? currentContent; + + private void updateDisplay() + { + if (currentContent != null) + { + currentContent.Hide(); + currentContent.Expire(); + } + + switch (header.Type.Value) + { + default: + case Header.Selection.Details: + currentContent = new BeatmapMetadataWedge(); + break; + + case Header.Selection.Ranking: + currentContent = new BeatmapLeaderboardWedge + { + Scope = { BindTarget = header.Scope }, + FilterBySelectedMods = { BindTarget = header.FilterBySelectedMods }, + }; + + break; + } + + contentContainer.Add(currentContent); + currentContent.Show(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs new file mode 100644 index 0000000000..73e964faf7 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs @@ -0,0 +1,139 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Screens.Select.Leaderboards; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapDetailsArea + { + public partial class Header : CompositeDrawable + { + private WedgeSelector tabControl = null!; + private FillFlowContainer leaderboardControls = null!; + + private ShearedDropdown scopeDropdown = null!; + private ShearedToggleButton selectedModsToggle = null!; + + public IBindable Type => tabControl.Current; + + public IBindable Scope => scopeDropdown.Current; + + public IBindable FilterBySelectedMods => selectedModsToggle.Active; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 20f }, + Children = new Drawable[] + { + tabControl = new WedgeSelector(20f) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 200, + Height = 22, + Margin = new MarginPadding { Top = 2f }, + }, + leaderboardControls = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5f, 0f), + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(128f, 30f), + Child = selectedModsToggle = new ShearedToggleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = @"Selected Mods", + Height = 30, + }, + }, + // new Container + // { + // Anchor = Anchor.CentreRight, + // Origin = Anchor.CentreRight, + // Size = new Vector2(150f, 33f), + // Child = new ShearedDropdown(@"Sort") + // { + // Width = 150f, + // Items = Enum.GetValues(), + // }, + // }, + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(160f, 32f), + Child = scopeDropdown = new ScopeDropdown + { + Width = 160f, + Current = { Value = BeatmapLeaderboardScope.Global }, + }, + }, + }, + }, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + tabControl.Current.BindValueChanged(v => + { + leaderboardControls.FadeTo(v.NewValue == Selection.Ranking ? 1 : 0, 300, Easing.OutQuint); + }, true); + } + + public enum Selection + { + Details, + Ranking, + } + + // public enum RankingsSort + // { + // Score, + // Accuracy, + // Combo, + // Misses, + // Date, + // } + + private partial class ScopeDropdown : ShearedDropdown + { + public ScopeDropdown() + : base("Scope") + { + Items = Enum.GetValues(); + } + + protected override LocalisableString GenerateItemText(BeatmapLeaderboardScope item) => item.ToString(); + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_WedgeSelector.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_WedgeSelector.cs new file mode 100644 index 0000000000..7509c3115a --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_WedgeSelector.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapDetailsArea + { + public partial class WedgeSelector : TabControl + where T : struct, Enum + { + private Circle strip = null!; + + protected override Dropdown? CreateDropdown() => null; + + protected override TabItem CreateTabItem(T value) => new TabItem(value); + + protected new TabItem SelectedTab => (TabItem)base.SelectedTab; + + public WedgeSelector(float spacing) + { + TabContainer.Spacing = new Vector2(spacing, 0f); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AddInternal(strip = new Circle + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Height = 2, + Colour = colourProvider.Highlight1, + }); + + foreach (var type in Enum.GetValues()) + AddItem(type); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateDisplay()); + + ScheduleAfterChildren(() => + { + updateDisplay(); + FinishTransforms(true); + }); + } + + private void updateDisplay() + { + strip.MoveToX(SelectedTab.Text.ToSpaceOfOtherDrawable(Vector2.Zero, this).X, 300, Easing.OutQuint); + strip.ResizeWidthTo(SelectedTab.Text.Width, 0, Easing.OutQuint); + } + + protected partial class TabItem : TabItem + { + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public readonly OsuSpriteText Text; + + public TabItem(T value) + : base(value) + { + AutoSizeAxes = Axes.Both; + + Children = new[] + { + Text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = value.ToString(), + Font = OsuFont.Style.Body, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateDisplay(); + } + + protected override void OnActivated() => updateDisplay(); + + protected override void OnDeactivated() => updateDisplay(); + + protected override bool OnHover(HoverEvent e) + { + updateDisplay(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) => updateDisplay(); + + private void updateDisplay() + { + if (Active.Value || IsHovered) + Text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint); + else + Text.FadeColour(colourProvider.Content2, 300, Easing.OutQuint); + + Text.Font = Text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular); + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 26294140a8..154374cbcb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -84,7 +84,6 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - Shear = OsuGame.SHEAR; Masking = true; CornerRadius = corner_radius; From dc4b0f8df1735d06fc01fd1dfc9dd0cebad1f3ef Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 06:05:23 -0400 Subject: [PATCH 094/152] Integrate all subcomponents with the main screen --- osu.Game/Screens/SelectV2/SongSelect.cs | 140 +++++++++++++++++++----- 1 file changed, 115 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index ca09b2a40a..3144168712 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -3,14 +3,21 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -21,12 +28,13 @@ namespace osu.Game.Screens.SelectV2 public abstract partial class SongSelect : OsuScreen { private const float logo_scale = 0.4f; + private const double fade_duration = 300; public const float WEDGE_CONTENT_MARGIN = CORNER_RADIUS_HIDE_OFFSET + OsuGame.SCREEN_EDGE_MARGIN; public const float CORNER_RADIUS_HIDE_OFFSET = 20f; public const float ENTER_DURATION = 600; - private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine) + private readonly ModSelectOverlay modSelectOverlay = new UserModSelectOverlay(OverlayColourScheme.Aquamarine) { ShowPresets = true, }; @@ -36,6 +44,11 @@ namespace osu.Game.Screens.SelectV2 private BeatmapCarousel carousel = null!; + private FilterControl filterControl = null!; + private BeatmapTitleWedge titleWedge = null!; + private BeatmapDetailsArea detailsArea = null!; + private FillFlowContainer wedgesContainer = null!; + public override bool ShowFooter => true; [Resolved] @@ -46,33 +59,89 @@ namespace osu.Game.Screens.SelectV2 { AddRangeInternal(new Drawable[] { - new GridContainer // used for max width implementation + new Box { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] + Width = 0.5f, + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0f)), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, + Child = new PopoverContainer { - new Dimension(), - new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750), - }, - Content = new[] - { - new[] + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Empty(), - new Container + new GridContainer // used for max width implementation { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, - Child = carousel = new BeatmapCarousel + ColumnDimensions = new[] { - RequestPresentBeatmap = _ => OnStart(), - RelativeSizeAxes = Axes.Both + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 850), + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750), }, + Content = new[] + { + new[] + { + wedgesContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Margin = new MarginPadding + { + Top = -CORNER_RADIUS_HIDE_OFFSET, + Left = -CORNER_RADIUS_HIDE_OFFSET + }, + Spacing = new Vector2(0f, 4f), + Direction = FillDirection.Vertical, + Shear = OsuGame.SHEAR, + Children = new Drawable[] + { + new ShearAligningWrapper(titleWedge = new BeatmapTitleWedge()), + new ShearAligningWrapper(detailsArea = new BeatmapDetailsArea()), + }, + }, + Empty(), + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new CompositeDrawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Top = FilterControl.HEIGHT_FROM_SCREEN_TOP + 5, + Bottom = 5, + }, + Children = new Drawable[] + { + carousel = new BeatmapCarousel + { + BleedTop = FilterControl.HEIGHT_FROM_SCREEN_TOP + 5, + BleedBottom = ScreenFooter.HEIGHT + 5, + RequestPresentBeatmap = _ => OnStart(), + RelativeSizeAxes = Axes.Both, + }, + } + }, + filterControl = new FilterControl + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + }, + } + }, + }, + } }, } - } + }, }, modSelectOverlay, }); @@ -98,34 +167,44 @@ namespace osu.Game.Screens.SelectV2 public override void OnEntering(ScreenTransitionEvent e) { + base.OnEntering(e); + this.FadeIn(); + titleWedge.Show(); + detailsArea.Show(); + filterControl.Show(); + modSelectOverlay.SelectedMods.BindTo(Mods); - - base.OnEntering(e); } - private const double fade_duration = 300; - public override void OnResuming(ScreenTransitionEvent e) { + base.OnResuming(e); + this.FadeIn(fade_duration, Easing.OutQuint); carousel.VisuallyFocusSelected = false; + titleWedge.Show(); + detailsArea.Show(); + filterControl.Show(); + // required due to https://github.com/ppy/osu-framework/issues/3218 modSelectOverlay.SelectedMods.Disabled = false; modSelectOverlay.SelectedMods.BindTo(Mods); - - base.OnResuming(e); } public override void OnSuspending(ScreenTransitionEvent e) { - this.Delay(100).FadeOut(fade_duration, Easing.OutQuint); + this.FadeOut(fade_duration, Easing.OutQuint); modSelectOverlay.SelectedMods.UnbindFrom(Mods); + titleWedge.Hide(); + detailsArea.Hide(); + filterControl.Hide(); + carousel.VisuallyFocusSelected = true; base.OnSuspending(e); @@ -134,6 +213,11 @@ namespace osu.Game.Screens.SelectV2 public override bool OnExiting(ScreenExitEvent e) { this.FadeOut(fade_duration, Easing.OutQuint); + + titleWedge.Hide(); + detailsArea.Hide(); + filterControl.Hide(); + return base.OnExiting(e); } @@ -192,5 +276,11 @@ namespace osu.Game.Screens.SelectV2 SearchText = query, }); } + + protected override void Update() + { + base.Update(); + detailsArea.Height = wedgesContainer.DrawHeight - titleWedge.LayoutSize.Y - 4; + } } } From c2a7687a666bf494ba20000831e165b49bd99835 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 18:33:49 +0900 Subject: [PATCH 095/152] Fix sheared components getting masked away due to negative margins --- osu.Game/Graphics/Containers/ShearAligningWrapper.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Graphics/Containers/ShearAligningWrapper.cs b/osu.Game/Graphics/Containers/ShearAligningWrapper.cs index d720120b4f..542f269f93 100644 --- a/osu.Game/Graphics/Containers/ShearAligningWrapper.cs +++ b/osu.Game/Graphics/Containers/ShearAligningWrapper.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Layout; using osuTK; @@ -18,6 +19,10 @@ namespace osu.Game.Graphics.Containers { private readonly LayoutValue layout = new LayoutValue(Invalidation.MiscGeometry); + // Sheared components regularly end up off the side of the screen due to padding considerations. + // If we use this class in places where performance is important, we should reconsider the handling of this. + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + public ShearAligningWrapper(Drawable drawable) { RelativeSizeAxes = drawable.RelativeSizeAxes; From 3f17d7227a33515264919892d20f8036f60daa8b Mon Sep 17 00:00:00 2001 From: Marvefect <125153184+Marvefect@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:01:23 +0300 Subject: [PATCH 096/152] Update AdjustedAttributesTooltip.cs Mods don't necessarily have to change speed, to change beatmap attributes. Also, speed mods don't affect beatmap attributes in mania, making the text misleading. --- osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs b/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs index bdb10a477c..b806059e19 100644 --- a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs +++ b/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Mods { new OsuSpriteText { - Text = "One or more values are being adjusted by mods that change speed.", + Text = "One or more values are being adjusted by mods.", }, attributesFillFlow = new FillFlowContainer { From 3151fe7ef16fe9ac06493fbaa096c4ca93ae2df1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 06:30:29 +0300 Subject: [PATCH 097/152] Clarify purpose of helper lookup entries in osu!mania skinning --- .../LegacyManiaSkinConfigurationLookup.cs | 12 ++- osu.Game/Skinning/LegacySkin.cs | 78 +++++++++---------- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index c4f5d6a53c..b198dd3203 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -38,8 +38,6 @@ namespace osu.Game.Skinning { ColumnWidth, LightImage, - LeftLineWidth, - RightLineWidth, HitPosition, ComboPosition, ScorePosition, @@ -55,10 +53,8 @@ namespace osu.Game.Skinning HoldNoteTailImage, HoldNoteBodyImage, HoldNoteLightImage, - HoldNoteLightScale, WidthForNoteHeightScale, ExplosionImage, - ExplosionScale, ColumnLineColour, JudgementLineColour, ColumnBackgroundColour, @@ -83,7 +79,15 @@ namespace osu.Game.Skinning KeysUnderNotes, NoteBodyStyle, LightFramePerSecond, + + // The following lookup entries are not directly tied to skin.ini settings + // but are defined to simplify the process of determining such values. + LeftColumnSpacing, RightColumnSpacing, + LeftLineWidth, + RightLineWidth, + ExplosionScale, + HoldNoteLightScale, } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 210050fddb..56fa0e4706 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -166,17 +166,6 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ExplosionImage: return SkinUtils.As(getManiaImage(existing, "LightingN")); - case LegacyManiaSkinConfigurationLookups.ExplosionScale: - Debug.Assert(maniaLookup.ColumnIndex != null); - - if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m) - return SkinUtils.As(new Bindable(1)); - - if (existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] != 0) - return SkinUtils.As(new Bindable(existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); - - return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); - case LegacyManiaSkinConfigurationLookups.ColumnLineColour: return SkinUtils.As(getCustomColour(existing, "ColourColumnLine")); @@ -232,17 +221,6 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.HoldNoteLightImage: return SkinUtils.As(getManiaImage(existing, "LightingL")); - case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: - Debug.Assert(maniaLookup.ColumnIndex != null); - - if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m) - return SkinUtils.As(new Bindable(1)); - - if (existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] != 0) - return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); - - return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); - case LegacyManiaSkinConfigurationLookups.KeyImage: Debug.Assert(maniaLookup.ColumnIndex != null); return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.ColumnIndex}")); @@ -266,13 +244,19 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.HitTargetImage: return SkinUtils.As(getManiaImage(existing, "StageHint")); - case LegacyManiaSkinConfigurationLookups.LeftLineWidth: - Debug.Assert(maniaLookup.ColumnIndex != null); - return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value])); + case LegacyManiaSkinConfigurationLookups.Hit0: + case LegacyManiaSkinConfigurationLookups.Hit50: + case LegacyManiaSkinConfigurationLookups.Hit100: + case LegacyManiaSkinConfigurationLookups.Hit200: + case LegacyManiaSkinConfigurationLookups.Hit300: + case LegacyManiaSkinConfigurationLookups.Hit300g: + return SkinUtils.As(getManiaImage(existing, maniaLookup.Lookup.ToString())); - case LegacyManiaSkinConfigurationLookups.RightLineWidth: - Debug.Assert(maniaLookup.ColumnIndex != null); - return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value + 1])); + case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: + return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); + + case LegacyManiaSkinConfigurationLookups.LightFramePerSecond: + return SkinUtils.As(new Bindable(existing.LightFramePerSecond)); case LegacyManiaSkinConfigurationLookups.LeftColumnSpacing: Debug.Assert(maniaLookup.ColumnIndex != null); @@ -288,19 +272,35 @@ namespace osu.Game.Skinning return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value] / 2)); - case LegacyManiaSkinConfigurationLookups.Hit0: - case LegacyManiaSkinConfigurationLookups.Hit50: - case LegacyManiaSkinConfigurationLookups.Hit100: - case LegacyManiaSkinConfigurationLookups.Hit200: - case LegacyManiaSkinConfigurationLookups.Hit300: - case LegacyManiaSkinConfigurationLookups.Hit300g: - return SkinUtils.As(getManiaImage(existing, maniaLookup.Lookup.ToString())); + case LegacyManiaSkinConfigurationLookups.LeftLineWidth: + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value])); - case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: - return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); + case LegacyManiaSkinConfigurationLookups.RightLineWidth: + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value + 1])); - case LegacyManiaSkinConfigurationLookups.LightFramePerSecond: - return SkinUtils.As(new Bindable(existing.LightFramePerSecond)); + case LegacyManiaSkinConfigurationLookups.ExplosionScale: + Debug.Assert(maniaLookup.ColumnIndex != null); + + if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m) + return SkinUtils.As(new Bindable(1)); + + if (existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] != 0) + return SkinUtils.As(new Bindable(existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: + Debug.Assert(maniaLookup.ColumnIndex != null); + + if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m) + return SkinUtils.As(new Bindable(1)); + + if (existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] != 0) + return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); } return null; From e744102b1c145a6c1df1c688c947fd29af929142 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 08:34:09 +0300 Subject: [PATCH 098/152] Fix beatmap title wedge sheared incorrectly in test scene --- osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index 18cb63f9d7..df334736e2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -65,6 +65,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new Container { RelativeSizeAxes = Axes.Both, + Shear = OsuGame.SHEAR, Children = new Drawable[] { titleWedge = new BeatmapTitleWedge From b84105d93b5ae67657937cf7f1bac605a2ff8d02 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 08:37:33 +0300 Subject: [PATCH 099/152] Add test stressing title wedge performance with a heavy beatmap --- .../TestSceneBeatmapTitleWedge.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index df334736e2..85d82e536d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -2,11 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -16,9 +21,12 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.SelectV2; +using osu.Game.Skinning; using osu.Game.Tests.Visual.SongSelect; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -193,6 +201,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkDisplayedBPM(expectedDisplay); } + [Test] + [Explicit] + public void TestPerformanceWithLongBeatmap() + { + AddStep("select heavy beatmap", () => Beatmap.Value = new HeavyWorkingBeatmap(Audio)); + + foreach (var rulesetInfo in rulesets.AvailableRulesets) + setRuleset(rulesetInfo); + } + private void setRuleset(RulesetInfo rulesetInfo) { AddStep("set ruleset", () => Ruleset.Value = rulesetInfo); @@ -238,5 +256,49 @@ namespace osu.Game.Tests.Visual.SongSelectV2 working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now; return (working, onlineSet); } + + private class TestHitObject : ConvertHitObject; + + private class HeavyWorkingBeatmap : WorkingBeatmap + { + private static readonly BeatmapInfo beatmap_info = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Author = { Username = "osuAuthor" }, + Artist = "osuArtist", + Source = "osuSource", + Title = "osuTitle" + }, + Ruleset = new OsuRuleset().RulesetInfo, + StarRating = 6, + DifficultyName = "osuVersion", + Difficulty = new BeatmapDifficulty() + }; + + public HeavyWorkingBeatmap(AudioManager audioManager) + : base(beatmap_info, audioManager) + { + } + + protected override IBeatmap GetBeatmap() + { + List objects = new List(); + + for (int i = 0; i < 200_000; i++) + objects.Add(new TestHitObject { StartTime = i * 1000 }); + + return new Beatmap + { + BeatmapInfo = beatmap_info, + HitObjects = objects + }; + } + + public override Texture? GetBackground() => null; + public override Stream? GetStream(string storagePath) => null; + protected override Track? GetBeatmapTrack() => null; + protected internal override ISkin? GetSkin() => null; + } } } From 4f79dcb41135587a92d91e1b01f17a5a965a6959 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 08:54:50 +0300 Subject: [PATCH 100/152] Fix length & BPM statistics computation causing direct beatmap load --- .../Screens/SelectV2/BeatmapTitleWedge.cs | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 154374cbcb..65ea89e96b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -246,25 +248,41 @@ namespace osu.Game.Screens.SelectV2 updateOnlineDisplay(); } + private CancellationTokenSource? lengthBpmCancellationSource; + private void updateLengthAndBpmStatistics() { - var beatmapInfo = beatmap.Value.BeatmapInfo; + lengthBpmCancellationSource?.Cancel(); + lengthBpmCancellationSource = new CancellationTokenSource(); - double rate = ModUtils.CalculateRateWithMods(mods.Value); + var token = lengthBpmCancellationSource.Token; - int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate); - int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate); - int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate); + Task.Run(() => + { + var beatmapInfo = beatmap.Value.BeatmapInfo; - double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); - double hitLength = Math.Round(beatmapInfo.Length / rate); + double rate = ModUtils.CalculateRateWithMods(mods.Value); - lengthStatistic.Text = hitLength.ToFormattedDuration(); - lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()); + int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate); + int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate); + int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate); - bpmStatistic.Text = bpmMin == bpmMax - ? $"{bpmMin}" - : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})"; + double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); + double hitLength = Math.Round(beatmapInfo.Length / rate); + + Schedule(() => + { + if (token.IsCancellationRequested) + return; + + lengthStatistic.Text = hitLength.ToFormattedDuration(); + lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()); + + bpmStatistic.Text = bpmMin == bpmMax + ? $"{bpmMin}" + : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})"; + }); + }, token); } private void refetchBeatmapSet() From e8161778b98b9f080bb44f7f45d9cc0bb3c1c793 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 08:54:59 +0300 Subject: [PATCH 101/152] Fix count statistics causing direct beatmap load --- .../BeatmapTitleWedge_DifficultyDisplay.cs | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 7e3589b001..ca714964a8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -241,8 +242,6 @@ namespace osu.Game.Screens.SelectV2 cancellationSource?.Cancel(); cancellationSource = new CancellationTokenSource(); - computeStarDifficulty(cancellationSource.Token); - if (beatmap.IsDefault) { ratingAndNameContainer.FadeOut(300, Easing.OutQuint); @@ -254,17 +253,53 @@ namespace osu.Game.Screens.SelectV2 difficultyText.Text = beatmap.Value.BeatmapInfo.DifficultyName; mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, beatmap.Value.Metadata.Author)); mapperText.Text = beatmap.Value.Metadata.Author.Username; - - var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value); - - countStatisticsDisplay.Statistics = playableBeatmap.GetStatistics() - .Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content)) - .ToList(); } + updateStarDifficulty(cancellationSource.Token); + updateCountStatistics(cancellationSource.Token); updateDifficultyStatistics(); } + private void updateStarDifficulty(CancellationToken cancellationToken) + { + difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken) + .ContinueWith(task => + { + Schedule(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + starRatingDisplay.Current.Value = task.GetResultSafely() ?? default; + }); + }, cancellationToken); + } + + private void updateCountStatistics(CancellationToken cancellationToken) + { + if (beatmap.IsDefault) + { + countStatisticsDisplay.Statistics = Array.Empty(); + return; + } + + Task.Run(() => + { + var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value); + var statistics = playableBeatmap.GetStatistics() + .Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content)) + .ToList(); + + Schedule(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + countStatisticsDisplay.Statistics = statistics; + }); + }, cancellationToken); + } + private void updateDifficultyStatistics() => Scheduler.AddOnce(() => { if (beatmap.IsDefault) @@ -321,21 +356,6 @@ namespace osu.Game.Screens.SelectV2 }; }); - private void computeStarDifficulty(CancellationToken cancellationToken) - { - difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken) - .ContinueWith(task => - { - Schedule(() => - { - if (cancellationToken.IsCancellationRequested) - return; - - starRatingDisplay.Current.Value = task.GetResultSafely() ?? default; - }); - }, cancellationToken); - } - protected override void Update() { base.Update(); From 512460e9f7774f2c00f3345cd045e2c8105def1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 May 2025 17:02:18 +0900 Subject: [PATCH 102/152] Extract beatmap variable and comment to better show why async is required --- .../Screens/SelectV2/BeatmapTitleWedge.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 65ea89e96b..a73fc78771 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.SelectV2 private const float corner_radius = 10; [Resolved] - private IBindable beatmap { get; set; } = null!; + private IBindable working { get; set; } = null!; [Resolved] private IBindable ruleset { get; set; } = null!; @@ -186,7 +186,7 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - beatmap.BindValueChanged(_ => updateDisplay()); + working.BindValueChanged(_ => updateDisplay()); ruleset.BindValueChanged(_ => updateDisplay()); mods.BindValueChanged(m => @@ -226,9 +226,9 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { - var metadata = beatmap.Value.Metadata; - var beatmapInfo = beatmap.Value.BeatmapInfo; - var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + var metadata = working.Value.Metadata; + var beatmapInfo = working.Value.BeatmapInfo; + var beatmapSetInfo = working.Value.BeatmapSetInfo; statusPill.Status = beatmapInfo.Status; @@ -259,15 +259,17 @@ namespace osu.Game.Screens.SelectV2 Task.Run(() => { - var beatmapInfo = beatmap.Value.BeatmapInfo; + var beatmapInfo = working.Value.BeatmapInfo; + // This can take time as it is a synchronous task. + var beatmap = working.Value.Beatmap; double rate = ModUtils.CalculateRateWithMods(mods.Value); - int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate); - int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate); - int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate); + int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate); + int bpmMin = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMinimum, rate); + int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.GetMostCommonBeatLength(), rate); - double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); + double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate); double hitLength = Math.Round(beatmapInfo.Length / rate); Schedule(() => @@ -287,7 +289,7 @@ namespace osu.Game.Screens.SelectV2 private void refetchBeatmapSet() { - var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + var beatmapSetInfo = working.Value.BeatmapSetInfo; currentRequest?.Cancel(); currentRequest = null; @@ -323,7 +325,7 @@ namespace osu.Game.Screens.SelectV2 else { var onlineBeatmapSet = currentOnlineBeatmapSet; - var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID); + var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID); playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1); favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0"); From acb9eba475a36386464e750af140afc892989baf Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 06:05:02 +0300 Subject: [PATCH 103/152] Limit maximum UI scale to 1.1x on mobile platforms --- osu.Game/Configuration/OsuConfigManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 0399f50ded..94cb58185d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -179,7 +179,10 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ScalingPositionX, 0.5f, 0f, 1f, 0.01f); SetDefault(OsuSetting.ScalingPositionY, 0.5f, 0f, 1f, 0.01f); - SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f); + if (RuntimeInfo.IsMobile) + SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.1f, 0.01f); + else + SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f); SetDefault(OsuSetting.UIHoldActivationDelay, 200.0, 0.0, 500.0, 50.0); From e46434731ebaf1351f173366c51d195ea6dee7a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 May 2025 19:55:45 +0900 Subject: [PATCH 104/152] Add note about multiple usage of `GetPlayableBeatmap` --- .../Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index ca714964a8..9aaf317cb0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -285,6 +285,8 @@ namespace osu.Game.Screens.SelectV2 Task.Run(() => { + // This can take time as it is a synchronous task. + // TODO: We're calling `GetPlayableBeatmap` multiple times every map load at song select. var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value); var statistics = playableBeatmap.GetStatistics() .Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content)) From 109b29c1da692c74a0710be9bebb77619025b279 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 3 May 2025 05:38:00 +0300 Subject: [PATCH 105/152] Fix test in old song select not working on Apple platforms --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d8ab367ebd..9dc6bc8a33 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1277,12 +1277,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll)); - AddStep("press ctrl-x", () => - { - InputManager.PressKey(Key.ControlLeft); - InputManager.Key(Key.X); - InputManager.ReleaseKey(Key.ControlLeft); - }); + AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut)); AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType().First().Text, () => Is.Empty); } From cc46cbf7801406ff34aa58c77e61f3df1d561b08 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 5 May 2025 09:46:06 +0300 Subject: [PATCH 106/152] `WaitForSorting` -> `WaitForFiltering` --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 2 +- .../TestSceneBeatmapCarouselScrolling.cs | 4 ++-- .../TestSceneBeatmapCarouselUpdateHandling.cs | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 28a0948696..a11e4063a2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); + protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index da3fc98c19..93b0332ac4 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); - WaitForSorting(); + WaitForFiltering(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); - WaitForSorting(); + WaitForFiltering(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index 31aa1b6f94..4c6202d94c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 BeatmapSets.Add(baseTestBeatmap); }); - WaitForSorting(); + WaitForFiltering(); } [Test] @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("update beatmap with same reference", () => BeatmapSets.ReplaceRange(1, 1, [baseTestBeatmap])); - WaitForSorting(); + WaitForFiltering(); AddAssert("drawables unchanged", () => Carousel.ChildrenOfType(), () => Is.EqualTo(originalDrawables)); } @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 updateBeatmap(b => b.Metadata = metadata); - WaitForSorting(); + WaitForFiltering(); AddAssert("drawables changed", () => Carousel.ChildrenOfType(), () => Is.Not.EqualTo(originalDrawables)); } @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(); - WaitForSorting(); + WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.DifficultyName = "new name"); - WaitForSorting(); + WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); updateBeatmap(b => b.OnlineID = b.OnlineID + 1); - WaitForSorting(); + WaitForFiltering(); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); From 540cfc92da1ea3f54961420c494907e5bc99c46e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 5 May 2025 09:46:41 +0300 Subject: [PATCH 107/152] Support mutating existing active filter criteria in carousel tests Allows maintaining sorting mode while modifying other filter criterias, thus simplifying some tests. --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 17 ++++++++++++++++- .../SongSelectV2/TestSceneBeatmapCarousel.cs | 12 ++++++++---- .../TestSceneBeatmapCarouselArtistGrouping.cs | 5 +++-- ...estSceneBeatmapCarouselDifficultyGrouping.cs | 5 +++-- .../TestSceneBeatmapCarouselNoGrouping.cs | 3 --- .../TestSceneBeatmapCarouselScrolling.cs | 5 +++-- 6 files changed, 33 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index a11e4063a2..39f6c2230b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -127,9 +128,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }, }; }); + + // Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable. + SortBy(SortMode.Title); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria)); + protected void SortBy(SortMode mode) => ApplyToFilter($"sort by {mode.ToString().ToLowerInvariant()}", c => c.Sort = mode); + protected void GroupBy(GroupMode mode) => ApplyToFilter($"group by {mode.ToString().ToLowerInvariant()}", c => c.Group = mode); + + protected void ApplyToFilter(string description, Action? apply) + { + AddStep(description, () => + { + var criteria = Carousel.Criteria; + apply?.Invoke(criteria); + Carousel.Filter(criteria); + }); + } protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForFiltering() => AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 5fd921645b..870225edb3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; @@ -34,9 +33,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Explicit] public void TestSorting() { - SortBy(new FilterCriteria { Sort = SortMode.Artist }); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); - SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + SortBy(SortMode.Artist); + GroupBy(GroupMode.All); + + SortBy(SortMode.Difficulty); + GroupBy(GroupMode.Difficulty); + + SortBy(SortMode.Artist); + GroupBy(GroupMode.Artist); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index f0caa796b6..84769f2cee 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -19,7 +18,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + + SortBy(SortMode.Artist); + GroupBy(GroupMode.Artist); AddBeatmaps(10, 3, true); WaitForDrawablePanels(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index a4cdf8abcb..37fb95ce86 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; -using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; @@ -21,7 +20,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + + SortBy(SortMode.Difficulty); + GroupBy(GroupMode.Difficulty); AddBeatmaps(10, 3); WaitForDrawablePanels(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index ac02d7a3a9..cdd55f0f0c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -6,8 +6,6 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; -using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; using osuTK.Input; @@ -22,7 +20,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria { Sort = SortMode.Title }); } /// diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index 93b0332ac4..f5574d2789 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -5,7 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Primitives; using osu.Framework.Testing; -using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -18,7 +18,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RemoveAllBeatmaps(); CreateCarousel(); - SortBy(new FilterCriteria()); + + SortBy(SortMode.Artist); AddBeatmaps(10); WaitForDrawablePanels(); From 981383b52b23e12e7e77fb11115cbc1a83c72b2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 13:23:13 +0900 Subject: [PATCH 108/152] Add minimal slider body transparency to "Argon" skins Addresses concerns in https://github.com/ppy/osu/discussions/24226. I basically adjusted opacity down until it started to visually detract from the skin. The pro level is lower than I'd want to see, but feels like a midpoint that some users may find usable. This is a band-aid fix until we can get proper support for settings like this into the skin editor. --- osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs | 9 +++++++++ .../Skinning/Argon/OsuArgonSkinTransformer.cs | 9 +++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs index c3d08116ac..abb414c82c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs @@ -3,12 +3,16 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Argon { public partial class ArgonSliderBody : PlaySliderBody { + // Eventually this would be a user setting. + public float BodyAlpha { get; init; } = 1; + protected override void LoadComplete() { const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2; @@ -26,6 +30,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath(); + protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) + { + return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(BodyAlpha); + } + private partial class DrawableSliderPath : Default.DrawableSliderPath { protected override Color4 ColourAt(float position) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs index 9f6f65c206..2d1d5826b1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs @@ -16,13 +16,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { + bool isPro = Skin is ArgonProSkin; + switch (lookup) { case SkinComponentLookup resultComponent: HitResult result = resultComponent.Component; // This should eventually be moved to a skin setting, when supported. - if (Skin is ArgonProSkin && (result == HitResult.Great || result == HitResult.Perfect)) + if (isPro && (result == HitResult.Great || result == HitResult.Perfect)) return Drawable.Empty(); switch (result) @@ -46,7 +48,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon return new ArgonMainCirclePiece(false); case OsuSkinComponents.SliderBody: - return new ArgonSliderBody(); + return new ArgonSliderBody + { + BodyAlpha = isPro ? 0.92f : 0.98f + }; case OsuSkinComponents.SliderBall: return new ArgonSliderBall(); From af874ca7307db927ab69eeff340e2099649d7240 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 07:58:37 +0300 Subject: [PATCH 109/152] Fix update handling test selecting wrong beatmap Order has changed after carousel was made to sort by title. --- .../SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index 4c6202d94c..b9a468d580 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestSelectionHeld() { - SelectPrevGroup(); + SelectNextGroup(); WaitForSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] // Checks that we keep selection based on online ID where possible. public void TestSelectionHeldDifficultyNameChanged() { - SelectPrevGroup(); + SelectNextGroup(); WaitForSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] // Checks that we fallback to keeping selection based on difficulty name. public void TestSelectionHeldDifficultyOnlineIDChanged() { - SelectPrevGroup(); + SelectNextGroup(); WaitForSelection(1, 0); AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); From 84f44eb3ad34da33a76c43221421b00d78e647ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 14:02:09 +0900 Subject: [PATCH 110/152] Cache local user supporter status between game executions Fixes startup sounds from potentially being fetched from the wrong source if API connection establishment takes longer than the intro screen takes to load. Closes https://github.com/ppy/osu/issues/22492. --- osu.Game/Configuration/OsuConfigManager.cs | 8 ++++++++ osu.Game/Online/API/APIAccess.cs | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 94cb58185d..167e52ad0d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -225,6 +225,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true); SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true); + + SetDefault(OsuSetting.WasSupporter, false); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -466,5 +468,11 @@ namespace osu.Game.Configuration EditorShowStoryboard, EditorSubmissionNotifyOnDiscussionReplies, EditorSubmissionLoadInBrowserAfterSubmission, + + /// + /// Cached state of whether local user is a supporter. + /// Used to allow early checks (ie for startup samples) to be in the correct state, even if the API authentication process has not completed. + /// + WasSupporter } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 51fadb521a..525eb98a86 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -72,6 +72,8 @@ namespace osu.Game.Online.API protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly Bindable configStatus = new Bindable(); + private readonly Bindable configSupporter = new Bindable(); + private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; @@ -104,6 +106,7 @@ namespace osu.Game.Online.API authentication.Token.ValueChanged += onTokenChanged; config.BindWith(OsuSetting.UserOnlineStatus, configStatus); + config.BindWith(OsuSetting.WasSupporter, configSupporter); if (HasLogin) { @@ -333,6 +336,7 @@ namespace osu.Game.Online.API Debug.Assert(ThreadSafety.IsUpdateThread); localUser.Value = me; + configSupporter.Value = me.IsSupporter; state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; }; @@ -368,7 +372,8 @@ namespace osu.Game.Online.API localUser.Value = new APIUser { - Username = ProvidedUsername + Username = ProvidedUsername, + IsSupporter = configSupporter.Value, }; } @@ -607,6 +612,7 @@ namespace osu.Game.Online.API Schedule(() => { localUser.Value = createGuestUser(); + configSupporter.Value = false; friends.Clear(); }); From 1b554d01d616bd30558309ed2ed17e34354bb31f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 3 May 2025 01:30:31 +0300 Subject: [PATCH 111/152] Add toolbar in song select test to determine selected ruleset --- .../SongSelectV2/TestSceneSongSelect.cs | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 986ad6fc46..29baf174d1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -12,13 +12,11 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Rulesets.Catch; -using osu.Game.Rulesets.Mania; +using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Taiko; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; @@ -30,11 +28,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public partial class TestSceneSongSelect : ScreenTestScene { [Cached] - private readonly ScreenFooter screenScreenFooter; + private readonly ScreenFooter screenFooter; [Cached] private readonly OsuLogo logo; + [Cached(typeof(INotificationOverlay))] + private readonly INotificationOverlay notificationOverlay = new NotificationOverlay(); + protected override bool UseOnlineAPI => true; public TestSceneSongSelect() @@ -44,16 +45,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = screenScreenFooter = new ScreenFooter + Children = new Drawable[] { - OnBack = () => Stack.CurrentScreen.Exit(), + new Toolbar + { + State = { Value = Visibility.Visible }, + }, + screenFooter = new ScreenFooter + { + OnBack = () => Stack.CurrentScreen.Exit(), + }, + logo = new OsuLogo + { + Alpha = 0f, + }, }, }, - logo = new OsuLogo - { - Alpha = 0f, - }, }; + + Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT }; } [BackgroundDependencyLoader] @@ -82,15 +92,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); } - [Test] - public void TestRulesets() - { - AddStep("set osu ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo); - AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - AddStep("set catch ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo); - AddStep("set mania ruleset", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); - } - #region Footer [Test] @@ -212,13 +213,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) { - screenScreenFooter.Show(); - screenScreenFooter.SetButtons(osuScreen.CreateFooterButtons()); + screenFooter.Show(); + screenFooter.SetButtons(osuScreen.CreateFooterButtons()); } else { - screenScreenFooter.Hide(); - screenScreenFooter.SetButtons(Array.Empty()); + screenFooter.Hide(); + screenFooter.SetButtons(Array.Empty()); } } } From cc6e52adeec57f31d456b31a83cd256523ff6ed7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 3 May 2025 01:35:57 +0300 Subject: [PATCH 112/152] Manually load song select in each test case --- .../SongSelectV2/TestSceneSongSelect.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 29baf174d1..5718bbfc50 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -83,20 +83,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Stack.ScreenExited += updateFooter; } - [SetUpSteps] - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("load screen", () => Stack.Push(new SoloSongSelect())); - AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); - } - #region Footer [Test] public void TestMods() { + loadSongSelect(); + AddStep("one mod", () => SelectedMods.Value = new List { new OsuModHidden() }); AddStep("two mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock() }); AddStep("three mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }); @@ -124,6 +117,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestShowOptions() { + loadSongSelect(); + AddStep("enable options", () => { var optionsButton = this.ChildrenOfType().Last(); @@ -136,6 +131,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestState() { + loadSongSelect(); + AddToggleStep("set options enabled state", state => this.ChildrenOfType().Last().Enabled.Value = state); } @@ -143,6 +140,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // [Test] // public void TestFooterRandom() // { + // loadSongSelect(); + // // AddStep("press F2", () => InputManager.Key(Key.F2)); // AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); // } @@ -150,6 +149,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // [Test] // public void TestFooterRandomViaMouse() // { + // loadSongSelect(); + // // AddStep("click button", () => // { // InputManager.MoveMouseTo(randomButton); @@ -161,6 +162,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // [Test] // public void TestFooterRewind() // { + // loadSongSelect(); + // // AddStep("press Shift+F2", () => // { // InputManager.PressKey(Key.LShift); @@ -174,6 +177,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // [Test] // public void TestFooterRewindViaShiftMouseLeft() // { + // loadSongSelect(); + // // AddStep("shift + click button", () => // { // InputManager.PressKey(Key.LShift); @@ -187,6 +192,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // [Test] // public void TestFooterRewindViaMouseRight() // { + // loadSongSelect(); + // // AddStep("right click button", () => // { // InputManager.MoveMouseTo(randomButton); @@ -198,6 +205,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestOverlayPresent() { + loadSongSelect(); + AddStep("Press F1", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); @@ -209,6 +218,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 #endregion + private void loadSongSelect() + { + AddStep("load screen", () => Stack.Push(new SoloSongSelect())); + AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); + } + private void updateFooter(IScreen? _, IScreen? newScreen) { if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) From 4de5d5adfe3147010bf5a6ff612daedf6997d983 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 6 May 2025 15:29:13 +0300 Subject: [PATCH 113/152] Hook filter control with beatmap carousel --- osu.Game/Screens/SelectV2/FilterControl.cs | 110 +++++++++++++++++++-- osu.Game/Screens/SelectV2/SongSelect.cs | 22 +++-- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index bb795e5717..5eda47391a 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -2,15 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osuTK; @@ -23,12 +31,32 @@ namespace osu.Game.Screens.SelectV2 private const float corner_radius = 8; + private SongSelectSearchTextBox searchTextBox = null!; private ShearedToggleButton showConvertedBeatmapsButton = null!; private DifficultyRangeSlider difficultyRangeSlider = null!; + private ShearedDropdown sortDropdown = null!; + private ShearedDropdown groupDropdown = null!; + private CollectionDropdown collectionDropdown = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; [Resolved] private OsuConfigManager config { get; set; } = null!; + public LocalisableString InformationalNote + { + get => searchTextBox.FilterText; + set => searchTextBox.FilterText = value; + } + + public event Action? CriteriaChanged; + + private FilterCriteria currentCriteria = null!; + [BackgroundDependencyLoader] private void load() { @@ -65,12 +93,10 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Shear = -OsuGame.SHEAR, - Child = new SongSelectSearchTextBox + Child = searchTextBox = new SongSelectSearchTextBox { RelativeSizeAxes = Axes.X, HoldFocus = true, - // TODO: pending implementation - FilterText = "12345 matches", }, }, new GridContainer @@ -123,20 +149,20 @@ namespace osu.Game.Screens.SelectV2 { new[] { - new ShearedDropdown(SortStrings.Default) + sortDropdown = new ShearedDropdown(SortStrings.Default) { RelativeSizeAxes = Axes.X, Items = Enum.GetValues(), }, Empty(), // todo: pending localisation - new ShearedDropdown("Group by") + groupDropdown = new ShearedDropdown("Group by") { RelativeSizeAxes = Axes.X, Items = Enum.GetValues(), }, Empty(), - new CollectionDropdown + collectionDropdown = new CollectionDropdown { RelativeSizeAxes = Axes.X, }, @@ -155,6 +181,78 @@ namespace osu.Game.Screens.SelectV2 difficultyRangeSlider.LowerBound = config.GetBindable(OsuSetting.DisplayStarsMinimum); difficultyRangeSlider.UpperBound = config.GetBindable(OsuSetting.DisplayStarsMaximum); config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmapsButton.Active); + config.BindWith(OsuSetting.SongSelectSortingMode, sortDropdown.Current); + config.BindWith(OsuSetting.SongSelectGroupingMode, groupDropdown.Current); + + ruleset.BindValueChanged(_ => updateCriteria()); + mods.BindValueChanged(m => + { + // The following is a note carried from old song select and may not be a valid reason anymore: + // // Mods are updated once by the mod select overlay when song select is entered, + // // regardless of if there are any mods or any changes have taken place. + // // Updating the criteria here so early triggers a re-ordering of panels on song select, via... some mechanism. + // // Todo: Investigate/fix and potentially remove this. + // TODO: this might be simply removable with the new song select & carousel code. + if (m.NewValue.SequenceEqual(m.OldValue)) + return; + + var rulesetCriteria = currentCriteria.RulesetCriteria; + if (rulesetCriteria?.FilterMayChangeFromMods(m) == true) + updateCriteria(); + }); + + searchTextBox.Current.BindValueChanged(_ => updateCriteria()); + difficultyRangeSlider.LowerBound.BindValueChanged(_ => updateCriteria()); + difficultyRangeSlider.UpperBound.BindValueChanged(_ => updateCriteria()); + showConvertedBeatmapsButton.Active.BindValueChanged(_ => updateCriteria()); + sortDropdown.Current.BindValueChanged(_ => updateCriteria()); + groupDropdown.Current.BindValueChanged(_ => updateCriteria()); + collectionDropdown.Current.BindValueChanged(_ => updateCriteria()); + updateCriteria(); + } + + /// + /// Creates a based on the current state of the controls. + /// + public FilterCriteria CreateCriteria() + { + string query = searchTextBox.Current.Value; + + var criteria = new FilterCriteria + { + Sort = sortDropdown.Current.Value, + Group = groupDropdown.Current.Value, + AllowConvertedBeatmaps = showConvertedBeatmapsButton.Active.Value, + Ruleset = ruleset.Value, + Mods = mods.Value, + CollectionBeatmapMD5Hashes = collectionDropdown.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes).ToImmutableHashSet() + }; + + if (!difficultyRangeSlider.LowerBound.IsDefault) + criteria.UserStarDifficulty.Min = difficultyRangeSlider.LowerBound.Value; + + if (!difficultyRangeSlider.UpperBound.IsDefault) + criteria.UserStarDifficulty.Max = difficultyRangeSlider.UpperBound.Value; + + criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria(); + + FilterQueryParser.ApplyQueries(criteria, query); + return criteria; + } + + private void updateCriteria() + { + currentCriteria = CreateCriteria(); + CriteriaChanged?.Invoke(currentCriteria); + } + + /// + /// Set the query to the search text box. + /// + /// The string to search. + public void Search(string query) + { + searchTextBox.Current.Value = query; } protected override void PopIn() diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 3144168712..3d2d85e037 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -158,6 +159,8 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); + filterControl.CriteriaChanged += criteriaChanged; + modSelectOverlay.State.BindValueChanged(v => { logo?.ScaleTo(v.NewValue == Visibility.Visible ? 0f : logo_scale, 400, Easing.OutQuint) @@ -264,19 +267,26 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } + #region Filtering + + private const double filter_delay = 250; + + private ScheduledDelegate? filterDebounce; + /// /// Set the query to the search text box. /// /// The string to search. - public void Search(string query) + public void Search(string query) => filterControl.Search(query); + + private void criteriaChanged(FilterCriteria criteria) { - carousel.Filter(new FilterCriteria - { - // TODO: this should only set the text of the current criteria, not use a completely new criteria. - SearchText = query, - }); + filterDebounce?.Cancel(); + filterDebounce = Scheduler.AddDelayed(() => carousel.Filter(criteria), filter_delay); } + #endregion + protected override void Update() { base.Update(); From 7918f6e7a149e36386320f38375f3313b847850f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 6 May 2025 15:29:45 +0300 Subject: [PATCH 114/152] Show loading layer if filtering actually takes long --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4af5e759a7..6e0917227a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; +using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; @@ -331,11 +332,21 @@ namespace osu.Game.Screens.SelectV2 public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); + private ScheduledDelegate? loadingDebounce; + public void Filter(FilterCriteria criteria) { Criteria = criteria; - loading.Show(); - FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide())); + + loadingDebounce ??= Scheduler.AddDelayed(() => loading.Show(), 250); + + FilterAsync().ContinueWith(_ => Schedule(() => + { + loadingDebounce?.Cancel(); + loadingDebounce = null; + + loading.Hide(); + })); } #endregion From 34119aab8e33a9efcea2cbbef06260edddb1c8ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 14:55:40 +0900 Subject: [PATCH 115/152] Adjust song select beatmap background transition to better support transparent backgrounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new background now briefly fades in. The reason we didn't do this to date is that there could be a perceived decrease in brightness as the old and new background transition through opacity. But a quick fade in, it doesn't seem to cause any visual artifacting. I've also added a scale effect because it felt quite nice. Willing to pull that if anyone has an issue with it, but it's a step in the direction of "adding more motion to song select", which is still an area I see lacking greatly – even compared to stable. --- osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 5f80c2cd96..3f53801372 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -108,12 +108,14 @@ namespace osu.Game.Screens.Backgrounds if (Background != null) { newDepth = Background.Depth + 1; - Background.FinishTransforms(); Background.FadeOut(250); Background.Expire(); } b.Depth = newDepth; + b.Anchor = b.Origin = Anchor.Centre; + b.FadeInFromZero(500, Easing.OutQuint); + b.ScaleTo(1.02f).ScaleTo(1, 3500, Easing.OutQuint); dimmable.Background = Background = b; } From a3aa4c7ba58fa97e2634e76496985acd88ca3297 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 6 May 2025 12:47:49 +0300 Subject: [PATCH 116/152] Add carousel filter for matching items against criteria (i.e. actually filter) --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + .../SelectV2/BeatmapCarouselFilterMatching.cs | 113 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4af5e759a7..3578fd46fa 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -49,6 +49,7 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { + new BeatmapCarouselFilterMatching(() => Criteria), new BeatmapCarouselFilterSorting(() => Criteria), grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs new file mode 100644 index 0000000000..f81f068ab7 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -0,0 +1,113 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; +using osu.Game.Screens.Select; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselFilterMatching : ICarouselFilter + { + private readonly Func getCriteria; + + public BeatmapCarouselFilterMatching(Func getCriteria) + { + this.getCriteria = getCriteria; + } + + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) + { + return await Task.Run(() => + { + var criteria = getCriteria(); + return matchItems(items, criteria); + }, cancellationToken).ConfigureAwait(false); + } + + private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) + { + foreach (var item in items) + { + var beatmap = (BeatmapInfo)item.Model; + + if (checkMatch(beatmap, criteria)) + yield return item; + } + } + + private static bool checkMatch(BeatmapInfo beatmap, FilterCriteria criteria) + { + bool match = criteria.Ruleset == null || + beatmap.Ruleset.ShortName == criteria.Ruleset.ShortName || + (beatmap.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps); + + if (beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) + { + // only check ruleset equality or convertability for selected beatmap + return match; + } + + if (!match) return false; + + if (criteria.SearchTerms.Length > 0) + { + match = beatmap.Match(criteria.SearchTerms); + + // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. + // this should be done after text matching so we can prioritise matching numbers in metadata. + if (!match && criteria.SearchNumber.HasValue) + { + match = (beatmap.OnlineID == criteria.SearchNumber.Value) || + (beatmap.BeatmapSet?.OnlineID == criteria.SearchNumber.Value); + } + } + + if (!match) return false; + + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(beatmap.StarRating); + match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(beatmap.Difficulty.ApproachRate); + match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(beatmap.Difficulty.DrainRate); + match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(beatmap.Difficulty.CircleSize); + match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(beatmap.Difficulty.OverallDifficulty); + match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(beatmap.Length); + match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(beatmap.LastPlayed ?? DateTimeOffset.MinValue); + match &= !criteria.DateRanked.HasFilter || (beatmap.BeatmapSet?.DateRanked != null && criteria.DateRanked.IsInRange(beatmap.BeatmapSet.DateRanked.Value)); + match &= !criteria.DateSubmitted.HasFilter || (beatmap.BeatmapSet?.DateSubmitted != null && criteria.DateSubmitted.IsInRange(beatmap.BeatmapSet.DateSubmitted.Value)); + match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(beatmap.BPM); + + match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(beatmap.BeatDivisor); + match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(beatmap.Status); + + if (!match) return false; + + match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(beatmap.Metadata.Author.Username); + match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(beatmap.Metadata.Artist) || + criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode); + match &= !criteria.Title.HasFilter || criteria.Title.Matches(beatmap.Metadata.Title) || + criteria.Title.Matches(beatmap.Metadata.TitleUnicode); + match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(beatmap.DifficultyName); + match &= !criteria.Source.HasFilter || criteria.Source.Matches(beatmap.Metadata.Source); + match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(beatmap.StarRating); + + if (!match) return false; + + match &= criteria.CollectionBeatmapMD5Hashes?.Contains(beatmap.MD5Hash) ?? true; + if (match && criteria.RulesetCriteria != null) + match &= criteria.RulesetCriteria.Matches(beatmap, criteria); + + if (match && criteria.HasOnlineID == true) + match &= beatmap.OnlineID >= 0; + + if (match && criteria.BeatmapSetId != null) + match &= criteria.BeatmapSetId == beatmap.BeatmapSet?.OnlineID; + + return match; + } + } +} From 90abd11ca5065edc831f0118ec4fe6eac809f580 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 6 May 2025 13:00:13 +0300 Subject: [PATCH 117/152] Add test coverage for filtering --- .../TestSceneBeatmapCarouselArtistGrouping.cs | 28 ++ ...tSceneBeatmapCarouselDifficultyGrouping.cs | 30 ++ .../TestSceneBeatmapCarouselFiltering.cs | 284 ++++++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 84769f2cee..8f822cbb1d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -174,5 +174,33 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectNextGroup(); WaitForGroupSelection(1, 1); } + + [Test] + public void TestBasicFiltering() + { + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + WaitForFiltering(); + + AddAssert("1 group + 1 set + 3 diffs displayed", () => Carousel.DisplayableItems == 5); + + CheckNoSelection(); + SelectNextPanel(); + Select(); + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 1); + + for (int i = 0; i < 6; i++) + SelectNextPanel(); + + Select(); + + WaitForGroupSelection(0, 2); + + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + WaitForFiltering(); + + AddAssert("5 groups + 10 sets + 30 diffs displayed", () => Carousel.DisplayableItems == 45); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 37fb95ce86..bf20825bdb 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -192,5 +192,35 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); } + + [Test] + public void TestBasicFiltering() + { + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + WaitForFiltering(); + + AddAssert("3 groups + 3 diffs displayed", () => Carousel.DisplayableItems == 6); + + CheckNoSelection(); + SelectNextPanel(); + Select(); + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 0); + + for (int i = 0; i < 5; i++) + SelectNextPanel(); + + Select(); + SelectNextPanel(); + Select(); + + WaitForGroupSelection(1, 0); + + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + WaitForFiltering(); + + AddAssert("3 groups + 30 diffs displayed", () => Carousel.DisplayableItems == 33); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs new file mode 100644 index 0000000000..cb1b0ec31f --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -0,0 +1,284 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselFiltering : BeatmapCarouselTestScene + { + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + } + + [Test] + public void TestBasicFiltering() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + WaitForFiltering(); + + AddAssert("3 diffs + 1 set displayed", () => Carousel.DisplayableItems == 4); + + SelectNextPanel(); + Select(); + + WaitForSelection(2, 0); + + for (int i = 0; i < 5; i++) + SelectNextPanel(); + + Select(); + WaitForSelection(2, 1); + + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + WaitForFiltering(); + + AddAssert("30 diffs + 10 sets displayed", () => Carousel.DisplayableItems == 40); + } + + [Test] + public void TestFilteringByUserStarDifficulty() + { + AddStep("add mixed difficulty set", () => + { + var set = TestResources.CreateTestBeatmapSetInfo(1); + set.Beatmaps.Clear(); + + for (int i = 1; i <= 15; i++) + { + set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata()) + { + BeatmapSet = set, + DifficultyName = $"Stars: {i}", + StarRating = i, + }); + } + + BeatmapSets.Add(set); + }); + + WaitForDrawablePanels(); + + ApplyToFilter("filter [5..]", c => + { + c.UserStarDifficulty.Min = 5; + c.UserStarDifficulty.Max = null; + }); + WaitForFiltering(); + AddAssert("1 set + 11 diffs displayed", () => Carousel.DisplayableItems == 12); + + ApplyToFilter("filter to [0..7]", c => + { + c.UserStarDifficulty.Min = null; + c.UserStarDifficulty.Max = 7; + }); + WaitForFiltering(); + AddAssert("1 set + 7 diffs displayed", () => Carousel.DisplayableItems == 8); + + ApplyToFilter("filter to [5..7]", c => + { + c.UserStarDifficulty.Min = 5; + c.UserStarDifficulty.Max = 7; + }); + + WaitForFiltering(); + AddAssert("1 set + 3 diffs displayed", () => Carousel.DisplayableItems == 4); + + ApplyToFilter("filter to [2..2]", c => + { + c.UserStarDifficulty.Min = 2; + c.UserStarDifficulty.Max = 2; + }); + + WaitForFiltering(); + AddAssert("`1 set + 1 diff displayed", () => Carousel.DisplayableItems == 2); + + ApplyToFilter("filter to [0..]", c => + { + c.UserStarDifficulty.Min = 0; + c.UserStarDifficulty.Max = null; + }); + WaitForFiltering(); + AddAssert("1 set + 15 diffs displayed", () => Carousel.DisplayableItems == 16); + } + + [Test] + public void TestCarouselRemembersSelection() + { + Guid selectedID = Guid.Empty; + + AddBeatmaps(50, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + SelectNextPanel(); + Select(); + + AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID); + + for (int i = 0; i < 5; i++) + { + ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); + AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + } + } + + [Test] + public void TestCarouselRemembersSelectionDifficultySort() + { + Guid selectedID = Guid.Empty; + + AddBeatmaps(50, 3); + WaitForDrawablePanels(); + + SortBy(SortMode.Difficulty); + + SelectNextGroup(); + + AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID); + + for (int i = 0; i < 5; i++) + { + ApplyToFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString()); + AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + ApplyToFilter("remove filter", c => c.SearchText = string.Empty); + AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID); + } + } + + [Test] + public void TestCarouselRetainsSelectionFromDifficultySort() + { + AddBeatmaps(50, 3); + WaitForDrawablePanels(); + + BeatmapInfo chosenBeatmap = null!; + + for (int i = 0; i < 3; i++) + { + int diff = i; + + AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]); + AddUntilStep("selection changed", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + + SortBy(SortMode.Difficulty); + AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + + SortBy(SortMode.Title); + AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap)); + } + } + + [Test] + public void TestExternalRulesetChange() + { + ApplyToFilter("allow converted beatmaps", c => c.AllowConvertedBeatmaps = true); + ApplyToFilter("filter to osu", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(0)); + + WaitForFiltering(); + + AddStep("add mixed ruleset beatmapset", () => + { + var testMixed = TestResources.CreateTestBeatmapSetInfo(3); + + for (int i = 0; i <= 2; i++) + testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); + + BeatmapSets.Add(testMixed); + }); + WaitForDrawablePanels(); + + SelectNextPanel(); + Select(); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = GetVisiblePanels(); + + return visibleBeatmapPanels.Count() == 1 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1; + }); + + ApplyToFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1)); + + WaitForFiltering(); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = GetVisiblePanels(); + + return visibleBeatmapPanels.Count() == 2 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1; + }); + + ApplyToFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2)); + + WaitForFiltering(); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = GetVisiblePanels(); + + return visibleBeatmapPanels.Count() == 2 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 2) == 1; + }); + } + + [Test] + [Ignore("Difficulty sorting is broken when set headers are included.")] // todo: fix. + public void TestSortingWithDifficultyFiltered() + { + const int diffs_per_set = 3; + const int local_set_count = 2; + + AddStep("populate beatmap sets", () => + { + for (int i = 0; i < local_set_count; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(diffs_per_set); + set.Beatmaps[0].StarRating = 3 - i; + set.Beatmaps[0].DifficultyName += $" ({3 - i}*)"; + set.Beatmaps[1].StarRating = 6 + i; + set.Beatmaps[1].DifficultyName += $" ({6 + i}*)"; + BeatmapSets.Add(set); + } + }); + + SortBy(SortMode.Difficulty); + + AddAssert($"3 sets + {local_set_count * diffs_per_set} diffs displayed", () => Carousel.DisplayableItems == 3 + local_set_count * diffs_per_set); + + ApplyToFilter("filter to normal", c => c.SearchText = "Normal"); + + AddAssert($"{local_set_count} sets + {local_set_count} diffs displayed", () => Carousel.DisplayableItems == local_set_count + local_set_count); + + ApplyToFilter("filter to insane", c => c.SearchText = "Insane"); + + AddAssert($"{local_set_count} sets + {local_set_count} diffs displayed", () => Carousel.DisplayableItems == local_set_count + local_set_count); + } + } +} From 725187245a4f839470603782f8955ce9403b099b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 08:30:59 +0300 Subject: [PATCH 118/152] Improve count check test assertions --- .../SongSelectV2/BeatmapCarouselTestScene.cs | 30 +++++++++++++++++++ .../TestSceneBeatmapCarouselArtistGrouping.cs | 8 +++-- ...tSceneBeatmapCarouselDifficultyGrouping.cs | 6 ++-- .../TestSceneBeatmapCarouselFiltering.cs | 26 +++++++++------- .../SelectV2/BeatmapCarouselFilterMatching.cs | 10 +++++++ 5 files changed, 66 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 39f6c2230b..f99433983b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -160,6 +160,36 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + protected void CheckDisplayedBeatmapsCount(int expected) + { + AddAssert($"{expected} diffs displayed", () => + { + var matchingFilter = Carousel.Filters.OfType().Single(); + return matchingFilter.BeatmapItemsCount; + }, () => Is.EqualTo(expected)); + } + + protected void CheckDisplayedBeatmapSetsCount(int expected) + { + AddAssert($"{expected} sets displayed", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + // Using groupingFilter.SetItems.Count alone doesn't work. + // When sorting by difficulty, there can be more than one set panel for the same set displayed. + return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo)); + }, () => Is.EqualTo(expected)); + } + + protected void CheckDisplayedGroupsCount(int expected) + { + AddAssert($"{expected} groups displayed", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + return groupingFilter.GroupItems.Count; + }, () => Is.EqualTo(expected)); + } + protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 8f822cbb1d..9cf9d07a94 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -181,7 +181,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); WaitForFiltering(); - AddAssert("1 group + 1 set + 3 diffs displayed", () => Carousel.DisplayableItems == 5); + CheckDisplayedGroupsCount(1); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); CheckNoSelection(); SelectNextPanel(); @@ -200,7 +202,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); - AddAssert("5 groups + 10 sets + 30 diffs displayed", () => Carousel.DisplayableItems == 45); + CheckDisplayedGroupsCount(5); + CheckDisplayedBeatmapSetsCount(10); + CheckDisplayedBeatmapsCount(30); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index bf20825bdb..2d39e40213 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -199,7 +199,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); WaitForFiltering(); - AddAssert("3 groups + 3 diffs displayed", () => Carousel.DisplayableItems == 6); + CheckDisplayedGroupsCount(3); + CheckDisplayedBeatmapsCount(3); CheckNoSelection(); SelectNextPanel(); @@ -220,7 +221,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); - AddAssert("3 groups + 30 diffs displayed", () => Carousel.DisplayableItems == 33); + CheckDisplayedGroupsCount(3); + CheckDisplayedBeatmapsCount(30); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index cb1b0ec31f..21c726f9ac 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -37,7 +37,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); WaitForFiltering(); - AddAssert("3 diffs + 1 set displayed", () => Carousel.DisplayableItems == 4); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); SelectNextPanel(); Select(); @@ -53,7 +54,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); - AddAssert("30 diffs + 10 sets displayed", () => Carousel.DisplayableItems == 40); + CheckDisplayedBeatmapSetsCount(10); + CheckDisplayedBeatmapsCount(30); } [Test] @@ -85,7 +87,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 c.UserStarDifficulty.Max = null; }); WaitForFiltering(); - AddAssert("1 set + 11 diffs displayed", () => Carousel.DisplayableItems == 12); + CheckDisplayedBeatmapsCount(11); ApplyToFilter("filter to [0..7]", c => { @@ -93,7 +95,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 c.UserStarDifficulty.Max = 7; }); WaitForFiltering(); - AddAssert("1 set + 7 diffs displayed", () => Carousel.DisplayableItems == 8); + CheckDisplayedBeatmapsCount(7); ApplyToFilter("filter to [5..7]", c => { @@ -102,7 +104,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); WaitForFiltering(); - AddAssert("1 set + 3 diffs displayed", () => Carousel.DisplayableItems == 4); + CheckDisplayedBeatmapsCount(3); ApplyToFilter("filter to [2..2]", c => { @@ -111,7 +113,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); WaitForFiltering(); - AddAssert("`1 set + 1 diff displayed", () => Carousel.DisplayableItems == 2); + CheckDisplayedBeatmapsCount(1); ApplyToFilter("filter to [0..]", c => { @@ -119,7 +121,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 c.UserStarDifficulty.Max = null; }); WaitForFiltering(); - AddAssert("1 set + 15 diffs displayed", () => Carousel.DisplayableItems == 16); + CheckDisplayedBeatmapsCount(15); } [Test] @@ -269,16 +271,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); SortBy(SortMode.Difficulty); + WaitForFiltering(); - AddAssert($"3 sets + {local_set_count * diffs_per_set} diffs displayed", () => Carousel.DisplayableItems == 3 + local_set_count * diffs_per_set); + CheckDisplayedBeatmapSetsCount(3); + CheckDisplayedBeatmapsCount(local_set_count * diffs_per_set); ApplyToFilter("filter to normal", c => c.SearchText = "Normal"); - AddAssert($"{local_set_count} sets + {local_set_count} diffs displayed", () => Carousel.DisplayableItems == local_set_count + local_set_count); + CheckDisplayedBeatmapSetsCount(local_set_count); + CheckDisplayedBeatmapsCount(local_set_count); ApplyToFilter("filter to insane", c => c.SearchText = "Insane"); - AddAssert($"{local_set_count} sets + {local_set_count} diffs displayed", () => Carousel.DisplayableItems == local_set_count + local_set_count); + CheckDisplayedBeatmapSetsCount(local_set_count); + CheckDisplayedBeatmapsCount(local_set_count); } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index f81f068ab7..4da23c1fd4 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -16,6 +16,11 @@ namespace osu.Game.Screens.SelectV2 { private readonly Func getCriteria; + /// + /// Counts total number of beatmap difficulties displayed post filter. + /// + public int BeatmapItemsCount { get; private set; } + public BeatmapCarouselFilterMatching(Func getCriteria) { this.getCriteria = getCriteria; @@ -32,12 +37,17 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) { + BeatmapItemsCount = 0; + foreach (var item in items) { var beatmap = (BeatmapInfo)item.Model; if (checkMatch(beatmap, criteria)) + { + BeatmapItemsCount++; yield return item; + } } } From d34e040b4e16e68df37c365711cd873a542fa772 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 3 May 2025 02:35:25 +0300 Subject: [PATCH 119/152] Add test coverage for song select filtering --- .../TestSceneSongSelectFiltering.cs | 336 ++++++++++++++++++ osu.Game/Graphics/Carousel/Carousel.cs | 2 +- 2 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs new file mode 100644 index 0000000000..806604cd63 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -0,0 +1,336 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Input; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Overlays.Toolbar; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens; +using osu.Game.Screens.Footer; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneSongSelectFiltering : ScreenTestScene + { + private BeatmapManager manager = null!; + private RulesetStore rulesets = null!; + private MusicController music = null!; + private OsuConfigManager config = null!; + + private SoloSongSelect songSelect = null!; + private BeatmapCarousel carousel => songSelect.ChildrenOfType().Single(); + + private FilterControl filter => songSelect.ChildrenOfType().Single(); + private ShearedFilterTextBox filterTextBox => songSelect.ChildrenOfType().Single(); + private int filterOperationsCount; + + [Cached] + private readonly ScreenFooter screenFooter; + + [Cached] + private readonly OsuLogo logo; + + [Cached(typeof(INotificationOverlay))] + private readonly INotificationOverlay notificationOverlay = new NotificationOverlay(); + + public TestSceneSongSelectFiltering() + { + Children = new Drawable[] + { + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Toolbar + { + State = { Value = Visibility.Visible }, + }, + screenFooter = new ScreenFooter + { + OnBack = () => Stack.CurrentScreen.Exit(), + }, + logo = new OsuLogo + { + Alpha = 0f, + }, + }, + }, + }; + + Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT }; + } + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + RealmDetachedBeatmapStore beatmapStore; + + // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. + // At a point we have isolated interactive test runs enough, this can likely be removed. + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); + Dependencies.Cache(Realm); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); + + Dependencies.Cache(music = new MusicController()); + + // required to get bindables attached + Add(music); + Add(beatmapStore); + + Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Stack.ScreenPushed += updateFooter; + Stack.ScreenExited += updateFooter; + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("reset defaults", () => + { + Ruleset.Value = new OsuRuleset().RulesetInfo; + + Beatmap.SetDefault(); + SelectedMods.SetDefault(); + + config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title); + config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.All); + + songSelect = null!; + filterOperationsCount = 0; + }); + + AddStep("delete all beatmaps", () => manager.Delete()); + } + + [Test] + public void TestSingleFilterOnEnter() + { + importBeatmapForRuleset(0); + importBeatmapForRuleset(0); + + loadSongSelect(); + + AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0)); + } + + [Test] + public void TestNoFilterOnSimpleResume() + { + importBeatmapForRuleset(0); + importBeatmapForRuleset(0); + + loadSongSelect(); + + AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); + waitForSuspension(); + + AddStep("return", () => songSelect.MakeCurrent()); + AddUntilStep("wait for current", () => songSelect.IsCurrentScreen()); + AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0)); + } + + [Test] + public void TestFilterOnResumeAfterChange() + { + importBeatmapForRuleset(0); + importBeatmapForRuleset(0); + + AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + + loadSongSelect(); + + AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); + waitForSuspension(); + + AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); + + AddStep("return", () => songSelect.MakeCurrent()); + AddUntilStep("wait for current", () => songSelect.IsCurrentScreen()); + AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1)); + } + + [Test] + public void TestSorting() + { + loadSongSelect(); + addManyTestMaps(); + + // TODO: old test has this step, but there doesn't seem to be any purpose for it. + // AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap); + + AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); + AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title)); + AddStep(@"Sort by Author", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author)); + AddStep(@"Sort by DateAdded", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded)); + AddStep(@"Sort by BPM", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM)); + AddStep(@"Sort by Length", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length)); + AddStep(@"Sort by Difficulty", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty)); + AddStep(@"Sort by Source", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source)); + } + + [Test] + public void TestCutInFilterTextBox() + { + loadSongSelect(); + + AddStep("set filter text", () => filterTextBox.Current.Value = "nonono"); + AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll)); + AddStep("press ctrl/cmd-x", () => InputManager.Keys(PlatformAction.Cut)); + + AddAssert("filter text cleared", () => filterTextBox.Current.Value, () => Is.Empty); + } + + [Test] + public void TestNonFilterableModChange() + { + importBeatmapForRuleset(0); + + loadSongSelect(); + + // Mod that is guaranteed to never re-filter. + AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() }); + AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0)); + + // Removing the mod should still not re-filter. + AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty()); + AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0)); + } + + [Test] + public void TestFilterableModChange() + { + importBeatmapForRuleset(3); + + loadSongSelect(); + + // Change to mania ruleset. + AddStep("filter to mania ruleset", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 3)); + AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1)); + + // Apply a mod, but this should NOT re-filter because there's no search text. + AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() }); + AddAssert("filter count is 1", () => filterOperationsCount, () => Is.EqualTo(1)); + + // Set search text. Should re-filter. + AddStep("set search text to match mods", () => filterTextBox.Current.Value = "keys=3"); + AddAssert("filter count is 2", () => filterOperationsCount, () => Is.EqualTo(2)); + + // Change filterable mod. Should re-filter. + AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() }); + AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3)); + + // Add non-filterable mod. Should NOT re-filter. + AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() }); + AddAssert("filter count is 3", () => filterOperationsCount, () => Is.EqualTo(3)); + + // Remove filterable mod. Should re-filter. + AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() }); + AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4)); + + // Remove non-filterable mod. Should NOT re-filter. + AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty()); + AddAssert("filter count is 4", () => filterOperationsCount, () => Is.EqualTo(4)); + + // Add filterable mod. Should re-filter. + AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() }); + AddAssert("filter count is 5", () => filterOperationsCount, () => Is.EqualTo(5)); + } + + private void loadSongSelect() + { + AddStep("load screen", () => Stack.Push(songSelect = new SoloSongSelect())); + AddUntilStep("wait for load", () => Stack.CurrentScreen == songSelect && songSelect.IsLoaded); + AddStep("hook events", () => + { + filterOperationsCount = 0; + filter.CriteriaChanged += _ => filterOperationsCount++; + }); + } + + private void importBeatmapForRuleset(int rulesetId) + { + int beatmapsCount = 0; + + AddStep($"import test map for ruleset {rulesetId}", () => + { + beatmapsCount = songSelect.IsNull() ? 0 : carousel.Filters.OfType().Single().SetItems.Count; + manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == rulesetId).ToArray())); + }); + + // This is specifically for cases where the add is happening post song select load. + // For cases where song select is null, the assertions are provided by the load checks. + AddUntilStep("wait for imported to arrive in carousel", () => songSelect.IsNull() || carousel.Filters.OfType().Single().SetItems.Count > beatmapsCount); + } + + private void changeRuleset(int rulesetId) + { + AddStep($"change ruleset to {rulesetId}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == rulesetId)); + } + + /// + /// Imports test beatmap sets to show in the carousel. + /// + /// + /// The exact count of difficulties to create for each beatmap set. + /// A value causes the count of difficulties to be selected randomly. + /// + private void addManyTestMaps(int? difficultyCountPerSet = null) + { + AddStep("import test maps", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); + + for (int i = 0; i < 10; i++) + manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)); + }); + } + + private void waitForSuspension() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen()); + + private void updateFooter(IScreen? _, IScreen? newScreen) + { + if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) + { + screenFooter.Show(); + screenFooter.SetButtons(osuScreen.CreateFooterButtons()); + } + else + { + screenFooter.Hide(); + screenFooter.SetButtons(Array.Empty()); + } + } + } +} diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 8d8289422b..45cf8a8205 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -68,7 +68,7 @@ namespace osu.Game.Graphics.Carousel public int ItemsTracked => Items.Count; /// - /// The number of carousel items currently in rotation for display. + /// The items currently in rotation for display. /// public int DisplayableItems => carouselItems?.Count ?? 0; From cc0c21a21683e19b21de26160d2313f5aac126d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 15:44:27 +0900 Subject: [PATCH 120/152] Ensure carousel filters are manifested to lists at each step The original `IEnumerable` flow prioritised slight performance gains, but a filter's implementation could actually make this detrimental to overall performance. I noticed in passing that there were already potentially multiple enumerations, via `updateYPositions` and the final `ToList` call. Rather than faffing around, let's keep things simple and require lists. In benchmarking, the difference is (currently) negiligible. Slight improvement if anything. --- osu.Game/Graphics/Carousel/Carousel.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 8d8289422b..82a42fe459 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -265,7 +265,7 @@ namespace osu.Game.Graphics.Carousel // Copy must be performed on update thread for now (see ConfigureAwait above). // Could potentially be optimised in the future if it becomes an issue. - IEnumerable items = new List(Items.Select(m => new CarouselItem(m))); + List items = new List(Items.Select(m => new CarouselItem(m))); await Task.Run(async () => { @@ -274,7 +274,13 @@ namespace osu.Game.Graphics.Carousel foreach (var filter in Filters) { log($"Performing {filter.GetType().ReadableName()}"); - items = await filter.Run(items, cts.Token).ConfigureAwait(false); + var filteredItems = await filter.Run(items, cts.Token).ConfigureAwait(false); + + // To avoid shooting ourselves in the foot, ensure that we manifest a list after each filter. + // + // A future improvement may be passing a reference list through each filter rather than copying each time, + // but this is the safest approach. + items = filteredItems as List ?? filteredItems.ToList(); } log("Updating Y positions"); @@ -292,7 +298,7 @@ namespace osu.Game.Graphics.Carousel Schedule(() => { log("Items ready for display"); - carouselItems = items.ToList(); + carouselItems = items; displayedRange = null; // Need to call this to ensure correct post-selection logic is handled on the new items list. From 5d04cc045d2348e31c9f6741e6214a9fb4bd5442 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 15:44:01 +0900 Subject: [PATCH 121/152] Adjust matching filter's code to conform to other filter implementations --- .../SelectV2/BeatmapCarouselFilterMatching.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index 4da23c1fd4..526e76b5f1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.SelectV2 private readonly Func getCriteria; /// - /// Counts total number of beatmap difficulties displayed post filter. + /// The total number of beatmap difficulties displayed post filter. /// public int BeatmapItemsCount { get; private set; } @@ -26,19 +26,16 @@ namespace osu.Game.Screens.SelectV2 this.getCriteria = getCriteria; } - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - return await Task.Run(() => - { - var criteria = getCriteria(); - return matchItems(items, criteria); - }, cancellationToken).ConfigureAwait(false); - } + BeatmapItemsCount = 0; + var criteria = getCriteria(); + + return matchItems(items, criteria); + }, cancellationToken).ConfigureAwait(false); private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) { - BeatmapItemsCount = 0; - foreach (var item in items) { var beatmap = (BeatmapInfo)item.Model; From 1f2fba6e235a913d8d5308001393703d091cb581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 May 2025 09:15:15 +0200 Subject: [PATCH 122/152] Ignore "image proxying" test scene Because it just failed thrice (https://github.com/ppy/osu/runs/41775675413#r0) and to me it seems like a profoundly bad idea. I considered having it retry like the framework precedent of this (https://github.com/ppy/osu-framework/blob/dd2b701ed84c687ff71f5c50338d3b325159ee45/osu.Framework.Tests/IO/TestWebRequest.cs#L69) before I noticed that it was also hitting hardcoded production endpoints at which point I decided it was just too weird to live. --- osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs index 3d7ee137ba..60b10b9899 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs @@ -12,6 +12,7 @@ using osu.Game.Overlays.Comments; namespace osu.Game.Tests.Visual.Online { + [Ignore("This test hits online resources (and online retrieval can fail at any time), and also performs network calls to the production instance of the website. Un-ignore this test when it's actually actively needed.")] public partial class TestSceneImageProxying : OsuTestScene { [Test] From 1c6e998c951de030da40e0f3389096ed9b1583df Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 10:22:20 +0300 Subject: [PATCH 123/152] Expose matched beatmaps count from `BeatmapCarousel` --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 6 +----- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +++++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index f99433983b..fd27f2a438 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -162,11 +162,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void CheckDisplayedBeatmapsCount(int expected) { - AddAssert($"{expected} diffs displayed", () => - { - var matchingFilter = Carousel.Filters.OfType().Single(); - return matchingFilter.BeatmapItemsCount; - }, () => Is.EqualTo(expected)); + AddAssert($"{expected} diffs displayed", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected)); } protected void CheckDisplayedBeatmapSetsCount(int expected) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 3578fd46fa..e9107e5505 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -31,8 +31,14 @@ namespace osu.Game.Screens.SelectV2 private readonly LoadingLayer loading; + private readonly BeatmapCarouselFilterMatching matching; private readonly BeatmapCarouselFilterGrouping grouping; + /// + /// Total number of beatmap difficulties displayed with the filter. + /// + public int MatchedBeatmapsCount => matching.BeatmapItemsCount; + protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) { if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo) @@ -49,7 +55,7 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { - new BeatmapCarouselFilterMatching(() => Criteria), + matching = new BeatmapCarouselFilterMatching(() => Criteria), new BeatmapCarouselFilterSorting(() => Criteria), grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; From 8775731c2442416015148c5984c312fd173c6849 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 10:28:00 +0300 Subject: [PATCH 124/152] Add `SortAndGroupBy` method to simplify usages --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 9 +++++++++ .../Visual/SongSelectV2/TestSceneBeatmapCarousel.cs | 11 +++-------- .../TestSceneBeatmapCarouselArtistGrouping.cs | 3 +-- .../TestSceneBeatmapCarouselDifficultyGrouping.cs | 3 +-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index fd27f2a438..9d30c44a11 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -136,6 +136,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected void SortBy(SortMode mode) => ApplyToFilter($"sort by {mode.ToString().ToLowerInvariant()}", c => c.Sort = mode); protected void GroupBy(GroupMode mode) => ApplyToFilter($"group by {mode.ToString().ToLowerInvariant()}", c => c.Group = mode); + protected void SortAndGroupBy(SortMode sort, GroupMode group) + { + ApplyToFilter($"sort by {sort.ToString().ToLowerInvariant()} & group by {group.ToString().ToLowerInvariant()}", c => + { + c.Sort = sort; + c.Group = group; + }); + } + protected void ApplyToFilter(string description, Action? apply) { AddStep(description, () => diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 870225edb3..21030e0b88 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -33,14 +33,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Explicit] public void TestSorting() { - SortBy(SortMode.Artist); - GroupBy(GroupMode.All); - - SortBy(SortMode.Difficulty); - GroupBy(GroupMode.Difficulty); - - SortBy(SortMode.Artist); - GroupBy(GroupMode.Artist); + SortAndGroupBy(SortMode.Artist, GroupMode.All); + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index 9cf9d07a94..e404317cbd 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -19,8 +19,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RemoveAllBeatmaps(); CreateCarousel(); - SortBy(SortMode.Artist); - GroupBy(GroupMode.Artist); + SortAndGroupBy(SortMode.Artist, GroupMode.Artist); AddBeatmaps(10, 3, true); WaitForDrawablePanels(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 2d39e40213..f8e818809d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -21,8 +21,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RemoveAllBeatmaps(); CreateCarousel(); - SortBy(SortMode.Difficulty); - GroupBy(GroupMode.Difficulty); + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); AddBeatmaps(10, 3); WaitForDrawablePanels(); From 7789f4dbb0c68ee2b2753ae9d1fa201c5ff38e54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 16:26:43 +0900 Subject: [PATCH 125/152] Ensure `BeatmapItemsCount` is stable during filter operations --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index 526e76b5f1..c1ce4aaa69 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -28,7 +28,6 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - BeatmapItemsCount = 0; var criteria = getCriteria(); return matchItems(items, criteria); @@ -36,16 +35,20 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) { + int countMatching = 0; + foreach (var item in items) { var beatmap = (BeatmapInfo)item.Model; if (checkMatch(beatmap, criteria)) { - BeatmapItemsCount++; + countMatching++; yield return item; } } + + BeatmapItemsCount = countMatching; } private static bool checkMatch(BeatmapInfo beatmap, FilterCriteria criteria) From 9b3812210c8dce1ed13794c428e9214e164e2e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 May 2025 09:40:10 +0200 Subject: [PATCH 126/152] Allow falling back to opening multiplayer room history in browser if it's ended --- osu.Game/Localisation/NotificationsStrings.cs | 5 +++++ osu.Game/OsuGame.cs | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index bb2990f782..3614ed9133 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -135,6 +135,11 @@ Click to see what's new!", version); /// public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update..."); + /// + /// "This multiplayer room has ended. Click to display room results." + /// + public static LocalisableString MultiplayerRoomEnded => new TranslatableString(getKey(@"multiplayer_room_ended"), @"This multiplayer room has ended. Click to display room results."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 9d3af413dd..fc9d99f687 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -755,7 +755,17 @@ namespace osu.Game { if (room.HasEnded) { - Notifications.Post(new SimpleNotification { Text = "This multiplayer room has ended." }); + // TODO: Eventually it should be possible to display ended multiplayer rooms in game too, + // but it generally will require turning off the entirety of communication with spectator server which is currently embedded into multiplayer screens. + Notifications.Post(new SimpleNotification + { + Text = NotificationsStrings.MultiplayerRoomEnded, + Activated = () => + { + OpenUrlExternally($@"/multiplayer/rooms/{room.RoomID}"); + return true; + } + }); return; } From ebd67768987f4d2d26cc733be53a4c22f68c2165 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 16:40:23 +0900 Subject: [PATCH 127/152] Change `ICarouselFilter` interface return type rather than manual `ToList` I was hung up on keeping `IEnumerable` but there doesn't seem to be a good reason to do so. --- osu.Game/Graphics/Carousel/Carousel.cs | 3 +-- osu.Game/Graphics/Carousel/ICarouselFilter.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 82a42fe459..e5319703be 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -274,13 +274,12 @@ namespace osu.Game.Graphics.Carousel foreach (var filter in Filters) { log($"Performing {filter.GetType().ReadableName()}"); - var filteredItems = await filter.Run(items, cts.Token).ConfigureAwait(false); + items = await filter.Run(items, cts.Token).ConfigureAwait(false); // To avoid shooting ourselves in the foot, ensure that we manifest a list after each filter. // // A future improvement may be passing a reference list through each filter rather than copying each time, // but this is the safest approach. - items = filteredItems as List ?? filteredItems.ToList(); } log("Updating Y positions"); diff --git a/osu.Game/Graphics/Carousel/ICarouselFilter.cs b/osu.Game/Graphics/Carousel/ICarouselFilter.cs index 570f480aab..a85b44b46a 100644 --- a/osu.Game/Graphics/Carousel/ICarouselFilter.cs +++ b/osu.Game/Graphics/Carousel/ICarouselFilter.cs @@ -18,6 +18,6 @@ namespace osu.Game.Graphics.Carousel /// The items to be filtered. /// A cancellation token. /// The post-filtered items. - Task> Run(IEnumerable items, CancellationToken cancellationToken); + Task> Run(IEnumerable items, CancellationToken cancellationToken); } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index a628595477..6fbaa19045 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 this.getCriteria = getCriteria; } - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) { return await Task.Run(() => { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index 22a67321db..2a4f534a47 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.SelectV2 this.getCriteria = getCriteria; } - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { var criteria = getCriteria(); @@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2 } return comparison; - })); + })).ToList(); }, cancellationToken).ConfigureAwait(false); } } From ecc0c945212c2d30d5a42ad19536a9adaff31b4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 17:46:12 +0900 Subject: [PATCH 128/152] Fix return type git somehow didn't notice --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index c1ce4aaa69..ee213f1e93 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -26,11 +26,11 @@ namespace osu.Game.Screens.SelectV2 this.getCriteria = getCriteria; } - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { var criteria = getCriteria(); - return matchItems(items, criteria); + return matchItems(items, criteria).ToList(); }, cancellationToken).ConfigureAwait(false); private IEnumerable matchItems(IEnumerable items, FilterCriteria criteria) From f0ab6dc86999979674d1e3254574987043633530 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 11:36:27 +0300 Subject: [PATCH 129/152] Fix group pill count moving with panel selection state --- osu.Game/Screens/SelectV2/PanelGroup.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index 4370146dbc..bf9ea0e3c6 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -26,6 +26,7 @@ namespace osu.Game.Screens.SelectV2 private Drawable iconContainer = null!; private OsuSpriteText titleText = null!; private TrianglesV2 triangles = null!; + private CircularContainer countPill = null!; private OsuSpriteText countText = null!; private Box glow = null!; @@ -86,12 +87,12 @@ namespace osu.Game.Screens.SelectV2 UseFullGlyphHeight = false, X = 10f, }, - new CircularContainer + countPill = new CircularContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 20f }, + Margin = new MarginPadding { Right = 30f }, Masking = true, Children = new Drawable[] { @@ -145,5 +146,13 @@ namespace osu.Game.Screens.SelectV2 titleText.Text = group.Title; countText.Text = Item.NestedItemCount.ToString("N0"); } + + protected override void Update() + { + base.Update(); + + // Move the count pill in the opposite direction to keep it pinned to the screen regardless of the X position of TopLevelContent. + countPill.X = -TopLevelContent.X; + } } } From 1e05223a81d790349339d66828024925a75f1777 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 18:52:24 +0900 Subject: [PATCH 130/152] Add test coverage of `Search` method --- .../SongSelectV2/TestSceneBeatmapFilterControl.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs index df7e5ee645..284484d2df 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs @@ -10,6 +10,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneBeatmapFilterControl : SongSelectComponentsTestScene { + private FilterControl filterControl = null!; + protected override Anchor ComponentAnchor => Anchor.TopRight; protected override float InitialRelativeWidth => 0.7f; @@ -20,12 +22,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = new FilterControl + Child = filterControl = new FilterControl { State = { Value = Visibility.Visible }, RelativeSizeAxes = Axes.X, }, }; }); + + [Test] + public void TestSearch() + { + AddStep("search for text", () => filterControl.Search("test search")); + } } } From d2622c8aed410be986f9dc31f0c11e4adf6d1726 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 18:59:49 +0900 Subject: [PATCH 131/152] Remove unnecessary dependencies for now --- .../SongSelectV2/TestSceneSongSelectFiltering.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 806604cd63..d1786a5744 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -36,8 +36,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public partial class TestSceneSongSelectFiltering : ScreenTestScene { private BeatmapManager manager = null!; - private RulesetStore rulesets = null!; - private MusicController music = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + private OsuConfigManager config = null!; private SoloSongSelect songSelect = null!; @@ -91,15 +93,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); - Dependencies.Cache(Realm); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); - Dependencies.Cache(music = new MusicController()); - - // required to get bindables attached - Add(music); Add(beatmapStore); Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); From 8cc2af4060bfb8a9c1f7ede893aae75bfb219e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 May 2025 12:02:36 +0200 Subject: [PATCH 132/152] Fix gameplay leaderboard not always being expanded in gameplay leaderboard I'd have preferred a `get; init;` property but tests were also attached at the hip to the public bindable. Without some extra composition this is the best that I can do. --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 2 +- .../MultiplayerGameplayLeaderboardTestScene.cs | 2 +- .../Multiplayer/TestSceneMultiSpectatorLeaderboard.cs | 2 +- .../TestSceneMultiplayerGameplayLeaderboardTeams.cs | 2 +- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 9 +++++---- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 4a1c0121ae..f8caa121a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("toggle expanded", () => { if (leaderboard.IsNotNull()) - leaderboard.Expanded.Value = !leaderboard.Expanded.Value; + leaderboard.ForceExpand.Value = !leaderboard.ForceExpand.Value; }); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 1481629ba0..3008edf41f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestScoreUpdates() { AddRepeatStep("update state", UpdateUserStatesRandomly, 100); - AddToggleStep("switch compact mode", expanded => Leaderboard!.Expanded.Value = expanded); + AddToggleStep("switch compact mode", expanded => Leaderboard!.ForceExpand.Value = expanded); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 806de68f07..131b644dcb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Expanded = { Value = true } + ForceExpand = { Value = true } } }); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 15efde7abe..40d8650c69 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Origin = Anchor.BottomCentre, Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] }, Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] }, - Expanded = { BindTarget = Leaderboard!.Expanded }, + Expanded = { BindTarget = Leaderboard!.ForceExpand }, }, Add); }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 59cbef0d15..06efffbf6e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }); leaderboardFlow.Insert(0, Leaderboard = new DrawableGameplayLeaderboard { - Expanded = { Value = true } + ForceExpand = { Value = true } }); LoadComponentAsync(new GameplayChatDisplay(room) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index af03a6b73f..fb064cd753 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Play.HUD { public partial class DrawableGameplayLeaderboard : CompositeDrawable, ISerialisableDrawable { - public Bindable Expanded = new Bindable(); + public readonly Bindable ForceExpand = new Bindable(); protected readonly FillFlowContainer Flow; @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD private readonly IBindable userPlayingState = new Bindable(); private readonly IBindable holdingForHUD = new Bindable(); - private const int max_panels = 8; + private readonly Bindable expanded = new Bindable(); /// /// Create a new leaderboard. @@ -100,6 +100,7 @@ namespace osu.Game.Screens.Play.HUD configVisibility.BindValueChanged(_ => Scheduler.AddOnce(updateState)); userPlayingState.BindValueChanged(_ => Scheduler.AddOnce(updateState)); holdingForHUD.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + ForceExpand.BindValueChanged(_ => Scheduler.AddOnce(updateState)); updateState(); } @@ -110,7 +111,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); Flow.FadeTo(player?.Configuration.ShowLeaderboard != false && configVisibility.Value ? 1 : 0, 100, Easing.OutQuint); - Expanded.Value = userPlayingState.Value == LocalUserPlayingState.Playing || holdingForHUD.Value; + expanded.Value = ForceExpand.Value || userPlayingState.Value == LocalUserPlayingState.Playing || holdingForHUD.Value; } /// @@ -128,7 +129,7 @@ namespace osu.Game.Screens.Play.HUD TrackedScore = drawable; } - drawable.Expanded.BindTo(Expanded); + drawable.Expanded.BindTo(expanded); Flow.Add(drawable); drawable.ScorePosition.BindValueChanged(_ => Scheduler.AddOnce(sort)); From 1fc68a3c485f090998595bfe1a18605b22515412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 May 2025 12:12:25 +0200 Subject: [PATCH 133/152] Fix back-to-front conditional --- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index fb064cd753..e04d91b5b7 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); Flow.FadeTo(player?.Configuration.ShowLeaderboard != false && configVisibility.Value ? 1 : 0, 100, Easing.OutQuint); - expanded.Value = ForceExpand.Value || userPlayingState.Value == LocalUserPlayingState.Playing || holdingForHUD.Value; + expanded.Value = ForceExpand.Value || userPlayingState.Value != LocalUserPlayingState.Playing || holdingForHUD.Value; } /// From b89669e56de69b65373c0fabdc8eba73eb9417ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 19:26:00 +0900 Subject: [PATCH 134/152] Fix edge case of last pattern not being processed correctly --- .../Mods/TestSceneTaikoModSimplifiedRhythm.cs | 47 +++++++---- .../Mods/TaikoModSimplifiedRhythm.cs | 80 +++++++++++-------- 2 files changed, 79 insertions(+), 48 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs index 09ff5fe266..1e2c2a21ce 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSimplifiedRhythm.cs @@ -103,31 +103,50 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods OneEighthConversion = { Value = true } }, Autoplay = false, - CreateBeatmap = () => new Beatmap + CreateBeatmap = () => { - HitObjects = new List + const double one_eighth_timing = 125; + + return new Beatmap { - new Hit { StartTime = 1000, Type = HitType.Centre }, - new Hit { StartTime = 1250, Type = HitType.Centre }, - new Hit { StartTime = 1500, Type = HitType.Centre }, - new Hit { StartTime = 1625, Type = HitType.Rim }, // mod removes this - new Hit { StartTime = 1750, Type = HitType.Centre }, - new Hit { StartTime = 2000, Type = HitType.Centre }, - }, + HitObjects = new List + { + new Hit { StartTime = 1000, Type = HitType.Centre }, + new Hit { StartTime = 1250, Type = HitType.Centre }, + new Hit { StartTime = 1500, Type = HitType.Centre }, + new Hit { StartTime = 1500 + one_eighth_timing * 1, Type = HitType.Rim }, // mod removes this + new Hit { StartTime = 1500 + one_eighth_timing * 2 }, + new Hit { StartTime = 2000, Type = HitType.Centre }, + new Hit { StartTime = 2000 + one_eighth_timing * 1, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 2000 + one_eighth_timing * 2, Type = HitType.Centre }, + new Hit { StartTime = 2000 + one_eighth_timing * 3, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 2000 + one_eighth_timing * 4, Type = HitType.Centre }, + new Hit { StartTime = 2000 + one_eighth_timing * 5, Type = HitType.Centre }, // mod removes this + new Hit { StartTime = 2000 + one_eighth_timing * 6, Type = HitType.Centre }, + new Hit { StartTime = 2000 + one_eighth_timing * 7, Type = HitType.Centre }, // mod removes this + }, + }; }, ReplayFrames = new List { new TaikoReplayFrame(1000, TaikoAction.LeftCentre), - new TaikoReplayFrame(1200), + new TaikoReplayFrame(1000), new TaikoReplayFrame(1250, TaikoAction.LeftCentre), - new TaikoReplayFrame(1450), + new TaikoReplayFrame(1250), new TaikoReplayFrame(1500, TaikoAction.LeftCentre), - new TaikoReplayFrame(1700), + new TaikoReplayFrame(1500), new TaikoReplayFrame(1750, TaikoAction.LeftCentre), - new TaikoReplayFrame(1900), + new TaikoReplayFrame(1750), new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2000), + new TaikoReplayFrame(2250, TaikoAction.LeftCentre), + new TaikoReplayFrame(2250), + new TaikoReplayFrame(2500, TaikoAction.LeftCentre), + new TaikoReplayFrame(2500), + new TaikoReplayFrame(2750, TaikoAction.LeftCentre), + new TaikoReplayFrame(2750), }, - PassCondition = () => Player.ScoreProcessor.Combo.Value == 5 && Player.ScoreProcessor.Accuracy.Value == 1 + PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1 }); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs index 661e932300..e690ff075b 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs @@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Taiko.Mods Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast().ToArray(); + if (hits.Length == 0) + return; + var conversions = new List<(int, int)>(); if (OneEighthConversion.Value) conversions.Add((8, 4)); @@ -62,40 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Mods inPattern = false; - // Iterate through the pattern - for (int j = patternStartIndex; j < i; j++) - { - int indexInPattern = j - patternStartIndex; - - switch (baseRhythm) - { - // 1/8: Remove every second note - case 8: - { - if (indexInPattern % 2 == 1) - { - taikoBeatmap.HitObjects.Remove(hits[j]); - } - - break; - } - - // 1/6 and 1/3: Remove every second note and adjust time of every third - case 6: - case 3: - { - if (indexInPattern % 3 == 1) - taikoBeatmap.HitObjects.Remove(hits[j]); - else if (indexInPattern % 3 == 2) - hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm; - - break; - } - - default: - throw new ArgumentOutOfRangeException(nameof(baseRhythm)); - } - } + processPattern(i); } else { @@ -106,6 +76,48 @@ namespace osu.Game.Rulesets.Taiko.Mods } } } + + // Process the last pattern if we reached the end of the beatmap and are still in a pattern. + if (inPattern) + processPattern(hits.Length); + + void processPattern(int patternEndIndex) + { + // Iterate through the pattern + for (int j = patternStartIndex; j < patternEndIndex; j++) + { + int indexInPattern = j - patternStartIndex; + + switch (baseRhythm) + { + // 1/8: Remove every second note + case 8: + { + if (indexInPattern % 2 == 1) + { + taikoBeatmap.HitObjects.Remove(hits[j]); + } + + break; + } + + // 1/6 and 1/3: Remove every second note and adjust time of every third + case 6: + case 3: + { + if (indexInPattern % 3 == 1) + taikoBeatmap.HitObjects.Remove(hits[j]); + else if (indexInPattern % 3 == 2) + hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm; + + break; + } + + default: + throw new ArgumentOutOfRangeException(nameof(baseRhythm)); + } + } + } } } From ad586cb5dda8300850baf48b4afb444308b4af9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 May 2025 12:30:23 +0200 Subject: [PATCH 135/152] Use better error messaging in case of beatmap ID mismatch Should maybe give users a better idea of what's wrong. --- osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs index fab080cdba..158b6bc02d 100644 --- a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit.Submission } if (playableBeatmap.BeatmapInfo.OnlineID > 0) - throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); + throw new InvalidOperationException($@"Difficulty ""{playableBeatmap.BeatmapInfo.DifficultyName}"" has BeatmapID {playableBeatmap.BeatmapInfo.OnlineID} that has not been assigned to it by the server!"); if (allocatedBeatmapIds.Count == 0) throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); From 588c1719787fc323d33d8583826c8df68ef7eb2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 May 2025 12:52:49 +0200 Subject: [PATCH 136/152] Improve logging around import-as-update flow - It was logging success before actually succeeding. - It appears in practice that this code can somehow actually nullref. Unfortunately logs provided in that instance were not enough to pinpoint what (because of lack of line numbers). I'm hoping that by logging as error, and therefore to sentry, we can actually retrieve this information so that there's no need to work blind. --- osu.Game/Beatmaps/BeatmapImporter.cs | 96 +++++++++++++++------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 77aca5eecf..28997509dc 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -72,58 +72,66 @@ namespace osu.Game.Beatmaps first.PerformWrite(updated => { - var realm = updated.Realm; - - Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database); - - // Re-fetch as we are likely on a different thread. - original = realm!.Find(originalId)!; - - // Generally the import process will do this for us if the OnlineIDs match, - // but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated). - original.DeletePending = true; - - // Transfer local values which should be persisted across a beatmap update. - updated.DateAdded = originalDateAdded; - - transferCollectionReferences(realm, original, updated); - - foreach (var beatmap in original.Beatmaps.ToArray()) + try { - var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash); + var realm = updated.Realm; - if (updatedBeatmap != null) + // Re-fetch as we are likely on a different thread. + original = realm!.Find(originalId)!; + + // Generally the import process will do this for us if the OnlineIDs match, + // but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated). + original.DeletePending = true; + + // Transfer local values which should be persisted across a beatmap update. + updated.DateAdded = originalDateAdded; + + transferCollectionReferences(realm, original, updated); + + foreach (var beatmap in original.Beatmaps.ToArray()) { - // If the updated beatmap matches an existing one, transfer any user data across.. - if (beatmap.Scores.Any()) + var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash); + + if (updatedBeatmap != null) { - Logger.Log($"Transferring {beatmap.Scores.Count()} scores for unchanged difficulty \"{beatmap}\"", LoggingTarget.Database); + // If the updated beatmap matches an existing one, transfer any user data across.. + if (beatmap.Scores.Any()) + { + Logger.Log($"Transferring {beatmap.Scores.Count()} scores for unchanged difficulty \"{beatmap}\"", LoggingTarget.Database); - foreach (var score in beatmap.Scores) - score.BeatmapInfo = updatedBeatmap; + foreach (var score in beatmap.Scores) + score.BeatmapInfo = updatedBeatmap; + } + + // ..then nuke the old beatmap completely. + // this is done instead of a soft deletion to avoid a user potentially creating weird + // interactions, like restoring the outdated beatmap then updating a second time + // (causing user data to be wiped). + original.Beatmaps.Remove(beatmap); + + realm.Remove(beatmap.Metadata); + realm.Remove(beatmap); + } + else + { + // If the beatmap differs in the original, leave it in a soft-deleted state but reset online info. + // This caters to the case where a user has made modifications they potentially want to restore, + // but after restoring we want to ensure it can't be used to trigger an update of the beatmap. + beatmap.ResetOnlineInfo(); } - - // ..then nuke the old beatmap completely. - // this is done instead of a soft deletion to avoid a user potentially creating weird - // interactions, like restoring the outdated beatmap then updating a second time - // (causing user data to be wiped). - original.Beatmaps.Remove(beatmap); - - realm.Remove(beatmap.Metadata); - realm.Remove(beatmap); - } - else - { - // If the beatmap differs in the original, leave it in a soft-deleted state but reset online info. - // This caters to the case where a user has made modifications they potentially want to restore, - // but after restoring we want to ensure it can't be used to trigger an update of the beatmap. - beatmap.ResetOnlineInfo(); } + + // If the original has no beatmaps left, delete the set as well. + if (!original.Beatmaps.Any()) + realm.Remove(original); + + Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database); + } + catch (Exception ex) + { + Logger.Error(ex, $"Failed to update beatmap \"{updated}\"", LoggingTarget.Database); + throw; } - - // If the original has no beatmaps left, delete the set as well. - if (!original.Beatmaps.Any()) - realm.Remove(original); }); return first; From 6c7fc4249fa86567a1be2e39675457b9c97484a2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 14:27:02 +0300 Subject: [PATCH 137/152] Fix song select filtering test scene reading from local database --- .../Visual/SongSelectV2/TestSceneSongSelectFiltering.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index d1786a5744..7134bb9ba2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -36,9 +36,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public partial class TestSceneSongSelectFiltering : ScreenTestScene { private BeatmapManager manager = null!; - - [Resolved] - private RulesetStore rulesets { get; set; } = null!; + private RealmRulesetStore rulesets = null!; private OsuConfigManager config = null!; @@ -93,6 +91,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // At a point we have isolated interactive test runs enough, this can likely be removed. + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); + Dependencies.Cache(Realm); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); From 8af687f751670aee888a437ba8711f80b55da7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 May 2025 13:50:11 +0200 Subject: [PATCH 138/152] Fix gameplay leaderboard score reading off wrong combo property Closes https://github.com/ppy/osu/issues/33006. Broke in c231571f06167b4445148bf29ac70c4facb3f8e3. The fact that this mistake can be made at all is... something, but it was made. --- .../Screens/Select/Leaderboards/GameplayLeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs index 2837da23f4..bf99472dd7 100644 --- a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Select.Leaderboards Tracked = tracked; TotalScore.Value = scoreInfo.TotalScore; Accuracy.Value = scoreInfo.Accuracy; - Combo.Value = scoreInfo.Combo; + Combo.Value = scoreInfo.MaxCombo; TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); GetDisplayScore = scoreInfo.GetDisplayScore; InitialPosition = scoreInfo.Position; From 9c40344c9e2017b59fd74d540658c21205e014d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 May 2025 14:23:35 +0200 Subject: [PATCH 139/152] Discard distance snapping result if it results in objects being placed out of playfield bounds Mainly an issue with "limit distance snap to current time". Reported in https://discord.com/channels/90072389919997952/1259818301517725707/1369037235797753999. This slightly changes behaviour of distance snap when the mouse is near the edges of the screen (will turn off snap rather than clamp to edge as previously), but I think that's probably fine. --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index ed3fc34d94..c28226fcf4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -259,6 +259,10 @@ namespace osu.Game.Rulesets.Osu.Edit var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime); + + if (pos.X < 0 || pos.X > OsuPlayfield.BASE_SIZE.X || pos.Y < 0 || pos.Y > OsuPlayfield.BASE_SIZE.Y) + return null; + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield); } From eca9389f4080091c33e50f00eb9f5405190242f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 8 May 2025 08:19:55 +0200 Subject: [PATCH 140/152] Use localised strings for user/mapper tags on beatmap set overlay --- osu.Game/Overlays/BeatmapSet/MetadataType.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/MetadataType.cs b/osu.Game/Overlays/BeatmapSet/MetadataType.cs index dba6a63679..fe38d23242 100644 --- a/osu.Game/Overlays/BeatmapSet/MetadataType.cs +++ b/osu.Game/Overlays/BeatmapSet/MetadataType.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; @@ -9,10 +8,10 @@ namespace osu.Game.Overlays.BeatmapSet { public enum MetadataType { - [Description("User Tags")] // TODO: use translated string after osu-resources update + [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoUserTags))] UserTags, - [Description("Mapper Tags")] // TODO: use translated string after osu-resources update + [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoMapperTags))] MapperTags, [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))] From c4794b2de33537553ce5f7aa4caee3ec2f2bd173 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 14:06:11 +0300 Subject: [PATCH 141/152] Add input gap test coverage for group + beatmapset + beatmap panels combination Fix gap tests not passing on certain aspect ratios Flooooating poiiinnts --- .../TestSceneBeatmapCarouselArtistGrouping.cs | 36 +++++++++++++++++++ ...tSceneBeatmapCarouselDifficultyGrouping.cs | 3 +- .../TestSceneBeatmapCarouselNoGrouping.cs | 3 +- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index e404317cbd..aabb2705fd 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -5,8 +5,10 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; +using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -174,6 +176,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForGroupSelection(1, 1); } + [Test] + public void TestInputHandlingWithinGaps() + { + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + + // Clicks just above the first group panel should not actuate any action. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1))); + + AddAssert("no sets visible", () => !GetVisiblePanels().Any()); + + // add lenience to avoid floating-point inaccuracies at edge. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 - 1))); + + AddUntilStep("wait for sets visible", () => GetVisiblePanels().Any()); + CheckNoSelection(); + + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + + ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForGroupSelection(0, 1); + + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + + // Beatmap panels expand their selection area to cover holes from spacing. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForGroupSelection(0, 1); + + ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForGroupSelection(0, 2); + + ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForGroupSelection(0, 5); + } + [Test] public void TestBasicFiltering() { diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index f8e818809d..6050d516d6 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -179,7 +179,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2))); + // add lenience to avoid floating-point inaccuracies at edge. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 - 1))); AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); CheckNoSelection(); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index cdd55f0f0c..efb39e2cc9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -218,7 +218,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2))); + // add lenience to avoid floating-point inaccuracies at edge. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 - 1))); AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); WaitForSelection(0, 0); From 25c26f6f2138306fa6299d52f3e13239cc7a6b60 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 13:36:21 +0300 Subject: [PATCH 142/152] Fix group panel overlapping with other panels --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index f1d49aae15..2bb7bd29ce 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -42,6 +42,11 @@ namespace osu.Game.Screens.SelectV2 protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) { + if ((top.Model is GroupDefinition || bottom.Model is GroupDefinition) && + !(top.Model is GroupDefinition && bottom.Model is GroupDefinition)) + // Group panels do not overlap with any other panel but should overlap with themselves. + return SPACING; + if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo) // Beatmap difficulty panels do not overlap with themselves or any other panel. return SPACING; From c2693dd6a22a9a227d36687a01067768ad04372d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 May 2025 17:23:28 +0900 Subject: [PATCH 143/152] Add slightly more spacing for groups --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 2bb7bd29ce..1e18aea961 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -42,13 +42,12 @@ namespace osu.Game.Screens.SelectV2 protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) { - if ((top.Model is GroupDefinition || bottom.Model is GroupDefinition) && - !(top.Model is GroupDefinition && bottom.Model is GroupDefinition)) - // Group panels do not overlap with any other panel but should overlap with themselves. - return SPACING; + // Group panels do not overlap with any other panel but should overlap with themselves. + if ((top.Model is GroupDefinition) ^ (bottom.Model is GroupDefinition)) + return SPACING * 2; + // Beatmap difficulty panels do not overlap with themselves or any other panel. if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo) - // Beatmap difficulty panels do not overlap with themselves or any other panel. return SPACING; return -SPACING; From 386100c7186c56e0454282d888fa478c3b7d6f1e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 14:30:47 +0300 Subject: [PATCH 144/152] Add "no results" placeholder --- .../Screens/SelectV2/NoResultsPlaceholder.cs | 156 ++++++++++++++++++ osu.Game/Screens/SelectV2/SongSelect.cs | 11 +- 2 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs diff --git a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs new file mode 100644 index 0000000000..ae526ef878 --- /dev/null +++ b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs @@ -0,0 +1,156 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Localisation; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Screens.Select; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class NoResultsPlaceholder : VisibilityContainer + { + private FilterCriteria? filter; + + private LinkFlowContainer textFlow = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Resolved] + private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; } + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + public FilterCriteria Filter + { + set + { + if (filter == value) + return; + + filter = value; + Scheduler.AddOnce(updateText); + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Masking = true; + CornerRadius = 10; + + Width = 400; + AutoSizeAxes = Axes.Y; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colours.Gray2, + RelativeSizeAxes = Axes.Both, + }, + new SpriteIcon + { + Icon = FontAwesome.Regular.SadTear, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding(10), + Size = new Vector2(50), + }, + textFlow = new LinkFlowContainer + { + Y = 60, + Padding = new MarginPadding(10), + TextAnchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + }; + } + + protected override void PopIn() + { + this.FadeIn(600, Easing.OutQuint); + + Scheduler.AddOnce(updateText); + } + + protected override void PopOut() + { + this.FadeOut(200, Easing.OutQuint); + } + + private void updateText() + { + // TODO: Refresh this text when new beatmaps are imported. Right now it won't get up-to-date suggestions. + + // Bounce should play every time the filter criteria is updated. + this.ScaleTo(0.9f) + .ScaleTo(1f, 1000, Easing.OutElastic); + + textFlow.Clear(); + + if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null) + { + textFlow.AddParagraph("No beatmaps found!"); + textFlow.AddParagraph(string.Empty); + + textFlow.AddParagraph("- Consider running the \""); + textFlow.AddLink(FirstRunSetupOverlayStrings.FirstRunSetupTitle, () => firstRunSetupOverlay?.Show()); + textFlow.AddText("\" to download or import some beatmaps!"); + } + else + { + textFlow.AddParagraph("No beatmaps match your filter criteria!"); + textFlow.AddParagraph(string.Empty); + + if (filter?.UserStarDifficulty.HasFilter == true) + { + textFlow.AddParagraph("- Try "); + textFlow.AddLink("removing", () => + { + config.SetValue(OsuSetting.DisplayStarsMinimum, 0.0); + config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1); + }); + + string lowerStar = $"{filter.UserStarDifficulty.Min ?? 0:N1}"; + string upperStar = filter.UserStarDifficulty.Max == null ? "∞" : $"{filter.UserStarDifficulty.Max:N1}"; + + textFlow.AddText($" the {lowerStar} - {upperStar} star difficulty filter."); + } + + // TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch). + // TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting. + if (filter?.Ruleset?.OnlineID != 0 && filter?.AllowConvertedBeatmaps == false) + { + textFlow.AddParagraph("- Try"); + textFlow.AddLink(" enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); + textFlow.AddText("automatic conversion!"); + } + } + + if (!string.IsNullOrEmpty(filter?.SearchText)) + { + textFlow.AddParagraph("- Try "); + textFlow.AddLink("searching online", LinkAction.SearchBeatmapSet, filter.SearchText); + textFlow.AddText($" for \"{filter.SearchText}\"."); + } + // TODO: add clickable link to reset criteria. + } + } +} diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 3d2d85e037..062d7cff2c 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -50,6 +50,8 @@ namespace osu.Game.Screens.SelectV2 private BeatmapDetailsArea detailsArea = null!; private FillFlowContainer wedgesContainer = null!; + private NoResultsPlaceholder noResultsPlaceholder = null!; + public override bool ShowFooter => true; [Resolved] @@ -128,6 +130,7 @@ namespace osu.Game.Screens.SelectV2 RequestPresentBeatmap = _ => OnStart(), RelativeSizeAxes = Axes.Both, }, + noResultsPlaceholder = new NoResultsPlaceholder(), } }, filterControl = new FilterControl @@ -282,7 +285,11 @@ namespace osu.Game.Screens.SelectV2 private void criteriaChanged(FilterCriteria criteria) { filterDebounce?.Cancel(); - filterDebounce = Scheduler.AddDelayed(() => carousel.Filter(criteria), filter_delay); + filterDebounce = Scheduler.AddDelayed(() => + { + noResultsPlaceholder.Filter = criteria; + carousel.Filter(criteria); + }, filter_delay); } #endregion @@ -290,7 +297,9 @@ namespace osu.Game.Screens.SelectV2 protected override void Update() { base.Update(); + detailsArea.Height = wedgesContainer.DrawHeight - titleWedge.LayoutSize.Y - 4; + noResultsPlaceholder.State.Value = carousel.MatchedBeatmapsCount == 0 ? Visibility.Visible : Visibility.Hidden; } } } From cdf70aa66be044b5d1bde9b07b81651888401005 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 7 May 2025 14:30:50 +0300 Subject: [PATCH 145/152] Add test coverage --- .../TestSceneSongSelectFiltering.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 7134bb9ba2..da78f19dc5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets; @@ -264,6 +265,54 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("filter count is 5", () => filterOperationsCount, () => Is.EqualTo(5)); } + [Test] + public void TestPlaceholderBeatmapPresence() + { + loadSongSelect(); + + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); + + importBeatmapForRuleset(0); + AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden); + + AddStep("delete all beatmaps", () => manager.Delete()); + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); + } + + [Test] + public void TestPlaceholderStarDifficulty() + { + importBeatmapForRuleset(0); + AddStep("change star filter", () => config.SetValue(OsuSetting.DisplayStarsMinimum, 10.0)); + + loadSongSelect(); + + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); + + AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick()); + + AddUntilStep("star filter reset", () => config.Get(OsuSetting.DisplayStarsMinimum) == 0.0); + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden); + } + + [Test] + public void TestPlaceholderConvertSetting() + { + importBeatmapForRuleset(0); + AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + + loadSongSelect(); + + changeRuleset(2); + + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); + + AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick()); + + AddUntilStep("convert setting changed", () => config.Get(OsuSetting.ShowConvertedBeatmaps)); + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden); + } + private void loadSongSelect() { AddStep("load screen", () => Stack.Push(songSelect = new SoloSongSelect())); @@ -275,6 +324,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + private NoResultsPlaceholder? getPlaceholder() => songSelect.ChildrenOfType().FirstOrDefault(); + private void importBeatmapForRuleset(int rulesetId) { int beatmapsCount = 0; From c53eb1c647caf6d63c0578baa2ae4d824328dc0c Mon Sep 17 00:00:00 2001 From: ohdj <71207981+ohdj@users.noreply.github.com> Date: Thu, 8 May 2025 22:36:26 +0800 Subject: [PATCH 146/152] Fix Show More button display based on osu-web --- .../Sections/PaginatedProfileSubsection.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 1c94048758..d5b0433d43 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -46,6 +46,8 @@ namespace osu.Game.Overlays.Profile.Sections private OsuSpriteText missing = null!; private readonly LocalisableString? missingText; + private bool hasMore { get; set; } + protected PaginatedProfileSubsection(Bindable user, LocalisableString? headerText = null, LocalisableString? missingText = null) : base(user, headerText, CounterVisibilityState.AlwaysVisible) { @@ -99,6 +101,7 @@ namespace osu.Game.Overlays.Profile.Sections CurrentPage = null; ItemsContainer.Clear(); + hasMore = false; if (e.NewValue?.User != null) { @@ -116,7 +119,7 @@ namespace osu.Game.Overlays.Profile.Sections CurrentPage = CurrentPage?.TakeNext(ItemsPerPage) ?? new PaginationParameters(InitialItemsCount); - retrievalRequest = CreateRequest(User.Value, CurrentPage.Value); + retrievalRequest = CreateRequest(User.Value, new PaginationParameters(CurrentPage.Value.Offset, CurrentPage.Value.Limit + 1)); retrievalRequest.Success += items => UpdateItems(items, loadCancellation); api.Queue(retrievalRequest); @@ -124,12 +127,11 @@ namespace osu.Game.Overlays.Profile.Sections protected virtual void UpdateItems(List items, CancellationTokenSource cancellationTokenSource) => Schedule(() => { - OnItemsReceived(items); - if (!items.Any() && CurrentPage?.Offset == 0) { moreButton.Hide(); moreButton.IsLoading = false; + hasMore = false; if (missingText.HasValue) missing.Show(); @@ -137,11 +139,19 @@ namespace osu.Game.Overlays.Profile.Sections return; } + // mutates items and returns whether there are more items than expectedCount. + hasMore = items.Count > CurrentPage?.Limit; + + if (hasMore) + items.RemoveAt(items.Count - 1); + + OnItemsReceived(items); + LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null).Cast(), drawables => { missing.Hide(); - moreButton.FadeTo(items.Count == CurrentPage?.Limit ? 1 : 0); + moreButton.FadeTo(hasMore ? 1 : 0); moreButton.IsLoading = false; ItemsContainer.AddRange(drawables); From f3de345d476090568665874be2769cfff4364962 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 May 2025 19:58:08 +0900 Subject: [PATCH 147/152] Update no results placeholder design to feel better alongside new song select --- .../Screens/Select/NoResultsPlaceholder.cs | 4 +- .../Screens/SelectV2/NoResultsPlaceholder.cs | 88 ++++++++++++------- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Select/NoResultsPlaceholder.cs b/osu.Game/Screens/Select/NoResultsPlaceholder.cs index 9f870503d3..50577d5fea 100644 --- a/osu.Game/Screens/Select/NoResultsPlaceholder.cs +++ b/osu.Game/Screens/Select/NoResultsPlaceholder.cs @@ -137,8 +137,8 @@ namespace osu.Game.Screens.Select // TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting. if (filter?.Ruleset?.OnlineID != 0 && filter?.AllowConvertedBeatmaps == false) { - textFlow.AddParagraph("- Try"); - textFlow.AddLink(" enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); + textFlow.AddParagraph("- Try "); + textFlow.AddLink("enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); textFlow.AddText("automatic conversion!"); } } diff --git a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs index ae526ef878..5ca6dad2a2 100644 --- a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs +++ b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs @@ -4,12 +4,12 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Online.Chat; using osu.Game.Overlays; @@ -33,6 +33,8 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OsuConfigManager config { get; set; } = null!; + protected override bool StartHidden => true; + public FilterCriteria Filter { set @@ -46,11 +48,8 @@ namespace osu.Game.Screens.SelectV2 } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - Masking = true; - CornerRadius = 10; - Width = 400; AutoSizeAxes = Axes.Y; @@ -59,27 +58,39 @@ namespace osu.Game.Screens.SelectV2 InternalChildren = new Drawable[] { - new Box + new FillFlowContainer { - Colour = colours.Gray2, - RelativeSizeAxes = Axes.Both, - }, - new SpriteIcon - { - Icon = FontAwesome.Regular.SadTear, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding(10), - Size = new Vector2(50), - }, - textFlow = new LinkFlowContainer - { - Y = 60, - Padding = new MarginPadding(10), - TextAnchor = Anchor.TopCentre, + Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - } + Children = new Drawable[] + { + new SpriteIcon + { + Icon = FontAwesome.Solid.Ghost, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding(10), + Size = new Vector2(50), + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Style.Title, + Text = "No beatmaps found" + }, + textFlow = new LinkFlowContainer + { + Alpha = 0, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding { Top = 20 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + }, }; } @@ -101,16 +112,16 @@ namespace osu.Game.Screens.SelectV2 // Bounce should play every time the filter criteria is updated. this.ScaleTo(0.9f) - .ScaleTo(1f, 1000, Easing.OutElastic); + .ScaleTo(1f, 1000, Easing.OutQuint); + + textFlow.FadeInFromZero(800, Easing.OutQuint); textFlow.Clear(); if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null) { - textFlow.AddParagraph("No beatmaps found!"); - textFlow.AddParagraph(string.Empty); - - textFlow.AddParagraph("- Consider running the \""); + addBulletPoint(); + textFlow.AddText("Consider running the \""); textFlow.AddLink(FirstRunSetupOverlayStrings.FirstRunSetupTitle, () => firstRunSetupOverlay?.Show()); textFlow.AddText("\" to download or import some beatmaps!"); } @@ -121,7 +132,8 @@ namespace osu.Game.Screens.SelectV2 if (filter?.UserStarDifficulty.HasFilter == true) { - textFlow.AddParagraph("- Try "); + addBulletPoint(); + textFlow.AddText("Try "); textFlow.AddLink("removing", () => { config.SetValue(OsuSetting.DisplayStarsMinimum, 0.0); @@ -138,19 +150,31 @@ namespace osu.Game.Screens.SelectV2 // TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting. if (filter?.Ruleset?.OnlineID != 0 && filter?.AllowConvertedBeatmaps == false) { - textFlow.AddParagraph("- Try"); - textFlow.AddLink(" enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); + addBulletPoint(); + textFlow.AddText("Try "); + textFlow.AddLink("enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); textFlow.AddText("automatic conversion!"); } } if (!string.IsNullOrEmpty(filter?.SearchText)) { - textFlow.AddParagraph("- Try "); + addBulletPoint(); + textFlow.AddText("Try "); textFlow.AddLink("searching online", LinkAction.SearchBeatmapSet, filter.SearchText); textFlow.AddText($" for \"{filter.SearchText}\"."); } // TODO: add clickable link to reset criteria. } + + private void addBulletPoint() + { + textFlow.NewLine(); + textFlow.AddIcon(FontAwesome.Solid.Circle, i => + { + i.Padding = new MarginPadding { Top = 24, Right = 15 }; + i.Scale *= 0.3f; + }); + } } } From c0361c41f5401856d8f4da93717309ca56f75d60 Mon Sep 17 00:00:00 2001 From: ohdj <71207981+ohdj@users.noreply.github.com> Date: Fri, 9 May 2025 13:49:29 +0800 Subject: [PATCH 148/152] Make `hasMore` a local variable in `UpdateItems` --- .../Profile/Sections/PaginatedProfileSubsection.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index d5b0433d43..0afc20d66d 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -46,8 +46,6 @@ namespace osu.Game.Overlays.Profile.Sections private OsuSpriteText missing = null!; private readonly LocalisableString? missingText; - private bool hasMore { get; set; } - protected PaginatedProfileSubsection(Bindable user, LocalisableString? headerText = null, LocalisableString? missingText = null) : base(user, headerText, CounterVisibilityState.AlwaysVisible) { @@ -101,7 +99,6 @@ namespace osu.Game.Overlays.Profile.Sections CurrentPage = null; ItemsContainer.Clear(); - hasMore = false; if (e.NewValue?.User != null) { @@ -131,7 +128,6 @@ namespace osu.Game.Overlays.Profile.Sections { moreButton.Hide(); moreButton.IsLoading = false; - hasMore = false; if (missingText.HasValue) missing.Show(); @@ -139,8 +135,7 @@ namespace osu.Game.Overlays.Profile.Sections return; } - // mutates items and returns whether there are more items than expectedCount. - hasMore = items.Count > CurrentPage?.Limit; + bool hasMore = items.Count > CurrentPage?.Limit; if (hasMore) items.RemoveAt(items.Count - 1); From a957f41ffc80fd6cac78204676fc699bb593c30b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 May 2025 14:49:38 +0900 Subject: [PATCH 149/152] Expose a way of knowing when carousel's displayed items are updated --- .../Visual/SongSelectV2/BeatmapCarouselTestScene.cs | 5 +++++ .../SongSelectV2/TestSceneBeatmapCarouselFiltering.cs | 6 ++++++ osu.Game/Graphics/Carousel/Carousel.cs | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 9d30c44a11..649dc7f6a4 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -49,6 +49,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private int beatmapCount; + protected int NewItemsPresentedInvocationCount; + protected BeatmapCarouselTestScene() { store = new TestBeatmapStore @@ -65,6 +67,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("create components", () => { + NewItemsPresentedInvocationCount = 0; + Box topBox; Children = new Drawable[] { @@ -98,6 +102,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Carousel = new BeatmapCarousel { + NewItemsPresented = () => NewItemsPresentedInvocationCount++, BleedTop = 50, BleedBottom = 50, Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 21c726f9ac..2381ebcf6e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -34,9 +34,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(10, 3); WaitForDrawablePanels(); + AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1)); + ApplyToFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); WaitForFiltering(); + AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2)); + CheckDisplayedBeatmapSetsCount(1); CheckDisplayedBeatmapsCount(3); @@ -54,6 +58,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilter("remove filter", c => c.SearchText = string.Empty); WaitForFiltering(); + AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3)); + CheckDisplayedBeatmapSetsCount(10); CheckDisplayedBeatmapsCount(30); } diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 77d4938a6a..34d1c39dcb 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -35,6 +35,11 @@ namespace osu.Game.Graphics.Carousel { #region Properties and methods for external usage + /// + /// Called after a filter operation or change in items results in the visible carousel items changing. + /// + public Action? NewItemsPresented { private get; init; } + /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. /// @@ -304,6 +309,8 @@ namespace osu.Game.Graphics.Carousel HandleItemSelected(currentSelection.Model); refreshAfterSelection(); + + NewItemsPresented?.Invoke(); }); void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); From 1edbdc5aac9f3d44e099f294991dced7a8ce43de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 May 2025 14:51:54 +0900 Subject: [PATCH 150/152] Update filter control's status text with beatmap displayed count This also fixes code running in `Update` which shouldn't be, by consuming the new `NewItemsPresented` callback. Fields and properties are renamed to knock some sense into things (was previously called two or three different things). --- .../TestSceneSongSelectFiltering.cs | 38 ++++++++++++++++-- .../TestSceneShearedSearchTextBox.cs | 2 +- .../UserInterface/ShearedFilterTextBox.cs | 10 ++--- osu.Game/Screens/SelectV2/FilterControl.cs | 6 +-- osu.Game/Screens/SelectV2/SongSelect.cs | 39 +++++++++++++------ 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index da78f19dc5..e88b47a287 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -31,6 +31,9 @@ using osu.Game.Screens.Menu; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; +using FilterControl = osu.Game.Screens.SelectV2.FilterControl; +using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -266,7 +269,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - public void TestPlaceholderBeatmapPresence() + public void TestPlaceholderVisibleAfterDeleteAll() { loadSongSelect(); @@ -280,7 +283,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - public void TestPlaceholderStarDifficulty() + public void TestPlaceholderVisibleAfterStarDifficultyFilter() { importBeatmapForRuleset(0); AddStep("change star filter", () => config.SetValue(OsuSetting.DisplayStarsMinimum, 10.0)); @@ -296,7 +299,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - public void TestPlaceholderConvertSetting() + public void TestPlaceholderVisibleWithConvertSetting() { importBeatmapForRuleset(0); AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); @@ -313,6 +316,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden); } + [Test] + public void TestCorrectMatchCountAfterDeleteAll() + { + loadSongSelect(); + checkMatchedBeatmaps(0); + + importBeatmapForRuleset(0); + checkMatchedBeatmaps(3); + + AddStep("delete all beatmaps", () => manager.Delete()); + checkMatchedBeatmaps(0); + } + + [Test] + public void TestCorrectMatchCountAfterHardDelete() + { + loadSongSelect(); + checkMatchedBeatmaps(0); + + importBeatmapForRuleset(0); + checkMatchedBeatmaps(3); + + AddStep("hard delete beatmap", () => Realm.Write(r => r.RemoveRange(r.All().Where(s => !s.Protected)))); + checkMatchedBeatmaps(0); + } + private void loadSongSelect() { AddStep("load screen", () => Stack.Push(songSelect = new SoloSongSelect())); @@ -364,6 +393,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + private void checkMatchedBeatmaps(int expected) => + AddUntilStep($"{expected} matching shown", () => carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected)); + private void waitForSuspension() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen()); private void updateFooter(IScreen? _, IScreen? newScreen) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs index d4141f2b64..0ecaf4900a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, RelativeSizeAxes = Axes.X, Width = 0.5f, - FilterText = "12345 matches", + StatusText = "12345 matches", }, } }, diff --git a/osu.Game/Graphics/UserInterface/ShearedFilterTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedFilterTextBox.cs index cffe34650c..635990ec9c 100644 --- a/osu.Game/Graphics/UserInterface/ShearedFilterTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedFilterTextBox.cs @@ -12,10 +12,10 @@ namespace osu.Game.Graphics.UserInterface { private const float filter_text_size = 12; - public LocalisableString FilterText + public LocalisableString StatusText { - get => ((InnerFilterTextBox)TextBox).FilterText.Text; - set => Schedule(() => ((InnerFilterTextBox)TextBox).FilterText.Text = value); + get => ((InnerFilterTextBox)TextBox).StatusText.Text; + set => Schedule(() => ((InnerFilterTextBox)TextBox).StatusText.Text = value); } public ShearedFilterTextBox() @@ -27,12 +27,12 @@ namespace osu.Game.Graphics.UserInterface protected partial class InnerFilterTextBox : InnerSearchTextBox { - public OsuSpriteText FilterText { get; private set; } = null!; + public OsuSpriteText StatusText { get; private set; } = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) { - TextContainer.Add(FilterText = new OsuSpriteText + TextContainer.Add(StatusText = new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 5eda47391a..69029cd131 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -47,10 +47,10 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OsuConfigManager config { get; set; } = null!; - public LocalisableString InformationalNote + public LocalisableString StatusText { - get => searchTextBox.FilterText; - set => searchTextBox.FilterText = value; + get => searchTextBox.StatusText; + set => searchTextBox.StatusText = value; } public event Action? CriteriaChanged; diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 062d7cff2c..d0749c8e6f 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -128,6 +128,7 @@ namespace osu.Game.Screens.SelectV2 BleedTop = FilterControl.HEIGHT_FROM_SCREEN_TOP + 5, BleedBottom = ScreenFooter.HEIGHT + 5, RequestPresentBeatmap = _ => OnStart(), + NewItemsPresented = newItemsPresented, RelativeSizeAxes = Axes.Both, }, noResultsPlaceholder = new NoResultsPlaceholder(), @@ -151,6 +152,12 @@ namespace osu.Game.Screens.SelectV2 }); } + /// + /// Called when a selection is made. + /// + /// If a resultant action occurred that takes the user away from SongSelect. + protected abstract bool OnStart(); + public override IReadOnlyList CreateFooterButtons() => new ScreenFooterButton[] { new FooterButtonMods(modSelectOverlay) { Current = Mods }, @@ -171,6 +178,15 @@ namespace osu.Game.Screens.SelectV2 }, true); } + protected override void Update() + { + base.Update(); + + detailsArea.Height = wedgesContainer.DrawHeight - titleWedge.LayoutSize.Y - 4; + } + + #region Transitions + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); @@ -250,12 +266,6 @@ namespace osu.Game.Screens.SelectV2 }; } - /// - /// Called when a selection is made. - /// - /// If a resultant action occurred that takes the user away from SongSelect. - protected abstract bool OnStart(); - protected override void LogoSuspending(OsuLogo logo) { base.LogoSuspending(logo); @@ -270,6 +280,8 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } + #endregion + #region Filtering private const double filter_delay = 250; @@ -292,14 +304,17 @@ namespace osu.Game.Screens.SelectV2 }, filter_delay); } - #endregion - - protected override void Update() + private void newItemsPresented() { - base.Update(); + int count = carousel.MatchedBeatmapsCount; - detailsArea.Height = wedgesContainer.DrawHeight - titleWedge.LayoutSize.Y - 4; - noResultsPlaceholder.State.Value = carousel.MatchedBeatmapsCount == 0 ? Visibility.Visible : Visibility.Hidden; + noResultsPlaceholder.State.Value = count == 0 ? Visibility.Visible : Visibility.Hidden; + + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + filterControl.StatusText = count != 1 ? $"{count:#,0} matches" : $"{count:#,0} match"; } + + #endregion } } From 10cb038f1230b89a1c07964cd25ae69ff7417c70 Mon Sep 17 00:00:00 2001 From: ohdj <71207981+ohdj@users.noreply.github.com> Date: Fri, 9 May 2025 14:02:26 +0800 Subject: [PATCH 151/152] Prevent increasing pagination limit on subsequent requests --- .../Overlays/Profile/Sections/PaginatedProfileSubsection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 0afc20d66d..0f613585e2 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Profile.Sections CurrentPage = CurrentPage?.TakeNext(ItemsPerPage) ?? new PaginationParameters(InitialItemsCount); - retrievalRequest = CreateRequest(User.Value, new PaginationParameters(CurrentPage.Value.Offset, CurrentPage.Value.Limit + 1)); + retrievalRequest = CreateRequest(User.Value, new PaginationParameters(CurrentPage.Value.Offset, InitialItemsCount + 1)); retrievalRequest.Success += items => UpdateItems(items, loadCancellation); api.Queue(retrievalRequest); From 93d2bb8a5e461ede155d5fd7e9e4e29882642f9c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 9 May 2025 09:19:16 +0300 Subject: [PATCH 152/152] Add delete hotkey functionality to new song select --- .../TestSceneSongSelectFiltering.cs | 33 +++++++++++++++ osu.Game/Screens/SelectV2/FilterControl.cs | 12 ++++++ osu.Game/Screens/SelectV2/SongSelect.cs | 42 +++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index e88b47a287..1e368dbee3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -19,6 +19,7 @@ using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Chat; using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania.Mods; @@ -31,9 +32,11 @@ using osu.Game.Screens.Menu; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; +using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; using FilterControl = osu.Game.Screens.SelectV2.FilterControl; using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder; +using BeatmapDeleteDialog = osu.Game.Screens.Select.BeatmapDeleteDialog; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -268,6 +271,36 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("filter count is 5", () => filterOperationsCount, () => Is.EqualTo(5)); } + // This test should probably not be in this test class, it has nothing to do with filtering. + // TestSceneSongSelect is a better place, but doesn't have local storage isolation setup (yet). + [Test] + public void TestDeleteHotkey() + { + loadSongSelect(); + + importBeatmapForRuleset(0); + + AddAssert("beatmap imported", () => manager.GetAllUsableBeatmapSets().Any(), () => Is.True); + + // song select should automatically select the beatmap for us but this is not implemented yet. + // todo: remove when that's the case. + AddAssert("no beatmap selected", () => Beatmap.IsDefault); + AddStep("select beatmap", () => Beatmap.Value = manager.GetWorkingBeatmap(manager.GetAllUsableBeatmapSets().Single().Beatmaps.First())); + AddAssert("beatmap selected", () => !Beatmap.IsDefault); + + AddStep("press shift-delete", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Delete); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + AddUntilStep("delete dialog shown", () => DialogOverlay.CurrentDialog, Is.InstanceOf); + AddStep("confirm deletion", () => DialogOverlay.CurrentDialog!.PerformAction()); + + AddAssert("beatmap set deleted", () => manager.GetAllUsableBeatmapSets().Any(), () => Is.False); + } + [Test] public void TestPlaceholderVisibleAfterDeleteAll() { diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 69029cd131..5845c36882 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -9,6 +9,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.Containers; @@ -21,6 +23,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.SelectV2 { @@ -274,6 +277,15 @@ namespace osu.Game.Screens.SelectV2 private partial class InnerTextBox : InnerFilterTextBox { public override bool HandleLeftRightArrows => false; + + public override bool OnPressed(KeyBindingPressEvent e) + { + // the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action. + if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete)) + return false; + + return base.OnPressed(e); + } } } } diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index d0749c8e6f..a647e04c0a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -9,8 +9,10 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -19,6 +21,7 @@ using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.SelectV2 { @@ -57,6 +60,9 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OsuLogo? logo { get; set; } + [Resolved] + private IDialogOverlay? dialogs { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -316,5 +322,41 @@ namespace osu.Game.Screens.SelectV2 } #endregion + + #region Beatmap management + + /// + /// Opens up with the given beatmap set. + /// + public void RequestDeleteBeatmap(BeatmapSetInfo set) + { + dialogs?.Push(new BeatmapDeleteDialog(set)); + } + + #endregion + + #region Hotkeys + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) return false; + + switch (e.Key) + { + case Key.Delete: + if (e.ShiftPressed) + { + if (!Beatmap.IsDefault) + RequestDeleteBeatmap(Beatmap.Value.BeatmapSetInfo); + return true; + } + + break; + } + + return base.OnKeyDown(e); + } + + #endregion } }