From 2a332896c1f19f89d4f2719803c88e1cd32f51a0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 05:38:13 -0400 Subject: [PATCH 01/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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 395510c2c65f5fe798ab0c124963a093529c298a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 27 Apr 2025 02:55:33 +0300 Subject: [PATCH 29/58] 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 30/58] 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 31/58] 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 32/58] 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 33/58] 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 34/58] 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 35/58] 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 36/58] 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 37/58] 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 38/58] 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 39/58] 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 40/58] 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 41/58] 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 42/58] 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 43/58] 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 44/58] 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 c01ff9f845c95955db97d58c7aa492d62a6aa7be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Apr 2025 23:02:30 +0900 Subject: [PATCH 45/58] 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 46/58] 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 47/58] 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 a8ca60497c4693d1c463e8a61ac59e41d56ad9c0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 30 Apr 2025 05:03:51 +0300 Subject: [PATCH 48/58] 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 49/58] 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 50/58] 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 51/58] 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 52/58] 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 53/58] 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 54/58] 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 55/58] 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 a39773747653936aad2e8d441de96871e8aed268 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Apr 2025 16:04:26 +0900 Subject: [PATCH 56/58] 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 57/58] 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 58/58] 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);