From 2a332896c1f19f89d4f2719803c88e1cd32f51a0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 05:38:13 -0400 Subject: [PATCH 01/92] 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 02/92] 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 03/92] 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 04/92] 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 05/92] 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 06/92] 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 07/92] 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 08/92] 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 09/92] 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 10/92] 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 11/92] 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 12/92] 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 13/92] 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 14/92] 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 15/92] 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 16/92] 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 17/92] 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 18/92] 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 19/92] 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 20/92] 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 21/92] 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 22/92] 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 23/92] 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 24/92] 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 25/92] 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 26/92] 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 06dc4235f079ed7d61cd34887516bab56646d413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Apr 2025 11:47:25 +0200 Subject: [PATCH 27/92] Use actual score positions in gameplay leaderboard Intends to close https://github.com/ppy/osu/issues/32859. The difference between this and https://github.com/ppy/osu/pull/32942 is that this PR takes the approach of completely moving the score sorting behaviour to `IGameplayLeaderboardProvider` implementations. This is going to be helpful for further work - to be precise, I am looking to add a leaderboard position indicator in the bottom right of multiplayer player to match stable, and having the position in the provider will make the implementation of that *much* easier. --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 30 +------- .../Online/Leaderboards/LeaderboardManager.cs | 9 ++- .../Play/HUD/DrawableGameplayLeaderboard.cs | 26 +------ .../HUD/DrawableGameplayLeaderboardScore.cs | 29 ++------ .../Leaderboards/GameplayLeaderboardScore.cs | 24 +++++- .../IGameplayLeaderboardProvider.cs | 9 --- .../MultiplayerLeaderboardProvider.cs | 27 +++++++ .../SoloGameplayLeaderboardProvider.cs | 73 +++++++++++++++++-- 8 files changed, 136 insertions(+), 91 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index bef43b3108..f0b2f710c6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -183,30 +183,6 @@ namespace osu.Game.Tests.Visual.Gameplay () => Does.Contain("#FF549A")); } - [Test] - public void TestTrackedScorePosition([Values] bool partial) - { - createLeaderboard(partial); - - AddStep("add many scores in one go", () => - { - for (int i = 0; i < 49; i++) - createRandomScore(new APIUser { Username = $"Player {i + 1}" }); - - // Add player at end to force an animation down the whole list. - playerScore.Value = 0; - createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true); - }); - - if (partial) - AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); - else - AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); - - AddStep("move tracked player to top", () => leaderboard.TrackedScore!.TotalScore.Value = 8_000_000); - AddUntilStep("all players have non-null position", () => leaderboard.AllScores.Select(s => s.ScorePosition), () => Does.Not.Contain(null)); - } - private void addLocalPlayer() { AddStep("add local player", () => @@ -216,12 +192,11 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void createLeaderboard(bool partial = false) + private void createLeaderboard() { AddStep("create leaderboard", () => { leaderboardProvider.Scores.Clear(); - leaderboardProvider.IsPartial = partial; Child = leaderboard = new TestDrawableGameplayLeaderboard { Anchor = Anchor.Centre, @@ -247,7 +222,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username); - return scoreItem != null && scoreItem.ScorePosition == expectedPosition; + return scoreItem != null && scoreItem.ScorePosition.Value == expectedPosition; } public IEnumerable GetAllScoresForUsername(string username) @@ -260,7 +235,6 @@ namespace osu.Game.Tests.Visual.Gameplay { IBindableList IGameplayLeaderboardProvider.Scores => Scores; public BindableList Scores { get; } = new BindableList(); - public bool IsPartial { get; set; } } } } 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/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 005cd784c4..af286731aa 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -5,7 +5,6 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -20,8 +19,6 @@ namespace osu.Game.Screens.Play.HUD { public partial class DrawableGameplayLeaderboard : CompositeDrawable { - private readonly Cached sorting = new Cached(); - public Bindable Expanded = new Bindable(); protected readonly FillFlowContainer Flow; @@ -87,7 +84,6 @@ namespace osu.Game.Screens.Play.HUD }, true); } - Scheduler.AddDelayed(sort, 1000, true); configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true); } @@ -109,8 +105,8 @@ namespace osu.Game.Screens.Play.HUD drawable.Expanded.BindTo(Expanded); Flow.Add(drawable); - drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true); - drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true); + drawable.ScorePosition.BindValueChanged(_ => Scheduler.AddOnce(sort)); + drawable.DisplayOrder.BindValueChanged(_ => Scheduler.AddOnce(sort), true); int displayCount = Math.Min(Flow.Count, max_panels); Height = displayCount * (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y); @@ -179,22 +175,8 @@ namespace osu.Game.Screens.Play.HUD private void sort() { - if (sorting.IsValid) - return; - - var orderedByScore = Flow - .OrderByDescending(i => i.TotalScore.Value) - .ThenBy(i => i.DisplayOrder.Value) - .ToList(); - - for (int i = 0; i < Flow.Count; i++) - { - var score = orderedByScore[i]; - Flow.SetLayoutPosition(score, i); - score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1; - } - - sorting.Validate(); + foreach (var score in Flow.ToArray()) + Flow.SetLayoutPosition(score, score.DisplayOrder.Value); } private partial class InputDisabledScrollContainer : OsuScrollContainer diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index b14e31983c..e4f2cc0d68 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -56,6 +56,7 @@ namespace osu.Game.Screens.Play.HUD public BindableDouble Accuracy { get; } = new BindableDouble(1); public BindableInt Combo { get; } = new BindableInt(); public BindableBool HasQuit { get; } = new BindableBool(); + public Bindable ScorePosition { get; } = new Bindable(); public Bindable DisplayOrder { get; } = new Bindable(); private Func? getDisplayScoreFunction; @@ -69,28 +70,6 @@ namespace osu.Game.Screens.Play.HUD public Color4? TextColour { get; set; } - private int? scorePosition; - - private bool scorePositionIsSet; - - public int? ScorePosition - { - get => scorePosition; - set - { - // We always want to run once, as the incoming value may be null and require a visual update to "-". - if (value == scorePosition && scorePositionIsSet) - return; - - scorePosition = value; - - positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "-"; - scorePositionIsSet = true; - - updateState(); - } - } - public IUser? User { get; } /// @@ -123,6 +102,7 @@ namespace osu.Game.Screens.Play.HUD Accuracy.BindTo(score.Accuracy); Combo.BindTo(score.Combo); HasQuit.BindTo(score.HasQuit); + ScorePosition.BindTo(score.Position); DisplayOrder.BindTo(score.DisplayOrder); GetDisplayScore = score.GetDisplayScore; @@ -334,6 +314,7 @@ namespace osu.Game.Screens.Play.HUD updateState(); Expanded.BindValueChanged(changeExpandedState, true); + ScorePosition.BindValueChanged(_ => updateState(), true); FinishTransforms(true); } @@ -392,7 +373,9 @@ namespace osu.Game.Screens.Play.HUD return; } - if (scorePosition == 1) + positionText.Text = ScorePosition.Value.HasValue ? $"#{ScorePosition.Value.Value.FormatRank()}" : "-"; + + if (ScorePosition.Value == 1) { widthExtension = true; panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33"); diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs index 2655fd8dba..b681306053 100644 --- a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select.Leaderboards /// An optional value to guarantee stable ordering. /// Lower numbers will appear higher in cases of ties. /// - public Bindable DisplayOrder { get; } = new BindableLong(); + public long TotalScoreTiebreaker { get; init; } /// /// A custom function which handles converting a score to a display score using a provided . @@ -68,6 +68,25 @@ namespace osu.Game.Screens.Select.Leaderboards /// public Colour4? TeamColour { get; init; } + /// + /// The initial position of the score on the leaderboard. + /// Mostly used for cases like the local user's best score on the global leaderboard (which will not be contiguous with the other scores). + /// + public int? InitialPosition { get; init; } = null; + + /// + /// The displayed rank of the score on the leaderboard. + /// + public Bindable Position { get; } = new Bindable(); + + /// + /// The index of the score on the leaderboard. + /// This differs from in that it is required (must always be known) + /// and that it doesn't represent the score's position on global leaderboards. + /// It's a property completely local to and relative to all scores provided by the managing . + /// + public Bindable DisplayOrder { get; } = new BindableLong(); + public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked) { User = user; @@ -95,8 +114,9 @@ namespace osu.Game.Screens.Select.Leaderboards TotalScore.Value = scoreInfo.TotalScore; Accuracy.Value = scoreInfo.Accuracy; Combo.Value = scoreInfo.Combo; - DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); + TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); GetDisplayScore = scoreInfo.GetDisplayScore; + InitialPosition = scoreInfo.Position; } /// diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs index 4399c422b4..468a5cbf9c 100644 --- a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -14,14 +14,5 @@ namespace osu.Game.Screens.Select.Leaderboards /// List of all scores to display on the leaderboard. /// public IBindableList Scores { get; } - - /// - /// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores), - /// or is a full leaderboard (contains all scores that there will ever be). - /// - /// - /// If this is and a tracked score is last on the leaderboard, it will show an "unknown" score position. - /// - bool IsPartial { get; } } } diff --git a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index edfccd0e7e..80a5692841 100644 --- a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; @@ -55,6 +56,8 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private OsuColour colours { get; set; } = null!; + private readonly Cached sorting = new Cached(); + public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users) { this.users = users; @@ -101,6 +104,8 @@ namespace osu.Game.Screens.Select.Leaderboards HasQuit = { BindTarget = trackedUser.UserQuit }, TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null, }; + leaderboardScore.TotalScore.BindValueChanged(_ => sorting.Invalidate()); + leaderboardScore.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true); scores.Add(leaderboardScore); } }); @@ -124,6 +129,8 @@ namespace osu.Game.Screens.Select.Leaderboards // new players are not supported. playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); playingUserIds.BindCollectionChanged(playingUsersChanged); + + Scheduler.AddDelayed(sort, 1000, true); } private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -174,6 +181,26 @@ namespace osu.Game.Screens.Select.Leaderboards } } + private void sort() + { + if (sorting.IsValid) + return; + + var orderedByScore = scores + .OrderByDescending(i => i.TotalScore.Value) + .ThenBy(i => i.TotalScoreTiebreaker) + .ToList(); + + for (int i = 0; i < orderedByScore.Count; i++) + { + var score = orderedByScore[i]; + score.DisplayOrder.Value = i; + score.Position.Value = i + 1; + } + + sorting.Validate(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 5cbbb3f3b0..41d57f7d24 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -1,8 +1,10 @@ // 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.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; @@ -12,8 +14,6 @@ namespace osu.Game.Screens.Select.Leaderboards { public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider { - public bool IsPartial { get; private set; } - public IBindableList Scores => scores; private readonly BindableList scores = new BindableList(); @@ -23,13 +23,16 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private GameplayState? gameplayState { get; set; } + private readonly Cached sorting = new Cached(); + private bool isPartial; + protected override void LoadComplete() { base.LoadComplete(); var globalScores = leaderboardManager?.Scores.Value; - IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; + isPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; if (globalScores != null) { @@ -39,12 +42,70 @@ namespace osu.Game.Screens.Select.Leaderboards if (gameplayState != null) { - scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + var localScore = new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) { // Local score should always show lower than any existing scores in cases of ties. - DisplayOrder = { Value = long.MaxValue } - }); + TotalScoreTiebreaker = long.MaxValue + }; + localScore.TotalScore.BindValueChanged(_ => sorting.Invalidate()); + scores.Add(localScore); } + + Scheduler.AddDelayed(sort, 1000, true); + } + + private void sort() + { + if (sorting.IsValid) + return; + + var orderedByScore = scores + .OrderByDescending(i => i.TotalScore.Value) + .ThenBy(i => i.TotalScoreTiebreaker) + .ToList(); + + int delta = 0; + + for (int i = 0; i < orderedByScore.Count; i++) + { + var score = orderedByScore[i]; + + score.DisplayOrder.Value = i + 1; + + // if we know we have all scores there can ever be, we can do the simple and obvious thing. + if (!isPartial) + score.Position.Value = i + 1; + else + { + // we have a partial leaderboard, with potential gaps. + // we have initial score positions which were valid at the point of starting play. + // the assumption here is that non-tracked scores here cannot move around, only tracked ones can. + if (score.Tracked) + { + int? previousScorePosition = i > 0 ? orderedByScore[i - 1].InitialPosition : 0; + int? nextScorePosition = i < orderedByScore.Count - 1 ? orderedByScore[i + 1].InitialPosition : null; + + // if the tracked score is perfectly between two scores which have known neighbouring initial positions, + // we can assign it the position of the previous score plus one... + if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition) + { + score.Position.Value = previousScorePosition + 1; + // but we also need to ensure all subsequent scores get shifted down one position, too. + delta++; + } + // conversely, if the tracked score is not between neighbouring two scores and the leaderboard is partial, + // we can't really assign a valid position at all. it could be any number between the two neighbours. + else + score.Position.Value = null; + } + // for non-tracked scores, we just need to apply any delta that might have come from the tracked scores + // which might have been encountered and assigned a position earlier. + else + score.Position.Value = score.InitialPosition + delta; + } + } + + sorting.Validate(); } } } From 7822412c422d32b91b5f6c59d59a214da33dccc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Apr 2025 12:40:53 +0200 Subject: [PATCH 28/92] Add test coverage of new behaviour of solo gameplay leaderboard --- ...estSceneSoloGameplayLeaderboardProvider.cs | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboardProvider.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboardProvider.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboardProvider.cs new file mode 100644 index 0000000000..964f53c973 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboardProvider.cs @@ -0,0 +1,162 @@ +// 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.Graphics; +using osu.Framework.Testing; +using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Tests.Gameplay; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [HeadlessTest] + public partial class TestSceneSoloGameplayLeaderboardProvider : OsuTestScene + { + [Test] + public void TestLocalLeaderboardHasPositionsAutofilled() + { + SoloGameplayLeaderboardProvider provider = null!; + + var leaderboardManager = new LeaderboardManager(); + LoadComponent(leaderboardManager); + var gameplayState = TestGameplayState.Create(new OsuRuleset()); + + AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Local, null))); + AddStep("set scores", () => + { + // this is dodgy but anything less dodgy is a lot of work + ((Bindable)leaderboardManager.Scores).Value = LeaderboardScores.Success( + Enumerable.Range(1, 100).Select(i => new ScoreInfo + { + TotalScore = 10_000 * (100 - i), + Position = i, + }).ToArray(), + null + ); + }); + AddStep("create content", () => Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(LeaderboardManager), leaderboardManager), + (typeof(GameplayState), gameplayState) + ], + Children = new Drawable[] + { + leaderboardManager, + provider = new SoloGameplayLeaderboardProvider() + } + }); + AddUntilStep("tracked score shows #101", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(101)); + AddUntilStep("tracked score ordered #101", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(101)); + AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000); + AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20)); + AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20)); + AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000); + AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1)); + AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1)); + } + + [Test] + public void TestFullGlobalLeaderboard() + { + SoloGameplayLeaderboardProvider provider = null!; + + var leaderboardManager = new LeaderboardManager(); + LoadComponent(leaderboardManager); + var gameplayState = TestGameplayState.Create(new OsuRuleset()); + + AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Global, null))); + AddStep("set scores", () => + { + // this is dodgy but anything less dodgy is a lot of work + ((Bindable)leaderboardManager.Scores).Value = LeaderboardScores.Success( + Enumerable.Range(1, 40).Select(i => new ScoreInfo + { + TotalScore = 600_000 + 10_000 * (40 - i), + Position = i, + }).ToArray(), + null + ); + }); + AddStep("create content", () => Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(LeaderboardManager), leaderboardManager), + (typeof(GameplayState), gameplayState) + ], + Children = new Drawable[] + { + leaderboardManager, + provider = new SoloGameplayLeaderboardProvider() + } + }); + AddUntilStep("tracked score shows #41", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(41)); + AddUntilStep("tracked score ordered #41", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(41)); + AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000); + AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20)); + AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20)); + AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000); + AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1)); + AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1)); + } + + [Test] + public void TestPartialGlobalLeaderboard() + { + SoloGameplayLeaderboardProvider provider = null!; + + var leaderboardManager = new LeaderboardManager(); + LoadComponent(leaderboardManager); + var gameplayState = TestGameplayState.Create(new OsuRuleset()); + + AddStep("fetch local", () => leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(Beatmap.Value.BeatmapInfo, Ruleset.Value, BeatmapLeaderboardScope.Global, null))); + AddStep("set scores", () => + { + // this is dodgy but anything less dodgy is a lot of work + ((Bindable)leaderboardManager.Scores).Value = LeaderboardScores.Success( + Enumerable.Range(1, 50).Select(i => new ScoreInfo + { + TotalScore = 500_000 + 10_000 * (50 - i), + Position = i + }).ToArray(), + new ScoreInfo { TotalScore = 200_000 } + ); + }); + AddStep("create content", () => Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(LeaderboardManager), leaderboardManager), + (typeof(GameplayState), gameplayState) + ], + Children = new Drawable[] + { + leaderboardManager, + provider = new SoloGameplayLeaderboardProvider() + } + }); + AddUntilStep("tracked score shows no position", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.Null); + AddUntilStep("tracked score ordered #52", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(52)); + AddStep("move score above user best", () => gameplayState.ScoreProcessor.TotalScore.Value = 202_000); + AddUntilStep("tracked score shows no position", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.Null); + AddUntilStep("tracked score ordered #51", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(51)); + AddStep("move score to #20", () => gameplayState.ScoreProcessor.TotalScore.Value = 802_000); + AddUntilStep("tracked score shows #20", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(20)); + AddUntilStep("tracked score ordered #20", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(20)); + AddStep("move score to #1", () => gameplayState.ScoreProcessor.TotalScore.Value = 1_002_000); + AddUntilStep("tracked score shows #1", () => provider.Scores.Single(s => s.Tracked).Position.Value, () => Is.EqualTo(1)); + AddUntilStep("tracked score ordered #1", () => provider.Scores.Single(s => s.Tracked).DisplayOrder.Value, () => Is.EqualTo(1)); + } + } +} From 3eb1ae225daad615a387277f7a76b0f3b8eaf3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Apr 2025 13:06:01 +0200 Subject: [PATCH 29/92] Exclude non-user-playable mods from mod filter in beatmap leaderboard For easier understanding, substitute "non-user-playable" with "autoplay". The reason that I'm bothering to do this is that if you put autoplay on and turn on the "Selected Mods" filter, the request will actually go through and hit web, and web will obviously return no scores. On song select that's *maybe* fine, even though probably unintended still, but now with the leaderboard state being global this also means gameplay gets impacted. Which also means that if you Ctrl-Enter to start a map in autoplay you're not going to get any gameplay leaderboard scores at all. --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 8197319102..ddb7814d12 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Select.Leaderboards // 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, filterMods ? mods.Value.ToArray() : null), forceRefresh: true); + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.Where(m => m.UserPlayable).ToArray() : null), forceRefresh: true); if (!initialFetchComplete) { From 395510c2c65f5fe798ab0c124963a093529c298a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 27 Apr 2025 02:55:33 +0300 Subject: [PATCH 30/92] 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 31/92] 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 32/92] 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 33/92] 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 34/92] 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 1e8d9b3482e585207d2d5806bdf12f7f2f5892c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Apr 2025 17:24:24 +0900 Subject: [PATCH 35/92] 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 6b298abf884b7d7d7e27890bc77e615f1fe2f18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Apr 2025 12:21:46 +0200 Subject: [PATCH 36/92] Remove redundant initialiser --- .../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 b681306053..2837da23f4 100644 --- a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Select.Leaderboards /// The initial position of the score on the leaderboard. /// Mostly used for cases like the local user's best score on the global leaderboard (which will not be contiguous with the other scores). /// - public int? InitialPosition { get; init; } = null; + public int? InitialPosition { get; init; } /// /// The displayed rank of the score on the leaderboard. From c1fbf5062250ef8e48465d1c8c30a4a01dd005b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Apr 2025 12:31:22 +0200 Subject: [PATCH 37/92] Remove failing test The sorting logic is now exercised in `TestSceneSoloGameplayLeaderboardProvider`. Trying to work around it with local test classes would mean that the test would be covering test code, i.e. complete nonsense and having a green test for the sake of having a green test. --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index f0b2f710c6..31037635cb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -77,33 +77,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); } - [Test] - public void TestPlayerScore() - { - createLeaderboard(); - addLocalPlayer(); - - var player2Score = new BindableLong(1234567); - var player3Score = new BindableLong(1111111); - - AddStep("add player 2", () => createLeaderboardScore(player2Score, new APIUser { Username = "Player 2" })); - AddStep("add player 3", () => createLeaderboardScore(player3Score, new APIUser { Username = "Player 3" })); - - AddUntilStep("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); - AddUntilStep("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); - AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); - - AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500); - AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); - AddUntilStep("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2)); - AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); - - AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456); - AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); - AddUntilStep("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2)); - AddUntilStep("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); - } - [Test] public void TestRandomScores() { @@ -218,17 +191,8 @@ namespace osu.Game.Tests.Visual.Gameplay { public float Spacing => Flow.Spacing.Y; - public bool CheckPositionByUsername(string username, int? expectedPosition) - { - var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username); - - return scoreItem != null && scoreItem.ScorePosition.Value == expectedPosition; - } - public IEnumerable GetAllScoresForUsername(string username) => Flow.Where(i => i.User?.Username == username); - - public IEnumerable AllScores => Flow; } private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider From c1bc3d7ff43efbcec5e4a24a289f79a1d4bd982c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 29 Apr 2025 03:33:29 +0300 Subject: [PATCH 38/92] 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 39/92] 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 40/92] 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 41/92] 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 42/92] 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 43/92] 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 44/92] 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 45/92] 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 46/92] 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 47/92] 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 48/92] 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 49/92] 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 50/92] 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 51/92] 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 52/92] 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 53/92] 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 54/92] 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 55/92] 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 56/92] 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 57/92] 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 58/92] 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 59/92] 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 60/92] 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 61/92] 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 cb3f8d7d835fa132b9603797a929f5197f8d6162 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 14:36:41 +0900 Subject: [PATCH 62/92] 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 63/92] 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 64/92] 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 65/92] 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 66/92] 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 67/92] 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 68/92] 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 69/92] 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 70/92] 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 71/92] 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 72/92] 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 73/92] 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 74/92] 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 75/92] 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 76/92] 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 77/92] 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 78/92] 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 79/92] 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 80/92] 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 81/92] 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 82/92] 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 83/92] 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 84/92] 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 85/92] 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 86/92] 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 87/92] 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 88/92] 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 981383b52b23e12e7e77fb11115cbc1a83c72b2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 13:23:13 +0900 Subject: [PATCH 89/92] 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 84f44eb3ad34da33a76c43221421b00d78e647ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 14:02:09 +0900 Subject: [PATCH 90/92] 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 34119aab8e33a9efcea2cbbef06260edddb1c8ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 May 2025 14:55:40 +0900 Subject: [PATCH 91/92] 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 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 92/92] 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]