From 2417d4de8302f7682bb5703ead03936c336fd33d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 21:46:33 -0500 Subject: [PATCH 01/79] Add `OsuOnlineStore` for proxying external media lookups --- osu.Game/Audio/PreviewTrackManager.cs | 7 ++++--- osu.Game/Online/OsuOnlineStore.cs | 29 +++++++++++++++++++++++++++ osu.Game/OsuGame.cs | 3 +++ osu.Game/OsuGameBase.cs | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Online/OsuOnlineStore.cs diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 1d710e6395..81564cc2e8 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -6,9 +6,10 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Online; +using osu.Game.Online.API; namespace osu.Game.Audio { @@ -28,9 +29,9 @@ namespace osu.Game.Audio } [BackgroundDependencyLoader] - private void load(AudioManager audioManager) + private void load(AudioManager audioManager, IAPIProvider api) { - trackStore = audioManager.GetTrackStore(new OnlineStore()); + trackStore = audioManager.GetTrackStore(new OsuOnlineStore(api.APIEndpointUrl)); } /// diff --git a/osu.Game/Online/OsuOnlineStore.cs b/osu.Game/Online/OsuOnlineStore.cs new file mode 100644 index 0000000000..bb69338b01 --- /dev/null +++ b/osu.Game/Online/OsuOnlineStore.cs @@ -0,0 +1,29 @@ +// 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.IO.Stores; + +namespace osu.Game.Online +{ + /// + /// An which proxies external media lookups through osu-web. + /// + public class OsuOnlineStore : OnlineStore + { + private readonly string apiEndpointUrl; + + public OsuOnlineStore(string apiEndpointUrl) + { + this.apiEndpointUrl = apiEndpointUrl; + } + + protected override string GetLookupUrl(string url) + { + if (Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) && uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase)) + return url; + + return $@"{apiEndpointUrl}/beatmapsets/discussions/media-url?url={url}"; + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dce24c6ee7..1fe41baf2f 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -29,6 +29,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.Handlers.Tablet; +using osu.Framework.IO.Stores; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Platform; @@ -819,6 +820,8 @@ namespace osu.Game protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); + protected override OnlineStore CreateOnlineStore() => new OsuOnlineStore(CreateEndpoints().APIEndpointUrl); + #region Beatmap progression private void beatmapChanged(ValueChangedEvent beatmap) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dc13924b4f..d231238699 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -279,7 +279,7 @@ namespace osu.Game dependencies.CacheAs(Storage); var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); - largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore())); + largeStore.AddTextureSource(Host.CreateTextureLoaderStore(CreateOnlineStore())); dependencies.Cache(largeStore); dependencies.CacheAs(LocalConfig); From 2a7133d6d3dc3bdb5a2cafddde7fc2df834b23e1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 21:47:10 -0500 Subject: [PATCH 02/79] Add test scene using `OsuOnlineStore` to test lookups --- .../Visual/Online/TestSceneMediaProxying.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneMediaProxying.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneMediaProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneMediaProxying.cs new file mode 100644 index 0000000000..868bfa6cc4 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneMediaProxying.cs @@ -0,0 +1,63 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Framework.Platform; +using osu.Game.Graphics.Containers.Markdown; +using osu.Game.Online; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneMediaProxying : OsuTestScene + { + [Resolved] + private GameHost host { get; set; } = null!; + + [Test] + public void TestExternalImageLink() + { + AddStep("load image", () => setup(new OsuMarkdownContainer + { + RelativeSizeAxes = Axes.Both, + Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", + })); + } + + [Test] + public void TestLocalImageLink() + { + AddStep("load image", () => setup(new OsuMarkdownContainer + { + RelativeSizeAxes = Axes.Both, + Text = "![](https://osu.ppy.sh/help/wiki/shared/news/banners/monthly-beatmapping-contest.png)", + })); + } + + [Test] + public void TestInvalidImageLink() + { + AddStep("load image", () => setup(new OsuMarkdownContainer + { + RelativeSizeAxes = Axes.Both, + Text = "![](https://this-site-does-not-exist.com/img.png)", + })); + } + + private void setup(Drawable drawable) + { + var onlineStore = new OsuOnlineStore(@"https://osu.ppy.sh"); + var textureStore = new TextureStore(host.Renderer, host.CreateTextureLoaderStore(onlineStore)); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(TextureStore), textureStore) }, + Child = drawable, + }; + } + } +} From 9a89d402b9d6e5591a4c67668203ee0d53f27ba3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 25 Nov 2024 00:39:32 -0500 Subject: [PATCH 03/79] Perform proxying only on osu! markdown images --- .../Graphics/Containers/Markdown/OsuMarkdownImage.cs | 3 +++ osu.Game/Online/OsuOnlineStore.cs | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs index 10207dd389..a36bbf4f6f 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs @@ -17,5 +17,8 @@ namespace osu.Game.Graphics.Containers.Markdown { TooltipText = linkInline.Title; } + + protected override ImageContainer CreateImageContainer(string url) + => base.CreateImageContainer($@"https://osu.ppy.sh/beatmapsets/discussions/media-url?url={url}"); } } diff --git a/osu.Game/Online/OsuOnlineStore.cs b/osu.Game/Online/OsuOnlineStore.cs index bb69338b01..dddd453faf 100644 --- a/osu.Game/Online/OsuOnlineStore.cs +++ b/osu.Game/Online/OsuOnlineStore.cs @@ -3,12 +3,10 @@ using System; using osu.Framework.IO.Stores; +using osu.Framework.Logging; namespace osu.Game.Online { - /// - /// An which proxies external media lookups through osu-web. - /// public class OsuOnlineStore : OnlineStore { private readonly string apiEndpointUrl; @@ -20,8 +18,11 @@ namespace osu.Game.Online protected override string GetLookupUrl(string url) { - if (Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) && uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase)) - return url; + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase)) + { + Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important); + return string.Empty; + } return $@"{apiEndpointUrl}/beatmapsets/discussions/media-url?url={url}"; } From 83f8fa7472175d053172459ac64ab9469832733d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 25 Nov 2024 00:41:40 -0500 Subject: [PATCH 04/79] Update test scene --- ...aProxying.cs => TestSceneImageProxying.cs} | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) rename osu.Game.Tests/Visual/Online/{TestSceneMediaProxying.cs => TestSceneImageProxying.cs} (59%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMediaProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs similarity index 59% rename from osu.Game.Tests/Visual/Online/TestSceneMediaProxying.cs rename to osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs index 868bfa6cc4..696073c10d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMediaProxying.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs @@ -12,7 +12,7 @@ using osu.Game.Online; namespace osu.Game.Tests.Visual.Online { - public partial class TestSceneMediaProxying : OsuTestScene + public partial class TestSceneImageProxying : OsuTestScene { [Resolved] private GameHost host { get; set; } = null!; @@ -20,44 +20,31 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestExternalImageLink() { - AddStep("load image", () => setup(new OsuMarkdownContainer + AddStep("load image", () => Child = new OsuMarkdownContainer { RelativeSizeAxes = Axes.Both, Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", - })); + }); } [Test] public void TestLocalImageLink() { - AddStep("load image", () => setup(new OsuMarkdownContainer + AddStep("load image", () => Child = new OsuMarkdownContainer { RelativeSizeAxes = Axes.Both, Text = "![](https://osu.ppy.sh/help/wiki/shared/news/banners/monthly-beatmapping-contest.png)", - })); + }); } [Test] public void TestInvalidImageLink() { - AddStep("load image", () => setup(new OsuMarkdownContainer + AddStep("load image", () => Child = new OsuMarkdownContainer { RelativeSizeAxes = Axes.Both, Text = "![](https://this-site-does-not-exist.com/img.png)", - })); - } - - private void setup(Drawable drawable) - { - var onlineStore = new OsuOnlineStore(@"https://osu.ppy.sh"); - var textureStore = new TextureStore(host.Renderer, host.CreateTextureLoaderStore(onlineStore)); - - Child = new DependencyProvidingContainer - { - RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] { (typeof(TextureStore), textureStore) }, - Child = drawable, - }; + }); } } } From dbe2741982ed3741ef527d79d7e37eea6ff7766b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 22:19:44 -0500 Subject: [PATCH 05/79] Update specified endpoint --- osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs index a36bbf4f6f..ff7df18f00 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs @@ -19,6 +19,6 @@ namespace osu.Game.Graphics.Containers.Markdown } protected override ImageContainer CreateImageContainer(string url) - => base.CreateImageContainer($@"https://osu.ppy.sh/beatmapsets/discussions/media-url?url={url}"); + => base.CreateImageContainer($@"https://osu.ppy.sh/media-url?url={url}"); } } From 9a4c419c568764fabd2d7624d288966c84986f5c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 30 Nov 2024 22:37:05 -0500 Subject: [PATCH 06/79] Remove unnecessary usage of link proxying in `OsuOnlineStore` Links are checked to be in the ppy.sh domain here. --- osu.Game/Online/OsuOnlineStore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/OsuOnlineStore.cs b/osu.Game/Online/OsuOnlineStore.cs index dddd453faf..c3e81c503f 100644 --- a/osu.Game/Online/OsuOnlineStore.cs +++ b/osu.Game/Online/OsuOnlineStore.cs @@ -18,13 +18,14 @@ namespace osu.Game.Online protected override string GetLookupUrl(string url) { + // add leading dot to avoid matching hosts named "ppy.sh" if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase)) { Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important); return string.Empty; } - return $@"{apiEndpointUrl}/beatmapsets/discussions/media-url?url={url}"; + return url; } } } From ee369ef86d54c6f1359742edc438237e01b718e3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 30 Nov 2024 22:38:43 -0500 Subject: [PATCH 07/79] Remove unused using directives --- osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs index 696073c10d..0cf6fec6f0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs @@ -1,14 +1,11 @@ // 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Textures; using osu.Framework.Platform; using osu.Game.Graphics.Containers.Markdown; -using osu.Game.Online; namespace osu.Game.Tests.Visual.Online { From e0a23000be9fe569cd79d2715314daa0aec3ad86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 12:37:57 +0900 Subject: [PATCH 08/79] Move `CreateOnlineStore` override to `OsuGameBase` and update endpoint references --- osu.Game/Audio/PreviewTrackManager.cs | 2 +- osu.Game/OsuGame.cs | 3 --- osu.Game/OsuGameBase.cs | 2 ++ 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 81564cc2e8..452be91bc0 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -31,7 +31,7 @@ namespace osu.Game.Audio [BackgroundDependencyLoader] private void load(AudioManager audioManager, IAPIProvider api) { - trackStore = audioManager.GetTrackStore(new OsuOnlineStore(api.APIEndpointUrl)); + trackStore = audioManager.GetTrackStore(new OsuOnlineStore(api.Endpoints.APIUrl)); } /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 547048c5c7..4a9154f14b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -28,7 +28,6 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.Handlers.Tablet; -using osu.Framework.IO.Stores; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Platform; @@ -824,8 +823,6 @@ namespace osu.Game protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); - protected override OnlineStore CreateOnlineStore() => new OsuOnlineStore(CreateEndpoints().APIEndpointUrl); - #region Beatmap progression private void beatmapChanged(ValueChangedEvent beatmap) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5c78618a0b..257b6a532b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -108,6 +108,8 @@ namespace osu.Game public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); + protected override OnlineStore CreateOnlineStore() => new OsuOnlineStore(CreateEndpoints().APIUrl); + public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); /// From 8a5b8784e640acc0723f3d43c87d1a1b16d3b70b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 12:40:09 +0900 Subject: [PATCH 09/79] Remove completely redundant comment --- osu.Game/Online/OsuOnlineStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/OsuOnlineStore.cs b/osu.Game/Online/OsuOnlineStore.cs index c3e81c503f..72d5ecf036 100644 --- a/osu.Game/Online/OsuOnlineStore.cs +++ b/osu.Game/Online/OsuOnlineStore.cs @@ -18,7 +18,6 @@ namespace osu.Game.Online protected override string GetLookupUrl(string url) { - // add leading dot to avoid matching hosts named "ppy.sh" if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase)) { Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important); From d840fcfb99ea886ac52793c5c4bac5b5b500bb15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 13:41:01 +0900 Subject: [PATCH 10/79] Make results screen usernames clickable to open user profile --- .../ContractedPanelMiddleContent.cs | 4 +- .../Expanded/ExpandedPanelTopContent.cs | 6 +-- osu.Game/Users/Drawables/ClickableUsername.cs | 48 +++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Users/Drawables/ClickableUsername.cs diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index e9d0bf3403..fbc0fd8a70 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -108,12 +108,10 @@ namespace osu.Game.Screens.Ranking.Contracted Offset = new Vector2(0, 1), } }, - new OsuSpriteText + new ClickableUsername(score.User) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = score.RealmUser.Username, - Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold) }, new FillFlowContainer { diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs index c834d541eb..b50996154b 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs @@ -8,8 +8,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users.Drawables; using osuTK; @@ -62,12 +60,10 @@ namespace osu.Game.Screens.Ranking.Expanded CornerExponent = 2.5f, Masking = true, }, - new OsuSpriteText + new ClickableUsername(user) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = user.Username, - Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold) } } }; diff --git a/osu.Game/Users/Drawables/ClickableUsername.cs b/osu.Game/Users/Drawables/ClickableUsername.cs new file mode 100644 index 0000000000..ef07fc8f8b --- /dev/null +++ b/osu.Game/Users/Drawables/ClickableUsername.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Users.Drawables +{ + internal class ClickableUsername : OsuHoverContainer, IHasCustomTooltip + { + public ITooltip GetCustomTooltip() => new ClickableAvatar.NoCardTooltip(); + + public APIUser? TooltipContent { get; } + + private readonly APIUser user; + + [Resolved] + private OsuGame? game { get; set; } + + public ClickableUsername(APIUser? user) + { + TooltipContent = this.user = user ?? new GuestUser(); + + AutoSizeAxes = Axes.Both; + + Child = new OsuSpriteText + { + Text = user!.Username, + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + }; + + if (user.Id != APIUser.SYSTEM_USER_ID) + Action = openProfile; + } + + private void openProfile() + { + if (user.Id > 1 || !string.IsNullOrEmpty(user.Username)) + game?.ShowUser(user); + } + } +} From 8353958b8ae518c8a872035f5bf8bb45984a4d67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 13:59:04 +0900 Subject: [PATCH 11/79] Make results screen beatmap metadata clickable to open beatmap overlay --- .../Expanded/ExpandedPanelMiddleContent.cs | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 0190a6f959..9669b8f851 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -106,22 +106,7 @@ namespace osu.Game.Screens.Ranking.Expanded Direction = FillDirection.Vertical, Children = new Drawable[] { - new TruncatingSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), - MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - }, - new TruncatingSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), - MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - }, + new ClickableMetadata(beatmap.OnlineID, metadata), new Container { Anchor = Anchor.TopCentre, @@ -316,5 +301,49 @@ namespace osu.Game.Screens.Ranking.Expanded time.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt")); } } + + internal class ClickableMetadata : OsuHoverContainer + { + [Resolved] + private OsuGame? game { get; set; } + + public ClickableMetadata(int beatmapId, IBeatmapMetadataInfo metadata) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), + MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, + }, + new TruncatingSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, + } + } + }; + + if (beatmapId > 0) + Action = () => game?.ShowBeatmap(beatmapId); + } + } } } From ed61f87eed779733d123f91c28e95a24d653a645 Mon Sep 17 00:00:00 2001 From: wezwery Date: Sat, 15 Mar 2025 17:12:11 +0200 Subject: [PATCH 12/79] Display mod name, if `SettingDescription` is empty. --- .../Leaderboards/LeaderboardScoreTooltip.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index ed3ee4d45e..d1fe2b799a 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -221,18 +221,18 @@ namespace osu.Game.Online.Leaderboards string description = mod.SettingDescription; - if (!string.IsNullOrEmpty(description)) + if (string.IsNullOrEmpty(description)) + description = mod.Name; + + container.Add(new OsuSpriteText { - container.Add(new OsuSpriteText - { - RelativeSizeAxes = Axes.Y, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = mod.SettingDescription, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Top = 1 }, - }); - } + RelativeSizeAxes = Axes.Y, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = description, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Top = 1 }, + }); } } } From b00be5477f2dbb029466bd51dd057b14f4728d7d Mon Sep 17 00:00:00 2001 From: wezwery Date: Sun, 16 Mar 2025 15:30:27 +0200 Subject: [PATCH 13/79] Use `IconTooltip` --- osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index d1fe2b799a..e79aff9e81 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -219,16 +219,11 @@ namespace osu.Game.Online.Leaderboards } }; - string description = mod.SettingDescription; - - if (string.IsNullOrEmpty(description)) - description = mod.Name; - container.Add(new OsuSpriteText { RelativeSizeAxes = Axes.Y, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = description, + Text = mod.IconTooltip, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Margin = new MarginPadding { Top = 1 }, From e8d245e9f1fc8d3c4aab64084d38741a46d45f63 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 17 Mar 2025 02:16:09 -0400 Subject: [PATCH 14/79] Rewrite test to contain asserts --- .../Visual/Online/TestSceneImageProxying.cs | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs index 0cf6fec6f0..320cc9d8a9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs @@ -1,10 +1,14 @@ // 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.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers.Markdown; +using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Graphics.Containers.Markdown; namespace osu.Game.Tests.Visual.Online @@ -17,31 +21,23 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestExternalImageLink() { - AddStep("load image", () => Child = new OsuMarkdownContainer + MarkdownContainer markdown = null!; + + // use base MarkdownContainer as a method of directly attempting to load an image without proxying logic. + AddStep("load external without proxying", () => Child = markdown = new MarkdownContainer { RelativeSizeAxes = Axes.Both, Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", }); - } + AddWaitStep("wait", 5); + AddAssert("image not loaded", () => markdown.ChildrenOfType().Single().Texture == null); - [Test] - public void TestLocalImageLink() - { - AddStep("load image", () => Child = new OsuMarkdownContainer + AddStep("load external with proxying", () => Child = markdown = new OsuMarkdownContainer { RelativeSizeAxes = Axes.Both, - Text = "![](https://osu.ppy.sh/help/wiki/shared/news/banners/monthly-beatmapping-contest.png)", - }); - } - - [Test] - public void TestInvalidImageLink() - { - AddStep("load image", () => Child = new OsuMarkdownContainer - { - RelativeSizeAxes = Axes.Both, - Text = "![](https://this-site-does-not-exist.com/img.png)", + Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", }); + AddUntilStep("image loaded", () => markdown.ChildrenOfType().SingleOrDefault()?.Texture != null); } } } From 0bc5a8103c278146b5c865b8f891da797dabda21 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 17 Mar 2025 02:19:03 -0400 Subject: [PATCH 15/79] Fix inconsistent access --- osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs index 320cc9d8a9..32afcc1450 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Online Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", }); AddWaitStep("wait", 5); - AddAssert("image not loaded", () => markdown.ChildrenOfType().Single().Texture == null); + AddAssert("image not loaded", () => markdown.ChildrenOfType().SingleOrDefault()?.Texture == null); AddStep("load external with proxying", () => Child = markdown = new OsuMarkdownContainer { From 597eec99e6b79d4ee6c18509d62eb589638f36dd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 17 Mar 2025 03:31:25 -0400 Subject: [PATCH 16/79] Fix code quality --- osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs | 5 ----- osu.Game/Audio/PreviewTrackManager.cs | 5 ++--- osu.Game/Online/OsuOnlineStore.cs | 7 ------- osu.Game/OsuGameBase.cs | 2 +- 4 files changed, 3 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs index 32afcc1450..17b437a051 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs @@ -3,11 +3,9 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Sprites; -using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Graphics.Containers.Markdown; @@ -15,9 +13,6 @@ namespace osu.Game.Tests.Visual.Online { public partial class TestSceneImageProxying : OsuTestScene { - [Resolved] - private GameHost host { get; set; } = null!; - [Test] public void TestExternalImageLink() { diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 452be91bc0..f9e74cd1b2 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online; -using osu.Game.Online.API; namespace osu.Game.Audio { @@ -29,9 +28,9 @@ namespace osu.Game.Audio } [BackgroundDependencyLoader] - private void load(AudioManager audioManager, IAPIProvider api) + private void load(AudioManager audioManager) { - trackStore = audioManager.GetTrackStore(new OsuOnlineStore(api.Endpoints.APIUrl)); + trackStore = audioManager.GetTrackStore(new OsuOnlineStore()); } /// diff --git a/osu.Game/Online/OsuOnlineStore.cs b/osu.Game/Online/OsuOnlineStore.cs index 72d5ecf036..f578043c5d 100644 --- a/osu.Game/Online/OsuOnlineStore.cs +++ b/osu.Game/Online/OsuOnlineStore.cs @@ -9,13 +9,6 @@ namespace osu.Game.Online { public class OsuOnlineStore : OnlineStore { - private readonly string apiEndpointUrl; - - public OsuOnlineStore(string apiEndpointUrl) - { - this.apiEndpointUrl = apiEndpointUrl; - } - protected override string GetLookupUrl(string url) { if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase)) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 257b6a532b..51c8788248 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -108,7 +108,7 @@ namespace osu.Game public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); - protected override OnlineStore CreateOnlineStore() => new OsuOnlineStore(CreateEndpoints().APIUrl); + protected override OnlineStore CreateOnlineStore() => new OsuOnlineStore(); public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); From 7f88619ab0001647f15694cbfb82ce44f3b59795 Mon Sep 17 00:00:00 2001 From: evill Date: Mon, 17 Mar 2025 14:37:02 +0200 Subject: [PATCH 17/79] show full ranks in results screen --- .../Ranking/Statistics/User/GlobalRankChangeRow.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs index 0d91d6f8f9..37268e05cd 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User protected override LocalisableString Label => UsersStrings.ShowRankGlobalSimple; protected override LocalisableString FormatCurrentValue(int? current) - => current == null ? string.Empty : current.Value.FormatRank(); + => current == null ? string.Empty : current.Value.ToString(); protected override int CalculateDifference(int? previous, int? current, out LocalisableString formattedDifference) { @@ -30,13 +30,13 @@ namespace osu.Game.Screens.Ranking.Statistics.User if (previous == null && current != null) { - formattedDifference = LocalisableString.Interpolate($"+{current.Value.FormatRank()}"); + formattedDifference = LocalisableString.Interpolate($"+{current.Value.ToString()}"); return 1; } if (previous != null && current == null) { - formattedDifference = LocalisableString.Interpolate($"-{previous.Value.FormatRank()}"); + formattedDifference = LocalisableString.Interpolate($"-{previous.Value.ToString()}"); return -1; } @@ -46,9 +46,9 @@ namespace osu.Game.Screens.Ranking.Statistics.User int difference = previous.Value - current.Value; if (difference < 0) - formattedDifference = difference.FormatRank(); + formattedDifference = difference.ToString(); else if (difference > 0) - formattedDifference = LocalisableString.Interpolate($"+{difference.FormatRank()}"); + formattedDifference = LocalisableString.Interpolate($"+{difference.ToString()}"); else formattedDifference = string.Empty; From 71d83e347bd03b38842d05741b606dd837a6b667 Mon Sep 17 00:00:00 2001 From: evill Date: Mon, 17 Mar 2025 15:05:47 +0200 Subject: [PATCH 18/79] add thousand separators --- .../Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs index 37268e05cd..ca1685e921 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Game.Utils; @@ -18,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User protected override LocalisableString Label => UsersStrings.ShowRankGlobalSimple; protected override LocalisableString FormatCurrentValue(int? current) - => current == null ? string.Empty : current.Value.ToString(); + => current == null ? string.Empty : current.Value.ToLocalisableString(@"N0"); protected override int CalculateDifference(int? previous, int? current, out LocalisableString formattedDifference) { @@ -46,9 +47,9 @@ namespace osu.Game.Screens.Ranking.Statistics.User int difference = previous.Value - current.Value; if (difference < 0) - formattedDifference = difference.ToString(); + formattedDifference = difference.ToLocalisableString(@"N0"); else if (difference > 0) - formattedDifference = LocalisableString.Interpolate($"+{difference.ToString()}"); + formattedDifference = LocalisableString.Interpolate($"+{difference.ToLocalisableString(@"N0")}"); else formattedDifference = string.Empty; From 2630d9437c60f56ea203d861c1b4e0abe226e205 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 18 Mar 2025 16:19:52 +0900 Subject: [PATCH 19/79] Build iOS tests project --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d75f09f184..1019569b5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,4 +136,4 @@ jobs: run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json - name: Build - run: dotnet build -c Debug osu.iOS + run: dotnet build -c Debug osu.iOS.slnf From 4a906b0a1e9effaab42b8726bee9846f0fd9c304 Mon Sep 17 00:00:00 2001 From: "Giovanni D." Date: Tue, 18 Mar 2025 19:26:38 -0700 Subject: [PATCH 20/79] Add spectate and multiplayer invite functionality to the drawable chat username. --- .../Overlays/Chat/DrawableChatUsername.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 83f67d1a8a..eca2817673 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -14,6 +15,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -22,7 +24,10 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Online.Multiplayer; using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens; +using osu.Game.Screens.Play; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; @@ -69,6 +74,12 @@ namespace osu.Game.Overlays.Chat [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private MultiplayerClient? multiplayerClient { get; set; } + + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + [Resolved(canBeNull: true)] private ChannelManager? chatManager { get; set; } @@ -169,6 +180,22 @@ namespace osu.Game.Overlays.Chat if (!user.Equals(api.LocalUser.Value)) items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); + if (!user.Equals(api.LocalUser.Value)) + { + if (user.IsOnline) + { + items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => + { + performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(user))); + })); + + if (multiplayerClient?.Room?.Users.All(u => u.UserID != user.Id) == true) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(user.Id))); + } + } + } + if (currentChannel?.Value != null) { items.Add(new OsuMenuItem(ChatStrings.MentionUser, MenuItemType.Standard, () => From c9afc6b3df8e5e251a3ae38ed0f94bff419f921e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 12:26:50 +0900 Subject: [PATCH 21/79] Adjust ordering of menu items and fix code quality issues --- .../Overlays/Chat/DrawableChatUsername.cs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index eca2817673..fdcadbaa10 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -172,29 +172,10 @@ namespace osu.Game.Overlays.Chat if (user.Equals(APIUser.SYSTEM_USER)) return Array.Empty(); - List items = new List - { - new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile) - }; + if (user.Equals(api.LocalUser.Value)) + return Array.Empty(); - if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); - - if (!user.Equals(api.LocalUser.Value)) - { - if (user.IsOnline) - { - items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => - { - performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(user))); - })); - - if (multiplayerClient?.Room?.Users.All(u => u.UserID != user.Id) == true) - { - items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(user.Id))); - } - } - } + List items = new List(); if (currentChannel?.Value != null) { @@ -204,8 +185,27 @@ namespace osu.Game.Overlays.Chat })); } - if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); + items.Add(new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile)); + + items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); + + if (user.IsOnline) + { + items.Add(new OsuMenuItemSpacer()); + + items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => + { + performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(user))); + })); + + if (multiplayerClient?.Room?.Users.All(u => u.UserID != user.Id) == true) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(user.Id))); + } + } + + items.Add(new OsuMenuItemSpacer()); + items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } From d6a06f2af6f5278fca49f35f5f166ac975f3d733 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 13:15:57 +0900 Subject: [PATCH 22/79] Fade out pause loop sound when the game window is inactive Closes https://github.com/ppy/osu/issues/32432. --- osu.Game/Screens/Play/PauseOverlay.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 3a471acba4..11f62939fb 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -5,9 +5,12 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Platform; using osu.Game.Audio; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -31,14 +34,24 @@ namespace osu.Game.Screens.Play OnResume?.Invoke(); }; + private IBindable? windowActive; + + private float targetVolume => windowActive?.Value != false && State.Value == Visibility.Visible ? 1.0f : 0; + [BackgroundDependencyLoader] - private void load() + private void load(GameHost? host) { AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("Gameplay/pause-loop")) { Looping = true, Volume = { Value = 0 } }); + + if (host != null) + { + windowActive = host.IsActive.GetBoundCopy(); + windowActive.BindValueChanged(_ => Schedule(() => pauseLoop.VolumeTo(targetVolume, 1000, Easing.Out))); + } } public void StopAllSamples() @@ -53,7 +66,7 @@ namespace osu.Game.Screens.Play { base.PopIn(); - pauseLoop.VolumeTo(1.0f, TRANSITION_DURATION, Easing.InQuint); + pauseLoop.VolumeTo(targetVolume, TRANSITION_DURATION, Easing.InQuint); pauseLoop.Play(); } @@ -61,7 +74,7 @@ namespace osu.Game.Screens.Play { base.PopOut(); - pauseLoop.VolumeTo(0, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); + pauseLoop.VolumeTo(targetVolume, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); } public override bool OnPressed(KeyBindingPressEvent e) From 76f286a01b07e4fecfb219ddef80a70f6ef08607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 14:14:32 +0900 Subject: [PATCH 23/79] Re-fetch status of any beatmaps stuck in qualified status Best we do this rather than leaving it up to users to fix their broken beatmaps. https://github.com/ppy/osu/discussions/32406 https://github.com/ppy/osu/discussions/32431 --- osu.Game/Beatmaps/BeatmapInfo.cs | 5 +++-- osu.Game/Database/RealmAccess.cs | 12 +++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 487b578317..a6b40a26de 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -125,9 +125,10 @@ namespace osu.Game.Beatmaps /// /// Reset any fetched online linking information (and history). /// - public void ResetOnlineInfo() + public void ResetOnlineInfo(bool resetOnlineId = true) { - OnlineID = -1; + if (resetOnlineId) + OnlineID = -1; LastOnlineUpdate = null; OnlineMD5Hash = string.Empty; if (Status != BeatmapOnlineStatus.LocallyModified) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 3212e17b7b..7142f2b300 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -97,8 +97,9 @@ namespace osu.Game.Database /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. /// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯. /// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions. + /// 48 2025-03-19 Clear online status for all qualified beatmaps (some were stuck in a qualified state due to local caching issues). /// - private const int schema_version = 47; + private const int schema_version = 48; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1245,6 +1246,15 @@ namespace osu.Game.Database break; } + + case 48: + const int qualified = (int)BeatmapOnlineStatus.Qualified; + + var beatmaps = migration.NewRealm.All().Where(b => b.StatusInt == qualified); + + foreach (var beatmap in beatmaps) + beatmap.ResetOnlineInfo(resetOnlineId: false); + break; } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); From b17ec5e69dd1c112d993536e4af6f4eb44d717c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 15:01:30 +0900 Subject: [PATCH 24/79] Rename `APIUser.IsOnline` and add better documentation around online checks --- osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs | 6 +++--- .../Visual/Online/TestSceneUserClickableAvatar.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 2 +- .../Visual/Online/TestSceneUserProfileHeader.cs | 4 ++-- osu.Game/Online/API/Requests/Responses/APIUser.cs | 8 +++++++- osu.Game/Online/Metadata/MetadataClient.cs | 3 +++ osu.Game/Overlays/Chat/DrawableChatUsername.cs | 5 ++++- osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs | 2 +- 8 files changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index f7fd95a6e1..25611cf8d5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Online { Username = "flyte", Id = 3103765, - IsOnline = true, + WasRecentlyOnline = true, Statistics = new UserStatistics { GlobalRank = 1111 }, CountryCode = CountryCode.JP, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" @@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.Online { Username = "peppy", Id = 2, - IsOnline = false, + WasRecentlyOnline = false, Statistics = new UserStatistics { GlobalRank = 2222 }, CountryCode = CountryCode.AU, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", @@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Online Id = 8195163, CountryCode = CountryCode.BY, CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, + WasRecentlyOnline = false, LastVisit = DateTimeOffset.Now } }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index fce888094d..29272f7336 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = countryCode, CoverUrl = cover, Colour = color ?? "000000", - IsOnline = true + WasRecentlyOnline = true }; return new ClickableAvatar(user, showPanel) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index f4fc15da20..896bda364a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3103765, CountryCode = CountryCode.JP, CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - IsOnline = true + WasRecentlyOnline = true }) { Width = 300 }, new UserGridPanel(new APIUser { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 6167d1f760..193b356d71 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online Id = 1001, Username = "IAmOnline", LastVisit = DateTimeOffset.Now, - IsOnline = true, + WasRecentlyOnline = true, }, new OsuRuleset().RulesetInfo)); AddStep("Show offline user", () => header.User.Value = new UserProfileData(new APIUser @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online Id = 1002, Username = "IAmOffline", LastVisit = DateTimeOffset.Now.AddDays(-10), - IsOnline = false, + WasRecentlyOnline = false, }, new OsuRuleset().RulesetInfo)); } diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 92b7d9d874..4e219cdf22 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -9,6 +9,7 @@ using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Game.Extensions; +using osu.Game.Online.Metadata; using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses @@ -111,8 +112,13 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"is_active")] public bool Active; + /// + /// From osu-web's perspective, whether a user was recently online. + /// This doesn't imply the user is online in a lazer client (may be updated from stable or web browser). + /// Use for real-time lazer online status checks. + /// [JsonProperty(@"is_online")] - public bool IsOnline; + public bool WasRecentlyOnline; [JsonProperty(@"pm_friends_only")] public bool PMFriendsOnly; diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 9885419b65..0679191a52 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -57,6 +57,9 @@ namespace osu.Game.Online.Metadata /// /// Attempts to retrieve the presence of a user. /// + /// + /// This will return data if the client is currently receiving presence data. See . + /// /// The user ID. /// The user presence, or null if not available or the user's offline. public UserPresence? GetPresence(int userId) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index fdcadbaa10..7cf23f6f7b 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -189,7 +189,10 @@ namespace osu.Game.Overlays.Chat items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); - if (user.IsOnline) + // Best effort checking against the recently online flag here. + // We can't use MetadataClient.GetPresence because we may not be requesting/receiving presences. + // This isn't really too bad – worst case scenario the client will open spectator view and show the user as "offline". + if (user.WasRecentlyOnline) { items.Add(new OsuMenuItemSpacer()); diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index 03c849052b..db93ec7e05 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Profile.Header addSpacer(topLinkContainer); - if (user.IsOnline) + if (user.WasRecentlyOnline) { topLinkContainer.AddText(UsersStrings.ShowLastvisitOnline); addSpacer(topLinkContainer); From 40da77c409ae41a37fb91b637a4ce8c409d343d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 15:16:03 +0900 Subject: [PATCH 25/79] Allow vertical layout for skinnable mod display --- .../SkinComponents/SkinnableModDisplayStrings.cs | 8 +++++++- osu.Game/Screens/Play/HUD/ModDisplay.cs | 10 ++++++++-- osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs | 4 ++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs index d3e8c0f8c8..22f9fe6d02 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs @@ -17,7 +17,13 @@ namespace osu.Game.Localisation.SkinComponents /// /// "Whether to show extended information for each mod." /// - public static LocalisableString ShowExtendedInformationDescription => new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod."); + public static LocalisableString ShowExtendedInformationDescription => + new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod."); + + /// + /// "Display direction" + /// + public static LocalisableString DisplayDirection => new TranslatableString(getKey(@"display_direction"), "Display direction"); /// /// "Expansion mode" diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 3ab4c15154..011b2b950a 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -67,6 +67,12 @@ namespace osu.Game.Screens.Play.HUD } } + public FillDirection FillDirection + { + get => iconsContainer.Direction; + set => iconsContainer.Direction = value; + } + private readonly FillFlowContainer iconsContainer; public ModDisplay(bool showExtendedInformation = true) @@ -122,13 +128,13 @@ namespace osu.Game.Screens.Play.HUD private void expand(double duration = 500) { if (ExpansionMode != ExpansionMode.AlwaysContracted) - iconsContainer.TransformSpacingTo(new Vector2(5, 0), duration, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(5), duration, Easing.OutQuint); } private void contract(double duration = 500) { if (ExpansionMode != ExpansionMode.AlwaysExpanded) - iconsContainer.TransformSpacingTo(new Vector2(-25, 0), duration, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(-25), duration, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs index 59bb1ade41..29b8429539 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs @@ -30,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpansionMode), nameof(SkinnableModDisplayStrings.ExpansionModeDescription))] public Bindable ExpansionModeSetting { get; } = new Bindable(); + [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.DisplayDirection))] + public Bindable Direction { get; } = new Bindable(); + [BackgroundDependencyLoader] private void load() { @@ -50,6 +53,7 @@ namespace osu.Game.Screens.Play.HUD ShowExtendedInformation.BindValueChanged(_ => modDisplay.ShowExtendedInformation = ShowExtendedInformation.Value, true); ExpansionModeSetting.BindValueChanged(_ => modDisplay.ExpansionMode = ExpansionModeSetting.Value, true); + Direction.BindValueChanged(_ => modDisplay.FillDirection = Direction.Value == Framework.Graphics.Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical, true); FinishTransforms(true); } From 252084de245263c580130d579d66759c03dbf351 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 15:23:31 +0900 Subject: [PATCH 26/79] Add note about local mod display in `HUDOverlay` --- osu.Game/Screens/Play/HUDOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 78c602d8f1..75a28a4240 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -147,6 +147,9 @@ namespace osu.Game.Screens.Play Direction = FillDirection.Vertical, Children = new Drawable[] { + // This display is potentially a duplicate of users with a local ModDisplay in their skins. + // It would be very nice to remove this, but the version here has special logic with regards to replays + // and initial states, so needs a bit of thought before doing so. ModDisplay = CreateModsContainer(), } }, From 23fa3060b2e91ffb70a107abbf95faffbcc4a078 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 15:32:29 +0900 Subject: [PATCH 27/79] Replace superfluous method with concrete implementation --- .../Multiplayer/TestSceneMatchStartControl.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 4 ++-- .../Multiplayer/TestSceneMultiplayerPlaylist.cs | 2 +- .../TestSceneMultiplayerQueueList.cs | 2 +- .../Visual/Multiplayer/TestMultiplayerClient.cs | 17 +---------------- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index fb9c801fb4..3e62417892 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer multiplayerRoom = new MultiplayerRoom(0) { - Playlist = { TestMultiplayerClient.CreateMultiplayerPlaylistItem(item) }, + Playlist = { new MultiplayerPlaylistItem(item) }, Users = { localUser }, Host = localUser, }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index ec0117a990..ae939c7b9e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -924,7 +924,7 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem( + AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { RulesetID = new OsuRuleset().RulesetInfo.OnlineID, @@ -956,7 +956,7 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem( + AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { RulesetID = new OsuRuleset().RulesetInfo.OnlineID, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 7c8691d5d1..1affa08813 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () => { - MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) + MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) { Expired = expired, PlayedAt = DateTimeOffset.Now diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 1a7b677798..7283e3a1fe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add playlist item", () => { - MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)); + MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)); MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely(); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index cc9a82c1ba..febd7f54ff 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -238,7 +238,7 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = ServerAPIRoom.QueueMode, AutoStartDuration = ServerAPIRoom.AutoStartDuration }, - Playlist = ServerAPIRoom.Playlist.Select(CreateMultiplayerPlaylistItem).ToList(), + Playlist = ServerAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)).ToList(), Users = { localUser }, Host = localUser }; @@ -687,21 +687,6 @@ namespace osu.Game.Tests.Visual.Multiplayer return MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); } - public static MultiplayerPlaylistItem CreateMultiplayerPlaylistItem(PlaylistItem item) => new MultiplayerPlaylistItem - { - ID = item.ID, - OwnerID = item.OwnerID, - BeatmapID = item.Beatmap.OnlineID, - BeatmapChecksum = item.Beatmap.MD5Hash, - RulesetID = item.RulesetID, - RequiredMods = item.RequiredMods.ToArray(), - AllowedMods = item.AllowedMods.ToArray(), - Expired = item.Expired, - PlaylistOrder = item.PlaylistOrder ?? 0, - PlayedAt = item.PlayedAt, - StarRating = item.Beatmap.StarRating, - }; - public override Task DisconnectInternal() { isConnected.Value = false; From b4a934a7979b8b6ef23db9d0c8919034b195111f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 15:41:24 +0900 Subject: [PATCH 28/79] Refactor spectate button to remove selected item bindable --- .../TestSceneMultiplayerSpectateButton.cs | 7 ++--- .../Match/MultiplayerMatchFooter.cs | 1 - .../Match/MultiplayerSpectateButton.cs | 26 +++++++++---------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 9e6734ce99..ff5436a87d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -5,7 +5,6 @@ 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.Graphics; using osu.Framework.Graphics.Containers; @@ -71,15 +70,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(200, 50), - SelectedItem = new Bindable(room.Playlist.First()) + Size = new Vector2(200, 50) }, startControl = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(200, 50), - SelectedItem = new Bindable(room.Playlist.First()) + Size = new Vector2(200, 50) } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index 2b592bd8b9..074961cc4f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -36,7 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match new MultiplayerSpectateButton { RelativeSizeAxes = Axes.Both, - SelectedItem = selectedItem }, null, new MatchStartControl diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 3186cf89a4..3c4f7eb9b1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -21,12 +21,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public partial class MultiplayerSpectateButton : CompositeDrawable { - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; @@ -36,10 +30,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private MultiplayerClient client { get; set; } = null!; - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private readonly RoundedButton button; private IBindable operationInProgress = null!; + private long? lastPlaylistItemId; public MultiplayerSpectateButton() { @@ -75,7 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true); client.RoomUpdated += onRoomUpdated; updateState(); } @@ -121,11 +114,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void checkForAutomaticDownload() { - PlaylistItem? item = SelectedItem.Value; - - downloadCheckCancellation?.Cancel(); - - if (item == null) + if (client.Room == null) return; if (!automaticallyDownload.Value) @@ -140,10 +129,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (client.LocalUser?.State != MultiplayerUserState.Spectating) return; + MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; + + if (item.ID == lastPlaylistItemId) + return; + + downloadCheckCancellation?.Cancel(); + // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache - .GetBeatmapAsync(item.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token) + .GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token) .ContinueWith(resolved => Schedule(() => { var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; @@ -156,6 +152,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match beatmapDownloader.Download(beatmapSet); })); + + lastPlaylistItemId = item.ID; } #endregion From ca2c48bbd611823228afdc220d0902777154b81e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 15:43:57 +0900 Subject: [PATCH 29/79] Refactor ready button to remove selected item bindable --- .../Multiplayer/TestSceneMatchStartControl.cs | 7 ++----- .../Multiplayer/Match/MatchStartControl.cs | 15 +++------------ .../Multiplayer/Match/MultiplayerMatchFooter.cs | 1 - 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index fb9c801fb4..1ac98db4ca 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -101,15 +101,13 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public void SetUpSteps() { - PlaylistItem item = null!; - AddStep("reset state", () => { multiplayerClient.Invocations.Clear(); beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable(); - item = new PlaylistItem(Beatmap.Value.BeatmapInfo) + PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; @@ -139,8 +137,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(250, 50), - SelectedItem = new Bindable(item) + Size = new Vector2(250, 50) }; }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 0d90d44496..a91b844900 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Threading; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; -using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osuTK; @@ -23,22 +22,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public partial class MatchStartControl : CompositeDrawable { - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; - [Resolved(canBeNull: true)] + [Resolved] private IDialogOverlay? dialogOverlay { get; set; } [Resolved] private MultiplayerClient client { get; set; } = null!; - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private readonly MultiplayerReadyButton readyButton; private readonly MultiplayerCountdownButton countdownButton; @@ -98,9 +90,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => updateState()); client.RoomUpdated += onRoomUpdated; client.LoadRequested += onLoadRequested; + updateState(); } @@ -214,8 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match readyButton.Enabled.Value = countdownButton.Enabled.Value = client.Room.State != MultiplayerRoomState.Closed - && SelectedItem.Value?.ID == client.Room.Settings.PlaylistItemId - && !client.Room.Playlist.Single(i => i.ID == client.Room.Settings.PlaylistItemId).Expired + && !client.Room.CurrentPlaylistItem.Expired && !operationInProgress.Value; // When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index 074961cc4f..979285701f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match new MatchStartControl { RelativeSizeAxes = Axes.Both, - SelectedItem = selectedItem }, null } From 606bf1eb9f89c9db4cf246833fba387dc4125ca2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 15:45:29 +0900 Subject: [PATCH 30/79] Remove no-longer used bindable from footer --- .../Multiplayer/TestSceneMultiplayerMatchFooter.cs | 7 +------ .../Multiplayer/Match/MultiplayerMatchFooter.cs | 10 ---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 5 +---- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs index edeb1708e0..c2d3b17ccb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -1,11 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; namespace osu.Game.Tests.Visual.Multiplayer @@ -29,10 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Height = 50, - Child = new MultiplayerMatchFooter - { - SelectedItem = new Bindable() - } + Child = new MultiplayerMatchFooter() } }; }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index 979285701f..b3923ddde3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -1,10 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { @@ -13,14 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private const float ready_button_width = 600; private const float spectate_button_width = 200; - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); - public MultiplayerMatchFooter() { RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 5a2da5a555..2b3243e01d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -254,10 +254,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer this.Push(new MultiplayerMatchFreestyleSelect(Room, item)); } - protected override Drawable CreateFooter() => new MultiplayerMatchFooter - { - SelectedItem = SelectedItem - }; + protected override Drawable CreateFooter() => new MultiplayerMatchFooter(); protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room); From 9272ada859e7aa2be993362c968936886ecaf79e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 16:13:56 +0900 Subject: [PATCH 31/79] Fix intermittent mod customisation panel test --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 6eb9263c7e..499b28fb49 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -993,7 +993,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType().Single().IsScrolledToStart()); AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); - AddAssert("customisation panel closed", + AddUntilStep("customisation panel closed", () => this.ChildrenOfType().Single().ExpandedState.Value, () => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); @@ -1018,7 +1018,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void assertCustomisationToggleState(bool disabled, bool active) { AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType().Single().Enabled.Value == !disabled); - AddAssert($"customisation panel is {(active ? "" : "not ")}active", + AddUntilStep($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType().Single().ExpandedState.Value, () => active ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); } From 67b48fd0ac1ed30e3c9e8782e44a0410dfe9634e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 16:18:23 +0900 Subject: [PATCH 32/79] Remove online state check altogether --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 7cf23f6f7b..57338dde9f 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -189,10 +189,9 @@ namespace osu.Game.Overlays.Chat items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); - // Best effort checking against the recently online flag here. - // We can't use MetadataClient.GetPresence because we may not be requesting/receiving presences. + // We should probably be checking against an online state here. + // But we can't use MetadataClient.GetPresence because we may not be requesting/receiving presences. // This isn't really too bad – worst case scenario the client will open spectator view and show the user as "offline". - if (user.WasRecentlyOnline) { items.Add(new OsuMenuItemSpacer()); From ad96cff7946d6bb97fbc3e8f0869a5d5802c8bef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 17:42:12 +0900 Subject: [PATCH 33/79] Reduce vertical spacing slightly --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 011b2b950a..986bc525cc 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Play.HUD private void expand(double duration = 500) { if (ExpansionMode != ExpansionMode.AlwaysContracted) - iconsContainer.TransformSpacingTo(new Vector2(5), duration, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(5, -10), duration, Easing.OutQuint); } private void contract(double duration = 500) From cfb14fbca64a87d53cf9a54fdc34ea1d77fca76c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 16:32:17 +0900 Subject: [PATCH 34/79] Remove unnecessary customisation settings in `DifficultySpectrumDisplay` --- .../TestSceneDifficultySpectrumDisplay.cs | 29 +---------- .../Cards/BeatmapCardExtraInfoRow.cs | 4 +- .../Drawables/DifficultySpectrumDisplay.cs | 50 +++---------------- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 2 - 4 files changed, 12 insertions(+), 73 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs index 11fa6ed92d..d4e5c1d966 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs @@ -15,8 +15,6 @@ namespace osu.Game.Tests.Visual.Beatmaps { public partial class TestSceneDifficultySpectrumDisplay : OsuTestScene { - private DifficultySpectrumDisplay display; - private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet { Beatmaps = difficulties.Select(difficulty => new APIBeatmap @@ -78,32 +76,9 @@ namespace osu.Game.Tests.Visual.Beatmaps createDisplay(beatmapSet); } - [Test] - public void TestAdjustableDotSize() - { - var beatmapSet = createBeatmapSetWith( - (rulesetId: 0, stars: 2.0), - (rulesetId: 3, stars: 2.3), - (rulesetId: 0, stars: 3.2), - (rulesetId: 1, stars: 4.3), - (rulesetId: 0, stars: 5.6)); - - createDisplay(beatmapSet); - - AddStep("change dot dimensions", () => - { - display.DotSize = new Vector2(8, 12); - display.DotSpacing = 2; - }); - AddStep("change dot dimensions back", () => - { - display.DotSize = new Vector2(4, 8); - display.DotSpacing = 1; - }); - } - - private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay(beatmapSetInfo) + private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = new DifficultySpectrumDisplay { + BeatmapSet = beatmapSetInfo, Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(3) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs index a11ef0f95c..41513ec7a2 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -36,11 +36,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards Origin = Anchor.CentreLeft, TextSize = 13f }, - new DifficultySpectrumDisplay(beatmapSet) + new DifficultySpectrumDisplay { + BeatmapSet = beatmapSet, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - DotSize = new Vector2(5, 10) } } }; diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 56f6c77ba8..60685cd31d 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -18,34 +18,6 @@ namespace osu.Game.Beatmaps.Drawables { public partial class DifficultySpectrumDisplay : CompositeDrawable { - private Vector2 dotSize = new Vector2(4, 8); - - public Vector2 DotSize - { - get => dotSize; - set - { - dotSize = value; - - if (IsLoaded) - updateDisplay(); - } - } - - private float dotSpacing = 1; - - public float DotSpacing - { - get => dotSpacing; - set - { - dotSpacing = value; - - if (IsLoaded) - updateDisplay(); - } - } - private IBeatmapSetInfo? beatmapSet; public IBeatmapSetInfo? BeatmapSet @@ -60,9 +32,10 @@ namespace osu.Game.Beatmaps.Drawables } } - private readonly FillFlowContainer flow; + private FillFlowContainer flow = null!; - public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null) + [BackgroundDependencyLoader] + private void load() { AutoSizeAxes = Axes.Both; @@ -72,8 +45,6 @@ namespace osu.Game.Beatmaps.Drawables Spacing = new Vector2(10, 0), Direction = FillDirection.Horizontal, }; - - BeatmapSet = beatmapSet; } protected override void LoadComplete() @@ -94,10 +65,7 @@ namespace osu.Game.Beatmaps.Drawables foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize) - { - Spacing = new Vector2(DotSpacing, 0f), - }); + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); } } @@ -106,14 +74,12 @@ namespace osu.Game.Beatmaps.Drawables private readonly int rulesetId; private readonly IEnumerable beatmapInfos; private readonly bool collapsed; - private readonly Vector2 dotSize; - public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed, Vector2 dotSize) + public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed) { this.rulesetId = rulesetId; this.beatmapInfos = beatmapInfos; this.collapsed = collapsed; - this.dotSize = dotSize; } [BackgroundDependencyLoader] @@ -133,7 +99,7 @@ namespace osu.Game.Beatmaps.Drawables if (!collapsed) { foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating)) - Add(new DifficultyDot(beatmapInfo.StarRating, dotSize)); + Add(new DifficultyDot(beatmapInfo.StarRating)); } else { @@ -153,10 +119,10 @@ namespace osu.Game.Beatmaps.Drawables { private readonly double starDifficulty; - public DifficultyDot(double starDifficulty, Vector2 dotSize) + public DifficultyDot(double starDifficulty) { this.starDifficulty = starDifficulty; - Size = dotSize; + Size = new Vector2(5, 10); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 512fbacec1..c599c3e534 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -106,8 +106,6 @@ namespace osu.Game.Screens.SelectV2 }, difficultiesDisplay = new DifficultySpectrumDisplay { - DotSize = new Vector2(5, 10), - DotSpacing = 2, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, From 63fcde55387a0782e26e91ae99102f1d7b0c8f66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 17:14:32 +0900 Subject: [PATCH 35/79] Fix `DifficultySpectrumDisplay` churning drawables Was causing so much GC that song select (v2) was grinding to a halt. --- .../TestSceneDifficultySpectrumDisplay.cs | 43 ++--- .../Drawables/DifficultySpectrumDisplay.cs | 149 +++++++++++++----- 2 files changed, 134 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs index d4e5c1d966..39de2b7bc9 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Beatmaps; +using osu.Framework.Testing; using osu.Game.Beatmaps.Drawables; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -15,14 +13,18 @@ namespace osu.Game.Tests.Visual.Beatmaps { public partial class TestSceneDifficultySpectrumDisplay : OsuTestScene { - private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet + private DifficultySpectrumDisplay display = null!; + + [SetUpSteps] + public void SetUpSteps() { - Beatmaps = difficulties.Select(difficulty => new APIBeatmap + AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay { - RulesetID = difficulty.rulesetId, - StarRating = difficulty.stars - }).ToArray() - }; + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(3) + }); + } [Test] public void TestSingleRuleset() @@ -32,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 0, stars: 3.2), (rulesetId: 0, stars: 5.6)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] @@ -45,7 +47,7 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 1, stars: 4.3), (rulesetId: 0, stars: 5.6)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] @@ -59,29 +61,30 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 0, stars: 5.6), (rulesetId: 15, stars: 7.8)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] public void TestMaximumUncollapsed() { var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 12).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray()); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] public void TestMinimumCollapsed() { var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 13).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray()); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } - private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = new DifficultySpectrumDisplay + private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet { - BeatmapSet = beatmapSetInfo, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(3) - }); + Beatmaps = difficulties.Select(difficulty => new APIBeatmap + { + RulesetID = difficulty.rulesetId, + StarRating = difficulty.stars + }).ToArray() + }; } } diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 60685cd31d..347ad3101c 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -34,6 +34,8 @@ namespace osu.Game.Beatmaps.Drawables private FillFlowContainer flow = null!; + private const int max_difficulties_before_collapsing = 12; + [BackgroundDependencyLoader] private void load() { @@ -55,31 +57,71 @@ namespace osu.Game.Beatmaps.Drawables private void updateDisplay() { - flow.Clear(); + foreach (var group in flow) + group.Alpha = 0; if (beatmapSet == null) + { + foreach (var group in flow) + group.Beatmaps = []; return; + } // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 - bool collapsed = beatmapSet.Beatmaps.Count() > 12; + bool collapsed = beatmapSet.Beatmaps.Count() > max_difficulties_before_collapsing; foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); + int rulesetId = rulesetGrouping.Key.OnlineID; + + var group = flow.SingleOrDefault(rg => rg.RulesetId == rulesetId); + + if (group == null) + { + group = new RulesetDifficultyGroup(rulesetId); + flow.Add(group); + flow.SetLayoutPosition(group, rulesetId); + } + + group.Alpha = 1; + group.Beatmaps = rulesetGrouping; + group.Collapsed = collapsed; } } private partial class RulesetDifficultyGroup : FillFlowContainer { - private readonly int rulesetId; - private readonly IEnumerable beatmapInfos; - private readonly bool collapsed; + public readonly int RulesetId; - public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed) + private IEnumerable beatmaps = []; + + public IEnumerable Beatmaps { - this.rulesetId = rulesetId; - this.beatmapInfos = beatmapInfos; - this.collapsed = collapsed; + get => beatmaps; + set + { + beatmaps = value; + updateDisplay(); + } + } + + private bool collapsed; + + public bool Collapsed + { + get => collapsed; + set + { + collapsed = value; + updateDisplay(); + } + } + + private OsuSpriteText countText = null!; + + public RulesetDifficultyGroup(int rulesetId) + { + RulesetId = rulesetId; } [BackgroundDependencyLoader] @@ -89,53 +131,84 @@ namespace osu.Game.Beatmaps.Drawables Spacing = new Vector2(1, 0); Direction = FillDirection.Horizontal; - var icon = rulesets.GetRuleset(rulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; + var icon = rulesets.GetRuleset(RulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; Add(icon.With(i => { i.Size = new Vector2(14); i.Anchor = i.Origin = Anchor.Centre; })); - if (!collapsed) + for (int i = 0; i < max_difficulties_before_collapsing; i++) + Add(new DifficultyDot()); + + Add(countText = new OsuSpriteText { - foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating)) - Add(new DifficultyDot(beatmapInfo.StarRating)); - } - else + Font = OsuFont.Default.With(size: 12), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Bottom = 1 } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateDisplay(); + } + + private void updateDisplay() + { + countText.Alpha = collapsed ? 1 : 0; + countText.Text = beatmaps.Count().ToLocalisableString(@"N0"); + + var orderedBeatmaps = beatmaps.OrderBy(bi => bi.StarRating).ToArray(); + var dots = this.OfType().ToArray(); + + for (int i = 0; i < max_difficulties_before_collapsing; i++) { - Add(new OsuSpriteText + var dot = dots[i]; + + if (collapsed || i >= orderedBeatmaps.Length) { - Text = beatmapInfos.Count().ToLocalisableString(@"N0"), - Font = OsuFont.Default.With(size: 12), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding { Bottom = 1 } - }); + dot.Alpha = 0; + continue; + } + + dot.Alpha = 1; + dot.StarDifficulty = orderedBeatmaps[i].StarRating; } } } - private partial class DifficultyDot : CircularContainer + private partial class DifficultyDot : Circle { - private readonly double starDifficulty; + private double starDifficulty; - public DifficultyDot(double starDifficulty) + public double StarDifficulty { - this.starDifficulty = starDifficulty; - Size = new Vector2(5, 10); + get => starDifficulty; + set + { + starDifficulty = value; + updateColour(); + } } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Anchor = Origin = Anchor.Centre; - Masking = true; + [Resolved] + private OsuColour colours { get; set; } = null!; - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(starDifficulty) - }; + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(5, 10); + Anchor = Origin = Anchor.Centre; + + updateColour(); + } + + private void updateColour() + { + Colour = colours.ForStarDifficulty(starDifficulty); } } } From c1604a797f71c5d8520f40bfbf1e3ea22bc0babc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 19:56:27 +0900 Subject: [PATCH 36/79] Ensure `BindValueChanged` is run after `LoadComplete` --- osu.Game/Screens/Play/PauseOverlay.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 11f62939fb..18d17c1317 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -34,9 +34,9 @@ namespace osu.Game.Screens.Play OnResume?.Invoke(); }; - private IBindable? windowActive; + private readonly IBindable windowActive = new Bindable(true); - private float targetVolume => windowActive?.Value != false && State.Value == Visibility.Visible ? 1.0f : 0; + private float targetVolume => windowActive.Value && State.Value == Visibility.Visible ? 1.0f : 0; [BackgroundDependencyLoader] private void load(GameHost? host) @@ -48,10 +48,15 @@ namespace osu.Game.Screens.Play }); if (host != null) - { - windowActive = host.IsActive.GetBoundCopy(); - windowActive.BindValueChanged(_ => Schedule(() => pauseLoop.VolumeTo(targetVolume, 1000, Easing.Out))); - } + windowActive.BindTo(host.IsActive); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Schedule required because host.IsActive doesn't seem to always run on the update thread. + windowActive.BindValueChanged(_ => Schedule(() => pauseLoop.VolumeTo(targetVolume, 1000, Easing.Out))); } public void StopAllSamples() From 149e160964f6e46c58f95037cf5086a5ffa2ce04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Mar 2025 01:50:16 +0900 Subject: [PATCH 37/79] Avoid multiple enumerations of beatmaps --- .../Drawables/DifficultySpectrumDisplay.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 347ad3101c..fc41c7c6dc 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; @@ -84,7 +83,7 @@ namespace osu.Game.Beatmaps.Drawables } group.Alpha = 1; - group.Beatmaps = rulesetGrouping; + group.Beatmaps = rulesetGrouping.ToArray(); group.Collapsed = collapsed; } } @@ -93,14 +92,13 @@ namespace osu.Game.Beatmaps.Drawables { public readonly int RulesetId; - private IEnumerable beatmaps = []; + private IBeatmapInfo[] beatmaps = []; - public IEnumerable Beatmaps + public IBeatmapInfo[] Beatmaps { - get => beatmaps; set { - beatmaps = value; + beatmaps = value.OrderBy(bi => bi.StarRating).ToArray(); updateDisplay(); } } @@ -159,23 +157,22 @@ namespace osu.Game.Beatmaps.Drawables private void updateDisplay() { countText.Alpha = collapsed ? 1 : 0; - countText.Text = beatmaps.Count().ToLocalisableString(@"N0"); + countText.Text = beatmaps.Length.ToLocalisableString(@"N0"); - var orderedBeatmaps = beatmaps.OrderBy(bi => bi.StarRating).ToArray(); var dots = this.OfType().ToArray(); for (int i = 0; i < max_difficulties_before_collapsing; i++) { var dot = dots[i]; - if (collapsed || i >= orderedBeatmaps.Length) + if (collapsed || i >= beatmaps.Length) { dot.Alpha = 0; continue; } dot.Alpha = 1; - dot.StarDifficulty = orderedBeatmaps[i].StarRating; + dot.StarDifficulty = beatmaps[i].StarRating; } } } From 44b4e04d8cecbdd45e28a612442fb0fb5152bf84 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 20 Mar 2025 12:20:00 +0900 Subject: [PATCH 38/79] Update iOS project to match Android project Pulls in `` and inclusion of test resources. --- osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index e4b9d2ba94..da07373037 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -1,4 +1,5 @@  + Exe net8.0-ios @@ -6,11 +7,19 @@ osu.Game.Tests osu.Game.Tests.iOS - + + $(NoWarn);CA2007 + %(RecursiveDir)%(Filename)%(Extension) + + + %(RecursiveDir)%(Filename)%(Extension) + iOS\%(RecursiveDir)%(Filename)%(Extension) + From 84cc2e6d7717837841ab825639de305da9208008 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Mar 2025 15:46:04 +0900 Subject: [PATCH 39/79] Avoid using invalidation / transforms for settings toolbox header hiding This was causing a performance issue due to transforms bunching up for off-screen toolboxes. It's much simpler to just update these values every frame. Closes https://github.com/ppy/osu/issues/32474. --- osu.Game/Overlays/SettingsToolboxGroup.cs | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index dd41f156f3..b1a0ca0ccd 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -3,14 +3,13 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Events; -using osu.Framework.Layout; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -29,8 +28,6 @@ namespace osu.Game.Overlays private const int header_height = 30; private const int corner_radius = 5; - private readonly Cached headerTextVisibilityCache = new Cached(); - protected override Container Content => content; private readonly FillFlowContainer content = new FillFlowContainer @@ -156,13 +153,9 @@ namespace osu.Game.Overlays { base.Update(); - if (!headerTextVisibilityCache.IsValid) - { - // These toolbox grouped may be contracted to only show icons. - // For now, let's hide the header to avoid text truncation weirdness in such cases. - headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); - headerTextVisibilityCache.Validate(); - } + // These toolbox grouped may be contracted to only show icons. + // For now, let's hide the header to avoid text truncation weirdness in such cases. + headerText.Alpha = (float)Interpolation.DampContinuously(headerText.Alpha, headerText.DrawWidth < DrawWidth ? 1 : 0, 40, Time.Elapsed); // Dragged child finished its drag operation. if (draggedChild != null && inputManager.DraggedDrawable != draggedChild) @@ -172,14 +165,6 @@ namespace osu.Game.Overlays } } - protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) - { - if (invalidation.HasFlag(Invalidation.DrawSize)) - headerTextVisibilityCache.Invalidate(); - - return base.OnInvalidate(invalidation, source); - } - private void updateExpandedState(bool animate) { // before we collapse down, let's double check the user is not dragging a UI control contained within us. From 80ff974594a69248960f3e720ab7d2693b9e1d8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Mar 2025 14:23:54 +0900 Subject: [PATCH 40/79] Remove non-visible "beat snap" text in editor --- .../Edit/Compose/Components/BeatDivisorControl.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index b8f2695259..22df917992 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -146,22 +146,11 @@ namespace osu.Game.Screens.Edit.Compose.Components } } }, - new Drawable[] - { - new TextFlowContainer(s => s.Font = s.Font.With(size: 14)) - { - Padding = new MarginPadding { Horizontal = 15, Vertical = 2 }, - Text = "beat snap", - RelativeSizeAxes = Axes.X, - TextAnchor = Anchor.TopCentre, - }, - }, }, RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, 40), new Dimension(GridSizeMode.Absolute, 20), - new Dimension(GridSizeMode.Absolute, 15) } } }; From b1131ffd23b101246b6a8ebccd88dcecf280de3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Mar 2025 14:24:17 +0900 Subject: [PATCH 41/79] Avoid `Triangles` draw node invalidation when nothing is changing Basically only helps when time is paused. ie in the editor. --- osu.Game/Graphics/Backgrounds/Triangles.cs | 13 +++++++++++-- osu.Game/Graphics/Backgrounds/TrianglesV2.cs | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index e877915fac..d22aa197bb 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -127,8 +127,6 @@ namespace osu.Game.Graphics.Backgrounds { base.Update(); - Invalidate(Invalidation.DrawNode); - if (CreateNewTriangles) addTriangles(false); @@ -138,6 +136,10 @@ namespace osu.Game.Graphics.Backgrounds : 1; float elapsedSeconds = (float)Time.Elapsed / 1000; + + if (elapsedSeconds == 0) + return; + // Since position is relative, the velocity needs to scale inversely with DrawHeight. // Since we will later multiply by the scale of individual triangles we normalize by // dividing by triangleScale. @@ -157,6 +159,8 @@ namespace osu.Game.Graphics.Backgrounds if (bottomPos < 0) parts.RemoveAt(i); } + + Invalidate(Invalidation.DrawNode); } /// @@ -183,8 +187,13 @@ namespace osu.Game.Graphics.Backgrounds int currentCount = parts.Count; + if (AimCount - currentCount == 0) + return; + for (int i = 0; i < AimCount - currentCount; i++) parts.Add(createTriangle(randomY)); + + Invalidate(Invalidation.DrawNode); } private TriangleParticle createTriangle(bool randomY) diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index 4143a6d76d..358e859cc8 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -91,12 +91,14 @@ namespace osu.Game.Graphics.Backgrounds { base.Update(); - Invalidate(Invalidation.DrawNode); - if (CreateNewTriangles) addTriangles(false); float elapsedSeconds = (float)Time.Elapsed / 1000; + + if (elapsedSeconds == 0) + return; + // Since position is relative, the velocity needs to scale inversely with DrawHeight. float movedDistance = -elapsedSeconds * Velocity * base_velocity / DrawHeight; @@ -112,6 +114,8 @@ namespace osu.Game.Graphics.Backgrounds if (bottomPos < 0) parts.RemoveAt(i); } + + Invalidate(Invalidation.DrawNode); } /// @@ -138,8 +142,13 @@ namespace osu.Game.Graphics.Backgrounds int currentCount = parts.Count; + if (AimCount - currentCount == 0) + return; + for (int i = 0; i < AimCount - currentCount; i++) parts.Add(createTriangle(randomY)); + + Invalidate(Invalidation.DrawNode); } private TriangleParticle createTriangle(bool randomY) From a7ee6a15252986e5bd744a822c11b62f2ae5a4f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Mar 2025 16:00:01 +0900 Subject: [PATCH 42/79] Rename online store class to better explain what it's doing --- osu.Game/Audio/PreviewTrackManager.cs | 2 +- .../Online/{OsuOnlineStore.cs => TrustedDomainOnlineStore.cs} | 2 +- osu.Game/OsuGameBase.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename osu.Game/Online/{OsuOnlineStore.cs => TrustedDomainOnlineStore.cs} (91%) diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index f9e74cd1b2..d3ab86a8a0 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Audio [BackgroundDependencyLoader] private void load(AudioManager audioManager) { - trackStore = audioManager.GetTrackStore(new OsuOnlineStore()); + trackStore = audioManager.GetTrackStore(new TrustedDomainOnlineStore()); } /// diff --git a/osu.Game/Online/OsuOnlineStore.cs b/osu.Game/Online/TrustedDomainOnlineStore.cs similarity index 91% rename from osu.Game/Online/OsuOnlineStore.cs rename to osu.Game/Online/TrustedDomainOnlineStore.cs index f578043c5d..2b47f159e6 100644 --- a/osu.Game/Online/OsuOnlineStore.cs +++ b/osu.Game/Online/TrustedDomainOnlineStore.cs @@ -7,7 +7,7 @@ using osu.Framework.Logging; namespace osu.Game.Online { - public class OsuOnlineStore : OnlineStore + public sealed class TrustedDomainOnlineStore : OnlineStore { protected override string GetLookupUrl(string url) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 51c8788248..4087a8b71e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -108,7 +108,7 @@ namespace osu.Game public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); - protected override OnlineStore CreateOnlineStore() => new OsuOnlineStore(); + protected override OnlineStore CreateOnlineStore() => new TrustedDomainOnlineStore(); public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); From 51c4e073d1c1f380ef9909d9d5520344b6d5b50d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Mar 2025 16:38:37 +0900 Subject: [PATCH 43/79] Disallow tagging beatmaps when playing as convert --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 758eabcf2e..9a46f8091a 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -238,6 +238,8 @@ namespace osu.Game.Screens.Ranking.Statistics if ( // We may want to iterate on this condition AchievedScore.Rank >= ScoreRank.C + // Tags are only relevant to the original ruleset of the map, so disallow tagging when playing as a convert. + && AchievedScore.Ruleset.OnlineID == AchievedScore.BeatmapInfo!.Ruleset.OnlineID ) { yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) From 36ba9dbc8cde5d37e4c74afa32340da0034657a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Mar 2025 10:32:40 +0100 Subject: [PATCH 44/79] Add visual test coverage for no convert conditional --- .../Ranking/TestSceneStatisticsPanel.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index df65023303..f82b32167c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -24,6 +24,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Scoring; @@ -243,6 +244,28 @@ namespace osu.Game.Tests.Visual.Ranking }); } + [Test] + public void TestTaggingConvert() + { + var score = TestResources.CreateTestScoreInfo(); + score.Ruleset = new ManiaRuleset().RulesetInfo; + + AddStep("load panel", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + AchievedScore = score, + } + }; + }); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new StatisticsPanel From a4e2b87bdd74b14217161d64b16f6aacd9ff2cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Mar 2025 10:42:18 +0100 Subject: [PATCH 45/79] Use better copy when preventing beatmap tagging after they've been played converted --- .../Ranking/Statistics/StatisticsPanel.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 9a46f8091a..9ead9ce91c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -235,12 +235,16 @@ namespace osu.Game.Screens.Ranking.Statistics && newScore.BeatmapInfo!.OnlineID > 0 && api.IsLoggedIn) { - if ( - // We may want to iterate on this condition - AchievedScore.Rank >= ScoreRank.C - // Tags are only relevant to the original ruleset of the map, so disallow tagging when playing as a convert. - && AchievedScore.Ruleset.OnlineID == AchievedScore.BeatmapInfo!.Ruleset.OnlineID - ) + string? preventTaggingReason = null; + + // We may want to iterate on the following conditions further in the future + + if (AchievedScore.Ruleset.OnlineID != AchievedScore.BeatmapInfo!.Ruleset.OnlineID) + preventTaggingReason = "Play the beatmap in its original ruleset to contribute to beatmap tags!"; + else if (AchievedScore.Rank < ScoreRank.C) + preventTaggingReason = "Set a better score to contribute to beatmap tags!"; + + if (preventTaggingReason == null) { yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) { @@ -256,7 +260,7 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.Centre, - Text = "Set a better score to contribute to beatmap tags!", + Text = preventTaggingReason, }); } } From 6f1c00ff97c83e150491efe2b773e2a0616091e5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 20 Mar 2025 20:51:38 +0900 Subject: [PATCH 46/79] Revert changes to spectate button cancellation --- .../Multiplayer/Match/MultiplayerSpectateButton.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 3c4f7eb9b1..13abe7bb14 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -33,7 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private readonly RoundedButton button; private IBindable operationInProgress = null!; - private long? lastPlaylistItemId; public MultiplayerSpectateButton() { @@ -114,6 +113,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void checkForAutomaticDownload() { + downloadCheckCancellation?.Cancel(); + if (client.Room == null) return; @@ -131,11 +132,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; - if (item.ID == lastPlaylistItemId) - return; - - downloadCheckCancellation?.Cancel(); - // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache @@ -152,8 +148,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match beatmapDownloader.Download(beatmapSet); })); - - lastPlaylistItemId = item.ID; } #endregion From e74a22b8841ff4c723dd725346505bf053d86081 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Mar 2025 11:14:40 +0900 Subject: [PATCH 47/79] Update framework and resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 4 ++-- osu.iOS.props | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 245d49abc2..8e383a705c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 260b0cc0c3..2fa83c3ab0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 845e5fd39fad2369366b0ff94b03d6dade0ebe8f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Mar 2025 15:34:24 +0900 Subject: [PATCH 48/79] Attempt to fix intermittent BackgroundDataStoreProcessor tests --- .../BackgroundDataStoreProcessorTests.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index c40624a3a0..bae8e7c76a 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -62,12 +62,11 @@ namespace osu.Game.Tests.Database }); }); - AddStep("Run background processor", () => - { - Add(new TestBackgroundDataStoreProcessor()); - }); + TestBackgroundDataStoreProcessor processor = null!; + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); + AddUntilStep("Wait for completion", () => processor.Completed); - AddUntilStep("wait for difficulties repopulated", () => + AddAssert("Difficulties repopulated", () => { return Realm.Run(r => { @@ -101,13 +100,10 @@ namespace osu.Game.Tests.Database }); }); - AddStep("Run background processor", () => - { - Add(new TestBackgroundDataStoreProcessor()); - }); + TestBackgroundDataStoreProcessor processor = null!; + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); AddWaitStep("wait some", 500); - AddAssert("Difficulty still not populated", () => { return Realm.Run(r => @@ -118,8 +114,9 @@ namespace osu.Game.Tests.Database }); AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying); + AddUntilStep("Wait for completion", () => processor.Completed); - AddUntilStep("wait for difficulties repopulated", () => + AddAssert("Difficulties repopulated", () => { return Realm.Run(r => { @@ -151,9 +148,11 @@ namespace osu.Game.Tests.Database }); }); - AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); + TestBackgroundDataStoreProcessor processor = null!; + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); + AddUntilStep("Wait for completion", () => processor.Completed); - AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION)); + AddAssert("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION)); AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); } @@ -183,7 +182,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); AddUntilStep("Wait for completion", () => processor.Completed); - AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); + AddAssert("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion)); } From f23a74d48451c45013fb5657acf65be35c6e9fb9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Mar 2025 21:32:38 +0900 Subject: [PATCH 49/79] Change autosize method to avoid input handling outside of text area Co-authored-by: Joseph Madamba --- .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 9669b8f851..ea32740a60 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -309,16 +309,14 @@ namespace osu.Game.Screens.Ranking.Expanded public ClickableMetadata(int beatmapId, IBeatmapMetadataInfo metadata) { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; Child = new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Children = new Drawable[] { From aacb5d86c835407d967d8de22947088a86dea9f8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Mar 2025 22:15:10 +0900 Subject: [PATCH 50/79] Fix `osu.Game.Tests` tests running twice --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1019569b5b..f041f2e916 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,8 +82,18 @@ jobs: run: dotnet build -c Debug -warnaserror osu.Desktop.slnf - name: Test - run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0 - shell: pwsh + run: > + dotnet test + osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll + osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll + osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll + osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll + osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll + osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll + Templates/**/*.Tests/bin/Debug/**/*.Tests.dll + --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" + -- + NUnit.ConsoleOut=0 # Attempt to upload results even if test fails. # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always From 0802eff1b8455a2df5bf32925be44a43adc89776 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 21 Mar 2025 12:08:10 -0700 Subject: [PATCH 51/79] Fix partial errors --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- osu.Game/Users/Drawables/ClickableUsername.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index ea32740a60..445d219c7f 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -302,7 +302,7 @@ namespace osu.Game.Screens.Ranking.Expanded } } - internal class ClickableMetadata : OsuHoverContainer + internal partial class ClickableMetadata : OsuHoverContainer { [Resolved] private OsuGame? game { get; set; } diff --git a/osu.Game/Users/Drawables/ClickableUsername.cs b/osu.Game/Users/Drawables/ClickableUsername.cs index ef07fc8f8b..74782ed6ed 100644 --- a/osu.Game/Users/Drawables/ClickableUsername.cs +++ b/osu.Game/Users/Drawables/ClickableUsername.cs @@ -12,7 +12,7 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Users.Drawables { - internal class ClickableUsername : OsuHoverContainer, IHasCustomTooltip + internal partial class ClickableUsername : OsuHoverContainer, IHasCustomTooltip { public ITooltip GetCustomTooltip() => new ClickableAvatar.NoCardTooltip(); From 3b53e221d6ec45323ba082ebd5e10e418e951f61 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 22 Mar 2025 21:25:30 -0400 Subject: [PATCH 52/79] Change mod select colour scheme to aquamarine in new song select --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index ad29f846c4..e295656a21 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.SelectV2 { private const float logo_scale = 0.4f; - private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay + private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine) { ShowPresets = true, }; From 0cc7cb02454b4e93abe95f7384e9916b4f97b279 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 22 Mar 2025 22:14:56 -0400 Subject: [PATCH 53/79] Fix mod select footer overlapping with mod unranked indicator --- osu.Game/Screens/Footer/ScreenFooter.cs | 94 +++++++++++++++---------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index ea32507ca0..f75250a832 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -33,7 +33,8 @@ namespace osu.Game.Screens.Footer private Box background = null!; private FillFlowContainer buttonsFlow = null!; - private Container removedButtonsContainer = null!; + private Container footerContentContainer = null!; + private Container hiddenButtonsContainer = null!; private LogoTrackingContainer logoTrackingContainer = null!; [Cached] @@ -71,15 +72,35 @@ namespace osu.Game.Screens.Footer RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5 }, - buttonsFlow = new FillFlowContainer + new GridContainer { - Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, - Y = 10f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(7, 0), - AutoSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + buttonsFlow = new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = 10f, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(7, 0), + AutoSizeAxes = Axes.Both, + }, + footerContentContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Y = -15f, + }, + }, + } }, BackButton = new ScreenBackButton { @@ -88,7 +109,7 @@ namespace osu.Game.Screens.Footer Origin = Anchor.BottomLeft, Action = onBackPressed, }, - removedButtonsContainer = new Container + hiddenButtonsContainer = new Container { Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, Y = 10f, @@ -153,7 +174,7 @@ namespace osu.Game.Screens.Footer var oldButton = oldButtons[i]; buttonsFlow.Remove(oldButton, false); - removedButtonsContainer.Add(oldButton); + hiddenButtonsContainer.Add(oldButton); if (buttons.Count > 0) makeButtonDisappearToRight(oldButton, i, oldButtons.Length, true); @@ -188,7 +209,7 @@ namespace osu.Game.Screens.Footer } private ShearedOverlayContainer? activeOverlay; - private Container? contentContainer; + private VisibilityContainer? activeFooterContent; private readonly List temporarilyHiddenButtons = new List(); @@ -210,33 +231,28 @@ namespace osu.Game.Screens.Footer ? buttonsFlow.SkipWhile(b => b != targetButton).Skip(1) : buttonsFlow); - for (int i = 0; i < temporarilyHiddenButtons.Count; i++) - makeButtonDisappearToBottom(temporarilyHiddenButtons[i], 0, 0, false); + for (int i = temporarilyHiddenButtons.Count - 1; i >= 0; i--) + { + var button = temporarilyHiddenButtons[i]; + buttonsFlow.Remove(button, false); + hiddenButtonsContainer.Add(button); - var fallbackPosition = buttonsFlow.Any() - ? buttonsFlow.ToSpaceOfOtherDrawable(Vector2.Zero, this) - : BackButton.ToSpaceOfOtherDrawable(BackButton.LayoutRectangle.TopRight + new Vector2(5f, 0f), this); - - var targetPosition = targetButton?.ToSpaceOfOtherDrawable(targetButton.LayoutRectangle.TopRight, this) ?? fallbackPosition; + makeButtonDisappearToBottom(button, 0, 0, false); + } updateColourScheme(overlay.ColourProvider.Hue); footerContent = overlay.CreateFooterContent(); + activeFooterContent = footerContent; + var content = footerContent; - var content = footerContent ?? Empty(); - - Add(contentContainer = new Container - { - Y = -15f, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = targetPosition.X }, - Child = content, - }); + if (content != null) + footerContentContainer.Child = content; if (temporarilyHiddenButtons.Count > 0) - this.Delay(60).Schedule(() => content.Show()); + this.Delay(60).Schedule(() => content?.Show()); else - content.Show(); + content?.Show(); return new InvokeOnDisposal(clearActiveOverlayContainer); } @@ -246,20 +262,26 @@ namespace osu.Game.Screens.Footer if (activeOverlay == null) return; - Debug.Assert(contentContainer != null); - contentContainer.Child.Hide(); + Debug.Assert(activeFooterContent != null); + activeFooterContent.Hide(); - double timeUntilRun = contentContainer.Child.LatestTransformEndTime - Time.Current; + double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current; for (int i = 0; i < temporarilyHiddenButtons.Count; i++) - makeButtonAppearFromBottom(temporarilyHiddenButtons[i], 0); + { + var button = temporarilyHiddenButtons[i]; + hiddenButtonsContainer.Remove(button, false); + buttonsFlow.Add(button); + + makeButtonAppearFromBottom(button, 0); + } temporarilyHiddenButtons.Clear(); updateColourScheme(OverlayColourScheme.Aquamarine.GetHue()); - contentContainer.Delay(timeUntilRun).Expire(); - contentContainer = null; + activeFooterContent.Delay(timeUntilRun).Expire(); + activeFooterContent = null; activeOverlay = null; } From b6068e6e296c02cf725eb75d505f2f9387ad4f56 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 22 Mar 2025 22:15:02 -0400 Subject: [PATCH 54/79] Add test coverage --- .../UserInterface/TestSceneScreenFooter.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index fc8777068d..054bbb39d1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -196,6 +196,37 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("external overlay content still not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); } + [Test] + public void TestButtonResizedAfterFooterIsDisplayed() + { + TestShearedOverlayContainer externalOverlay = null!; + + AddStep("add overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer())); + AddStep("set buttons", () => screenFooter.SetButtons(new[] + { + new ScreenFooterButton(externalOverlay) + { + AccentColour = Dependencies.Get().Orange1, + Icon = FontAwesome.Solid.Toolbox, + Text = "One", + }, + new ScreenFooterButton { Text = "Two", Action = () => { } }, + new ScreenFooterButton { Text = "Three", Action = () => { } }, + })); + AddWaitStep("wait for transition", 3); + + AddStep("show overlay", () => externalOverlay.Show()); + AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType().Single().IsPresent); + AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.Child.Parent!.Y > 0)); + + AddStep("resize active button", () => this.ChildrenOfType().First().ResizeWidthTo(240, 300, Easing.OutQuint)); + AddStep("resize active button back", () => this.ChildrenOfType().First().ResizeWidthTo(116, 300, Easing.OutQuint)); + + AddStep("hide overlay", () => externalOverlay.Hide()); + AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.ChildrenOfType().First().Y == 0)); + } + private partial class TestShearedOverlayContainer : ShearedOverlayContainer { public TestShearedOverlayContainer() From 25e404c7346f140eb9c500fe8c3d5dfd50b85334 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 21 Mar 2025 21:46:20 -0400 Subject: [PATCH 55/79] Add sheared dropdown --- .../UserInterface/TestSceneShearedDropdown.cs | 43 +++ .../Graphics/UserInterface/OsuDropdown.cs | 2 +- .../UserInterfaceV2/ShearedDropdown.cs | 293 ++++++++++++++++++ 3 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneShearedDropdown.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedDropdown.cs new file mode 100644 index 0000000000..d650ce6c36 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedDropdown.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneShearedDropdown : ThemeComparisonTestScene + { + public TestSceneShearedDropdown() + : base(false) + { + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black.Opacity(0.75f), + RelativeSizeAxes = Axes.Both, + }, + new ShearedDropdown("Test") + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 300f, + Width = 140, + Current = new Bindable(), + Items = new[] { "Global", "Friends", "Local", "Really lonnnnnnng option" }, + } + } + }; + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index dc42216c55..5a1fbaa3a4 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface #region OsuDropdownMenu - protected partial class OsuDropdownMenu : DropdownMenu + public partial class OsuDropdownMenu : DropdownMenu { public override bool HandleNonPositionalInput => State == MenuState.Open; diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs new file mode 100644 index 0000000000..deb55daab4 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -0,0 +1,293 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class ShearedDropdown : Dropdown, IKeyBindingHandler + { + protected override DropdownHeader CreateHeader() => new ShearedDropdownHeader(); + + protected override DropdownMenu CreateMenu() => new ShearedDropdownMenu(); + + public ShearedDropdown(LocalisableString label) + { + if (Header is ShearedDropdownHeader osuHeader) + { + osuHeader.Dropdown = this; + osuHeader.LeftSideLabel = label; + } + } + + protected override void Update() + { + base.Update(); + + var header = (ShearedDropdownHeader)Header; + var menu = (ShearedDropdownMenu)Menu; + + menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 15f }; + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) return false; + + if (e.Action == GlobalAction.Back) + return Back(); + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu + { + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + public ShearedDropdownMenu() + { + Margin = new MarginPadding { Top = 5f }; + } + } + + public partial class ShearedDropdownHeader : DropdownHeader + { + private LocalisableString label; + + protected override LocalisableString Label + { + get => label; + set + { + label = value; + valueText.Text = value; + } + } + + public LocalisableString LeftSideLabel + { + set => labelText.Text = value; + } + + private readonly OsuSpriteText labelText; + private readonly OsuSpriteText valueText; + private readonly Box labelBox; + private readonly SpriteIcon chevron; + + public Container LabelContainer { get; } + + public ShearedDropdown Dropdown = null!; + private ShearedDropdownSearchBar searchBar = null!; + + private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public ShearedDropdownHeader() + { + Shear = shear; + CornerRadius = 5f; + Masking = true; + + Foreground.Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new[] + { + LabelContainer = new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + // required to fix colour bleeding around the edges of the dropdown on hover + Padding = new MarginPadding { Vertical = -1f, Left = -1f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 5f, + Masking = true, + Child = labelBox = new Box + { + RelativeSizeAxes = Axes.Both, + } + }, + }, + labelText = new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f }, + Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + Shear = -shear, + }, + }, + }, + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10f }, + Shear = -shear, + Children = new Drawable[] + { + valueText = new TruncatingSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = 15f }, + Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + chevron = new SpriteIcon + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Y = 1f, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(10f), + } + }, + }, + } + } + }, + }; + + AddInternal(LabelContainer.CreateProxy()); + } + + [BackgroundDependencyLoader] + private void load() + { + labelBox.Colour = colourProvider.Background3; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Dropdown.Menu.StateChanged += _ => updateChevron(); + SearchBar.State.ValueChanged += _ => updateColour(); + Enabled.BindValueChanged(_ => updateColour()); + updateColour(); + } + + protected override void Update() + { + base.Update(); + searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth }; + } + + protected override bool OnHover(HoverEvent e) + { + updateColour(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateColour(); + } + + private void updateColour() + { + bool hovered = Enabled.Value && IsHovered; + var hoveredColour = colourProvider.Light4; + var unhoveredColour = colourProvider.Background5; + + Colour = Color4.White; + Alpha = Enabled.Value ? 1 : 0.3f; + + if (SearchBar.State.Value == Visibility.Visible) + { + chevron.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White; + Background.Colour = unhoveredColour; + } + else + { + chevron.Colour = Color4.White; + Background.Colour = hovered ? hoveredColour : unhoveredColour; + } + } + + private void updateChevron() + { + Debug.Assert(Dropdown != null); + bool open = Dropdown.Menu.State == MenuState.Open; + chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); + } + + protected override DropdownSearchBar CreateSearchBar() => searchBar = new ShearedDropdownSearchBar(); + + private partial class ShearedDropdownSearchBar : DropdownSearchBar + { + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); + + protected override TextBox CreateTextBox() => new DropdownSearchTextBox + { + FontSize = OsuFont.Default.Size, + }; + + private partial class DropdownSearchTextBox : OsuTextBox + { + private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? colourProvider) + { + TextContainer.Shear = -shear; + BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + } + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + BorderThickness = 0; + } + } + } + } + } +} From 429819e68b07a54458b6e9bd3e6896f186d9fa63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Mar 2025 12:40:43 +0100 Subject: [PATCH 56/79] Adjust user activity updates - Changed copy of the multiplayer spectator activity to be ruleset-agnostic, thus I guess closing https://github.com/ppy/osu/issues/32307 maybe? There is still the ruleset icon in discord RPC but that one is taken from the game-global ruleset so it's a bit more involved to fix. - Added new activities for daily challenge screens, therefore partially addressing https://github.com/ppy/osu/discussions/29200. I didn't add skin editor because (a) it's not easy to make work because skin editor isn't a screen and (b) I'm not sure we want that to begin with? Kind of a weird one. I've tested backwards compatibility; old server will raise exceptions that then will be logged as unobserved by the clients when receiving the new statuses, and an old client, when given one of the new statuses by a new server, seems to completely ignore it. Seems pretty acceptable to me. --- osu.Game/Online/SignalRWorkaroundTypes.cs | 2 ++ .../DailyChallenge/DailyChallenge.cs | 5 +++- .../DailyChallenge/DailyChallengePlayer.cs | 20 +++++++++++++ osu.Game/Users/UserActivity.cs | 30 ++++++++++++++++++- 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 59a12b3bf1..757bb07ec8 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -44,6 +44,8 @@ namespace osu.Game.Online (typeof(UserActivity.EditingBeatmap), typeof(UserActivity)), (typeof(UserActivity.ModdingBeatmap), typeof(UserActivity)), (typeof(UserActivity.TestingBeatmap), typeof(UserActivity)), + (typeof(UserActivity.InDailyChallengeLobby), typeof(UserActivity)), + (typeof(UserActivity.PlayingDailyChallenge), typeof(UserActivity)), }; } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 6de11ec34c..5c8b500c93 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -39,6 +39,7 @@ using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; +using osu.Game.Users; using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge @@ -107,6 +108,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public override bool? ApplyModTrackAdjustments => true; + protected override UserActivity InitialActivity => new UserActivity.InDailyChallengeLobby(); + public DailyChallenge(Room room) { this.room = room; @@ -526,7 +529,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void startPlay() { sampleStart?.Play(); - this.Push(new PlayerLoader(() => new PlaylistsPlayer(room, playlistItem) + this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, playlistItem) { Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) })); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs new file mode 100644 index 0000000000..a5c61b8386 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Screens.Play; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengePlayer : PlaylistsPlayer + { + protected override UserActivity InitialActivity => new UserActivity.PlayingDailyChallenge(Beatmap.Value.BeatmapInfo, Ruleset.Value); + + public DailyChallengePlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration? configuration = null) + : base(room, playlistItem, configuration) + { + } + } +} diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index a792424562..16b30546de 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -34,6 +34,8 @@ namespace osu.Game.Users [Union(41, typeof(EditingBeatmap))] [Union(42, typeof(ModdingBeatmap))] [Union(43, typeof(TestingBeatmap))] + [Union(51, typeof(InDailyChallengeLobby))] + [Union(52, typeof(PlayingDailyChallenge))] public abstract class UserActivity { public abstract string GetStatus(bool hideIdentifiableInformation = false); @@ -58,6 +60,7 @@ namespace osu.Game.Users [Union(23, typeof(InMultiplayerGame))] [Union(24, typeof(SpectatingMultiplayerGame))] [Union(31, typeof(InPlaylistGame))] + [Union(52, typeof(PlayingDailyChallenge))] public abstract class InGame : UserActivity { [Key(0)] @@ -244,7 +247,7 @@ namespace osu.Game.Users [SerializationConstructor] public SpectatingMultiplayerGame() { } - public override string GetStatus(bool hideIdentifiableInformation = false) => $"Watching others {base.GetStatus(hideIdentifiableInformation).ToLowerInvariant()}"; + public override string GetStatus(bool hideIdentifiableInformation = false) => @"Spectating a multiplayer game"; } [MessagePackObject] @@ -277,5 +280,30 @@ namespace osu.Game.Users ? null : RoomName; } + + [MessagePackObject] + public class InDailyChallengeLobby : UserActivity + { + [SerializationConstructor] + public InDailyChallengeLobby() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => @"In daily challenge lobby"; + } + + [MessagePackObject] + public class PlayingDailyChallenge : InGame + { + public PlayingDailyChallenge(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + [SerializationConstructor] + public PlayingDailyChallenge() + { + } + + public override string GetStatus(bool hideIdentifiableInformation = false) => @$"{RulesetPlayingVerb} in daily challenge"; + } } } From 6e635f124aee13d3d95d26ba10a08c321360ceb7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 13:41:18 -0400 Subject: [PATCH 57/79] Change `SettingDescription` to differentiate between settings and values --- .../Mods/CatchModDifficultyAdjust.cs | 25 ++++++++------- .../Mods/OsuModDifficultyAdjust.cs | 19 ++++++------ .../Mods/TaikoModDifficultyAdjust.cs | 15 +++++---- .../Leaderboards/LeaderboardScoreTooltip.cs | 22 ++++++++----- osu.Game/Rulesets/Mods/Mod.cs | 31 +++---------------- .../Rulesets/Mods/ModAccuracyChallenge.cs | 18 +++++++++-- osu.Game/Rulesets/Mods/ModBarrelRoll.cs | 10 +++++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 15 ++++----- .../Rulesets/Mods/ModEasyWithExtraLives.cs | 12 ++++++- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 13 ++++++-- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 10 +++++- 11 files changed, 108 insertions(+), 82 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 6efb415880..1312f45cdc 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -1,8 +1,9 @@ // 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 System.Collections.Generic; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Beatmaps; @@ -35,21 +36,21 @@ 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 string SettingDescription + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}"; - string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns"; + if (!CircleSize.IsDefault) + yield return ("Circle size", $"{CircleSize.Value:N1}"); - return string.Join(", ", new[] - { - circleSize, - base.SettingDescription, - approachRate, - spicyPatterns, - }.Where(s => !string.IsNullOrEmpty(s))); + foreach (var setting in base.SettingDescription) + yield return setting; + + if (!ApproachRate.IsDefault) + yield return ("Approach rate", $"{ApproachRate.Value:N1}"); + + if (!HardRockOffsets.IsDefault) + yield return ("Spicy patterns", "On"); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 10282ff988..77e9aeb123 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -1,7 +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.Linq; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -36,19 +36,18 @@ namespace osu.Game.Rulesets.Osu.Mods ReadCurrentFromDifficulty = diff => diff.ApproachRate, }; - public override string SettingDescription + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}"; + if (!CircleSize.IsDefault) + yield return ("Circle size", $"{CircleSize.Value:N1}"); - return string.Join(", ", new[] - { - circleSize, - base.SettingDescription, - approachRate - }.Where(s => !string.IsNullOrEmpty(s))); + foreach (var setting in base.SettingDescription) + yield return setting; + + if (!ApproachRate.IsDefault) + yield return ("Approach rate", $"{ApproachRate.Value:N1}"); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 99a064d35f..000736e9f7 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -1,7 +1,8 @@ // 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 System.Collections.Generic; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -19,17 +20,15 @@ namespace osu.Game.Rulesets.Taiko.Mods ReadCurrentFromDifficulty = _ => 1, }; - public override string SettingDescription + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get { - string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}"; + foreach (var setting in base.SettingDescription) + yield return setting; - return string.Join(", ", new[] - { - base.SettingDescription, - scrollSpeed - }.Where(s => !string.IsNullOrEmpty(s))); + if (!ScrollSpeed.IsDefault) + yield return ("Scroll speed", $"x{ScrollSpeed.Value:N2}"); } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index e79aff9e81..ee497bf3fd 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.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.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Containers; @@ -219,15 +220,20 @@ namespace osu.Game.Online.Leaderboards } }; - container.Add(new OsuSpriteText + string description = string.Join(", ", mod.SettingDescription.Select(svp => $"{svp.setting}: {svp.value}")); + + if (!string.IsNullOrEmpty(description)) { - RelativeSizeAxes = Axes.Y, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = mod.IconTooltip, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Top = 1 }, - }); + container.Add(new OsuSpriteText + { + RelativeSizeAxes = Axes.Y, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = description, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Top = 1 }, + }); + } } } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 1b21216235..f23f16fd44 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Extensions; -using osu.Game.Rulesets.UI; using osu.Game.Utils; namespace osu.Game.Rulesets.Mods @@ -43,36 +42,16 @@ namespace osu.Game.Rulesets.Mods public abstract LocalisableString Description { get; } /// - /// The tooltip to display for this mod when used in a . - /// - /// - /// Differs from , as the value of attributes (AR, CS, etc) changeable via the mod - /// are displayed in the tooltip. - /// - [JsonIgnore] - public string IconTooltip - { - get - { - string description = SettingDescription; - - return string.IsNullOrEmpty(description) ? Name : $"{Name} ({description})"; - } - } - - /// - /// The description of editable settings of a mod to use in the . + /// The description of editable settings of a mod. /// /// /// Parentheses are added to the tooltip, surrounding the value of this property. If this property is string.Empty, /// the tooltip will not have parentheses. /// - public virtual string SettingDescription + public virtual IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get { - var tooltipTexts = new List(); - foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { var bindable = (IBindable)property.GetValue(this)!; @@ -82,7 +61,7 @@ namespace osu.Game.Rulesets.Mods switch (bindable) { case Bindable b: - valueText = b.Value ? "on" : "off"; + valueText = b.Value ? "On" : "Off"; break; default: @@ -91,10 +70,8 @@ namespace osu.Game.Rulesets.Mods } if (!bindable.IsDefault) - tooltipTexts.Add($"{attr.Label}: {valueText}"); + yield return (attr.Label, valueText); } - - return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index 9570cddb0a..db16e771d3 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Globalization; +using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Localisation.HUD; @@ -33,7 +34,20 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; - public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo)); + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + if (!MinimumAccuracy.IsDefault) + yield return ("Minimum accuracy", $"{MinimumAccuracy.Value:##%}"); + + if (!AccuracyJudgeMode.IsDefault) + yield return ("Accuracy mode", AccuracyJudgeMode.Value.ToLocalisableString()); + + if (!Restart.IsDefault) + yield return ("Restart on fail", "On"); + } + } [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider))] public BindableNumber MinimumAccuracy { get; } = new BindableDouble diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 67f9da37be..ceaa9aa6e5 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -38,7 +39,14 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "The whole playfield is on a wheel!"; public override double ScoreMultiplier => 1; - public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}"; + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); + yield return ("Direction", Direction.Value.GetDescription()); + } + } private PlayfieldAdjustmentContainer playfieldAdjustmentContainer = null!; diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index f4c6be4f77..cdde1b73b6 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -65,18 +65,15 @@ namespace osu.Game.Rulesets.Mods } } - public override string SettingDescription + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get { - string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:N1}"; - string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:N1}"; + if (!DrainRate.IsDefault) + yield return ("HP drain", $"{DrainRate.Value:N1}"); - return string.Join(", ", new[] - { - drainRate, - overallDifficulty - }.Where(s => !string.IsNullOrEmpty(s))); + if (!OverallDifficulty.IsDefault) + yield return ("Accuracy", $"{OverallDifficulty.Value:N1}"); } } diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index e101ac440e..1a2cb08a53 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using Humanizer; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; @@ -20,7 +22,15 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; - public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + if (!Retries.IsDefault) + yield return ("Extra lives", "lives".ToQuantity(Retries.Value)); + } + } + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray(); private int retries; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index e5af758b4f..358034541c 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { @@ -24,8 +26,13 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp), typeof(ModAdaptiveSpeed), typeof(ModRateAdjust) }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; - - public override string ExtendedIconInformation => SettingDescription; + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + if (!SpeedChange.IsDefault) + yield return ("Speed change", $"{SpeedChange.Value:N2}x"); + } + } } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 36e4522771..fd85709b52 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Overlays.Settings; @@ -34,7 +36,13 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; - public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); + } + } private double finalRateTime; private double beginRampTime; From 9b55325526b862273eb1bde535f70a7892c90914 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 13:43:21 -0400 Subject: [PATCH 58/79] Introduce rich mod tooltip --- osu.Game/Rulesets/UI/ModIcon.cs | 12 +- osu.Game/Rulesets/UI/ModTooltip.cs | 141 ++++++++++++++++++ .../SelectV2/Footer/ScreenFooterButtonMods.cs | 6 +- .../Leaderboards/LeaderboardScoreV2.cs | 13 +- 4 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Rulesets/UI/ModTooltip.cs diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 6abc7355d5..ee0103a8e5 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -10,11 +10,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.UI /// /// Display the specified mod at a fixed size. /// - public partial class ModIcon : Container, IHasTooltip + public partial class ModIcon : Container, IHasCustomTooltip { public readonly BindableBool Selected = new BindableBool(); @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.UI public static readonly Vector2 MOD_ICON_SIZE = new Vector2(80); - public virtual LocalisableString TooltipText => showTooltip ? ((mod as Mod)?.IconTooltip ?? mod.Name) : string.Empty; + public Mod? TooltipContent { get; private set; } private IMod mod; @@ -70,6 +70,9 @@ namespace osu.Game.Rulesets.UI [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } + private Color4 backgroundColour; private Sprite extendedBackground = null!; @@ -188,6 +191,7 @@ namespace osu.Game.Rulesets.UI modAcronym.Text = value.Acronym; modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question; + TooltipContent = showTooltip ? value as Mod : null; if (value.Icon == null) { @@ -227,5 +231,7 @@ namespace osu.Game.Rulesets.UI base.Dispose(isDisposing); modSettingsChangeTracker?.Dispose(); } + + public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); } } diff --git a/osu.Game/Rulesets/UI/ModTooltip.cs b/osu.Game/Rulesets/UI/ModTooltip.cs new file mode 100644 index 0000000000..07bb30e15a --- /dev/null +++ b/osu.Game/Rulesets/UI/ModTooltip.cs @@ -0,0 +1,141 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.UI +{ + public partial class ModTooltip : VisibilityContainer, ITooltip + { + private readonly OverlayColourProvider colourProvider; + + private OsuSpriteText nameText = null!; + private TextFlowContainer settingsLabelsFlow = null!; + private TextFlowContainer settingsValuesFlow = null!; + + public ModTooltip(OverlayColourProvider? colourProvider = null) + { + this.colourProvider = colourProvider ?? new OverlayColourProvider(OverlayColourScheme.Aquamarine); + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + CornerRadius = 7; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.2f), + Radius = 10f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding(10f), + Spacing = new Vector2(20f, 0f), + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + nameText = new OsuSpriteText + { + Font = OsuFont.Torus.With(size: 16f, weight: FontWeight.SemiBold), + Colour = colourProvider.Content1, + UseFullGlyphHeight = false, + }, + settingsLabelsFlow = new TextFlowContainer(t => + { + t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold); + }) + { + AutoSizeAxes = Axes.Both, + Colour = colourProvider.Content2, + }, + }, + }, + settingsValuesFlow = new TextFlowContainer(t => + { + t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold); + }) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Colour = colourProvider.Content1, + TextAnchor = Anchor.TopRight, + }, + }, + } + }; + } + + private Mod? displayedContent; + + public void SetContent(Mod content) + { + if (content == displayedContent) + return; + + displayedContent = content; + nameText.Text = content.Name; + settingsLabelsFlow.Clear(); + settingsValuesFlow.Clear(); + + if (content.SettingDescription.Any()) + { + settingsLabelsFlow.Show(); + settingsValuesFlow.Show(); + + foreach (var part in content.SettingDescription) + { + settingsLabelsFlow.AddText(part.setting); + settingsLabelsFlow.NewLine(); + + settingsValuesFlow.AddText(part.value); + settingsValuesFlow.NewLine(); + } + } + else + { + settingsLabelsFlow.Hide(); + settingsValuesFlow.Hide(); + } + } + + 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; + } +} diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 0992203dbc..869aef1470 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -244,18 +244,18 @@ namespace osu.Game.Screens.SelectV2.Footer Mods.BindValueChanged(v => Text = ModSelectOverlayStrings.Mods(v.NewValue.Count).ToUpper(), true); } - public ITooltip> GetCustomTooltip() => new ModTooltip(colourProvider); + public ITooltip> GetCustomTooltip() => new ModOverflowTooltip(colourProvider); public IReadOnlyList? TooltipContent => Mods.Value; - public partial class ModTooltip : VisibilityContainer, ITooltip> + public partial class ModOverflowTooltip : VisibilityContainer, ITooltip> { private ModDisplay extendedModDisplay = null!; [Cached] private OverlayColourProvider colourProvider; - public ModTooltip(OverlayColourProvider colourProvider) + public ModOverflowTooltip(OverlayColourProvider colourProvider) { this.colourProvider = colourProvider; } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index b54f007f38..16599a2080 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -715,18 +715,21 @@ namespace osu.Game.Screens.SelectV2.Leaderboards public LocalisableString TooltipText { get; } } - private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasTooltip + private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasCustomTooltip { - private readonly IMod mod; + public Mod? TooltipContent { get; } - public ColouredModSwitchTiny(IMod mod) + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public ColouredModSwitchTiny(Mod mod) : base(mod) { - this.mod = mod; + TooltipContent = mod; Active.Value = true; } - public LocalisableString TooltipText => (mod as Mod)?.IconTooltip ?? mod.Name; + public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); } private sealed partial class MoreModSwitchTiny : CompositeDrawable From 623e705704148d66ed31cc936366c8ff3cdb6b0c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 13:43:38 -0400 Subject: [PATCH 59/79] Update mod preset tooltip design accordingly --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 29 ++++++-- osu.Game/Overlays/Mods/ModPresetRow.cs | 75 ++++++++++++++++----- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 16 ++++- 3 files changed, 94 insertions(+), 26 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 526ab6fc63..8014126942 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -8,6 +8,7 @@ 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.Input.Events; using osu.Game.Database; using osu.Game.Graphics; @@ -75,17 +76,31 @@ namespace osu.Game.Overlays.Mods TabbableContentContainer = this, Current = { Value = preset.PerformRead(p => p.Description) }, }, - new OsuScrollContainer + new Container { RelativeSizeAxes = Axes.X, Height = 100, - Padding = new MarginPadding(7), - Child = scrollContent = new FillFlowContainer + CornerRadius = 10, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(7), - Spacing = new Vector2(7), + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(7), + Child = scrollContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(7), + Spacing = new Vector2(7), + } + }, } }, new FillFlowContainer diff --git a/osu.Game/Overlays/Mods/ModPresetRow.cs b/osu.Game/Overlays/Mods/ModPresetRow.cs index 4829e93b87..4f001eba9b 100644 --- a/osu.Game/Overlays/Mods/ModPresetRow.cs +++ b/osu.Game/Overlays/Mods/ModPresetRow.cs @@ -1,10 +1,11 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -14,12 +15,20 @@ namespace osu.Game.Overlays.Mods { public partial class ModPresetRow : FillFlowContainer { + private readonly Mod mod; + public ModPresetRow(Mod mod) + { + this.mod = mod; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; - Spacing = new Vector2(4); + Spacing = new Vector2(5); InternalChildren = new Drawable[] { new FillFlowContainer @@ -39,26 +48,58 @@ namespace osu.Game.Overlays.Mods }, new OsuSpriteText { - Text = mod.Name, - Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Bottom = 2 } - } + Origin = Anchor.CentreLeft, + Font = OsuFont.Torus.With(size: 16f, weight: FontWeight.SemiBold), + Colour = colourProvider.Content1, + UseFullGlyphHeight = false, + Text = mod.Name, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10f }, + Alpha = mod.SettingDescription.Any() ? 1 : 0, + Children = new Drawable[] + { + new TextFlowContainer(t => + { + t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold); + }) + { + AutoSizeAxes = Axes.Both, + Colour = colourProvider.Content2, + Text = string.Join('\n', mod.SettingDescription.Select(svp => svp.setting)), + }, + new TextFlowContainer(t => + { + t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold); + }) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Colour = colourProvider.Content1, + TextAnchor = Anchor.TopRight, + Text = string.Join('\n', mod.SettingDescription.Select(svp => svp.value)), + }, } } }; - if (!string.IsNullOrEmpty(mod.SettingDescription)) - { - AddInternal(new OsuTextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 14 }, - Text = mod.SettingDescription - }); - } + // if (!string.IsNullOrEmpty(mod.SettingDescription)) + // { + // AddInternal(new OsuTextFlowContainer + // { + // RelativeSizeAxes = Axes.X, + // AutoSizeAxes = Axes.Y, + // Padding = new MarginPadding { Left = 14 }, + // // Text = mod.SettingDescription + // }); + // } } } } diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 6ffcfca1e0..6204d75057 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -14,6 +15,9 @@ namespace osu.Game.Overlays.Mods { public partial class ModPresetTooltip : VisibilityContainer, ITooltip { + [Cached] + private readonly OverlayColourProvider colourProvider; + protected override Container Content { get; } private const double transition_duration = 200; @@ -22,6 +26,8 @@ namespace osu.Game.Overlays.Mods public ModPresetTooltip(OverlayColourProvider colourProvider) { + this.colourProvider = colourProvider; + Width = 250; AutoSizeAxes = Axes.Y; @@ -39,7 +45,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 }, + Padding = new MarginPadding(10f), Spacing = new Vector2(7), Children = new[] { @@ -64,7 +70,13 @@ namespace osu.Game.Overlays.Mods if (ReferenceEquals(preset, lastPreset)) return; - descriptionText.Text = preset.Description; + if (!string.IsNullOrEmpty(preset.Description)) + { + descriptionText.Show(); + descriptionText.Text = preset.Description; + } + else + descriptionText.Hide(); lastPreset = preset; From 54eb0b33b04ef41c4128362c379b1c23f92c95fe Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 13:46:19 -0400 Subject: [PATCH 60/79] Add extra margin below description text --- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 6204d75057..4464ba22f1 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -57,6 +57,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 5f }, } } } From b3d572029f44c1cc9b3cb2182356b954e2715d7f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 21:38:35 -0400 Subject: [PATCH 61/79] Add failing test case --- .../Visual/Online/TestSceneImageProxying.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs index 17b437a051..3d7ee137ba 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Game.Graphics.Containers.Markdown; +using osu.Game.Overlays.Comments; namespace osu.Game.Tests.Visual.Online { @@ -34,5 +35,18 @@ namespace osu.Game.Tests.Visual.Online }); AddUntilStep("image loaded", () => markdown.ChildrenOfType().SingleOrDefault()?.Texture != null); } + + [Test] + public void TestExternalImageLinkInComments() + { + MarkdownContainer markdown = null!; + + AddStep("load external with proxying", () => Child = markdown = new CommentMarkdownContainer + { + RelativeSizeAxes = Axes.Both, + Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", + }); + AddUntilStep("image loaded", () => markdown.ChildrenOfType().SingleOrDefault()?.Texture != null); + } } } From 655c799c589ee95f177f10e3c9cdcbd7ac987193 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 21:32:33 -0400 Subject: [PATCH 62/79] Fix images embedded in comments not displaying --- osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs index ff7df18f00..340e59dd91 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs @@ -13,12 +13,9 @@ namespace osu.Game.Graphics.Containers.Markdown public LocalisableString TooltipText { get; } public OsuMarkdownImage(LinkInline linkInline) - : base(linkInline.Url) + : base($"https://osu.ppy.sh/media-url?url={linkInline.Url}") { TooltipText = linkInline.Title; } - - protected override ImageContainer CreateImageContainer(string url) - => base.CreateImageContainer($@"https://osu.ppy.sh/media-url?url={url}"); } } From b2c9a572fa30ec8a1a4139ad4cba1a10feafc5ec Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 22:26:48 -0400 Subject: [PATCH 63/79] Apply better fix for background bleeding Co-authored-by: Joseph Madamba --- .../UserInterfaceV2/ShearedDropdown.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index deb55daab4..11686a7af6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -76,6 +76,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 public partial class ShearedDropdownHeader : DropdownHeader { + private const float corner_radius = 5f; + private LocalisableString label; protected override LocalisableString Label @@ -111,7 +113,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdownHeader() { Shear = shear; - CornerRadius = 5f; + CornerRadius = corner_radius; Masking = true; Foreground.Children = new Drawable[] @@ -132,24 +134,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 { LabelContainer = new Container { + CornerRadius = corner_radius, + Masking = true, AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new Container + labelBox = new Box { - RelativeSizeAxes = Axes.Both, - // required to fix colour bleeding around the edges of the dropdown on hover - Padding = new MarginPadding { Vertical = -1f, Left = -1f }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - CornerRadius = 5f, - Masking = true, - Child = labelBox = new Box - { - RelativeSizeAxes = Axes.Both, - } - }, + RelativeSizeAxes = Axes.Both }, labelText = new OsuSpriteText { @@ -215,6 +207,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 { base.Update(); searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth }; + + // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. + Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - corner_radius }; } protected override bool OnHover(HoverEvent e) From 4cd631f9f905fae10578cca9be9e73392d2f7835 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 24 Mar 2025 02:17:54 -0400 Subject: [PATCH 64/79] Apply shearing to dropdown menu as well Co-authored-by: Dean Herbert --- .../UserInterfaceV2/ShearedDropdown.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index 11686a7af6..0b9c5f294c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -43,7 +43,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 var header = (ShearedDropdownHeader)Header; var menu = (ShearedDropdownMenu)Menu; - menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 15f }; + menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 10f, Right = 6f }; } public bool OnPressed(KeyBindingPressEvent e) @@ -62,6 +62,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu { + private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); + public new MarginPadding Padding { get => base.Padding; @@ -70,8 +72,26 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdownMenu() { + Shear = shear; Margin = new MarginPadding { Top = 5f }; } + + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item) + { + BackgroundColourHover = HoverColour, + BackgroundColourSelected = SelectionColour + }; + + public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem + { + private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); + + public ShearedMenuItem(MenuItem item) + : base(item) + { + Foreground.Shear = -shear; + } + } } public partial class ShearedDropdownHeader : DropdownHeader From bb998ad687279aee44a1c4e1fb09d665655e418c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Mar 2025 16:14:32 +0900 Subject: [PATCH 65/79] Fix taiko legacy skins playing scale animations even when skins contain animations Closes https://github.com/ppy/osu/issues/32477. Can be tested using https://drive.google.com/drive/folders/1HUCsTL_iqCGPCyruSSfdSoxL19EQuy_a. --- .../Skinning/Legacy/LegacyHitExplosion.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs index b9a432f3bd..b67648a3b8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs @@ -60,11 +60,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { const double animation_time = 120; - (sprite as IFramedAnimation)?.GotoFrame(0); + var animation = sprite as IFramedAnimation; + + animation?.GotoFrame(0); (strongSprite as IFramedAnimation)?.GotoFrame(0); this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5); + // legacy judgements don't play any transforms if they are an animation. + if (animation?.FrameCount > 1) + return; + this.ScaleTo(0.6f) .Then().ScaleTo(1.1f, animation_time * 0.8) .Then().ScaleTo(0.9f, animation_time * 0.4) From 303d0c56c32fc75e3fa3531ffeaaff0753f06190 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 24 Mar 2025 17:37:11 +0900 Subject: [PATCH 66/79] Validate freestyle selection post-selection --- .../MultiplayerMatchFreestyleSelect.cs | 3 ++ .../OnlinePlay/OnlinePlayFreestyleSelect.cs | 35 +++++++++++++++++++ .../Playlists/PlaylistsRoomFreestyleSelect.cs | 7 ++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs index 0c04c2712c..846f781cdc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs @@ -60,6 +60,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return false; } + if (!base.OnStart()) + return false; + selectionOperation = operationTracker.BeginOperation(); client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs index 4844d096ce..66218c0e9e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs @@ -7,6 +7,7 @@ using Humanizer; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.Rooms; @@ -43,6 +44,40 @@ namespace osu.Game.Screens.OnlinePlay LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; } + protected override bool OnStart() + { + FilterCriteria criteria = FilterControl.CreateCriteria(); + + // Beatmaps with too different of a duration are filtered away; this is just a final safety. + if (!criteria.Length.IsInRange(Beatmap.Value.BeatmapInfo.Length)) + { + Logger.Log("The selected beatmap's duration differs too much from the host's selection.", level: LogLevel.Error); + return false; + } + + // Beatmaps without a valid online ID are filtered away; this is just a final safety. + if (Beatmap.Value.BeatmapInfo.OnlineID < 0) + { + Logger.Log("The selected beatmap is not available online.", level: LogLevel.Error); + return false; + } + + // Beatmaps from different sets are filtered away; this is just a final safety. + if (Beatmap.Value.BeatmapSetInfo.OnlineID != criteria.BeatmapSetId) + { + Logger.Log("The selected beatmap is from a different beatmap set.", level: LogLevel.Error); + return false; + } + + if (Ruleset.Value.OnlineID < 0) + { + Logger.Log("The selected ruleset is not available online.", level: LogLevel.Error); + return false; + } + + return true; + } + protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs index 9c85088cc9..1f0f92aea2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs @@ -21,15 +21,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override bool OnStart() { - // Beatmaps without a valid online ID are filtered away; this is just a final safety. - if (base.Beatmap.Value.BeatmapInfo.OnlineID < 0) - return false; - - if (base.Ruleset.Value.OnlineID < 0) + if (!base.OnStart()) return false; Beatmap.Value = base.Beatmap.Value.BeatmapInfo; Ruleset.Value = base.Ruleset.Value; + this.Exit(); return true; } From 130d0c8b70ade7d2eaa12a1b19990651bc2d0514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Mar 2025 12:28:40 +0100 Subject: [PATCH 67/79] Fix code quality --- .../Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs index ca1685e921..542a43be7d 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; -using osu.Game.Utils; namespace osu.Game.Screens.Ranking.Statistics.User { @@ -19,7 +18,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User protected override LocalisableString Label => UsersStrings.ShowRankGlobalSimple; protected override LocalisableString FormatCurrentValue(int? current) - => current == null ? string.Empty : current.Value.ToLocalisableString(@"N0"); + => current?.ToLocalisableString(@"N0") ?? string.Empty; protected override int CalculateDifference(int? previous, int? current, out LocalisableString formattedDifference) { From f6dd2e4ac278202636c77beebbc5b02a70125ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Mar 2025 12:28:46 +0100 Subject: [PATCH 68/79] Add extra test coverage --- .../Visual/Ranking/TestSceneOverallRanking.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs index b406ea369f..e29063b5f8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs @@ -104,6 +104,40 @@ namespace osu.Game.Tests.Visual.Ranking displayUpdate(statistics, statistics); } + [Test] + public void TestFromNothing() + { + createDisplay(); + displayUpdate( + new UserStatistics(), + new UserStatistics + { + GlobalRank = 12_345, + Accuracy = 98.99, + MaxCombo = 2_322, + RankedScore = 23_123_543_456, + TotalScore = 123_123_543_456, + PP = 5_072 + }); + } + + [Test] + public void TestToNothing() + { + createDisplay(); + displayUpdate( + new UserStatistics + { + GlobalRank = 12_345, + Accuracy = 98.99, + MaxCombo = 2_322, + RankedScore = 23_123_543_456, + TotalScore = 123_123_543_456, + PP = 5_072 + }, + new UserStatistics()); + } + private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking { Width = 400, From 334b1e85bc80dee75d64bb37f88f58c4c446a197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Mar 2025 12:30:03 +0100 Subject: [PATCH 69/79] Fix inconsistent formatting --- .../Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs index 542a43be7d..5ffea094cd 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs @@ -30,13 +30,13 @@ namespace osu.Game.Screens.Ranking.Statistics.User if (previous == null && current != null) { - formattedDifference = LocalisableString.Interpolate($"+{current.Value.ToString()}"); + formattedDifference = LocalisableString.Interpolate($"+{current.Value:N0}"); return 1; } if (previous != null && current == null) { - formattedDifference = LocalisableString.Interpolate($"-{previous.Value.ToString()}"); + formattedDifference = LocalisableString.Interpolate($"-{previous.Value:N0}"); return -1; } @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User if (difference < 0) formattedDifference = difference.ToLocalisableString(@"N0"); else if (difference > 0) - formattedDifference = LocalisableString.Interpolate($"+{difference.ToLocalisableString(@"N0")}"); + formattedDifference = LocalisableString.Interpolate($"+{difference:N0}"); else formattedDifference = string.Empty; From 9630002a68908225a0854a26e3cd2421d5271cac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Mar 2025 14:27:24 +0900 Subject: [PATCH 70/79] Fix overlapping placeholders in beatmap info leaderboard Closes https://github.com/ppy/osu/issues/32508. --- osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 9b9661f83d..cc06383274 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -249,6 +249,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores getScoresRequest = null; noScoresPlaceholder.Hide(); + noTeamPlaceholder.Hide(); + notSupporterPlaceholder.Hide(); if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.Status <= BeatmapOnlineStatus.Pending)) { @@ -271,9 +273,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; } - noTeamPlaceholder.Hide(); - notSupporterPlaceholder.Hide(); - Show(); loading.Show(); From 0d8f328fe6201b360a05951933ccaf5264dcd171 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Mar 2025 14:48:03 +0900 Subject: [PATCH 71/79] Fix christmas menu track potentially playing out of season Closes https://github.com/ppy/osu/issues/32502. --- osu.Game/Overlays/MusicController.cs | 32 +++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 87920fdf55..328a9b1d3e 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -20,6 +20,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets.Mods; +using osu.Game.Seasonal; namespace osu.Game.Overlays { @@ -256,8 +257,10 @@ namespace osu.Game.Overlays playableSet = getNextRandom(-1, allowProtectedTracks); else { - playableSet = getBeatmapSets().TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Value.Protected || allowProtectedTracks) - ?? getBeatmapSets().LastOrDefault(s => !s.Value.Protected || allowProtectedTracks); + var beatmapSets = getBeatmapSets(allowProtectedTracks); + + playableSet = beatmapSets.TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Value.Protected || allowProtectedTracks) + ?? beatmapSets.LastOrDefault(s => !s.Value.Protected || allowProtectedTracks); } if (playableSet != null) @@ -352,10 +355,9 @@ namespace osu.Game.Overlays playableSet = getNextRandom(1, allowProtectedTracks); else { - playableSet = getBeatmapSets().SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)) - .Where(i => !i.Value.Protected || allowProtectedTracks) - .ElementAtOrDefault(1) - ?? getBeatmapSets().FirstOrDefault(i => !i.Value.Protected || allowProtectedTracks); + var beatmapSets = getBeatmapSets(allowProtectedTracks); + + playableSet = beatmapSets.SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).ElementAtOrDefault(1) ?? beatmapSets.FirstOrDefault(); } var playableBeatmap = playableSet?.Value.Beatmaps.FirstOrDefault(); @@ -376,12 +378,13 @@ namespace osu.Game.Overlays { Live result; - var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToList(); + var possibleSets = getBeatmapSets(allowProtectedTracks).ToList(); if (possibleSets.Count == 0) return null; - // if there is only one possible set left, play it, even if it is the same as the current track. + // if there is only + // one possible set left, play it, even if it is the same as the current track. // looping is preferable over playing nothing. if (possibleSets.Count == 1) return possibleSets.Single(); @@ -459,9 +462,12 @@ namespace osu.Game.Overlays private TrackChangeDirection? queuedDirection; - private IEnumerable> getBeatmapSets() => realm.Realm.All().Where(s => !s.DeletePending) - .AsEnumerable() - .Select(s => new RealmLive(s, realm)); + private IEnumerable> getBeatmapSets(bool allowProtectedTracks) => + realm.Realm.All().Where(s => !s.DeletePending) + .AsEnumerable() + .Select(s => new RealmLive(s, realm)) + .Where(i => (allowProtectedTracks || !i.Value.Protected) + && (SeasonalUIConfig.ENABLED || i.Value.Hash != IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH)); private void changeBeatmap(WorkingBeatmap newWorking) { @@ -488,8 +494,8 @@ namespace osu.Game.Overlays else { // figure out the best direction based on order in playlist. - int last = getBeatmapSets().TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count(); - int next = getBeatmapSets().TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count(); + int last = getBeatmapSets(allowProtectedTracks: false).TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count(); + int next = getBeatmapSets(allowProtectedTracks: false).TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count(); direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } From 1bb85bab89c89a0e5bb2ce3a7cdce0d6e5d24a04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Mar 2025 16:34:42 +0900 Subject: [PATCH 72/79] Remove pointless left-over code --- osu.Game/Overlays/Mods/ModPresetRow.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetRow.cs b/osu.Game/Overlays/Mods/ModPresetRow.cs index 4f001eba9b..408c541bf5 100644 --- a/osu.Game/Overlays/Mods/ModPresetRow.cs +++ b/osu.Game/Overlays/Mods/ModPresetRow.cs @@ -89,17 +89,6 @@ namespace osu.Game.Overlays.Mods } } }; - - // if (!string.IsNullOrEmpty(mod.SettingDescription)) - // { - // AddInternal(new OsuTextFlowContainer - // { - // RelativeSizeAxes = Axes.X, - // AutoSizeAxes = Axes.Y, - // Padding = new MarginPadding { Left = 14 }, - // // Text = mod.SettingDescription - // }); - // } } } } From 68cdb60bee1b9b27c6e911377fe57c92181797d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Mar 2025 17:13:13 +0900 Subject: [PATCH 73/79] Remove unnecessary conditionals --- osu.Game/Overlays/MusicController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 328a9b1d3e..6d7c26b51b 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -259,8 +259,8 @@ namespace osu.Game.Overlays { var beatmapSets = getBeatmapSets(allowProtectedTracks); - playableSet = beatmapSets.TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Value.Protected || allowProtectedTracks) - ?? beatmapSets.LastOrDefault(s => !s.Value.Protected || allowProtectedTracks); + playableSet = beatmapSets.TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault() + ?? beatmapSets.LastOrDefault(); } if (playableSet != null) From 31487545d0d17c4337d4b4cc5d4afb3ba1dae838 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Mar 2025 17:21:35 +0900 Subject: [PATCH 74/79] Reduce spacing in new beatmap carousel --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 1c1f6fa7fb..994b0fb6c0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.SelectV2 { public Action? RequestPresentBeatmap { private get; init; } - public const float SPACING = 5f; + public const float SPACING = 3f; private IBindableList detachedBeatmaps = null!; From d908464f990cc8aac7466f875e8d22d8a3964051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Mar 2025 13:19:17 +0100 Subject: [PATCH 75/79] Fix menu star fountains getting stuck looping sounds when leaving menu - Alternative to / closes https://github.com/ppy/osu/pull/32565 - Closes https://github.com/ppy/osu/issues/32516 --- osu.Game/Screens/Menu/MainMenu.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 135b3dba17..7d792a6bb8 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -20,6 +20,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -45,7 +46,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Menu { - public partial class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler + public partial class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler, ISamplePlaybackDisabler { public const float FADE_IN_DURATION = 300; @@ -84,6 +85,10 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } + // used to stop kiai fountain samples when navigating to other screens + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; + private readonly Bindable samplePlaybackDisabled = new Bindable(); + protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override bool PlayExitSound => false; @@ -369,6 +374,8 @@ namespace osu.Game.Screens.Menu supporterDisplay .FadeOut(500, Easing.OutQuint); + + samplePlaybackDisabled.Value = true; } public override void OnResuming(ScreenTransitionEvent e) @@ -389,6 +396,8 @@ namespace osu.Game.Screens.Menu bottomElementsFlow .ScaleTo(1, 1000, Easing.OutQuint) .FadeIn(1000, Easing.OutQuint); + + samplePlaybackDisabled.Value = false; } public override bool OnExiting(ScreenExitEvent e) From 40d20f4e1084480a35a60792dd98fbb69b84b5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Mar 2025 14:16:26 +0100 Subject: [PATCH 76/79] Allow tagging already played beatmaps without playing another time Addresses https://github.com/ppy/osu/discussions/32568#discussioncomment-12610577. No changes in criteria (yet?), just allowing locally imported plays to count the same way as full beatmap completion does. The test scene is a bit rough / semi-manual but dealing with score imports is a bit of a pain in general. The way to semi-manually test with the test scene is to import a subset of scores, then recreate the statistics panel, and observe behaviour. I'm not sure it's worth it to be putting subscriptions in there, so the full recreation of the panel is necessary. --- .../Ranking/TestSceneStatisticsPanel.cs | 135 +++++++++++++++--- .../Ranking/Statistics/StatisticsPanel.cs | 27 +++- 2 files changed, 141 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index f82b32167c..1749ea38b5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -8,11 +8,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -27,6 +31,7 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; @@ -43,6 +48,22 @@ namespace osu.Game.Tests.Visual.Ranking { private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + private ScoreManager scoreManager = null!; + private RulesetStore rulesetStore = null!; + private BeatmapManager beatmapManager = 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(Realm); + + return dependencies; + } + [Test] public void TestScoreWithPositionStatistics() { @@ -163,6 +184,24 @@ namespace osu.Game.Tests.Visual.Ranking { var score = TestResources.CreateTestScoreInfo(); + setUpTaggingRequests(() => score.BeatmapInfo); + AddStep("load panel", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + AchievedScore = score, + } + }; + }); + } + + private void setUpTaggingRequests(Func beatmap) => AddStep("set up network requests", () => { dummyAPI.HandleRequest = request => @@ -176,7 +215,11 @@ namespace osu.Game.Tests.Visual.Ranking Tags = [ new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", }, - new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", }, + new APITag + { + Id = 2, Name = "alt", + Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", + }, new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", }, new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", }, ] @@ -186,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking case GetBeatmapSetRequest getBeatmapSetRequest: { - var beatmapSet = CreateAPIBeatmapSet(score.BeatmapInfo); + var beatmapSet = CreateAPIBeatmapSet(beatmap.Invoke()); beatmapSet.Beatmaps.Single().TopTags = [ new APIBeatmapTag { TagId = 3, VoteCount = 9 }, @@ -206,21 +249,6 @@ namespace osu.Game.Tests.Visual.Ranking return false; }; }); - AddStep("load panel", () => - { - Child = new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - Child = new StatisticsPanel - { - RelativeSizeAxes = Axes.Both, - State = { Value = Visibility.Visible }, - Score = { Value = score }, - AchievedScore = score, - } - }; - }); - } [Test] public void TestTaggingWhenRankTooLow() @@ -266,6 +294,79 @@ namespace osu.Game.Tests.Visual.Ranking }); } + [Test] + public void TestTaggingInteractionWithLocalScores() + { + BeatmapInfo beatmapInfo = null!; + string originalHash = string.Empty; + + AddStep(@"Import beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + }); + + AddStep("import bad score", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + score.BeatmapHash = beatmapInfo.Hash; + score.Ruleset = beatmapInfo.Ruleset; + score.Rank = ScoreRank.D; + score.User = API.LocalUser.Value; + scoreManager.Import(score); + }); + + AddStep("import score by another user", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + score.BeatmapHash = beatmapInfo.Hash; + score.Ruleset = beatmapInfo.Ruleset; + score.Rank = ScoreRank.D; + score.User = new APIUser { Username = "notme", Id = 5678 }; + scoreManager.Import(score); + }); + + AddStep("import convert score", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + score.BeatmapHash = beatmapInfo.Hash; + score.Ruleset = new OsuRuleset().RulesetInfo; + score.User = API.LocalUser.Value; + scoreManager.Import(score); + }); + + AddStep("import correct score", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + score.BeatmapHash = beatmapInfo.Hash; + score.Ruleset = beatmapInfo.Ruleset; + score.User = API.LocalUser.Value; + scoreManager.Import(score); + }); + + setUpTaggingRequests(() => beatmapInfo); + AddStep("load panel", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + } + }; + }); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new StatisticsPanel diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 9ead9ce91c..ad868e58f0 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -14,14 +14,18 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics.User; using osuTK; +using Realms; namespace osu.Game.Screens.Ranking.Statistics { @@ -43,6 +47,9 @@ namespace osu.Game.Screens.Ranking.Statistics [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; + [Resolved] + private RealmAccess realm { get; set; } = null!; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -231,17 +238,29 @@ namespace osu.Game.Screens.Ranking.Statistics }); } - if (AchievedScore != null - && newScore.BeatmapInfo!.OnlineID > 0 + if (newScore.BeatmapInfo!.OnlineID > 0 && api.IsLoggedIn) { string? preventTaggingReason = null; // We may want to iterate on the following conditions further in the future - if (AchievedScore.Ruleset.OnlineID != AchievedScore.BeatmapInfo!.Ruleset.OnlineID) + var localUserScore = AchievedScore ?? realm.Run(r => + r.All() + .Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $@" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, newScore.BeatmapInfo.ID, newScore.BeatmapInfo.Ruleset.ShortName) + .AsEnumerable() + .OrderByDescending(score => score.Ruleset.MatchesOnlineID(newScore.BeatmapInfo.Ruleset)) + .ThenByDescending(score => score.Rank) + .FirstOrDefault()); + + if (localUserScore == null) + preventTaggingReason = "Play the beatmap to contribute to beatmap tags!"; + else if (localUserScore.Ruleset.OnlineID != newScore.BeatmapInfo!.Ruleset.OnlineID) preventTaggingReason = "Play the beatmap in its original ruleset to contribute to beatmap tags!"; - else if (AchievedScore.Rank < ScoreRank.C) + else if (localUserScore.Rank < ScoreRank.C) preventTaggingReason = "Set a better score to contribute to beatmap tags!"; if (preventTaggingReason == null) From f36bc51520882b3769bfd34356a9e970d053cfe8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Mar 2025 22:25:56 +0900 Subject: [PATCH 77/79] Inline method calls to make multiple enumerations explicit --- osu.Game/Overlays/MusicController.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 6d7c26b51b..da5388534c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -257,10 +257,8 @@ namespace osu.Game.Overlays playableSet = getNextRandom(-1, allowProtectedTracks); else { - var beatmapSets = getBeatmapSets(allowProtectedTracks); - - playableSet = beatmapSets.TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault() - ?? beatmapSets.LastOrDefault(); + playableSet = getBeatmapSets(allowProtectedTracks).TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault() + ?? getBeatmapSets(allowProtectedTracks).LastOrDefault(); } if (playableSet != null) @@ -355,9 +353,8 @@ namespace osu.Game.Overlays playableSet = getNextRandom(1, allowProtectedTracks); else { - var beatmapSets = getBeatmapSets(allowProtectedTracks); - - playableSet = beatmapSets.SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).ElementAtOrDefault(1) ?? beatmapSets.FirstOrDefault(); + playableSet = getBeatmapSets(allowProtectedTracks).SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).ElementAtOrDefault(1) + ?? getBeatmapSets(allowProtectedTracks).FirstOrDefault(); } var playableBeatmap = playableSet?.Value.Beatmaps.FirstOrDefault(); From 28f3d9cec9c37dd01f7dd929013e186320bb6b97 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 25 Mar 2025 21:47:22 -0400 Subject: [PATCH 78/79] Open teams page externally when clicking team flags --- osu.Game/Users/Drawables/UpdateableTeamFlag.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 2fcec66aa7..517eb589b9 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -8,8 +8,10 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Users.Drawables @@ -64,6 +66,12 @@ namespace osu.Game.Users.Drawables public LocalisableString TooltipText { get; } + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + public TeamFlag(APITeam team) { this.team = team; @@ -91,6 +99,12 @@ namespace osu.Game.Users.Drawables } }; } + + protected override bool OnClick(ClickEvent e) + { + game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/teams/{team.Id}"); + return true; + } } } } From 90bc2318e15e9b2fbbc6bb75bb0d3857dfce7da7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Mar 2025 19:10:14 +0900 Subject: [PATCH 79/79] Remove unused local --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 1749ea38b5..814c0519a3 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -298,7 +298,6 @@ namespace osu.Game.Tests.Visual.Ranking public void TestTaggingInteractionWithLocalScores() { BeatmapInfo beatmapInfo = null!; - string originalHash = string.Empty; AddStep(@"Import beatmap", () => {