From 2417d4de8302f7682bb5703ead03936c336fd33d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 21:46:33 -0500 Subject: [PATCH 001/349] 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 002/349] 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 003/349] 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 004/349] 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 005/349] 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 006/349] 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 007/349] 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 ad422295c85d257044edd33dba7284b7c8d9b631 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Jan 2025 21:38:37 +0900 Subject: [PATCH 008/349] Add ctor to create Rooms from MultiplayerRooms --- osu.Game/Online/Rooms/Room.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index f8660a656e..7647134646 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,6 +342,29 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; + public Room() + { + } + + /// + /// Creates a from a . + /// + public Room(MultiplayerRoom room) + { + RoomID = room.RoomID; + Host = room.Host?.User; + + Name = room.Settings.Name; + Password = room.Settings.Password; + Type = room.Settings.MatchType; + QueueMode = room.Settings.QueueMode; + AutoStartDuration = room.Settings.AutoStartDuration; + AutoSkip = room.Settings.AutoSkip; + + Playlist = room.Playlist.Select(item => new PlaylistItem(item)).ToArray(); + CurrentPlaylistItem = Playlist.FirstOrDefault(item => item.ID == room.Settings.PlaylistItemId); + } + /// /// Copies values from another into this one. /// From 3d2d4ee89f06a88feabcfdda1b73ac1cbeaf1c49 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Jan 2025 22:07:13 +0900 Subject: [PATCH 009/349] Add ctor to create MultiplayerPlaylistItem from PlaylistItem --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8be703e620..6e467c1d26 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -60,5 +60,20 @@ namespace osu.Game.Online.Rooms public MultiplayerPlaylistItem() { } + + public MultiplayerPlaylistItem(PlaylistItem item) + { + 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; + } } } From ad28de8ae3aa1d2817fc8511929d52c2c3ab0b20 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 21:44:40 +0900 Subject: [PATCH 010/349] Create multiplayer rooms via multiplayer server --- .../Multiplayer/IMultiplayerLoungeServer.cs | 2 + .../Online/Multiplayer/MultiplayerClient.cs | 42 ++++++++++++------ .../Online/Multiplayer/MultiplayerRoom.cs | 9 ++++ .../Multiplayer/MultiplayerRoomSettings.cs | 14 ++++++ .../Multiplayer/OnlineMultiplayerClient.cs | 25 +++++++++++ osu.Game/Online/Rooms/Room.cs | 23 ---------- .../Match/MultiplayerMatchSettingsOverlay.cs | 44 +++++++++---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 5 +-- .../Multiplayer/TestMultiplayerClient.cs | 5 +++ 9 files changed, 105 insertions(+), 64 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index f266c38b8b..c5eb6f9b36 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -10,6 +10,8 @@ namespace osu.Game.Online.Multiplayer /// public interface IMultiplayerLoungeServer { + Task CreateRoom(MultiplayerRoom room); + /// /// Request to join a multiplayer room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4a28124583..d0c3a1fa06 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -165,6 +165,15 @@ namespace osu.Game.Online.Multiplayer private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); private CancellationTokenSource? joinCancellationSource; + public async Task CreateRoom(Room room) + { + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + + var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token); + } + /// /// Joins the for a given API . /// @@ -175,34 +184,34 @@ namespace osu.Game.Online.Multiplayer if (Room != null) throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + Debug.Assert(room.RoomID != null); + var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token); + } + + private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) + { await joinOrLeaveTaskChain.Add(async () => { - Debug.Assert(room.RoomID != null); - - // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false); - Debug.Assert(joinedRoom != null); + // Initialise the server-side room. + MultiplayerRoom joinedRoom = await initFunc(room).ConfigureAwait(false); // Populate users. - Debug.Assert(joinedRoom.Users != null); await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await runOnUpdateThreadAsync(() => { Debug.Assert(Room == null); + Debug.Assert(APIRoom == null); Room = joinedRoom; APIRoom = room; - Debug.Assert(joinedRoom.Playlist.Count > 0); - + APIRoom.RoomID = joinedRoom.RoomID; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); - - // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. APIRoom.EndDate = null; Debug.Assert(LocalUser != null); @@ -216,8 +225,8 @@ namespace osu.Game.Online.Multiplayer postServerShuttingDownNotification(); OnRoomJoined(); - }, cancellationSource.Token).ConfigureAwait(false); - }, cancellationSource.Token).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); } /// @@ -227,6 +236,13 @@ namespace osu.Game.Online.Multiplayer { } + /// + /// Creates the with the given settings. + /// + /// The room. + /// The joined + protected abstract Task CreateRoom(MultiplayerRoom room); + /// /// Joins the with a given ID. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index 00048fa931..f7bd4490ff 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using MessagePack; using Newtonsoft.Json; using osu.Game.Online.Rooms; @@ -65,6 +66,14 @@ namespace osu.Game.Online.Multiplayer RoomID = roomId; } + public MultiplayerRoom(Room room) + { + RoomID = room.RoomID ?? 0; + Settings = new MultiplayerRoomSettings(room); + Host = room.Host != null ? new MultiplayerRoomUser(room.Host.OnlineID) : null; + Playlist = room.Playlist.Select(p => new MultiplayerPlaylistItem(p)).ToArray(); + } + public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]"; } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index c73b02874e..c264ec1eef 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -35,6 +35,20 @@ namespace osu.Game.Online.Multiplayer [IgnoreMember] public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero; + public MultiplayerRoomSettings() + { + } + + public MultiplayerRoomSettings(Room room) + { + Name = room.Name; + Password = room.Password ?? string.Empty; + MatchType = room.Type; + QueueMode = room.QueueMode; + AutoStartDuration = room.AutoStartDuration; + AutoSkip = room.AutoSkip; + } + public bool Equals(MultiplayerRoomSettings? other) { if (ReferenceEquals(this, other)) return true; diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 40436d730e..524873ef66 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -266,6 +266,31 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } + protected override async Task CreateRoom(MultiplayerRoom room) + { + if (!IsConnected.Value) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + + try + { + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room); + } + catch (HubException exception) + { + if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) + { + Debug.Assert(connector != null); + + await connector.Reconnect().ConfigureAwait(false); + return await CreateRoom(room).ConfigureAwait(false); + } + + throw; + } + } + public override Task DisconnectInternal() { if (connector == null) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 7647134646..f8660a656e 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,29 +342,6 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; - public Room() - { - } - - /// - /// Creates a from a . - /// - public Room(MultiplayerRoom room) - { - RoomID = room.RoomID; - Host = room.Host?.User; - - Name = room.Settings.Name; - Password = room.Settings.Password; - Type = room.Settings.MatchType; - QueueMode = room.Settings.QueueMode; - AutoStartDuration = room.Settings.AutoStartDuration; - AutoSkip = room.Settings.AutoSkip; - - Playlist = room.Playlist.Select(item => new PlaylistItem(item)).ToArray(); - CurrentPlaylistItem = Playlist.FirstOrDefault(item => item.ID == room.Settings.PlaylistItemId); - } - /// /// Copies values from another into this one. /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 1372054149..279b140d36 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -29,12 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public partial class MultiplayerMatchSettingsOverlay : RoomSettingsOverlay { - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - protected override OsuButton SubmitButton => settings.ApplyButton; protected override bool IsLoading => ongoingOperationTracker.InProgress.Value; @@ -56,7 +50,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Y, SettingsApplied = Hide, - SelectedItem = { BindTarget = SelectedItem } }; protected partial class MatchSettings : CompositeDrawable @@ -65,7 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; - public readonly Bindable SelectedItem = new Bindable(); public Action? SettingsApplied; public OsuTextBox NameField = null!; @@ -86,9 +78,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } = null!; - [Resolved] - private IRoomManager manager { get; set; } = null!; - [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -279,7 +268,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { RelativeSizeAxes = Axes.X, Height = DrawableRoomPlaylistItem.HEIGHT, - SelectedItem = { BindTarget = SelectedItem } }, selectBeatmapButton = new RoundedButton { @@ -482,19 +470,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } else { - room.Name = NameField.Text; - room.Type = TypePicker.Current.Value; - room.Password = PasswordTextBox.Current.Value; - room.QueueMode = QueueModeDropdown.Current.Value; - room.AutoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value); - room.AutoSkip = AutoSkipCheckbox.Current.Value; + client.CreateRoom(room).ContinueWith(t => Schedule(() => + { + if (t.IsCompleted) + onSuccess(room); + else if (t.IsFaulted) + { + Exception? exception = t.Exception; - if (int.TryParse(MaxParticipantsField.Text, out int max)) - room.MaxParticipants = max; - else - room.MaxParticipants = null; + if (exception is AggregateException ae) + exception = ae.InnerException; - manager.CreateRoom(room, onSuccess, onError); + Debug.Assert(exception != null); + + if (exception.GetHubExceptionMessage() is string message) + onError(message); + else + onError($"Error creating room: {exception}"); + } + else + onError("Error creating room."); + })); } } @@ -520,7 +516,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (text.StartsWith(not_found_prefix, StringComparison.Ordinal)) { ErrorText.Text = "The selected beatmap is not available online."; - SelectedItem.Value?.MarkInvalid(); + room.Playlist.SingleOrDefault()?.MarkInvalid(); } else { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edc45dbf7c..06ea5ee033 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -233,10 +233,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem }; - protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room) - { - SelectedItem = SelectedItem - }; + protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room); protected override void UpdateMods() { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4d812abf11..70e298f3e0 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -483,6 +483,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); + protected override Task CreateRoom(MultiplayerRoom room) + { + throw new NotImplementedException(); + } + private async Task changeMatchType(MatchType type) { Debug.Assert(ServerRoom != null); From 001d9cacf21cbe9dee9330b01b9e496e7be1f4f5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 Jan 2025 19:31:49 +0900 Subject: [PATCH 011/349] Configure awaiters --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index d0c3a1fa06..e5eade8c1d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -171,7 +171,7 @@ namespace osu.Game.Online.Multiplayer throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token); + await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); } /// @@ -187,7 +187,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID != null); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token); + await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); } private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 524873ef66..05f3e44405 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -275,7 +275,7 @@ namespace osu.Game.Online.Multiplayer try { - return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room); + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); } catch (HubException exception) { From 02369baec43f0a68a26a960bef20980289b1f6ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 Jan 2025 21:44:45 +0900 Subject: [PATCH 012/349] Join/Leave rooms via multiplayer server Relevant functionality has been removed from `RoomManager` in the process. --- .../TestSceneMultiplayerLoungeSubScreen.cs | 26 ------ .../Online/Multiplayer/MultiplayerClient.cs | 3 + osu.Game/Online/Rooms/CreateRoomRequest.cs | 2 +- osu.Game/Online/Rooms/JoinRoomRequest.cs | 1 + .../OnlinePlay/Components/RoomManager.cs | 80 ------------------- .../DailyChallenge/DailyChallenge.cs | 10 +-- osu.Game/Screens/OnlinePlay/IRoomManager.cs | 22 ----- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 9 ++- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 6 +- .../OnlinePlay/Multiplayer/Multiplayer.cs | 3 - .../Multiplayer/MultiplayerLoungeSubScreen.cs | 34 ++++---- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 + .../Multiplayer/MultiplayerRoomManager.cs | 72 ----------------- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 12 +-- .../Screens/OnlinePlay/OnlinePlaySubScreen.cs | 4 - .../Playlists/PlaylistsLoungeSubScreen.cs | 15 ++++ .../Playlists/PlaylistsRoomSettingsOverlay.cs | 9 ++- .../Playlists/PlaylistsRoomSubScreen.cs | 2 + .../Multiplayer/MultiplayerTestScene.cs | 2 +- .../Multiplayer/TestMultiplayerRoomManager.cs | 10 +-- .../Visual/OnlinePlay/TestRoomManager.cs | 13 ++- 21 files changed, 74 insertions(+), 263 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 9951f62c77..d06a91433d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; @@ -21,23 +20,13 @@ namespace osu.Game.Tests.Visual.Multiplayer protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private LoungeSubScreen loungeScreen = null!; - private Room? lastJoinedRoom; - private string? lastJoinedPassword; public override void SetUpSteps() { base.SetUpSteps(); AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen())); - AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); - - AddStep("bind to event", () => - { - lastJoinedRoom = null; - lastJoinedPassword = null; - RoomManager.JoinRoomRequested = onRoomJoined; - }); } [Test] @@ -46,9 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == null); } [Test] @@ -126,9 +112,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == "password"); } [Test] @@ -142,15 +125,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press enter", () => InputManager.Key(Key.Enter)); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == "password"); - } - - private void onRoomJoined(Room room, string? password) - { - lastJoinedRoom = room; - lastJoinedPassword = password; } } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index e5eade8c1d..7dfe974651 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -253,6 +253,9 @@ namespace osu.Game.Online.Multiplayer public Task LeaveRoom() { + if (Room == null) + return Task.CompletedTask; + // The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled. // This includes the setting of Room itself along with the initial update of the room settings on join. joinCancellationSource?.Cancel(); diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs index 63a3b7bfa8..9773bb5e7d 100644 --- a/osu.Game/Online/Rooms/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -15,6 +15,7 @@ namespace osu.Game.Online.Rooms public CreateRoomRequest(Room room) { Room = room; + Success += r => Room.CopyFrom(r); } protected override WebRequest CreateWebRequest() @@ -23,7 +24,6 @@ namespace osu.Game.Online.Rooms req.ContentType = "application/json"; req.Method = HttpMethod.Post; - req.AddRaw(JsonConvert.SerializeObject(Room)); return req; diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index dfc7a53fb2..13e7ac8c84 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -16,6 +16,7 @@ namespace osu.Game.Online.Rooms { Room = room; Password = password; + Success += r => Room.CopyFrom(r); } protected override WebRequest CreateWebRequest() diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 73f980f0a3..3abb4098fb 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -5,12 +5,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Logging; -using osu.Game.Online.API; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components @@ -23,89 +21,11 @@ namespace osu.Game.Screens.OnlinePlay.Components public IBindableList Rooms => rooms; - protected IBindable JoinedRoom => joinedRoom; - private readonly Bindable joinedRoom = new Bindable(); - - [Resolved] - private IAPIProvider api { get; set; } = null!; - public RoomManager() { RelativeSizeAxes = Axes.Both; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - PartRoom(); - } - - public virtual void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - { - room.Host = api.LocalUser.Value; - - var req = new CreateRoomRequest(room); - - req.Success += result => - { - joinedRoom.Value = room; - - AddOrUpdateRoom(result); - room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere. - - // The server may not contain all properties (such as password), so invoke success with the given room. - onSuccess?.Invoke(room); - }; - - req.Failure += exception => - { - onError?.Invoke(req.Response?.Error ?? exception.Message); - }; - - api.Queue(req); - } - - private JoinRoomRequest? currentJoinRoomRequest; - - public virtual void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - currentJoinRoomRequest?.Cancel(); - currentJoinRoomRequest = new JoinRoomRequest(room, password); - - currentJoinRoomRequest.Success += result => - { - joinedRoom.Value = room; - - AddOrUpdateRoom(result); - room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere. - - onSuccess?.Invoke(room); - }; - - currentJoinRoomRequest.Failure += exception => - { - if (exception is OperationCanceledException) - return; - - onError?.Invoke(exception.Message); - }; - - api.Queue(currentJoinRoomRequest); - } - - public virtual void PartRoom() - { - currentJoinRoomRequest?.Cancel(); - - if (joinedRoom.Value == null) - return; - - if (api.State.Value == APIState.Online) - api.Queue(new PartRoomRequest(joinedRoom.Value)); - - joinedRoom.Value = null; - } - private readonly HashSet ignoredRooms = new HashSet(); public void AddOrUpdateRoom(Room room) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 13a282dd52..e3d6d42c05 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -34,7 +34,6 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -71,9 +70,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); - [Cached(Type = typeof(IRoomManager))] - private RoomManager roomManager { get; set; } - [Cached] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -115,7 +111,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { this.room = room; playlistItem = room.Playlist.Single(); - roomManager = new RoomManager(); Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; } @@ -131,7 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - roomManager, beatmapAvailabilityTracker, new ScreenStack(new RoomBackgroundScreen(playlistItem)) { @@ -426,7 +420,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge base.OnEntering(e); waves.Show(); - roomManager.JoinRoom(room); + API.Queue(new JoinRoomRequest(room, null)); startLoopingTrack(this, musicController); metadataClient.BeginWatchingMultiplayerRoom(room.RoomID!.Value).ContinueWith(t => @@ -480,7 +474,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge previewTrackManager.StopAnyPlaying(this); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); - roomManager.PartRoom(); + API.Queue(new PartRoomRequest(room)); metadataClient.EndWatchingMultiplayerRoom(room.RoomID!.Value).FireAndForget(); return base.OnExiting(e); diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs index ed4fb7b15e..8ecb1dd7e0 100644 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/IRoomManager.cs @@ -38,27 +38,5 @@ namespace osu.Game.Screens.OnlinePlay /// Removes all s from this . /// void ClearRooms(); - - /// - /// Creates a new . - /// - /// The to create. - /// An action to be invoked if the creation succeeds. - /// An action to be invoked if an error occurred. - void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null); - - /// - /// Joins a . - /// - /// The to join. must be populated. - /// An optional password to use for the join operation. - /// - /// - void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null); - - /// - /// Parts the currently-joined . - /// - void PartRoom(); } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f00cf7427c..f3f4df166a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -263,6 +263,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge music.EnsurePlayingSomething(); onReturning(); + + // Poll for any newly-created rooms (including potentially the user's own). + ListingPollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -297,14 +300,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge popoverContainer.HidePopover(); } - public virtual void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() => + public void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() => { if (joiningRoomOperation != null) return; joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - RoomManager?.JoinRoom(room, password, _ => + TryJoin(room, password, r => { Open(room); joiningRoomOperation?.Dispose(); @@ -318,6 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }); }); + protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); + /// /// Copies a room and opens it as a fresh (not-yet-created) one. /// diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..d37f3b877c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -343,7 +343,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!ensureExitConfirmed()) return true; - RoomManager?.PartRoom(); + if (Room.RoomID != null) + PartRoom(); + Mods.Value = Array.Empty(); onLeaving(); @@ -351,6 +353,8 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnExiting(e); } + protected abstract void PartRoom(); + private bool ensureExitConfirmed() { if (ExitConfirmed) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index bf316bb3da..dfed32aebc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -8,7 +8,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -97,8 +96,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override string ScreenTitle => "Multiplayer"; - protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager(); - protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); public void Join(Room room, string? password) => Schedule(() => Lounge.Join(room, password)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index dd61caa3db..e901ecbdce 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -1,12 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; -using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; @@ -32,19 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private Dropdown roomAccessTypeDropdown = null!; private OsuCheckbox showInProgress = null!; - public override void OnResuming(ScreenTransitionEvent e) - { - base.OnResuming(e); - - // Upon having left a room, we don't know whether we were the only participant, and whether the room is now closed as a result of leaving it. - // To work around this, temporarily remove the room and trigger an immediate listing poll. - if (e.Last is MultiplayerMatchSubScreen match) - { - RoomManager?.RemoveRoom(match.Room); - ListingPollingComponent.PollImmediately(); - } - } - protected override IEnumerable CreateFilterControls() { foreach (var control in base.CreateFilterControls()) @@ -93,6 +81,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); + protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + { + client.JoinRoom(room, password).ContinueWith(result => + { + if (result.IsCompletedSuccessfully) + onSuccess(room); + else + { + const string message = "Failed to join multiplayer room."; + + if (result.Exception != null) + Logger.Error(result.Exception, message); + + onFailure.Invoke(result.Exception?.AsSingular().Message ?? message); + } + }); + } + protected override void OpenNewRoom(Room room) { if (!client.IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 06ea5ee033..553c0c9182 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -278,6 +278,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return base.OnExiting(e); } + protected override void PartRoom() => client.LeaveRoom(); + private ModSettingChangeTracker? modSettingChangeTracker; private ScheduledDelegate? debouncedModSettingsUpdate; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs deleted file mode 100644 index 7f09c9cbe9..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.ExceptionExtensions; -using osu.Framework.Logging; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer -{ - public partial class MultiplayerRoomManager : RoomManager - { - [Resolved] - private MultiplayerClient multiplayerClient { get; set; } = null!; - - public override void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - => base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password, onSuccess, onError), onError); - - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - if (!multiplayerClient.IsConnected.Value) - { - onError?.Invoke("Not currently connected to the multiplayer server."); - return; - } - - // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. - // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. - if (room.HasEnded) - { - onError?.Invoke("Cannot join an ended room."); - return; - } - - base.JoinRoom(room, password, r => joinMultiplayerRoom(r, password, onSuccess, onError), onError); - } - - public override void PartRoom() - { - if (JoinedRoom.Value == null) - return; - - base.PartRoom(); - multiplayerClient.LeaveRoom(); - } - - private void joinMultiplayerRoom(Room room, string? password, Action? onSuccess = null, Action? onError = null) - { - Debug.Assert(room.RoomID != null); - - multiplayerClient.JoinRoom(room, password).ContinueWith(t => - { - if (t.IsCompletedSuccessfully) - Schedule(() => onSuccess?.Invoke(room)); - else if (t.IsFaulted) - { - const string message = "Failed to join multiplayer room."; - - if (t.Exception != null) - Logger.Error(t.Exception, message); - - PartRoom(); - Schedule(() => onError?.Invoke(t.Exception?.AsSingular().Message ?? message)); - } - }); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 17fb667e14..16462b90c1 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -36,12 +36,12 @@ namespace osu.Game.Screens.OnlinePlay private readonly ScreenStack screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }; private OnlinePlayScreenWaveContainer waves = null!; - [Cached(Type = typeof(IRoomManager))] - protected RoomManager RoomManager { get; private set; } - [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + [Cached(Type = typeof(IRoomManager))] + private readonly RoomManager roomManager = new RoomManager(); + [Resolved] protected IAPIProvider API { get; private set; } = null!; @@ -51,8 +51,6 @@ namespace osu.Game.Screens.OnlinePlay Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; - - RoomManager = CreateRoomManager(); } private readonly IBindable apiState = new Bindable(); @@ -67,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay { screenStack, new Header(ScreenTitle, screenStack), - RoomManager, + roomManager, ongoingOperationTracker, } }; @@ -165,8 +163,6 @@ namespace osu.Game.Screens.OnlinePlay subScreen.Exit(); } - RoomManager.PartRoom(); - waves.Hide(); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index fa1ee004c9..9b35a794a3 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -15,9 +14,6 @@ namespace osu.Game.Screens.OnlinePlay protected sealed override bool PlayExitSound => false; - [Resolved] - protected IRoomManager? RoomManager { get; private set; } - protected OnlinePlaySubScreen() { Anchor = Anchor.Centre; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index d66b4f844c..92415e0eb1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -59,6 +60,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return criteria; } + protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + { + var joinRoomRequest = new JoinRoomRequest(room, password); + + joinRoomRequest.Success += r => onSuccess(r); + joinRoomRequest.Failure += exception => + { + if (exception is not OperationCanceledException) + onFailure(exception.Message); + }; + + api.Queue(joinRoomRequest); + } + protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton(); protected override Room CreateNewRoom() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 88af161cc8..b3d1d577ed 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -75,9 +75,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PurpleRoundedButton editPlaylistButton = null!; - [Resolved] - private IRoomManager? manager { get; set; } - [Resolved] private IAPIProvider api { get; set; } = null!; @@ -449,7 +446,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists room.Duration = DurationField.Current.Value; loadingLayer.Show(); - manager?.CreateRoom(room, onSuccess, onError); + + var req = new CreateRoomRequest(room); + req.Success += onSuccess; + req.Failure += e => onError(req.Response?.Error ?? e.Message); + api.Queue(req); } private void hideError() => ErrorText.FadeOut(50); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9b4630ac0b..064c355a69 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -290,6 +290,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists })); } + protected override void PartRoom() => api.Queue(new PartRoomRequest(Room)); + protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) { return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 42cf317829..dca1fc8f3c 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => { SelectedRoom.Value = CreateRoom(); - RoomManager.CreateRoom(SelectedRoom.Value); + API.Queue(new CreateRoomRequest(SelectedRoom.Value)); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index b998a638e5..59ac9a9749 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.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. -using System; using System.Collections.Generic; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer @@ -15,7 +13,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// A for use in multiplayer test scenes. /// Should generally not be used by itself outside of a . /// - public partial class TestMultiplayerRoomManager : MultiplayerRoomManager + public partial class TestMultiplayerRoomManager : RoomManager { private readonly TestRoomRequestsHandler requestsHandler; @@ -26,12 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms; - public override void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - => base.CreateRoom(room, r => onSuccess?.Invoke(r), onError); - - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - => base.JoinRoom(room, password, r => onSuccess?.Invoke(r), onError); - /// /// Adds a room to a local "server-side" list that's returned when a is fired. /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index b1e3eafacc..60d169a46f 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -15,15 +17,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public partial class TestRoomManager : RoomManager { - public Action? JoinRoomRequested; - private int currentRoomId; - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - JoinRoomRequested?.Invoke(room, password); - base.JoinRoom(room, password, onSuccess, onError); - } + [Resolved] + private IAPIProvider api { get; set; } = null!; public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) { @@ -49,7 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public void AddRoom(Room room) { room.RoomID = -currentRoomId; - CreateRoom(room); + api.Queue(new CreateRoomRequest(room)); currentRoomId++; } } From 9a623257f5bd8cfed7f2d691fbb1c2959483c111 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 16:19:09 +0900 Subject: [PATCH 013/349] Adjust + fix tests --- .../StatefulMultiplayerClientTest.cs | 7 +- .../TestSceneDrawableLoungeRoom.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 15 ++-- .../TestSceneMultiplayerLoungeSubScreen.cs | 58 +++++++++++--- .../TestSceneMultiplayerPlaylist.cs | 10 +-- .../TestScenePlaylistsLoungeSubScreen.cs | 30 ++++++- .../TestScenePlaylistsMatchSettingsOverlay.cs | 78 +++++++------------ .../Visual/TestMultiplayerComponents.cs | 24 ++---- osu.Game/Online/Rooms/Room.cs | 17 ++++ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 14 +--- .../OnlinePlay/Lounge/IOnlinePlayLounge.cs | 32 ++++++++ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 19 +++-- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 2 - .../IMultiplayerTestSceneDependencies.cs | 6 -- .../Multiplayer/MultiplayerTestScene.cs | 3 +- .../MultiplayerTestSceneDependencies.cs | 6 +- .../Multiplayer/TestMultiplayerClient.cs | 35 +++++++-- .../Multiplayer/TestMultiplayerRoomManager.cs | 34 -------- .../OnlinePlayTestSceneDependencies.cs | 4 +- .../Visual/OnlinePlay/TestRoomManager.cs | 20 +++-- 20 files changed, 232 insertions(+), 184 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs delete mode 100644 osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 559db16751..be30e06ed4 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -8,7 +8,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.NonVisual.Multiplayer @@ -72,10 +71,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("create room initially in gameplay", () => { - var newRoom = new Room(); - newRoom.CopyFrom(SelectedRoom.Value!); - - newRoom.RoomID = null; MultiplayerClient.RoomSetupAction = room => { room.State = MultiplayerRoomState.Playing; @@ -86,7 +81,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer }); }; - RoomManager.CreateRoom(newRoom); + MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs index c5fb52461a..459a90d096 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load() { - var mockLounge = new Mock(); + var mockLounge = new Mock(); mockLounge .Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>())) .Callback, Action>((_, _, _, d) => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index fb653cea8b..0966c61a3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -58,7 +58,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents = null!; private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; - private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -257,7 +256,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Playlist = @@ -286,7 +285,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Playlist = @@ -336,7 +335,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Password = "password", @@ -789,7 +788,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", QueueMode = QueueMode.AllPlayers, @@ -810,8 +809,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { - roomManager.ServerSideRooms[0].Name = "New name"; - roomManager.ServerSideRooms[0].Playlist = + multiplayerClient.ServerSideRooms[0].Name = "New name"; + multiplayerClient.ServerSideRooms[0].Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { @@ -828,7 +827,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("local room has correct settings", () => { var localRoom = this.ChildrenOfType().Single().Room; - return localRoom.Name == roomManager.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2; + return localRoom.Name == multiplayerClient.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2; }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index d06a91433d..4a259149e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -9,18 +9,26 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public partial class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene + public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene { protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private LoungeSubScreen loungeScreen = null!; + private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + + public TestSceneMultiplayerLoungeSubScreen() + : base(false) + { + } + public override void SetUpSteps() { base.SetUpSteps(); @@ -32,15 +40,17 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); + addRoom(false); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); + + AddAssert("room joined", () => MultiplayerClient.RoomJoined); } [Test] public void TestPopoverHidesOnBackButton() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -53,18 +63,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("hit escape", () => InputManager.Key(Key.Escape)); AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] public void TestPopoverHidesOnLeavingScreen() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); AddStep("exit screen", () => Stack.Exit()); AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -72,16 +86,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); - AddAssert("room not joined", () => loungeScreen.IsCurrentScreen()); + AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen()); AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible); AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -89,16 +105,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong"); AddStep("press enter", () => InputManager.Key(Key.Enter)); - AddAssert("room not joined", () => loungeScreen.IsCurrentScreen()); + AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen()); AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible); AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -106,12 +124,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); + + AddUntilStep("room joined", () => MultiplayerClient.RoomJoined); } [Test] @@ -119,12 +139,30 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddAssert("room joined", () => MultiplayerClient.RoomJoined); } + + private void addRoom(bool withPassword) + { + int initialRoomCount = 0; + + AddStep("add room", () => + { + initialRoomCount = roomsContainer.Rooms.Count; + RoomManager.AddRooms(1, withPassword: withPassword); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for room to appear", () => roomsContainer.Rooms.Count == initialRoomCount + 1); + } + + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 36f5bba384..77b75f407b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer addItemStep(); AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0)); @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertQueueTabCount(2); - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); assertQueueTabCount(0); } @@ -157,12 +157,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithMixedItemsAddedInCorrectLists() { - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); AddStep("join room with items", () => { - RoomManager.CreateRoom(new Room + API.Queue(new CreateRoomRequest(new Room { Name = "test name", Playlist = @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Expired = true } ] - }); + })); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 8c8dc8d69a..0897a3b2f5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -35,7 +35,13 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(500)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(500); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); } [Test] @@ -43,7 +49,12 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => RoomManager.AddRooms(30)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(30); + loungeScreen.RefreshRooms(); + }); + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -60,7 +71,12 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => RoomManager.AddRooms(30)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(30); + loungeScreen.RefreshRooms(); + }); + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -74,7 +90,13 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => RoomManager.AddRooms(1)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(1); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 5868331451..51e39e1b7f 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -3,14 +3,13 @@ using System; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Visual.OnlinePlay; @@ -21,13 +20,33 @@ namespace osu.Game.Tests.Visual.Playlists protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private TestRoomSettings settings = null!; - - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + private Func? handleRequest; public override void SetUpSteps() { base.SetUpSteps(); + AddStep("setup api", () => + { + handleRequest = null; + ((DummyAPIAccess)API).HandleRequest = req => + { + if (req is not CreateRoomRequest createReq || handleRequest == null) + return false; + + if (handleRequest(createReq.Room) is string errorText) + createReq.TriggerFailure(new APIException(errorText, null)); + else + { + var createdRoom = new APICreatedRoom(); + createdRoom.CopyFrom(createReq.Room); + createReq.TriggerSuccess(createdRoom); + } + + return true; + }; + }); + AddStep("create overlay", () => { SelectedRoom.Value = new Room(); @@ -75,10 +94,10 @@ namespace osu.Game.Tests.Visual.Playlists settings.DurationField.Current.Value = expectedDuration; SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; - RoomManager.CreateRequested = r => + handleRequest = r => { createdRoom = r; - return string.Empty; + return null; }; }); @@ -103,7 +122,7 @@ namespace osu.Game.Tests.Visual.Playlists errorMessage = $"{not_found_prefix} {beatmap.OnlineID}"; - RoomManager.CreateRequested = _ => errorMessage; + handleRequest = _ => errorMessage; }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); @@ -128,7 +147,7 @@ namespace osu.Game.Tests.Visual.Playlists SelectedRoom.Value!.Name = "Test Room"; SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; - RoomManager.CreateRequested = _ => failText; + handleRequest = _ => failText; }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); @@ -159,48 +178,5 @@ namespace osu.Game.Tests.Visual.Playlists { } } - - private class TestDependencies : OnlinePlayTestSceneDependencies - { - protected override IRoomManager CreateRoomManager() => new TestRoomManager(); - } - - protected class TestRoomManager : IRoomManager - { - public Func? CreateRequested; - - public event Action RoomsUpdated - { - add { } - remove { } - } - - public IBindable InitialRoomsReceived { get; } = new Bindable(true); - - public IBindableList Rooms => null!; - - public void AddOrUpdateRoom(Room room) => throw new NotImplementedException(); - - public void RemoveRoom(Room room) => throw new NotImplementedException(); - - public void ClearRooms() => throw new NotImplementedException(); - - public void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - { - if (CreateRequested == null) - return; - - string error = CreateRequested.Invoke(room); - - if (!string.IsNullOrEmpty(error)) - onError?.Invoke(error); - else - onSuccess?.Invoke(room); - } - - public void JoinRoom(Room room, string? password, Action? onSuccess = null, Action? onError = null) => throw new NotImplementedException(); - - public void PartRoom() => throw new NotImplementedException(); - } } } diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index 1814fb70c8..e385ff3a03 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; @@ -26,15 +25,12 @@ namespace osu.Game.Tests.Visual /// Provides a to be resolved as a dependency in the screen, /// which is typically a part of . /// Rebinds the to handle requests via a . - /// Provides a for the screen. /// ///

/// public partial class TestMultiplayerComponents : OsuScreen { - public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen; - - public TestMultiplayerRoomManager RoomManager => multiplayerScreen.RoomManager; + public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen { get; } public IScreen CurrentScreen => screenStack.CurrentScreen; @@ -53,17 +49,17 @@ namespace osu.Game.Tests.Visual private BeatmapManager beatmapManager { get; set; } private readonly OsuScreenStack screenStack; - private readonly TestMultiplayer multiplayerScreen; + private readonly TestRoomRequestsHandler requestsHandler = new TestRoomRequestsHandler(); public TestMultiplayerComponents() { - multiplayerScreen = new TestMultiplayer(); + MultiplayerScreen = new Screens.OnlinePlay.Multiplayer.Multiplayer(); InternalChildren = new Drawable[] { userLookupCache, beatmapLookupCache, - MultiplayerClient = new TestMultiplayerClient(RoomManager), + MultiplayerClient = new TestMultiplayerClient(requestsHandler), screenStack = new OsuScreenStack { Name = nameof(TestMultiplayerComponents), @@ -71,13 +67,13 @@ namespace osu.Game.Tests.Visual } }; - screenStack.Push(multiplayerScreen); + screenStack.Push(MultiplayerScreen); } [BackgroundDependencyLoader] private void load(IAPIProvider api) { - ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager); + ((DummyAPIAccess)api).HandleRequest = request => requestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager); } public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton(); @@ -90,13 +86,5 @@ namespace osu.Game.Tests.Visual screenStack.Exit(); return true; } - - private partial class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer - { - public new TestMultiplayerRoomManager RoomManager { get; private set; } - public TestRoomRequestsHandler RequestsHandler { get; private set; } - - protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager(RequestsHandler = new TestRoomRequestsHandler()); - } } } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index f8660a656e..c5e292a19d 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,6 +342,23 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; + public Room() + { + } + + public Room(MultiplayerRoom room) + { + RoomID = room.RoomID; + Name = room.Settings.Name; + Password = room.Settings.Password; + Type = room.Settings.MatchType; + QueueMode = room.Settings.QueueMode; + AutoStartDuration = room.Settings.AutoStartDuration; + AutoSkip = room.Settings.AutoSkip; + Host = room.Host != null ? new APIUser { Id = room.Host.UserID } : null; + Playlist = room.Playlist.Select(p => new PlaylistItem(p)).ToArray(); + } + /// /// Copies values from another into this one. /// diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 0a55472c2d..032a231ad3 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -24,7 +24,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; @@ -51,7 +50,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge } [Resolved(canBeNull: true)] - private LoungeSubScreen? lounge { get; set; } + private IOnlinePlayLounge? lounge { get; set; } [Resolved] private IDialogOverlay? dialogOverlay { get; set; } @@ -163,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - lounge?.OpenCopy(Room); + lounge?.Clone(Room); }) }; @@ -171,12 +170,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => { - dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => - { - var request = new ClosePlaylistRequest(Room.RoomID!.Value); - request.Success += () => lounge?.RefreshRooms(); - api.Queue(request); - })); + dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => lounge?.Close(Room))); })); } @@ -239,7 +233,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly Room room; [Resolved(canBeNull: true)] - private LoungeSubScreen? lounge { get; set; } + private IOnlinePlayLounge? lounge { get; set; } public override bool HandleNonPositionalInput => true; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs new file mode 100644 index 0000000000..8fa7d0751f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs @@ -0,0 +1,32 @@ +// 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.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Lounge +{ + public interface IOnlinePlayLounge + { + /// + /// Attempts to join the given room. + /// + /// The room to join. + /// The password. + /// A delegate to invoke if the user joined the room. + /// A delegate to invoke if the user is not able join the room. + void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null); + + /// + /// Clones the given room and opens it as a fresh (not-yet-created) one. + /// + /// The room to clone. + void Clone(Room room); + + /// + /// Closes the given room. + /// + /// The room to close. + void Close(Room room); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f3f4df166a..df17063fdf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -33,7 +34,8 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge { [Cached] - public abstract partial class LoungeSubScreen : OnlinePlaySubScreen + [Cached(typeof(IOnlinePlayLounge))] + public abstract partial class LoungeSubScreen : OnlinePlaySubScreen, IOnlinePlayLounge { public override string Title => "Lounge"; @@ -323,11 +325,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); - /// - /// Copies a room and opens it as a fresh (not-yet-created) one. - /// - /// The room to copy. - public void OpenCopy(Room room) + public void Clone(Room room) { Debug.Assert(room.RoomID != null); @@ -363,6 +361,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge api.Queue(req); } + public void Close(Room room) + { + Debug.Assert(room.RoomID != null); + + var request = new ClosePlaylistRequest(room.RoomID.Value); + request.Success += RefreshRooms; + api.Queue(request); + } + /// /// Push a room as a new subscreen. /// diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 16462b90c1..8988c82dee 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -220,8 +220,6 @@ namespace osu.Game.Screens.OnlinePlay protected abstract string ScreenTitle { get; } - protected virtual RoomManager CreateRoomManager() => new RoomManager(); - protected abstract LoungeSubScreen CreateLounge(); ScreenStack IHasSubScreenStack.SubScreenStack => screenStack; diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index efd0b80ebf..262816ae89 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.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 osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; @@ -17,11 +16,6 @@ namespace osu.Game.Tests.Visual.Multiplayer /// TestMultiplayerClient MultiplayerClient { get; } - /// - /// The cached . - /// - new TestMultiplayerRoomManager RoomManager { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index dca1fc8f3c..d1497d5142 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -17,7 +17,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public const int PLAYER_2_ID = 56; public TestMultiplayerClient MultiplayerClient => OnlinePlayDependencies.MultiplayerClient; - public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public TestSpectatorClient SpectatorClient => OnlinePlayDependencies.SpectatorClient; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; @@ -56,7 +55,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => { SelectedRoom.Value = CreateRoom(); - API.Queue(new CreateRoomRequest(SelectedRoom.Value)); + MultiplayerClient.CreateRoom(SelectedRoom.Value).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index 88202d4327..24c33f2f49 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -3,7 +3,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; -using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; @@ -16,19 +15,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { public TestMultiplayerClient MultiplayerClient { get; } public TestSpectatorClient SpectatorClient { get; } - public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; public MultiplayerTestSceneDependencies() { - MultiplayerClient = new TestMultiplayerClient(RoomManager); + MultiplayerClient = new TestMultiplayerClient(RequestsHandler); SpectatorClient = CreateSpectatorClient(); CacheAs(MultiplayerClient); CacheAs(SpectatorClient); } - protected override IRoomManager CreateRoomManager() => new TestMultiplayerRoomManager(RequestsHandler); - protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient(); } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 70e298f3e0..d514fc0d7e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -10,6 +10,7 @@ using MessagePack; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -17,6 +18,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { @@ -65,15 +67,15 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly TestMultiplayerRoomManager roomManager; - private MultiplayerPlaylistItem? currentItem => ServerRoom?.Playlist[currentIndex]; private int currentIndex; private long lastPlaylistItemId; - public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) + private readonly TestRoomRequestsHandler apiRequestHandler; + + public TestMultiplayerClient(TestRoomRequestsHandler? apiRequestHandler = null) { - this.roomManager = roomManager; + this.apiRequestHandler = apiRequestHandler ?? new TestRoomRequestsHandler(); } public void Connect() => isConnected.Value = true; @@ -214,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer roomId = clone(roomId); password = clone(password); - ServerAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID == roomId); + ServerAPIRoom = ServerSideRooms.Single(r => r.RoomID == roomId); if (password != ServerAPIRoom.Password) throw new InvalidOperationException("Invalid password."); @@ -485,7 +487,15 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task CreateRoom(MultiplayerRoom room) { - throw new NotImplementedException(); + Room apiRoom = new Room(room) + { + Type = room.Settings.MatchType == MatchType.Playlists + ? MatchType.HeadToHead + : room.Settings.MatchType + }; + + AddServerSideRoom(apiRoom, api.LocalUser.Value); + return JoinRoom(apiRoom.RoomID!.Value, room.Settings.Password); } private async Task changeMatchType(MatchType type) @@ -680,5 +690,18 @@ namespace osu.Game.Tests.Visual.Multiplayer isConnected.Value = false; return Task.CompletedTask; } + + #region API Room Handling + + public IReadOnlyList ServerSideRooms + => apiRequestHandler.ServerSideRooms; + + public void AddServerSideRoom(Room room, APIUser host) + => apiRequestHandler.AddServerSideRoom(room, host); + + public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager) + => apiRequestHandler.HandleRequest(request, localUser, beatmapManager); + + #endregion } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs deleted file mode 100644 index 59ac9a9749..0000000000 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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 osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Tests.Visual.OnlinePlay; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - /// - /// A for use in multiplayer test scenes. - /// Should generally not be used by itself outside of a . - /// - public partial class TestMultiplayerRoomManager : RoomManager - { - private readonly TestRoomRequestsHandler requestsHandler; - - public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler) - { - this.requestsHandler = requestsHandler; - } - - public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms; - - /// - /// Adds a room to a local "server-side" list that's returned when a is fired. - /// - /// The room. - /// The host. - public void AddServerSideRoom(Room room, APIUser host) => requestsHandler.AddServerSideRoom(room, host); - } -} diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index e2670c9ad8..203922c057 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); - RoomManager = CreateRoomManager(); + RoomManager = new TestRoomManager(); UserLookupCache = new TestUserLookupCache(); BeatmapLookupCache = new BeatmapLookupCache(); @@ -80,7 +80,5 @@ namespace osu.Game.Tests.Visual.OnlinePlay if (instance is Drawable drawable) drawableComponents.Add(drawable); } - - protected virtual IRoomManager CreateRoomManager() => new TestRoomManager(); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 60d169a46f..bff2753929 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -22,8 +22,14 @@ namespace osu.Game.Tests.Visual.OnlinePlay [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) { + // Can't reference Osu ruleset project here. + ruleset ??= rulesets.GetRuleset(0)!; + for (int i = 0; i < count; i++) { AddRoom(new Room @@ -33,12 +39,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay Duration = TimeSpan.FromSeconds(10), Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, Password = withPassword ? @"password" : null, - PlaylistItemStats = ruleset == null - ? null - : new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, - Playlist = ruleset == null - ? Array.Empty() - : [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] + PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, + Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] }); } } @@ -46,7 +48,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay public void AddRoom(Room room) { room.RoomID = -currentRoomId; - api.Queue(new CreateRoomRequest(room)); + + var req = new CreateRoomRequest(room); + req.Success += AddOrUpdateRoom; + api.Queue(req); + currentRoomId++; } } From 7c38089c7559350de5080cdad9b55d0e5165d41b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 16:22:52 +0900 Subject: [PATCH 014/349] Rename methods --- .../Online/Multiplayer/MultiplayerClient.cs | 37 +++++++------ .../Multiplayer/OnlineMultiplayerClient.cs | 54 +++++++++---------- .../Multiplayer/TestMultiplayerClient.cs | 6 +-- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 7dfe974651..a8f314d372 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -171,7 +171,7 @@ namespace osu.Game.Online.Multiplayer throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); + await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); } /// @@ -187,7 +187,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID != null); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); + await initRoom(room, r => JoinRoomInternal(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); } private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) @@ -236,21 +236,6 @@ namespace osu.Game.Online.Multiplayer { } - /// - /// Creates the with the given settings. - /// - /// The room. - /// The joined - protected abstract Task CreateRoom(MultiplayerRoom room); - - /// - /// Joins the with a given ID. - /// - /// The room ID. - /// An optional password to use when joining the room. - /// The joined . - protected abstract Task JoinRoom(long roomId, string? password = null); - public Task LeaveRoom() { if (Room == null) @@ -279,6 +264,24 @@ namespace osu.Game.Online.Multiplayer }); } + /// + /// Creates the with the given settings. + /// + /// The room. + /// The joined + protected abstract Task CreateRoomInternal(MultiplayerRoom room); + + /// + /// Joins the with a given ID. + /// + /// The room ID. + /// An optional password to use when joining the room. + /// The joined . + protected abstract Task JoinRoomInternal(long roomId, string? password = null); + + /// + /// Leaves the currently-joined . + /// protected abstract Task LeaveRoomInternal(); public abstract Task InvitePlayer(int userId); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 05f3e44405..068ba27789 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -75,7 +75,32 @@ namespace osu.Game.Online.Multiplayer } } - protected override async Task JoinRoom(long roomId, string? password = null) + protected override async Task CreateRoomInternal(MultiplayerRoom room) + { + if (!IsConnected.Value) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + + try + { + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); + } + catch (HubException exception) + { + if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) + { + Debug.Assert(connector != null); + + await connector.Reconnect().ConfigureAwait(false); + return await CreateRoomInternal(room).ConfigureAwait(false); + } + + throw; + } + } + + protected override async Task JoinRoomInternal(long roomId, string? password = null) { if (!IsConnected.Value) throw new OperationCanceledException(); @@ -93,7 +118,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(connector != null); await connector.Reconnect().ConfigureAwait(false); - return await JoinRoom(roomId, password).ConfigureAwait(false); + return await JoinRoomInternal(roomId, password).ConfigureAwait(false); } throw; @@ -266,31 +291,6 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } - protected override async Task CreateRoom(MultiplayerRoom room) - { - if (!IsConnected.Value) - throw new OperationCanceledException(); - - Debug.Assert(connection != null); - - try - { - return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); - } - catch (HubException exception) - { - if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) - { - Debug.Assert(connector != null); - - await connector.Reconnect().ConfigureAwait(false); - return await CreateRoom(room).ConfigureAwait(false); - } - - throw; - } - } - public override Task DisconnectInternal() { if (connector == null) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index d514fc0d7e..359b223ad2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(clone(userId), clone(user.BeatmapAvailability)); } - protected override async Task JoinRoom(long roomId, string? password = null) + protected override async Task JoinRoomInternal(long roomId, string? password = null) { if (RoomJoined || ServerAPIRoom != null) throw new InvalidOperationException("Already joined a room"); @@ -485,7 +485,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); - protected override Task CreateRoom(MultiplayerRoom room) + protected override Task CreateRoomInternal(MultiplayerRoom room) { Room apiRoom = new Room(room) { @@ -495,7 +495,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }; AddServerSideRoom(apiRoom, api.LocalUser.Value); - return JoinRoom(apiRoom.RoomID!.Value, room.Settings.Password); + return JoinRoomInternal(apiRoom.RoomID!.Value, room.Settings.Password); } private async Task changeMatchType(MatchType type) From a198b0830affdab861037c0a90525946fa446b5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 17:18:01 +0900 Subject: [PATCH 015/349] Add comment indicating RoomManager shouldn't exist --- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 3abb4098fb..a1b61ea7a3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -13,6 +13,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { + // Todo: This class should be inlined into the lounge. public partial class RoomManager : Component, IRoomManager { public event Action? RoomsUpdated; From f2d8ea299777ad6168eb90d04a574d10bf083837 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 18:25:55 +0900 Subject: [PATCH 016/349] Fix incorrect continuation --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 279b140d36..72b581eac1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -472,7 +472,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { client.CreateRoom(room).ContinueWith(t => Schedule(() => { - if (t.IsCompleted) + if (t.IsCompletedSuccessfully) onSuccess(room); else if (t.IsFaulted) { From 6dbf466009f6ab12f2613eebb970a2a1d1e101b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 18:30:11 +0900 Subject: [PATCH 017/349] Fix incorrect exception handling In particular, when the exception is: `AggregateException { AggregateException { HubException } }`, then the existing code will only unwrap the first aggregate exception. The overlay's code was copied from the extension so both have been adjusted here. --- .../Online/Multiplayer/MultiplayerClientExtensions.cs | 9 +++------ .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 8 ++------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs index d846e7f566..1cc5a8e70a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; namespace osu.Game.Online.Multiplayer @@ -16,12 +17,8 @@ namespace osu.Game.Online.Multiplayer { if (t.IsFaulted) { - Exception? exception = t.Exception; - - if (exception is AggregateException ae) - exception = ae.InnerException; - - Debug.Assert(exception != null); + Debug.Assert(t.Exception != null); + Exception exception = t.Exception.AsSingular(); if (exception.GetHubExceptionMessage() is string message) // Hub exceptions generally contain something we can show the user directly. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 72b581eac1..2a5a83fadf 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -476,12 +476,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match onSuccess(room); else if (t.IsFaulted) { - Exception? exception = t.Exception; - - if (exception is AggregateException ae) - exception = ae.InnerException; - - Debug.Assert(exception != null); + Debug.Assert(t.Exception != null); + Exception exception = t.Exception.AsSingular(); if (exception.GetHubExceptionMessage() is string message) onError(message); From e9d6411e615ba85a2989511a9f374682b20d25cf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 19:10:11 +0900 Subject: [PATCH 018/349] Clean up error handling --- .../Match/MultiplayerMatchSettingsOverlay.cs | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 2a5a83fadf..eda3bace40 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -463,9 +463,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match .ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) - onSuccess(room); + onSuccess(); else - onError(t.Exception?.AsSingular().Message ?? "Error changing settings."); + onError(t.Exception, "Error changing settings"); })); } else @@ -473,26 +473,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match client.CreateRoom(room).ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) - onSuccess(room); - else if (t.IsFaulted) - { - Debug.Assert(t.Exception != null); - Exception exception = t.Exception.AsSingular(); - - if (exception.GetHubExceptionMessage() is string message) - onError(message); - else - onError($"Error creating room: {exception}"); - } + onSuccess(); else - onError("Error creating room."); + onError(t.Exception, "Error creating room"); })); } } private void hideError() => ErrorText.FadeOut(50); - private void onSuccess(Room room) => Schedule(() => + private void onSuccess() => Schedule(() => { Debug.Assert(applyingSettingsOperation != null); @@ -502,28 +492,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match applyingSettingsOperation = null; }); - private void onError(string text) => Schedule(() => + private void onError(Exception? exception, string description) { - Debug.Assert(applyingSettingsOperation != null); + if (exception is AggregateException aggregateException) + exception = aggregateException.AsSingular(); - // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. - const string not_found_prefix = "beatmaps not found:"; + string message = exception?.GetHubExceptionMessage() ?? $"{description} ({exception?.Message})"; - if (text.StartsWith(not_found_prefix, StringComparison.Ordinal)) + Schedule(() => { - ErrorText.Text = "The selected beatmap is not available online."; - room.Playlist.SingleOrDefault()?.MarkInvalid(); - } - else - { - ErrorText.Text = text; - } + Debug.Assert(applyingSettingsOperation != null); - ErrorText.FadeIn(50); + // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. + const string not_found_prefix = "beatmaps not found:"; - applyingSettingsOperation.Dispose(); - applyingSettingsOperation = null; - }); + if (message.StartsWith(not_found_prefix, StringComparison.Ordinal)) + { + ErrorText.Text = "The selected beatmap is not available online."; + room.Playlist.SingleOrDefault()?.MarkInvalid(); + } + else + ErrorText.Text = message; + + ErrorText.FadeIn(50); + + applyingSettingsOperation.Dispose(); + applyingSettingsOperation = null; + }); + } protected override void Dispose(bool isDisposing) { From ab4162e2aafc4e246ba070870e4967ab7a6e00cb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 25 Jan 2025 19:27:21 +0900 Subject: [PATCH 019/349] Various refactorings and cleanups --- .../TestSceneMultiplayerLoungeSubScreen.cs | 28 +++++-------------- .../TestScenePlaylistsLoungeSubScreen.cs | 28 +++---------------- .../Multiplayer/IMultiplayerLoungeServer.cs | 5 ++++ .../Online/Multiplayer/MultiplayerClient.cs | 3 +- osu.Game/Online/Rooms/CreateRoomRequest.cs | 2 ++ osu.Game/Online/Rooms/JoinRoomRequest.cs | 2 ++ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 +- .../OnlinePlay/Lounge/IOnlinePlayLounge.cs | 6 ++-- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 6 ++-- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 3 ++ .../Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- .../Playlists/PlaylistsLoungeSubScreen.cs | 2 +- 12 files changed, 34 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 4a259149e2..eb649acd2d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - addRoom(false); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnBackButton() { - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnLeavingScreen() { - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -149,20 +149,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room joined", () => MultiplayerClient.RoomJoined); } - private void addRoom(bool withPassword) - { - int initialRoomCount = 0; - - AddStep("add room", () => - { - initialRoomCount = roomsContainer.Rooms.Count; - RoomManager.AddRooms(1, withPassword: withPassword); - loungeScreen.RefreshRooms(); - }); - - AddUntilStep("wait for room to appear", () => roomsContainer.Rooms.Count == initialRoomCount + 1); - } - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 0897a3b2f5..53c7873de5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -35,12 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(500); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(500)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); } @@ -49,12 +44,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => - { - RoomManager.AddRooms(30); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(30)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -71,12 +61,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(30); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(30)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -90,12 +75,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(1); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(1)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index c5eb6f9b36..0ee9fa54cd 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -10,6 +10,11 @@ namespace osu.Game.Online.Multiplayer /// public interface IMultiplayerLoungeServer { + /// + /// Request to create a multiplayer room. + /// + /// The room to create. + /// The created multiplayer room. Task CreateRoom(MultiplayerRoom room); /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a8f314d372..6749ed9535 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -168,7 +168,7 @@ namespace osu.Game.Online.Multiplayer public async Task CreateRoom(Room room) { if (Room != null) - throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + throw new InvalidOperationException("Cannot create a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); @@ -212,6 +212,7 @@ namespace osu.Game.Online.Multiplayer APIRoom.RoomID = joinedRoom.RoomID; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); + // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. APIRoom.EndDate = null; Debug.Assert(LocalUser != null); diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs index 9773bb5e7d..5b2ea77aad 100644 --- a/osu.Game/Online/Rooms/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -15,6 +15,8 @@ namespace osu.Game.Online.Rooms public CreateRoomRequest(Room room) { Room = room; + + // Also copy back to the source model, since it is likely to have been stored elsewhere. Success += r => Room.CopyFrom(r); } diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 13e7ac8c84..610e887242 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -16,6 +16,8 @@ namespace osu.Game.Online.Rooms { Room = room; Password = password; + + // Also copy back to the source model, since it is likely to have been stored elsewhere. Success += r => Room.CopyFrom(r); } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 032a231ad3..5de35ef101 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - lounge?.Clone(Room); + lounge?.OpenCopy(Room); }) }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs index 8fa7d0751f..73ab84af13 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs @@ -18,10 +18,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null); /// - /// Clones the given room and opens it as a fresh (not-yet-created) one. + /// Copies the given room and opens it as a fresh (not-yet-created) one. /// - /// The room to clone. - void Clone(Room room); + /// The room to copy. + void OpenCopy(Room room); /// /// Closes the given room. diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index df17063fdf..0e08e398a4 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -309,7 +309,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - TryJoin(room, password, r => + JoinInternal(room, password, r => { Open(room); joiningRoomOperation?.Dispose(); @@ -323,9 +323,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }); }); - protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); + protected abstract void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure); - public void Clone(Room room) + public void OpenCopy(Room room) { Debug.Assert(room.RoomID != null); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index d37f3b877c..80b3961f44 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -353,6 +353,9 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnExiting(e); } + /// + /// Parts from the current room. + /// protected abstract void PartRoom(); private bool ensureExitConfirmed() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index e901ecbdce..873a9cde88 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); - protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { client.JoinRoom(room, password).ContinueWith(result => { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 92415e0eb1..6ed367328c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return criteria; } - protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { var joinRoomRequest = new JoinRoomRequest(room, password); From 8c85616d1c8677a859bf007291997b092786f94c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 21:28:21 +0900 Subject: [PATCH 020/349] Fix test --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 66c465cbed..bd1e15d06d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay Bindable playingState = new Bindable(); GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); TestSpectatorClient spectatorClient = new TestSpectatorClient(); - TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler())); + TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestRoomRequestsHandler()); AddStep("create spectator list", () => { From 068a66e7d4c9bef92ea39e5237b37bc628e9e14f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 18:35:35 +0900 Subject: [PATCH 021/349] Move room tracking to lounge subscreen --- .../TestSceneLoungeRoomsContainer.cs | 28 ++++----- .../TestScenePlaylistsLoungeSubScreen.cs | 30 ++++----- .../Components/ListingPollingComponent.cs | 38 ++---------- .../Components/SelectionPollingComponent.cs | 5 +- .../Lounge/Components/RoomsContainer.cs | 45 ++++---------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 62 ++++++++++++++----- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 34 ---------- .../Playlists/PlaylistsLoungeSubScreen.cs | 3 - 8 files changed, 95 insertions(+), 150 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 797b69ec72..10df77f88c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -50,17 +50,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true)); - AddAssert("has 5 rooms", () => container.Rooms.Count == 5); + AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5); - AddAssert("all spotlights at top", () => container.Rooms + AddAssert("all spotlights at top", () => container.DrawableRooms .SkipWhile(r => r.Room.Category == RoomCategory.Spotlight) .All(r => r.Room.Category == RoomCategory.Normal)); AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0))); - AddAssert("has 4 rooms", () => container.Rooms.Count == 4); - AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID != 0)); + AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4); + AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0)); - AddStep("select first room", () => container.Rooms.First().TriggerClick()); + AddStep("select first room", () => container.DrawableRooms.First().TriggerClick()); AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!)); @@ -137,15 +137,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => RoomManager.AddRooms(4)); - AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); + AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" }); - AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1); + AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1); AddStep("remove filter", () => container.Filter.Value = null); - AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); + AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); } [Test] @@ -156,13 +156,13 @@ namespace osu.Game.Tests.Visual.Multiplayer // Todo: What even is this case...? AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria()); - AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); + AddUntilStep("5 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 5); AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo }); - AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); + AddUntilStep("2 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2); AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }); - AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); + AddUntilStep("3 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 3); } [Test] @@ -176,15 +176,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("apply default filter", () => container.Filter.SetDefault()); - AddUntilStep("both rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); + AddUntilStep("both rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2); AddStep("filter public rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Public }); - AddUntilStep("private room hidden", () => container.Rooms.All(r => !r.Room.HasPassword)); + AddUntilStep("private room hidden", () => container.DrawableRooms.All(r => !r.Room.HasPassword)); AddStep("filter private rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Private }); - AddUntilStep("public room hidden", () => container.Rooms.All(r => r.Room.HasPassword)); + AddUntilStep("public room hidden", () => container.DrawableRooms.All(r => r.Room.HasPassword)); } [Test] diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 53c7873de5..9d65be2a19 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Playlists public void TestManyRooms() { AddStep("add rooms", () => RoomManager.AddRooms(500)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 500); } [Test] @@ -45,45 +45,45 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); + AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.Rooms[2])); + AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.Rooms[0])); + AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[0])); AddAssert("first and second room masked", () - => !checkRoomVisible(roomsContainer.Rooms[0]) && - !checkRoomVisible(roomsContainer.Rooms[1])); + => !checkRoomVisible(roomsContainer.DrawableRooms[0]) && + !checkRoomVisible(roomsContainer.DrawableRooms[1])); } [Test] public void TestScrollSelectedIntoView() { AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); + AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddStep("select last room", () => roomsContainer.Rooms[^1].TriggerClick()); + AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); - AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms[0])); - AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1])); + AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.DrawableRooms[0])); + AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); } [Test] public void TestEnteringRoomTakesLeaseOnSelection() { AddStep("add rooms", () => RoomManager.AddRooms(1)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); - AddStep("select room", () => roomsContainer.Rooms[0].TriggerClick()); + AddStep("select room", () => roomsContainer.DrawableRooms[0].TriggerClick()); AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - AddStep("enter room", () => roomsContainer.Rooms[0].TriggerClick()); + AddStep("enter room", () => roomsContainer.DrawableRooms[0].TriggerClick()); AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen); diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index 21452727b8..5cb4c9420a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -1,9 +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; using System.Linq; using System.Threading.Tasks; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -15,23 +15,8 @@ namespace osu.Game.Screens.OnlinePlay.Components /// public partial class ListingPollingComponent : RoomPollingComponent { - public IBindable InitialRoomsReceived => initialRoomsReceived; - private readonly Bindable initialRoomsReceived = new Bindable(); - - public readonly Bindable Filter = new Bindable(); - - [BackgroundDependencyLoader] - private void load() - { - Filter.BindValueChanged(_ => - { - RoomManager.ClearRooms(); - initialRoomsReceived.Value = false; - - if (IsLoaded) - PollImmediately(); - }); - } + public required Action RoomsReceived { get; init; } + public readonly IBindable Filter = new Bindable(); private GetRoomsRequest? lastPollRequest; @@ -43,26 +28,14 @@ namespace osu.Game.Screens.OnlinePlay.Components if (Filter.Value == null) return base.Poll(); - var tcs = new TaskCompletionSource(); - lastPollRequest?.Cancel(); + var tcs = new TaskCompletionSource(); var req = new GetRoomsRequest(Filter.Value); req.Success += result => { - result = result.Where(r => r.Category != RoomCategory.DailyChallenge).ToList(); - - foreach (var existing in RoomManager.Rooms.ToArray()) - { - if (result.All(r => r.RoomID != existing.RoomID)) - RoomManager.RemoveRoom(existing); - } - - foreach (var incoming in result) - RoomManager.AddOrUpdateRoom(incoming); - - initialRoomsReceived.Value = true; + RoomsReceived(result.Where(r => r.Category != RoomCategory.DailyChallenge).ToArray()); tcs.SetResult(true); }; @@ -71,6 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Components API.Queue(req); lastPollRequest = req; + return tcs.Task; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 7cee8b3546..f04fd6a096 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -28,15 +28,14 @@ namespace osu.Game.Screens.OnlinePlay.Components if (room.RoomID == null) return base.Poll(); - var tcs = new TaskCompletionSource(); - lastPollRequest?.Cancel(); + var tcs = new TaskCompletionSource(); var req = new GetRoomRequest(room.RoomID.Value); req.Success += result => { - RoomManager.AddOrUpdateRoom(result); + room.CopyFrom(result); tcs.SetResult(true); }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 6eda993f94..6681cbe720 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -7,10 +7,8 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -24,17 +22,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public partial class RoomsContainer : CompositeDrawable, IKeyBindingHandler { + public readonly BindableList Rooms = new BindableList(); public readonly Bindable SelectedRoom = new Bindable(); public readonly Bindable Filter = new Bindable(); - public IReadOnlyList Rooms => roomFlow.FlowingChildren.Cast().ToArray(); + public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); - private readonly IBindableList rooms = new BindableList(); private readonly FillFlowContainer roomFlow; - [Resolved] - private IRoomManager roomManager { get; set; } = null!; - // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; @@ -62,11 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override void LoadComplete() { - rooms.CollectionChanged += roomsChanged; - roomManager.RoomsUpdated += updateSorting; - - rooms.BindTo(roomManager.Rooms); - + Rooms.BindCollectionChanged(roomsChanged, true); Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true); } @@ -155,7 +146,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void addRooms(IEnumerable rooms) { foreach (var room in rooms) - roomFlow.Add(new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }); + { + var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }; + + roomFlow.Add(drawableRoom); + + // Always show spotlight playlists at the top of the listing. + roomFlow.SetLayoutPosition(drawableRoom, room.Category > RoomCategory.Normal ? float.MinValue : -(room.RoomID ?? 0)); + } applyFilterCriteria(Filter.Value); } @@ -181,17 +179,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedRoom.Value = null; } - private void updateSorting() - { - foreach (var room in roomFlow) - { - roomFlow.SetLayoutPosition(room, room.Room.Category > RoomCategory.Normal - // Always show spotlight playlists at the top of the listing. - ? float.MinValue - : -(room.Room.RoomID ?? 0)); - } - } - protected override bool OnClick(ClickEvent e) { if (!SelectedRoom.Disabled) @@ -226,7 +213,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (SelectedRoom.Disabled) return; - var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent); + var visibleRooms = DrawableRooms.AsEnumerable().Where(r => r.IsPresent); Room? room; @@ -246,13 +233,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } #endregion - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (roomManager.IsNotNull()) - roomManager.RoomsUpdated -= updateSorting; - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 0e08e398a4..78501a56d7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -53,8 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge AutoSizeAxes = Axes.Both }; - protected ListingPollingComponent ListingPollingComponent { get; private set; } = null!; - protected readonly Bindable SelectedRoom = new Bindable(); [Resolved] @@ -75,12 +73,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] protected OsuConfigManager Config { get; private set; } = null!; - private IDisposable? joiningRoomOperation { get; set; } + private IDisposable? joiningRoomOperation; private LeasedBindable? selectionLease; + private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); + private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); + private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private RoomsContainer roomsContainer = null!; @@ -100,7 +101,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - ListingPollingComponent = CreatePollingComponent().With(c => c.Filter.BindTarget = filter), + listingPollingComponent = new ListingPollingComponent + { + RoomsReceived = onListingReceived, + Filter = { BindTarget = filter } + }, popoverContainer = new PopoverContainer { Name = @"Rooms area", @@ -116,8 +121,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = SelectedRoom }, Filter = { BindTarget = filter }, - SelectedRoom = { BindTarget = SelectedRoom } } }, }, @@ -178,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge // scroll selected room into view on selection. SelectedRoom.BindValueChanged(val => { - var drawable = roomsContainer.Rooms.FirstOrDefault(r => r.Room == val.NewValue); + var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); if (drawable != null) scrollContainer.ScrollIntoView(drawable); }); @@ -190,7 +196,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced()); ruleset.BindValueChanged(_ => UpdateFilter()); - isIdle.BindValueChanged(_ => updatePollingRate(this.IsCurrentScreen()), true); if (ongoingOperationTracker != null) @@ -199,11 +204,38 @@ namespace osu.Game.Screens.OnlinePlay.Lounge operationInProgress.BindValueChanged(_ => updateLoadingLayer()); } - ListingPollingComponent.InitialRoomsReceived.BindValueChanged(_ => updateLoadingLayer(), true); + hasListingResults.BindValueChanged(_ => updateLoadingLayer()); + + filter.BindValueChanged(_ => + { + rooms.Clear(); + hasListingResults.Value = false; + listingPollingComponent.PollImmediately(); + }); updateFilter(); } + private void onListingReceived(Room[] result) + { + Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); + + // Remove all local rooms no longer in the result set. + rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + + // Add or update local rooms with the result set. + foreach (var r in result) + { + if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) + existingRoom.CopyFrom(r); + else + rooms.Add(r); + } + + hasListingResults.Value = true; + } + #region Filtering public void UpdateFilter() => Scheduler.AddOnce(updateFilter); @@ -267,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - ListingPollingComponent.PollImmediately(); + listingPollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -392,11 +424,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge this.Push(CreateRoomSubScreen(room)); } - public void RefreshRooms() => ListingPollingComponent.PollImmediately(); + public void RefreshRooms() => listingPollingComponent.PollImmediately(); private void updateLoadingLayer() { - if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value) + if (operationInProgress.Value || !hasListingResults.Value) loadingLayer.Show(); else loadingLayer.Hide(); @@ -405,11 +437,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - ListingPollingComponent.TimeBetweenPolls.Value = 0; + listingPollingComponent.TimeBetweenPolls.Value = 0; else - ListingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + listingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {ListingPollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {listingPollingComponent.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); @@ -421,7 +453,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected abstract Room CreateNewRoom(); protected abstract RoomSubScreen CreateRoomSubScreen(Room room); - - protected abstract ListingPollingComponent CreatePollingComponent(); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 873a9cde88..3cf873ec78 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -79,8 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); - protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); - protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { client.JoinRoom(room, password).ContinueWith(result => @@ -109,37 +107,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.OpenNewRoom(room); } - - private partial class MultiplayerListingPollingComponent : ListingPollingComponent - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private readonly IBindable isConnected = new Bindable(); - - [BackgroundDependencyLoader] - private void load() - { - isConnected.BindTo(client.IsConnected); - isConnected.BindValueChanged(_ => Scheduler.AddOnce(poll), true); - } - - private void poll() - { - if (isConnected.Value && IsLoaded) - PollImmediately(); - } - - protected override Task Poll() - { - if (!isConnected.Value) - return Task.CompletedTask; - - if (client.Room != null) - return Task.CompletedTask; - - return base.Poll(); - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 6ed367328c..26eae50797 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -87,8 +86,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); - protected override ListingPollingComponent CreatePollingComponent() => new ListingPollingComponent(); - private enum PlaylistsCategory { Any, From f146a7d116bbebec4880c8a3dd7124d20dc58022 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:09:58 +0900 Subject: [PATCH 022/349] Remove `RoomManager` and related components --- .../TestSceneLoungeRoomsContainer.cs | 54 ++++++------ .../TestSceneMultiplayerLoungeSubScreen.cs | 33 +++++--- .../TestScenePlaylistsLoungeSubScreen.cs | 30 +++---- .../TestScenePlaylistsMatchSettingsOverlay.cs | 2 - .../Components/ListingPollingComponent.cs | 14 +++- .../OnlinePlay/Components/RoomManager.cs | 82 ------------------- .../Components/RoomPollingComponent.cs | 18 ---- .../Components/SelectionPollingComponent.cs | 14 +++- osu.Game/Screens/OnlinePlay/IRoomManager.cs | 42 ---------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 12 +-- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 5 -- .../IOnlinePlayTestSceneDependencies.cs | 5 -- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 32 +++++++- .../OnlinePlayTestSceneDependencies.cs | 3 - .../Visual/OnlinePlay/TestRoomManager.cs | 59 ------------- .../OnlinePlay/TestRoomRequestsHandler.cs | 3 +- 16 files changed, 121 insertions(+), 287 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomManager.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs delete mode 100644 osu.Game/Screens/OnlinePlay/IRoomManager.cs delete mode 100644 osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 10df77f88c..9daad960c7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -19,8 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - + private BindableList rooms = null!; private RoomsContainer container = null!; public override void SetUpSteps() @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create container", () => { + rooms = new BindableList(); Child = new PopoverContainer { RelativeSizeAxes = Axes.X, @@ -36,9 +37,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, - Child = container = new RoomsContainer { + Rooms = { BindTarget = rooms }, SelectedRoom = { BindTarget = SelectedRoom } } }; @@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBasicListChanges() { - AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withSpotlightRooms: true))); AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5); @@ -56,49 +57,50 @@ namespace osu.Game.Tests.Visual.Multiplayer .SkipWhile(r => r.Room.Category == RoomCategory.Spotlight) .All(r => r.Room.Category == RoomCategory.Normal)); - AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0))); + AddStep("remove first room", () => rooms.RemoveAt(0)); AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4); AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0)); AddStep("select first room", () => container.DrawableRooms.First().TriggerClick()); - AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddAssert("first spotlight selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); - AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!)); - AddAssert("first spotlight still selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddStep("remove last room", () => rooms.RemoveAt(rooms.Count - 1)); + AddAssert("first spotlight still selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); - AddStep("remove spotlight room", () => RoomManager.RemoveRoom(RoomManager.Rooms.Single(r => r.Category == RoomCategory.Spotlight))); + AddStep("remove spotlight room", () => rooms.RemoveAll(r => r.Category == RoomCategory.Spotlight)); AddAssert("selection vacated", () => checkRoomSelected(null)); } [Test] public void TestKeyboardNavigation() { - AddStep("add rooms", () => RoomManager.AddRooms(3)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3))); AddAssert("no selection", () => checkRoomSelected(null)); press(Key.Down); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); press(Key.Up); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); press(Key.Down); press(Key.Down); - AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last())); + AddAssert("last room selected", () => checkRoomSelected(container.DrawableRooms.Last().Room)); } [Test] public void TestKeyboardNavigationAfterOrderChange() { - AddStep("add rooms", () => RoomManager.AddRooms(3)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3))); AddStep("reorder rooms", () => { - var room = RoomManager.Rooms[1]; + var room = rooms[1]; + rooms.Remove(room); - RoomManager.RemoveRoom(room); - RoomManager.AddOrUpdateRoom(room); + room.RoomID += 3; + rooms.Add(room); }); AddAssert("no selection", () => checkRoomSelected(null)); @@ -116,12 +118,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestClickDeselection() { - AddStep("add room", () => RoomManager.AddRooms(1)); + AddStep("add room", () => rooms.AddRange(GenerateRooms(1))); AddAssert("no selection", () => checkRoomSelected(null)); press(Key.Down); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); AddStep("click away", () => InputManager.Click(MouseButton.Left)); AddAssert("no selection", () => checkRoomSelected(null)); @@ -135,11 +137,11 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStringFiltering() { - AddStep("add rooms", () => RoomManager.AddRooms(4)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(4))); AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); - AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" }); + AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = rooms.First().Name }); AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1); @@ -151,8 +153,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestRulesetFiltering() { - AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo)); - AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(2, new OsuRuleset().RulesetInfo))); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, new CatchRuleset().RulesetInfo))); // Todo: What even is this case...? AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria()); @@ -170,8 +172,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => { - RoomManager.AddRooms(1, withPassword: true); - RoomManager.AddRooms(1, withPassword: false); + rooms.AddRange(GenerateRooms(1, withPassword: true)); + rooms.AddRange(GenerateRooms(1, withPassword: false)); }); AddStep("apply default filter", () => container.Filter.SetDefault()); @@ -190,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPasswordProtectedRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(3, withPassword: true)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index eb649acd2d..b4ec9d5858 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -8,8 +8,8 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; -using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; @@ -18,11 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - - private LoungeSubScreen loungeScreen = null!; - - private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + private MultiplayerLoungeSubScreen loungeScreen = null!; public TestSceneMultiplayerLoungeSubScreen() : base(false) @@ -40,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); + createRooms(GenerateRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); @@ -50,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnBackButton() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -70,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnLeavingScreen() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -86,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -105,7 +101,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -124,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -149,6 +145,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room joined", () => MultiplayerClient.RoomJoined); } + private void createRooms(params Room[] rooms) + { + AddStep("create rooms", () => + { + foreach (var room in rooms) + API.Queue(new CreateRoomRequest(room)); + }); + + AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); + } + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 9d65be2a19..94a81ecdc7 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - private TestLoungeSubScreen loungeScreen = null!; public override void SetUpSteps() @@ -26,7 +24,6 @@ namespace osu.Game.Tests.Visual.Playlists base.SetUpSteps(); AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen())); - AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } @@ -35,8 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(500)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 500); + createRooms(GenerateRooms(500)); } [Test] @@ -44,10 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); + createRooms(GenerateRooms(30)); AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); @@ -61,10 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); + createRooms(GenerateRooms(30)); AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); @@ -75,8 +65,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => RoomManager.AddRooms(1)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 1); + createRooms(GenerateRooms(1)); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); @@ -95,6 +84,17 @@ namespace osu.Game.Tests.Visual.Playlists loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad .Contains(room.ScreenSpaceDrawQuad.Centre); + private void createRooms(params Room[] rooms) + { + AddStep("create rooms", () => + { + foreach (var room in rooms) + API.Queue(new CreateRoomRequest(room)); + }); + + AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); + } + private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen { public new Bindable SelectedRoom => base.SelectedRoom; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 51e39e1b7f..f7b0bc0d58 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - private TestRoomSettings settings = null!; private Func? handleRequest; diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index 5cb4c9420a..1495f97de4 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -4,17 +4,23 @@ using System; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Screens.OnlinePlay.Components { /// - /// A that polls for the lounge listing. + /// A that polls for the lounge listing. /// - public partial class ListingPollingComponent : RoomPollingComponent + public partial class ListingPollingComponent : PollingComponent { + [Resolved] + private IAPIProvider api { get; set; } = null!; + public required Action RoomsReceived { get; init; } public readonly IBindable Filter = new Bindable(); @@ -22,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components protected override Task Poll() { - if (!API.IsLoggedIn) + if (!api.IsLoggedIn) return base.Poll(); if (Filter.Value == null) @@ -41,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += _ => tcs.SetResult(false); - API.Queue(req); + api.Queue(req); lastPollRequest = req; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs deleted file mode 100644 index a1b61ea7a3..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using osu.Framework.Bindables; -using osu.Framework.Development; -using osu.Framework.Graphics; -using osu.Framework.Logging; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - // Todo: This class should be inlined into the lounge. - public partial class RoomManager : Component, IRoomManager - { - public event Action? RoomsUpdated; - - private readonly BindableList rooms = new BindableList(); - - public IBindableList Rooms => rooms; - - public RoomManager() - { - RelativeSizeAxes = Axes.Both; - } - - private readonly HashSet ignoredRooms = new HashSet(); - - public void AddOrUpdateRoom(Room room) - { - Debug.Assert(ThreadSafety.IsUpdateThread); - Debug.Assert(room.RoomID != null); - - if (ignoredRooms.Contains(room.RoomID.Value)) - return; - - try - { - var existing = rooms.FirstOrDefault(e => e.RoomID == room.RoomID); - if (existing == null) - rooms.Add(room); - else - existing.CopyFrom(room); - } - catch (Exception ex) - { - Logger.Error(ex, $"Failed to update room: {room.Name}."); - - ignoredRooms.Add(room.RoomID.Value); - rooms.Remove(room); - } - - notifyRoomsUpdated(); - } - - public void RemoveRoom(Room room) - { - Debug.Assert(ThreadSafety.IsUpdateThread); - - rooms.Remove(room); - notifyRoomsUpdated(); - } - - public void ClearRooms() - { - Debug.Assert(ThreadSafety.IsUpdateThread); - - rooms.Clear(); - notifyRoomsUpdated(); - } - - private void notifyRoomsUpdated() - { - Scheduler.AddOnce(invokeRoomsUpdated); - - void invokeRoomsUpdated() => RoomsUpdated?.Invoke(); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs deleted file mode 100644 index 0ba7f20f1c..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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.Game.Online; -using osu.Game.Online.API; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - public abstract partial class RoomPollingComponent : PollingComponent - { - [Resolved] - protected IAPIProvider API { get; private set; } = null!; - - [Resolved] - protected IRoomManager RoomManager { get; private set; } = null!; - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index f04fd6a096..bfa059f72e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -2,15 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { /// - /// A that polls for the currently-selected room. + /// A that polls for and updates a room. /// - public partial class SelectionPollingComponent : RoomPollingComponent + public partial class SelectionPollingComponent : PollingComponent { + [Resolved] + private IAPIProvider api { get; set; } = null!; + private readonly Room room; public SelectionPollingComponent(Room room) @@ -22,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components protected override Task Poll() { - if (!API.IsLoggedIn) + if (!api.IsLoggedIn) return base.Poll(); if (room.RoomID == null) @@ -41,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += _ => tcs.SetResult(false); - API.Queue(req); + api.Queue(req); lastPollRequest = req; diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs deleted file mode 100644 index 8ecb1dd7e0..0000000000 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay -{ - [Cached(typeof(IRoomManager))] - public interface IRoomManager - { - /// - /// Invoked when the s have been updated. - /// - event Action RoomsUpdated; - - /// - /// All the active s. - /// - IBindableList Rooms { get; } - - /// - /// Adds a to this . - /// If already existing, the local room will be updated with the given one. - /// - /// The incoming . - void AddOrUpdateRoom(Room room); - - /// - /// Removes a from this . - /// - /// The to remove. - void RemoveRoom(Room room); - - /// - /// Removes all s from this . - /// - void ClearRooms(); - } -} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 78501a56d7..6c383f1bf6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -54,6 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }; protected readonly Bindable SelectedRoom = new Bindable(); + protected readonly BindableList Rooms = new BindableList(); [Resolved] private MusicController music { get; set; } = null!; @@ -76,7 +77,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IDisposable? joiningRoomOperation; private LeasedBindable? selectionLease; - private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); @@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { - Rooms = { BindTarget = rooms }, + Rooms = { BindTarget = Rooms }, SelectedRoom = { BindTarget = SelectedRoom }, Filter = { BindTarget = filter }, } @@ -208,7 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - rooms.Clear(); + Rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -218,11 +218,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -230,7 +230,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - rooms.Add(r); + Rooms.Add(r); } hasListingResults.Value = true; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 8988c82dee..812e42479b 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Menu; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Users; @@ -39,9 +38,6 @@ namespace osu.Game.Screens.OnlinePlay [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - [Cached(Type = typeof(IRoomManager))] - private readonly RoomManager roomManager = new RoomManager(); - [Resolved] protected IAPIProvider API { get; private set; } = null!; @@ -65,7 +61,6 @@ namespace osu.Game.Screens.OnlinePlay { screenStack, new Header(ScreenTitle, screenStack), - roomManager, ongoingOperationTracker, } }; diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 8ddc5325db..5780cf6eff 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -18,11 +18,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// Bindable SelectedRoom { get; } - /// - /// The cached - /// - IRoomManager RoomManager { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 3f6c175fbd..c3a5e1c3ec 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -10,7 +10,9 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay; namespace osu.Game.Tests.Visual.OnlinePlay @@ -21,7 +23,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies { public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom; - public IRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker; public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache; @@ -34,9 +35,13 @@ namespace osu.Game.Tests.Visual.OnlinePlay protected override Container Content => content; + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + private readonly Container content; private readonly Container drawableDependenciesContainer; private DelegatedDependencyContainer dependencies = null!; + private int currentRoomId; protected OnlinePlayTestScene() { @@ -93,6 +98,31 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies(); + protected Room[] GenerateRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) + { + Room[] rooms = new Room[count]; + + // Can't reference Osu ruleset project here. + ruleset ??= rulesets.GetRuleset(0)!; + + for (int i = 0; i < count; i++) + { + rooms[i] = new Room + { + RoomID = currentRoomId++, + Name = $@"Room {currentRoomId}", + Host = new APIUser { Username = @"Host" }, + Duration = TimeSpan.FromSeconds(10), + Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, + Password = withPassword ? @"password" : null, + PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, + Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] + }; + } + + return rooms; + } + /// /// A providing a mutable lookup source for online play dependencies. /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 203922c057..cc448beea0 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -19,7 +19,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies { public Bindable SelectedRoom { get; } - public IRoomManager RoomManager { get; } public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } @@ -40,7 +39,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); - RoomManager = new TestRoomManager(); UserLookupCache = new TestUserLookupCache(); BeatmapLookupCache = new BeatmapLookupCache(); @@ -48,7 +46,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(RequestsHandler); CacheAs(SelectedRoom); - CacheAs(RoomManager); CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs deleted file mode 100644 index bff2753929..0000000000 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay.Components; - -namespace osu.Game.Tests.Visual.OnlinePlay -{ - /// - /// A very simple for use in online play test scenes. - /// - public partial class TestRoomManager : RoomManager - { - private int currentRoomId; - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) - { - // Can't reference Osu ruleset project here. - ruleset ??= rulesets.GetRuleset(0)!; - - for (int i = 0; i < count; i++) - { - AddRoom(new Room - { - Name = $@"Room {currentRoomId}", - Host = new APIUser { Username = @"Host" }, - Duration = TimeSpan.FromSeconds(10), - Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, - Password = withPassword ? @"password" : null, - PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, - Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] - }); - } - } - - public void AddRoom(Room room) - { - room.RoomID = -currentRoomId; - - var req = new CreateRoomRequest(room); - req.Success += AddOrUpdateRoom; - api.Queue(req); - - currentRoomId++; - } - } -} diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index c9149bda22..63bc9325fa 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -36,8 +36,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay private int currentScoreId = 1; /// - /// Handles an API request, while also updating the local state to match - /// how the server would eventually respond and update an . + /// Handles an API request, while also updating the local state to match how the server would eventually respond. /// /// The API request to handle. /// The local user to store in responses where required. From 1b07b6d16f49fd06572c3366685a08f2a2641669 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:48:59 +0900 Subject: [PATCH 023/349] Remove selected room leasing, make bindables private I believe once upon a time the `SelectedRoom` bindable used to be bound to `RoomManager.JoinedRoom` or similar. But now it's effectively private to the lounge subscreen and so a lease is unnecessary. --- .../TestScenePlaylistsLoungeSubScreen.cs | 28 +------------ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 39 ++++++------------- 2 files changed, 13 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 94a81ecdc7..35bf6dc28a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -3,7 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.Containers; @@ -17,13 +16,13 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - private TestLoungeSubScreen loungeScreen = null!; + private PlaylistsLoungeSubScreen loungeScreen = null!; public override void SetUpSteps() { base.SetUpSteps(); - AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen())); + AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen())); AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } @@ -62,24 +61,6 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); } - [Test] - public void TestEnteringRoomTakesLeaseOnSelection() - { - createRooms(GenerateRooms(1)); - - AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); - - AddStep("select room", () => roomsContainer.DrawableRooms[0].TriggerClick()); - AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - - AddStep("enter room", () => roomsContainer.DrawableRooms[0].TriggerClick()); - - AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen); - - AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - AddAssert("selected room is disabled", () => loungeScreen.SelectedRoom.Disabled); - } - private bool checkRoomVisible(DrawableRoom room) => loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad .Contains(room.ScreenSpaceDrawQuad.Centre); @@ -94,10 +75,5 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); } - - private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen - { - public new Bindable SelectedRoom => base.SelectedRoom; - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 6c383f1bf6..7bb0c67990 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = SelectedRoom } + SelectedRoom = { BindTarget = selectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -53,9 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge AutoSizeAxes = Axes.Both }; - protected readonly Bindable SelectedRoom = new Bindable(); - protected readonly BindableList Rooms = new BindableList(); - [Resolved] private MusicController music { get; set; } = null!; @@ -75,8 +72,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected OsuConfigManager Config { get; private set; } = null!; private IDisposable? joiningRoomOperation; - private LeasedBindable? selectionLease; + private readonly Bindable selectedRoom = new Bindable(); + private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); @@ -121,8 +119,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { - Rooms = { BindTarget = Rooms }, - SelectedRoom = { BindTarget = SelectedRoom }, + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = selectedRoom }, Filter = { BindTarget = filter }, } }, @@ -182,7 +180,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }; // scroll selected room into view on selection. - SelectedRoom.BindValueChanged(val => + selectedRoom.BindValueChanged(val => { var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); if (drawable != null) @@ -208,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - Rooms.Clear(); + rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -218,11 +216,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = Rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -230,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - Rooms.Add(r); + rooms.Add(r); } hasListingResults.Value = true; @@ -286,14 +284,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.OnResuming(e); - Debug.Assert(selectionLease != null); - - selectionLease.Return(); - selectionLease = null; - - if (SelectedRoom.Value?.RoomID == null) - SelectedRoom.Value = new Room(); - music.EnsurePlayingSomething(); onReturning(); @@ -415,14 +405,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge OpenNewRoom(room ?? CreateNewRoom()); }); - protected virtual void OpenNewRoom(Room room) - { - selectionLease = SelectedRoom.BeginLease(false); - Debug.Assert(selectionLease != null); - selectionLease.Value = room; - - this.Push(CreateRoomSubScreen(room)); - } + protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); public void RefreshRooms() => listingPollingComponent.PollImmediately(); From 74ccac37ae665ea2a9a603316077453520a8b9de Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:57:18 +0900 Subject: [PATCH 024/349] Encapsulate RoomsContainer scroll a bit better --- .../TestSceneLoungeRoomsContainer.cs | 4 +-- .../Lounge/Components/RoomsContainer.cs | 35 ++++++++++++------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 26 +++----------- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 9daad960c7..772eb91174 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -32,13 +32,13 @@ namespace osu.Game.Tests.Visual.Multiplayer rooms = new BindableList(); Child = new PopoverContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, Child = container = new RoomsContainer { + RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, SelectedRoom = { BindTarget = SelectedRoom } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 6681cbe720..65f969bc7b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; @@ -28,6 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); + private readonly ScrollContainer scroll; private readonly FillFlowContainer roomFlow; // handle deselection @@ -35,28 +37,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public RoomsContainer() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - // account for the fact we are in a scroll container and want a bit of spacing from the scroll bar. - Padding = new MarginPadding { Right = 5 }; - - InternalChild = new OsuContextMenuContainer + InternalChild = scroll = new OsuScrollContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = roomFlow = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Padding = new MarginPadding { Right = 5 }, + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), + Child = roomFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + } } }; } protected override void LoadComplete() { + SelectedRoom.BindValueChanged(onSelectedRoomChanged, true); Rooms.BindCollectionChanged(roomsChanged, true); Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true); } @@ -119,6 +122,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private void onSelectedRoomChanged(ValueChangedEvent room) + { + // scroll selected room into view on selection. + var drawable = DrawableRooms.FirstOrDefault(r => r.Room == room.NewValue); + if (drawable != null) + scroll.ScrollIntoView(drawable); + } + private void roomsChanged(object? sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 7bb0c67990..1877244c03 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -17,7 +17,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Configuration; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; @@ -82,7 +81,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; - private RoomsContainer roomsContainer = null!; private SearchTextBox searchTextBox = null!; protected Dropdown StatusDropdown { get; private set; } = null!; @@ -95,8 +93,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - OsuScrollContainer scrollContainer; - InternalChildren = new Drawable[] { listingPollingComponent = new ListingPollingComponent @@ -113,17 +109,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = scrollContainer = new OsuScrollContainer + Child = new RoomsContainer { RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - Child = roomsContainer = new RoomsContainer - { - Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = selectedRoom }, - Filter = { BindTarget = filter }, - } - }, + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = selectedRoom }, + Filter = { BindTarget = filter }, + } }, loadingLayer = new LoadingLayer(true), new FillFlowContainer @@ -178,14 +170,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }, }, }; - - // scroll selected room into view on selection. - selectedRoom.BindValueChanged(val => - { - var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); - if (drawable != null) - scrollContainer.ScrollIntoView(drawable); - }); } protected override void LoadComplete() From 43928c94db5b4695b2baab8acfb41d58198322aa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:03:22 +0900 Subject: [PATCH 025/349] Remove remaining bindables --- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 17 +++++++---------- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 --- .../OnlinePlay/TestRoomRequestsHandler.cs | 2 -- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 1877244c03..2e78e88ccf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = selectedRoom } + SelectedRoom = { BindTarget = roomsContainer.SelectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -72,12 +72,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IDisposable? joiningRoomOperation; - private readonly Bindable selectedRoom = new Bindable(); - private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); + private RoomsContainer roomsContainer = null!; private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; @@ -109,11 +108,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = new RoomsContainer + Child = roomsContainer = new RoomsContainer { RelativeSizeAxes = Axes.Both, - Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = selectedRoom }, Filter = { BindTarget = filter }, } }, @@ -190,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - rooms.Clear(); + roomsContainer.Rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -200,11 +197,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = roomsContainer.Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + roomsContainer.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -212,7 +209,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - rooms.Add(r); + roomsContainer.Rooms.Add(r); } hasListingResults.Value = true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 3cf873ec78..6191cfd975 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; using osu.Framework.Graphics; @@ -15,7 +13,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 63bc9325fa..617a4cff79 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -15,7 +15,6 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; using osu.Game.Utils; @@ -28,7 +27,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public class TestRoomRequestsHandler { public IReadOnlyList ServerSideRooms => serverSideRooms; - private readonly List serverSideRooms = new List(); private int currentRoomId = 1; From 24cc77287e5e715a0fc684999f0a9aadd1355380 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:21:04 +0900 Subject: [PATCH 026/349] Refactor polling components (namespace/namings) --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 3 +-- .../LoungePollingComponent.cs} | 4 ++-- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 17 ++++++++--------- .../Playlists/PlaylistsRoomSubScreen.cs | 8 ++++---- .../PlaylistsRoomUpdater.cs} | 6 +++--- 5 files changed, 18 insertions(+), 20 deletions(-) rename osu.Game/Screens/OnlinePlay/{Components/ListingPollingComponent.cs => Lounge/LoungePollingComponent.cs} (92%) rename osu.Game/Screens/OnlinePlay/{Components/SelectionPollingComponent.cs => Playlists/PlaylistsRoomUpdater.cs} (88%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 0966c61a3a..a87216287d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -33,7 +33,6 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -806,7 +805,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); - AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); + AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { multiplayerClient.ServerSideRooms[0].Name = "New name"; diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs similarity index 92% rename from osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs index 1495f97de4..420a96cf8a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs @@ -11,12 +11,12 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; -namespace osu.Game.Screens.OnlinePlay.Components +namespace osu.Game.Screens.OnlinePlay.Lounge { /// /// A that polls for the lounge listing. /// - public partial class ListingPollingComponent : PollingComponent + public partial class LoungePollingComponent : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 2e78e88ccf..3a4da96ba1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -24,7 +24,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; @@ -77,7 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); private RoomsContainer roomsContainer = null!; - private ListingPollingComponent listingPollingComponent = null!; + private LoungePollingComponent pollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private SearchTextBox searchTextBox = null!; @@ -94,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - listingPollingComponent = new ListingPollingComponent + pollingComponent = new LoungePollingComponent { RoomsReceived = onListingReceived, Filter = { BindTarget = filter } @@ -189,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { roomsContainer.Rooms.Clear(); hasListingResults.Value = false; - listingPollingComponent.PollImmediately(); + pollingComponent.PollImmediately(); }); updateFilter(); @@ -270,7 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - listingPollingComponent.PollImmediately(); + pollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -388,7 +387,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); - public void RefreshRooms() => listingPollingComponent.PollImmediately(); + public void RefreshRooms() => pollingComponent.PollImmediately(); private void updateLoadingLayer() { @@ -401,11 +400,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - listingPollingComponent.TimeBetweenPolls.Value = 0; + pollingComponent.TimeBetweenPolls.Value = 0; else - listingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + pollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {listingPollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {pollingComponent.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index bf0e428483..a74ae642fb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private IdleTracker? idleTracker { get; set; } private MatchLeaderboard leaderboard = null!; - private SelectionPollingComponent selectionPollingComponent = null!; + private PlaylistsRoomUpdater roomUpdater = null!; private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - AddInternal(selectionPollingComponent = new SelectionPollingComponent(Room)); + AddInternal(roomUpdater = new PlaylistsRoomUpdater(Room)); } protected override void LoadComplete() @@ -328,8 +328,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void updatePollingRate() { - selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; - Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); + roomUpdater.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; + Logger.Log($"Polling adjusted (selection: {roomUpdater.TimeBetweenPolls.Value})"); } private void closePlaylist() diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs similarity index 88% rename from osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs index bfa059f72e..f68703750a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs @@ -7,19 +7,19 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.OnlinePlay.Components +namespace osu.Game.Screens.OnlinePlay.Playlists { /// /// A that polls for and updates a room. /// - public partial class SelectionPollingComponent : PollingComponent + public partial class PlaylistsRoomUpdater : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; private readonly Room room; - public SelectionPollingComponent(Room room) + public PlaylistsRoomUpdater(Room room) { this.room = room; } From 205d6ecffbc989d75c1a32e53a29a9342b88c175 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:51:25 +0900 Subject: [PATCH 027/349] Remove `SelectedRoom` abstraction from `OnlinePlayTestScene` --- .../StatefulMultiplayerClientTest.cs | 6 ++ .../TestSceneDrawableRoomParticipantsList.cs | 15 +++-- .../TestSceneLoungeRoomsContainer.cs | 7 +- .../TestSceneMatchBeatmapDetailArea.cs | 10 +-- .../Multiplayer/TestSceneMatchLeaderboard.cs | 4 +- .../TestSceneMultiSpectatorLeaderboard.cs | 2 + .../TestSceneMultiSpectatorScreen.cs | 6 +- .../TestSceneMultiplayerLoungeSubScreen.cs | 5 -- .../TestSceneMultiplayerMatchSongSelect.cs | 13 +++- .../TestSceneMultiplayerMatchSubScreen.cs | 28 ++++---- .../TestSceneMultiplayerParticipantsList.cs | 6 +- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 6 ++ .../TestSceneMultiplayerPlaylist.cs | 5 +- .../TestSceneMultiplayerQueueList.cs | 5 +- .../TestSceneMultiplayerSpectateButton.cs | 11 +-- .../TestScenePlaylistsSongSelect.cs | 23 ++++--- .../TestScenePlaylistsMatchSettingsOverlay.cs | 29 ++++---- .../TestScenePlaylistsParticipantsList.cs | 10 +-- .../TestScenePlaylistsRoomCreation.cs | 12 ++-- .../Multiplayer/MultiplayerTestScene.cs | 67 ++++++++++--------- .../IOnlinePlayTestSceneDependencies.cs | 6 -- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 2 - .../OnlinePlayTestSceneDependencies.cs | 4 -- 23 files changed, 149 insertions(+), 133 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index be30e06ed4..c0ca387260 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -15,6 +15,12 @@ namespace osu.Game.Tests.NonVisual.Multiplayer [HeadlessTest] public partial class StatefulMultiplayerClientTest : MultiplayerTestScene { + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(); + } + [Test] public void TestUserAddedOnJoin() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index c1662bf944..2fd1268c8a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -15,6 +15,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene { + private Room room = null!; private DrawableRoomParticipantsList list = null!; public override void SetUpSteps() @@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create list", () => { - SelectedRoom.Value = new Room + room = new Room { Name = "test room", Host = new APIUser @@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }; - Child = list = new DrawableRoomParticipantsList(SelectedRoom.Value) + Child = list = new DrawableRoomParticipantsList(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -119,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); - AddStep("remove from end", () => removeUserAt(SelectedRoom.Value!.RecentParticipants.Count - 1)); + AddStep("remove from end", () => removeUserAt(room.RecentParticipants.Count - 1)); AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); @@ -138,18 +139,18 @@ namespace osu.Game.Tests.Visual.Multiplayer private void addUser(int id) { - SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Append(new APIUser + room.RecentParticipants = room.RecentParticipants.Append(new APIUser { Id = id, Username = $"User {id}" }).ToArray(); - SelectedRoom.Value!.ParticipantCount++; + room.ParticipantCount++; } private void removeUserAt(int index) { - SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Where(u => !u.Equals(SelectedRoom.Value!.RecentParticipants[index])).ToArray(); - SelectedRoom.Value!.ParticipantCount--; + room.RecentParticipants = room.RecentParticipants.Where(u => !u.Equals(room.RecentParticipants[index])).ToArray(); + room.ParticipantCount--; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 772eb91174..e83a966144 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { private BindableList rooms = null!; + private Bindable selectedRoom = null!; private RoomsContainer container = null!; public override void SetUpSteps() @@ -30,6 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create container", () => { rooms = new BindableList(); + selectedRoom = new Bindable(); + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, @@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = SelectedRoom } + SelectedRoom = { BindTarget = selectedRoom } } }; }); @@ -195,7 +198,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } - private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room; + private bool checkRoomSelected(Room? room) => selectedRoom.Value == room; private Room? getRoomInFlow(int index) => (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 813a420cbd..e372d63fde 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -16,15 +16,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene { + private Room room = null!; + public override void SetUpSteps() { base.SetUpSteps(); AddStep("create area", () => { - SelectedRoom.Value = new Room(); - - Child = new MatchBeatmapDetailArea(SelectedRoom.Value) + Child = new MatchBeatmapDetailArea(room = new Room()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewItem() { - SelectedRoom.Value!.Playlist = SelectedRoom.Value.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + room.Playlist = room.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - ID = SelectedRoom.Value.Playlist.Count, + ID = room.Playlist.Count, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 38522db4d4..39ad21d0b0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -61,9 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create leaderboard", () => { - SelectedRoom.Value = new Room { RoomID = 3 }; - - Child = new MatchLeaderboard(SelectedRoom.Value) + Child = new MatchLeaderboard(new Room { RoomID = 3 }) { Origin = Anchor.Centre, Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 3245b3c6a9..1821c2f3bc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -24,6 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(); + AddStep("reset", () => { leaderboard?.RemoveAndDisposeImmediately(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 0a3d48828e..6cbd8a3fed 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; @@ -42,6 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmapManager { get; set; } = null!; private MultiSpectatorScreen spectatorScreen = null!; + private Room room = null!; private readonly List playingUsers = new List(); @@ -63,6 +65,8 @@ namespace osu.Game.Tests.Visual.Multiplayer base.SetUpSteps(); AddStep("clear playing users", () => playingUsers.Clear()); + + JoinDefaultRoom(r => room = r); } [TestCase(1)] @@ -455,7 +459,7 @@ namespace osu.Game.Tests.Visual.Multiplayer applyToBeatmap?.Invoke(Beatmap.Value); - LoadScreen(spectatorScreen = new MultiSpectatorScreen(SelectedRoom.Value!, playingUsers.ToArray())); + LoadScreen(spectatorScreen = new MultiSpectatorScreen(room, playingUsers.ToArray())); }); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index b4ec9d5858..56187f8778 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -20,11 +20,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerLoungeSubScreen loungeScreen = null!; - public TestSceneMultiplayerLoungeSubScreen() - : base(false) - { - } - public override void SetUpSteps() { base.SetUpSteps(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 298e6e1b3c..287d7f5816 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerMatchSongSelect songSelect = null!; private Live importedBeatmapSet = null!; + private Room room = null!; [Resolved] private OsuConfigManager configManager { get; set; } = null!; @@ -58,6 +59,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Add(beatmapStore); } + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + } + private void setUp() { AddStep("create song select", () => @@ -66,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.SetDefault(); SelectedMods.SetDefault(); - LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!)); + LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(room)); }); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); @@ -138,8 +145,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create song select", () => { - SelectedRoom.Value!.Playlist.Single().RulesetID = 2; - songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single()); + room.Playlist.Single().RulesetID = 2; + songSelect = new TestMultiplayerMatchSongSelect(room, room.Playlist.Single()); songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo; LoadScreen(songSelect); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e95209f993..18e926ca5d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -43,11 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiplayerMatchSubScreen screen = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; - - public TestSceneMultiplayerMatchSubScreen() - : base(false) - { - } + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -66,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("load match", () => { - SelectedRoom.Value = new Room { Name = "Test Room" }; - LoadScreen(screen = new TestMultiplayerMatchSubScreen(SelectedRoom.Value!)); + room = new Room { Name = "Test Room" }; + LoadScreen(screen = new TestMultiplayerMatchSubScreen(room)); }); AddUntilStep("wait for load", () => screen.IsCurrentScreen()); @@ -78,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -97,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo) { @@ -122,7 +118,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set playlist", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) { @@ -170,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with allowed mod", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -199,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with allowed mod", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -223,7 +219,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with no allowed mods", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -246,7 +242,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add two playlist items", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) { @@ -285,7 +281,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 238a716f91..e7e6112297 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -25,9 +25,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene { - [SetUpSteps] - public void SetupSteps() + public override void SetUpSteps() { + base.SetUpSteps(); + + JoinDefaultRoom(); createNewParticipantsList(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 94dd114c32..1a5be48cad 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -22,6 +22,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerPlayer player = null!; + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(); + } + [Test] public void TestGameplay() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 77b75f407b..406c6cacae 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -46,9 +47,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create list", () => { - Child = list = new MultiplayerPlaylist(SelectedRoom.Value!) + Child = list = new MultiplayerPlaylist(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 3ef2e4ecf4..5eba67bab5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -42,9 +43,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create playlist", () => { - Child = playlist = new MultiplayerQueueList(SelectedRoom.Value!) + Child = playlist = new MultiplayerQueueList(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 1429f86164..f92721b04b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerSpectateButton spectateButton = null!; private MatchStartControl startControl = null!; + private Room room = null!; private BeatmapSetInfo importedSet = null!; private BeatmapManager beatmaps = null!; @@ -46,11 +47,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create button", () => { - PlaylistItem item = SelectedRoom.Value!.Playlist.First(); - - AvailabilityTracker.SelectedItem.Value = item; + AvailabilityTracker.SelectedItem.Value = room.Playlist.First(); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); @@ -69,14 +70,14 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - SelectedItem = new Bindable(item) + SelectedItem = new Bindable(room.Playlist.First()) }, startControl = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - SelectedItem = new Bindable(item) + SelectedItem = new Bindable(room.Playlist.First()) } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 726d0ac9f9..7c73fb8321 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private BeatmapManager manager = null!; private TestPlaylistsSongSelect songSelect = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -51,13 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("reset", () => { - SelectedRoom.Value = new Room(); + room = new Room(); Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); SelectedMods.Value = Array.Empty(); }); - AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(SelectedRoom.Value!))); + AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(room))); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } @@ -65,14 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestItemAddedIfEmptyOnStart() { AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] public void TestItemAddedWhenCreateNewItemClicked() { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] @@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] @@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("playlist has 2 items", () => SelectedRoom.Value!.Playlist.Count == 2); + AddAssert("playlist has 2 items", () => room.Playlist.Count == 2); } [Test] @@ -96,10 +97,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddStep("rearrange", () => SelectedRoom.Value!.Playlist = SelectedRoom.Value!.Playlist.Skip(1).Append(SelectedRoom.Value!.Playlist[0]).ToArray()); + AddStep("rearrange", () => room.Playlist = room.Playlist.Skip(1).Append(room.Playlist[0]).ToArray()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("new item has id 2", () => SelectedRoom.Value!.Playlist.Last().ID == 2); + AddAssert("new item has id 2", () => room.Playlist.Last().ID == 2); } /// @@ -115,13 +116,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 1 has rate 1.5", () => { - var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + var mod = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(1.5, mod.SpeedChange.Value); }); AddAssert("item 2 has rate 2", () => { - var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); + var mod = (OsuModDoubleTime)room.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(2, mod.SpeedChange.Value); }); } @@ -147,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); AddAssert("item has rate 1.5", () => { - var m = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + var m = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(1.5, m.SpeedChange.Value); }); } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index f7b0bc0d58..c714c39e22 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -18,6 +18,7 @@ namespace osu.Game.Tests.Visual.Playlists public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene { private TestRoomSettings settings = null!; + private Room room = null!; private Func? handleRequest; public override void SetUpSteps() @@ -47,9 +48,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("create overlay", () => { - SelectedRoom.Value = new Room(); - - Child = settings = new TestRoomSettings(SelectedRoom.Value!) + Child = settings = new TestRoomSettings(room = new Room()) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible } @@ -62,19 +61,19 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("clear name and beatmap", () => { - SelectedRoom.Value!.Name = ""; - SelectedRoom.Value!.Playlist = []; + room.Name = ""; + room.Playlist = []; }); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set name", () => SelectedRoom.Value!.Name = "Room name"); + AddStep("set name", () => room.Name = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]); + AddStep("set beatmap", () => room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); - AddStep("clear name", () => SelectedRoom.Value!.Name = ""); + AddStep("clear name", () => room.Name = ""); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); } @@ -90,7 +89,7 @@ namespace osu.Game.Tests.Visual.Playlists { settings.NameField.Current.Value = expected_name; settings.DurationField.Current.Value = expectedDuration; - SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; + room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; handleRequest = r => { @@ -115,8 +114,8 @@ namespace osu.Game.Tests.Visual.Playlists { var beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo; - SelectedRoom.Value!.Name = "Test Room"; - SelectedRoom.Value!.Playlist = [new PlaylistItem(beatmap)]; + room.Name = "Test Room"; + room.Playlist = [new PlaylistItem(beatmap)]; errorMessage = $"{not_found_prefix} {beatmap.OnlineID}"; @@ -124,13 +123,13 @@ namespace osu.Game.Tests.Visual.Playlists }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); - AddAssert("playlist item valid", () => SelectedRoom.Value!.Playlist[0].Valid.Value); + AddAssert("playlist item valid", () => room.Playlist[0].Valid.Value); AddStep("create room", () => settings.ApplyButton.Action.Invoke()); AddAssert("error displayed", () => settings.ErrorText.IsPresent); AddAssert("error has custom text", () => settings.ErrorText.Text != errorMessage); - AddAssert("playlist item marked invalid", () => !SelectedRoom.Value!.Playlist[0].Valid.Value); + AddAssert("playlist item marked invalid", () => !room.Playlist[0].Valid.Value); } [Test] @@ -142,8 +141,8 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("setup", () => { - SelectedRoom.Value!.Name = "Test Room"; - SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; + room.Name = "Test Room"; + room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; handleRequest = _ => failText; }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index c60b208ffc..e1ec30d02a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -14,13 +14,15 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsParticipantsList : OnlinePlayTestScene { + private Room room = null!; + public override void SetUpSteps() { base.SetUpSteps(); - AddStep("create list", () => + AddStep("create room", () => { - SelectedRoom.Value = new Room + room = new Room { RoomID = 7, RecentParticipants = Enumerable.Range(0, 50).Select(_ => new APIUser @@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("create component", () => { - Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Horizontal) + Child = new ParticipantsDisplay(room, Direction.Horizontal) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -52,7 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("create component", () => { - Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Vertical) + Child = new ParticipantsDisplay(room, Direction.Vertical) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 0270840597..a748d61d44 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists private BeatmapManager manager = null!; private TestPlaylistsRoomSubScreen match = null!; private BeatmapSetInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -47,11 +48,9 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetupSteps() { - AddStep("set room", () => SelectedRoom.Value = new Room()); - importBeatmap(); - AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value!))); + AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(room = new Room()))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -119,7 +118,7 @@ namespace osu.Game.Tests.Visual.Playlists ]; }); - AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value!.Playlist[0]); + AddAssert("first playlist item selected", () => match.SelectedItem.Value == room.Playlist[0]); } [Test] @@ -197,10 +196,9 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash); } - private void setupAndCreateRoom(Action room) + private void setupAndCreateRoom(Action setupFunc) { - AddStep("setup room", () => room(SelectedRoom.Value!)); - + AddStep("setup room", () => setupFunc(room)); AddStep("click create button", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index d1497d5142..97c213c7b1 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Online.Rooms; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual.OnlinePlay; @@ -23,43 +24,43 @@ namespace osu.Game.Tests.Visual.Multiplayer public bool RoomJoined => MultiplayerClient.RoomJoined; - private readonly bool joinRoom; - - protected MultiplayerTestScene(bool joinRoom = true) + /// + /// Creates and joins a basic multiplayer room. + /// + /// A callback that may be used to further set up the room. + protected void JoinDefaultRoom(Action? setupFunc = null) { - this.joinRoom = joinRoom; - } - - protected virtual Room CreateRoom() - { - return new Room + AddStep("join room", () => { - Name = "test name", - Type = MatchType.HeadToHead, - Playlist = - [ - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID - } - ] - }; - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - if (joinRoom) - { - AddStep("join room", () => + Room room = new Room { - SelectedRoom.Value = CreateRoom(); - MultiplayerClient.CreateRoom(SelectedRoom.Value).ConfigureAwait(false); - }); + Name = "test name", + Type = MatchType.HeadToHead, + Playlist = + [ + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID + } + ] + }; - AddUntilStep("wait for room join", () => RoomJoined); - } + setupFunc?.Invoke(room); + + MultiplayerClient.CreateRoom(room).ConfigureAwait(false); + }); + + AddUntilStep("wait for room join", () => RoomJoined); + } + + /// + /// Creates and joins the given room. + /// + /// The room to create. If null, a default room will be created. + protected void JoinRoom(Room room) + { + AddStep("join room", () => MultiplayerClient.CreateRoom(room).ConfigureAwait(false)); + AddUntilStep("wait for room join", () => RoomJoined); } protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 5780cf6eff..60730ee9a4 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.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 osu.Framework.Bindables; using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -13,11 +12,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public interface IOnlinePlayTestSceneDependencies { - /// - /// The cached . - /// - Bindable SelectedRoom { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index c3a5e1c3ec..ce8df36590 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -22,7 +21,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies { - public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom; public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker; public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache; diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index cc448beea0..9537c7958c 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Database; using osu.Game.Online.Rooms; @@ -18,7 +17,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies { - public Bindable SelectedRoom { get; } public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } @@ -35,7 +33,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OnlinePlayTestSceneDependencies() { - SelectedRoom = new Bindable(); RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -45,7 +42,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay dependencies = new DependencyContainer(); CacheAs(RequestsHandler); - CacheAs(SelectedRoom); CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); From d923a478e9a044432cd611424ff57b5862d69865 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 00:04:33 +0900 Subject: [PATCH 028/349] Remove unused method --- .../Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 97c213c7b1..8150807f4f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -53,16 +53,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room join", () => RoomJoined); } - /// - /// Creates and joins the given room. - /// - /// The room to create. If null, a default room will be created. - protected void JoinRoom(Room room) - { - AddStep("join room", () => MultiplayerClient.CreateRoom(room).ConfigureAwait(false)); - AddUntilStep("wait for room join", () => RoomJoined); - } - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } From d930da62104bb2c65fede2771d9a670524b80c60 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 18:10:19 +0900 Subject: [PATCH 029/349] Rewrite playlists to not inherit `RoomSubScreen` --- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 3 +- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 +- .../Playlists/PlaylistsLoungeSubScreen.cs | 3 +- .../Playlists/PlaylistsRoomSubScreen.cs | 955 +++++++++++++----- 4 files changed, 716 insertions(+), 248 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f00cf7427c..c3b3d63dcb 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -26,7 +26,6 @@ using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; using osuTK; @@ -408,7 +407,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge /// The created . protected abstract Room CreateNewRoom(); - protected abstract RoomSubScreen CreateRoomSubScreen(Room room); + protected abstract OnlinePlaySubScreen CreateRoomSubScreen(Room room); protected abstract ListingPollingComponent CreatePollingComponent(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index dd61caa3db..1d728998b9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -17,7 +17,6 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match; namespace osu.Game.Screens.OnlinePlay.Multiplayer { @@ -89,7 +88,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Type = MatchType.HeadToHead, }; - protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); + protected override OnlinePlaySubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index d66b4f844c..8670fcf78f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -13,7 +13,6 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -70,7 +69,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }; } - protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); + protected override OnlinePlaySubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); protected override ListingPollingComponent CreatePollingComponent() => new ListingPollingComponent(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 7f2255e482..22b9006e47 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -2,60 +2,126 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Users; +using osu.Game.Utils; using osuTK; using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class PlaylistsRoomSubScreen : RoomSubScreen + public partial class PlaylistsRoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner { public override string Title { get; } public override string ShortTitle => "playlist"; - private readonly IBindable isIdle = new BindableBool(); + public override bool? ApplyModTrackAdjustments => true; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + /// + /// Whether the user has confirmed they want to exit this screen in the presence of unsaved changes. + /// + protected bool ExitConfirmed { get; private set; } [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved(CanBeNull = true)] + [Resolved] + private AudioManager audio { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + + [Resolved] + private MusicController music { get; set; } = null!; + + [Resolved] private IdleTracker? idleTracker { get; set; } - private MatchLeaderboard leaderboard = null!; + [Resolved] + private OnlinePlayScreen? parentScreen { get; set; } + + [Resolved] + private IOverlayManager? overlayManager { get; set; } + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + [Cached] + private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); + + protected readonly Bindable SelectedItem = new Bindable(); + private readonly Bindable userBeatmap = new Bindable(); + private readonly Bindable userRuleset = new Bindable(); + private readonly Bindable> userMods = new Bindable>(Array.Empty()); + + private readonly IBindable isIdle = new BindableBool(); + private readonly Room room; + + private Drawable roomContent = null!; private SelectionPollingComponent selectionPollingComponent = null!; + private PlaylistsRoomSettingsOverlay settingsOverlay = null!; + + private MatchLeaderboard leaderboard = null!; private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; - private readonly Bindable userBeatmap = new Bindable(); - private readonly Bindable userRuleset = new Bindable(); + private FillFlowContainer userModsSection = null!; + private RoomModSelectOverlay userModsSelectOverlay = null!; + + private FillFlowContainer userStyleSection = null!; + private Container userStyleDisplayContainer = null!; + + private Sample? sampleStart; + private IDisposable? userModsSelectOverlayRegistration; public PlaylistsRoomSubScreen(Room room) - : base(room, false) // Editing is temporarily not allowed. { + this.room = room; + Title = room.RoomID == null ? "New playlist" : room.Name; Activity.Value = new UserActivity.InLobby(room); + + Padding = new MarginPadding { Top = Header.HEIGHT }; } [BackgroundDependencyLoader] @@ -64,32 +130,350 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - AddInternal(selectionPollingComponent = new SelectionPollingComponent(Room)); + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + + InternalChild = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + selectionPollingComponent = new SelectionPollingComponent(room), + beatmapAvailabilityTracker, + new MultiplayerRoomSounds(), + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 50) + }, + Content = new[] + { + // Padded main content (drawable room + main content) + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = 30 + }, + Children = new[] + { + roomContent = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10) + }, + Content = new[] + { + new Drawable[] + { + new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new DrawableMatchRoom(room, false) + { + OnEdit = () => settingsOverlay.Show(), + SelectedItem = SelectedItem + } + } + }, + null, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(20), + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + }, + Content = new[] + { + new Drawable?[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 5 }, + Content = new[] + { + new Drawable[] { new OverlinedPlaylistHeader(room), }, + new Drawable[] + { + drawablePlaylist = new DrawableRoomPlaylist + { + RelativeSizeAxes = Axes.Both, + SelectedItem = { BindTarget = SelectedItem }, + AllowSelection = true, + AllowShowingResults = true, + RequestResults = item => + { + Debug.Assert(room.RoomID != null); + parentScreen?.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, + api.LocalUser.Value.Id)); + } + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + userModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Height = 30, + Text = "Select", + Action = showUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = userMods, + Scale = new Vector2(0.8f), + }, + } + } + } + }, + }, + new Drawable[] + { + userStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + userStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, + }, + new Drawable[] + { + progressSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Margin = new MarginPadding { Bottom = 10 }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OverlinedHeader("Progress"), + new RoomLocalUserInfo(room), + } + }, + }, + new Drawable[] + { + new OverlinedHeader("Leaderboard") + }, + new Drawable[] { leaderboard = new MatchLeaderboard(room) { RelativeSizeAxes = Axes.Both }, }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { new OverlinedHeader("Chat") }, + new Drawable[] { new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both } } + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + }, + }, + } + } + } + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + } + } + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + // Resolves 1px masking errors between the settings overlay and the room panel. + Padding = new MarginPadding(-1), + Child = settingsOverlay = new PlaylistsRoomSettingsOverlay(room) + { + EditPlaylist = () => + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistsSongSelect(room)); + }, + } + } + }, + }, + }, + // Footer + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = new PlaylistsRoomFooter(room) + { + OnStart = startPlay, + OnClose = closePlaylist, + } + }, + } + } + } + } + } + } + }; + + LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay + { + SelectedItem = { BindTarget = SelectedItem }, + SelectedMods = { BindTarget = userMods }, + IsValidMod = _ => false + }); } protected override void LoadComplete() { base.LoadComplete(); - SelectedItem.BindValueChanged(onSelectedItemChanged, true); + userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); + + room.PropertyChanged += onRoomPropertyChanged; + isIdle.BindValueChanged(_ => updatePollingRate(), true); + SelectedItem.BindValueChanged(_ => onSelectedItemChanged()); + + beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); + + userBeatmap.BindValueChanged(_ => updateGameplayState()); + userRuleset.BindValueChanged(_ => updateGameplayState()); + userMods.BindValueChanged(_ => updateGameplayState()); - Room.PropertyChanged += onRoomPropertyChanged; updateSetupState(); - updateRoomMaxAttempts(); - updateRoomPlaylist(); + updateGameplayState(); } - private void onSelectedItemChanged(ValueChangedEvent item) - { - // Simplest for now. - userBeatmap.Value = null; - userRuleset.Value = null; - } - - protected override IBeatmapInfo GetGameplayBeatmap() => userBeatmap.Value ?? base.GetGameplayBeatmap(); - protected override RulesetInfo GetGameplayRuleset() => userRuleset.Value ?? base.GetGameplayRuleset(); + #region Room/property updates + /// + /// Responds to changes of the 's properties. + /// + /// The that changed. + /// Describes the property that changed. private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -97,255 +481,342 @@ namespace osu.Game.Screens.OnlinePlay.Playlists case nameof(Room.RoomID): updateSetupState(); break; - - case nameof(Room.MaxAttempts): - updateRoomMaxAttempts(); - break; - - case nameof(Room.Playlist): - updateRoomPlaylist(); - break; } } + /// + /// Adjusts the visibility of the settings and main content when changes. + /// Only the settings overlay is visible while the room isn't created, and only the main content is visible after creation. + /// private void updateSetupState() { - if (Room.RoomID != null) + if (room.RoomID == null) { - // Set the first playlist item. - // This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). - Schedule(() => SelectedItem.Value = Room.Playlist.FirstOrDefault()); + // A new room is being created. + // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. + roomContent.Hide(); + settingsOverlay.Show(); + } + else + { + roomContent.Show(); + settingsOverlay.Hide(); + + progressSection.Alpha = room.MaxAttempts != null ? 1 : 0; + drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); + SelectedItem.Value = room.Playlist.FirstOrDefault(); } } - private void updateRoomMaxAttempts() - => progressSection.Alpha = Room.MaxAttempts != null ? 1 : 0; - - private void updateRoomPlaylist() - => drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, Room.Playlist); - - protected override Drawable CreateMainContent() => new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, - Child = new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - }, - Content = new[] - { - new Drawable?[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Content = new[] - { - new Drawable[] { new OverlinedPlaylistHeader(Room), }, - new Drawable[] - { - drawablePlaylist = new DrawableRoomPlaylist - { - RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem }, - AllowSelection = true, - AllowShowingResults = true, - RequestResults = item => - { - Debug.Assert(Room.RoomID != null); - ParentScreen?.Push(new PlaylistItemUserBestResultsScreen(Room.RoomID.Value, item, api.LocalUser.Value.Id)); - } - } - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new[] - { - UserModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Extra mods"), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Height = 30, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } - } - } - }, - }, - new[] - { - UserStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, - }, - new Drawable[] - { - progressSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OverlinedHeader("Progress"), - new RoomLocalUserInfo(Room), - } - }, - }, - new Drawable[] - { - new OverlinedHeader("Leaderboard") - }, - new Drawable[] { leaderboard = new MatchLeaderboard(Room) { RelativeSizeAxes = Axes.Both }, }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { new OverlinedHeader("Chat") }, - new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } } - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - }, - }, - } - } - }; - - protected override Drawable CreateFooter() => new PlaylistsRoomFooter(Room) - { - OnStart = StartPlay, - OnClose = closePlaylist, - }; - - protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new PlaylistsRoomSettingsOverlay(room) - { - EditPlaylist = () => - { - if (this.IsCurrentScreen()) - this.Push(new PlaylistsSongSelect(Room)); - }, - }; - - protected override void OpenStyleSelection() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) - return; - - this.Push(new PlaylistsRoomFreestyleSelect(Room, item) - { - Beatmap = { BindTarget = userBeatmap }, - Ruleset = { BindTarget = userRuleset } - }); - } - + /// + /// Adjusts the rate at which the is updated. + /// private void updatePollingRate() { selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); } - private void closePlaylist() + /// + /// Responds to changes in the selected playlist item to validate the user's beatmap/ruleset/mod style and update UI components as necessary. + /// + private void onSelectedItemChanged() { - DialogOverlay?.Push(new ClosePlaylistDialog(Room, () => + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + // Reset entire user style when disabled. + if (!item.Freestyle) { - var request = new ClosePlaylistRequest(Room.RoomID!.Value); - request.Success += () => Room.EndDate = DateTimeOffset.UtcNow; - API.Queue(request); + userBeatmap.Value = null; + userRuleset.Value = null; + } + + // Reset beatmap style when no longer from the same beatmap set. + if (userBeatmap.Value != null && userBeatmap.Value.BeatmapSet!.OnlineID != item.Beatmap.BeatmapSet!.OnlineID) + userBeatmap.Value = null; + + // Reset ruleset style when no longer valid for the beatmap. + if (userRuleset.Value != null) + { + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + int beatmapRuleset = gameplayBeatmap.Ruleset.OnlineID; + + if (beatmapRuleset > 0 && userRuleset.Value.OnlineID != beatmapRuleset) + userRuleset.Value = null; + } + + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); + Mod[] allowedMods = item.Freestyle + ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray() + : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + // Remove any user mods that are no longer allowed. + Mod[] newUserMods = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + if (!newUserMods.SequenceEqual(userMods.Value)) + userMods.Value = newUserMods; + + if (allowedMods.Length > 0) + { + userModsSection.Show(); + userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + else + { + userModsSection.Hide(); + userModsSelectOverlay.Hide(); + userModsSelectOverlay.IsValidMod = _ => false; + } + + if (item.Freestyle) + userStyleSection.Show(); + else + userStyleSection.Hide(); + + updateGameplayState(); + } + + /// + /// Adjusts the global beatmap/ruleset/mods values in preparation for a gameplay session. + /// + private void updateGameplayState() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); + Mod[] gameplayMods = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); + + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + int beatmapId = gameplayBeatmap.OnlineID; + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + Ruleset.Value = gameplayRuleset; + Mods.Value = gameplayMods; + + if (item.Freestyle) + { + PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); + PlaylistItem? currentItem = userStyleDisplayContainer.SingleOrDefault()?.Item; + + if (!gameplayItem.Equals(currentItem)) + { + userStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => showUserStyleSelect() + }; + } + } + } + + #endregion + + /// + /// Pushes a to start gameplay with the current selection. + /// + private void startPlay() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + // Required for validation inside the player. + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); + + sampleStart?.Play(); + + // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). + var targetScreen = (Screen?)parentScreen ?? this; + targetScreen.Push(new PlayerLoader(() => new PlaylistsPlayer(room, gameplayItem) + { + Exited = () => leaderboard.RefetchScores() })); } - protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) + /// + /// Shows the user mod selection. + /// + private void showUserModSelect() { - return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) + return; + + userModsSelectOverlay.Show(); + } + + /// + /// Shows the user style selection. + /// + private void showUserStyleSelect() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + this.Push(new PlaylistsRoomFreestyleSelect(room, item) { - Exited = () => leaderboard.RefetchScores() + Beatmap = { BindTarget = userBeatmap }, + Ruleset = { BindTarget = userRuleset } }); } + /// + /// May be invoked by the owner of the room to permanently close the room ahead of its intended end date. + /// + private void closePlaylist() + { + dialogOverlay?.Push(new ClosePlaylistDialog(room, () => + { + var request = new ClosePlaylistRequest(room.RoomID!.Value); + request.Success += () => room.EndDate = DateTimeOffset.UtcNow; + api.Queue(request); + })); + } + + #region Screen transition / track handling + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + updateGameplayState(); + beginHandlingTrack(); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + endHandlingTrack(); + base.OnSuspending(e); + } + + public override void OnResuming(ScreenTransitionEvent e) + { + base.OnResuming(e); + updateGameplayState(); + beginHandlingTrack(); + } + + public override bool OnExiting(ScreenExitEvent e) + { + if (!ensureExitConfirmed()) + return true; + + RoomManager?.PartRoom(); + + endHandlingTrack(); + return base.OnExiting(e); + } + + public override bool OnBackButton() + { + if (room.RoomID == null) + { + if (!ensureExitConfirmed()) + return true; + + settingsOverlay.Hide(); + return base.OnBackButton(); + } + + if (userModsSelectOverlay.State.Value == Visibility.Visible) + { + userModsSelectOverlay.Hide(); + return true; + } + + if (settingsOverlay.State.Value == Visibility.Visible) + { + settingsOverlay.Hide(); + return true; + } + + return base.OnBackButton(); + } + + /// + /// Handles changes in the track to keep it looping while active. + /// + private void beginHandlingTrack() + { + Beatmap.BindValueChanged(applyLoopingToTrack, true); + } + + /// + /// Stops looping the current track and stops handling further changes to the track. + /// + private void endHandlingTrack() + { + Beatmap.ValueChanged -= applyLoopingToTrack; + Beatmap.Value.Track.Looping = false; + + previewTrackManager.StopAnyPlaying(this); + } + + /// + /// Invoked on changes to the beatmap to loop the track. See: . + /// + /// The beatmap change event. + private void applyLoopingToTrack(ValueChangedEvent beatmap) + { + if (!this.IsCurrentScreen()) + return; + + beatmap.NewValue.PrepareTrackForPreview(true); + music.EnsurePlayingSomething(); + } + + /// + /// Prompts the user to discard unsaved changes to the room before exiting. + /// + /// true if the user has confirmed they want to exit. + private bool ensureExitConfirmed() + { + if (ExitConfirmed) + return true; + + if (api.State.Value == APIState.Online) + return true; + + bool hasUnsavedChanges = room.RoomID == null && room.Playlist.Count > 0; + + if (dialogOverlay == null || !hasUnsavedChanges) + return true; + + // if the dialog is already displayed, block exiting until the user explicitly makes a decision. + if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) + { + discardChangesDialog.Flash(); + return false; + } + + dialogOverlay.Push(new ConfirmDiscardChangesDialog(() => + { + ExitConfirmed = true; + settingsOverlay.Hide(); + this.Exit(); + })); + + return false; + } + + #endregion + + protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(room.Playlist.FirstOrDefault()) + { + SelectedItem = { BindTarget = SelectedItem } + }; + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - Room.PropertyChanged -= onRoomPropertyChanged; + + userModsSelectOverlayRegistration?.Dispose(); + room.PropertyChanged -= onRoomPropertyChanged; } } } From 1cb4956cacd6e677af269419ba64e7fdadbf6f65 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 20:30:54 +0900 Subject: [PATCH 030/349] Fix not properly selecting the first playlist item --- .../Playlists/PlaylistsRoomSubScreen.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 22b9006e47..76f0c04295 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -467,8 +467,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists updateGameplayState(); } - #region Room/property updates - /// /// Responds to changes of the 's properties. /// @@ -485,7 +483,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Adjusts the visibility of the settings and main content when changes. + /// Responds to changes in to adjust the visibility of the settings and main content. /// Only the settings overlay is visible while the room isn't created, and only the main content is visible after creation. /// private void updateSetupState() @@ -502,9 +500,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists roomContent.Show(); settingsOverlay.Hide(); - progressSection.Alpha = room.MaxAttempts != null ? 1 : 0; - drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); - SelectedItem.Value = room.Playlist.FirstOrDefault(); + // Scheduled because room properties are updated in arbitrary order. + Schedule(() => + { + progressSection.Alpha = room.MaxAttempts != null ? 1 : 0; + drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); + + // Select an initial item for the user to help them get into a playable state quicker. + SelectedItem.Value = room.Playlist.FirstOrDefault(); + }); } } @@ -518,7 +522,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Responds to changes in the selected playlist item to validate the user's beatmap/ruleset/mod style and update UI components as necessary. + /// Responds to changes in the selected playlist item to validate the user's style selection. /// private void onSelectedItemChanged() { @@ -615,8 +619,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } - #endregion - /// /// Pushes a to start gameplay with the current selection. /// @@ -679,8 +681,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists })); } - #region Screen transition / track handling - public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); @@ -804,8 +804,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return false; } - #endregion - protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(room.Playlist.FirstOrDefault()) { SelectedItem = { BindTarget = SelectedItem } From 9458f0d01d2ae41d980a8a7a6a4bdc972e09457c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 20:38:44 +0900 Subject: [PATCH 031/349] Remove unnecessary update, document other usage --- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 76f0c04295..45e220cce9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -684,7 +684,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); - updateGameplayState(); beginHandlingTrack(); } @@ -697,8 +696,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - updateGameplayState(); beginHandlingTrack(); + + // Required when resuming from style selection. + updateGameplayState(); } public override bool OnExiting(ScreenExitEvent e) From e17383edbdfc6bac0c14d8ab1cdc6eea2d7d8045 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 21:47:23 +0900 Subject: [PATCH 032/349] Rewrite/optimise layout Reducing the amount of nesting to make things more readable. --- .../Playlists/PlaylistsRoomSubScreen.cs | 496 +++++++++--------- 1 file changed, 243 insertions(+), 253 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 45e220cce9..87bec5d8e1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -13,7 +13,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Screens; @@ -43,6 +42,31 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public partial class PlaylistsRoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner { + /// + /// Footer height. + /// + private const float footer_height = 50; + + /// + /// Padding between content and footer. + /// + private const float footer_padding = 30; + + /// + /// Internal padding of the content. + /// + private const float content_padding = 20; + + /// + /// Padding between columns of the content. + /// + private const float column_padding = 10; + + /// + /// Padding between rows of the content. + /// + private const float row_padding = 10; + public override string Title { get; } public override string ShortTitle => "playlist"; @@ -132,7 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); - InternalChild = new PopoverContainer + InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -140,297 +164,263 @@ namespace osu.Game.Screens.OnlinePlay.Playlists selectionPollingComponent = new SelectionPollingComponent(room), beatmapAvailabilityTracker, new MultiplayerRoomSounds(), - new GridContainer + new Container { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Padding = new MarginPadding { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 50) + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = footer_height + footer_padding }, - Content = new[] + Children = new[] { - // Padded main content (drawable room + main content) - new Drawable[] + roomContent = new GridContainer { - new Container + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + }, + Content = new[] + { + new Drawable[] { - Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Bottom = 30 + new DrawableMatchRoom(room, false) + { + OnEdit = () => settingsOverlay.Show(), + SelectedItem = SelectedItem + } }, - Children = new[] + null, + new Drawable[] { - roomContent = new GridContainer + new Container { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Masking = true, + CornerRadius = 10, + Children = new Drawable[] { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 10) - }, - Content = new[] - { - new Drawable[] + new Box { - new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = new DrawableMatchRoom(room, false) - { - OnEdit = () => settingsOverlay.Show(), - SelectedItem = SelectedItem - } - } + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. }, - null, - new Drawable[] + new GridContainer { - new Container + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(content_padding), + ColumnDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Children = new[] + new Dimension(), + new Dimension(GridSizeMode.Absolute, column_padding), + new Dimension(), + new Dimension(GridSizeMode.Absolute, column_padding), + new Dimension(), + }, + Content = new[] + { + new Drawable?[] { - new Container + new GridContainer { RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Child = new Box + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + new Dimension(GridSizeMode.AutoSize), + new Dimension(), }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(20), - Child = new Container + Content = new[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, - Child = new OsuContextMenuContainer + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = new GridContainer + new OverlinedPlaylistHeader(room), + }, + new Drawable[] + { + drawablePlaylist = new DrawableRoomPlaylist { RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] + SelectedItem = { BindTarget = SelectedItem }, + AllowSelection = true, + AllowShowingResults = true, + RequestResults = item => { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - }, - Content = new[] - { - new Drawable?[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Content = new[] - { - new Drawable[] { new OverlinedPlaylistHeader(room), }, - new Drawable[] - { - drawablePlaylist = new DrawableRoomPlaylist - { - RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem }, - AllowSelection = true, - AllowShowingResults = true, - RequestResults = item => - { - Debug.Assert(room.RoomID != null); - parentScreen?.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, - api.LocalUser.Value.Id)); - } - } - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - userModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Extra mods"), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Height = 30, - Text = "Select", - Action = showUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = userMods, - Scale = new Vector2(0.8f), - }, - } - } - } - }, - }, - new Drawable[] - { - userStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - userStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, - }, - new Drawable[] - { - progressSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OverlinedHeader("Progress"), - new RoomLocalUserInfo(room), - } - }, - }, - new Drawable[] - { - new OverlinedHeader("Leaderboard") - }, - new Drawable[] { leaderboard = new MatchLeaderboard(room) { RelativeSizeAxes = Axes.Both }, }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { new OverlinedHeader("Chat") }, - new Drawable[] { new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both } } - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - }, - }, + Debug.Assert(room.RoomID != null); + parentScreen?.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, + api.LocalUser.Value.Id)); + } } } } }, - new Container + null, + new GridContainer { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + userModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Height = 30, + Text = "Select", + Action = showUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = userMods, + Scale = new Vector2(0.8f), + } + } + } + } + } + }, + null, + new Drawable[] + { + userStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + userStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + } + }, + null, + new Drawable[] + { + progressSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OverlinedHeader("Progress"), + new RoomLocalUserInfo(room), + } + } + }, + null, + new Drawable[] + { + new OverlinedHeader("Leaderboard") + }, + new Drawable[] + { + leaderboard = new MatchLeaderboard(room) + { + RelativeSizeAxes = Axes.Both + }, + } + } }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OverlinedHeader("Chat") + }, + new Drawable[] + { + new MatchChatDisplay(room) + { + RelativeSizeAxes = Axes.Both + } + } + } + } } } } } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - // Resolves 1px masking errors between the settings overlay and the room panel. - Padding = new MarginPadding(-1), - Child = settingsOverlay = new PlaylistsRoomSettingsOverlay(room) - { - EditPlaylist = () => - { - if (this.IsCurrentScreen()) - this.Push(new PlaylistsSongSelect(room)); - }, - } } - }, - }, - }, - // Footer - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d") // Temporary. - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(5), - Child = new PlaylistsRoomFooter(room) - { - OnStart = startPlay, - OnClose = closePlaylist, - } - }, } } + }, + settingsOverlay = new PlaylistsRoomSettingsOverlay(room) + { + EditPlaylist = () => + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistsSongSelect(room)); + } + } + } + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = footer_height, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = new PlaylistsRoomFooter(room) + { + OnStart = startPlay, + OnClose = closePlaylist + } } } } From f2c75ef593a816851c491fc5f2bdb51e83bcef60 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 22:08:32 +0900 Subject: [PATCH 033/349] Always update selection when anything changes If style changes, then we also need to re-validate mods. Thus, we should just update the entire selection anyway, and merge the "gameplay state" into it for simplicity. --- .../Playlists/PlaylistsRoomSubScreen.cs | 77 ++++++++----------- 1 file changed, 32 insertions(+), 45 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 87bec5d8e1..38768097a4 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -444,17 +444,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists room.PropertyChanged += onRoomPropertyChanged; isIdle.BindValueChanged(_ => updatePollingRate(), true); - SelectedItem.BindValueChanged(_ => onSelectedItemChanged()); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSelectionState()); - userBeatmap.BindValueChanged(_ => updateGameplayState()); - userRuleset.BindValueChanged(_ => updateGameplayState()); - userMods.BindValueChanged(_ => updateGameplayState()); + SelectedItem.BindValueChanged(_ => updateSelectionState()); + userBeatmap.BindValueChanged(_ => updateSelectionState()); + userRuleset.BindValueChanged(_ => updateSelectionState()); + userMods.BindValueChanged(_ => updateSelectionState()); updateSetupState(); - updateGameplayState(); + updateSelectionState(); } /// @@ -512,9 +512,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Responds to changes in the selected playlist item to validate the user's style selection. + /// Responds to changes in the selected playlist item or user style (beatmap/ruleset/mods) to validate and update global states in preparation for a gameplay session. /// - private void onSelectedItemChanged() + private void updateSelectionState() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; @@ -530,28 +530,40 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (userBeatmap.Value != null && userBeatmap.Value.BeatmapSet!.OnlineID != item.Beatmap.BeatmapSet!.OnlineID) userBeatmap.Value = null; + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + // Reset ruleset style when no longer valid for the beatmap. if (userRuleset.Value != null) { - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; int beatmapRuleset = gameplayBeatmap.Ruleset.OnlineID; - if (beatmapRuleset > 0 && userRuleset.Value.OnlineID != beatmapRuleset) userRuleset.Value = null; } RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); + + // Remove any user mods that are no longer allowed. Mod[] allowedMods = item.Freestyle ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - - // Remove any user mods that are no longer allowed. Mod[] newUserMods = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(userMods.Value)) userMods.Value = newUserMods; - if (allowedMods.Length > 0) + // Update global gameplay state to correspond to the new selection. + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + int beatmapId = gameplayBeatmap.OnlineID; + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + Ruleset.Value = gameplayRuleset; + Mods.Value = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); + + // Update UI elements to reflect the new selection. + bool freemods = allowedMods.Length > 0; + bool freestyle = item.Freestyle; + + if (freemods) { userModsSection.Show(); userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); @@ -563,37 +575,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists userModsSelectOverlay.IsValidMod = _ => false; } - if (item.Freestyle) - userStyleSection.Show(); - else - userStyleSection.Hide(); - - updateGameplayState(); - } - - /// - /// Adjusts the global beatmap/ruleset/mods values in preparation for a gameplay session. - /// - private void updateGameplayState() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) - return; - - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; - RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; - Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); - Mod[] gameplayMods = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); - - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - int beatmapId = gameplayBeatmap.OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); - - Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); - Ruleset.Value = gameplayRuleset; - Mods.Value = gameplayMods; - - if (item.Freestyle) + if (freestyle) { + userStyleSection.Show(); + PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); PlaylistItem? currentItem = userStyleDisplayContainer.SingleOrDefault()?.Item; @@ -607,6 +592,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }; } } + else + userStyleSection.Hide(); } /// @@ -688,8 +675,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists base.OnResuming(e); beginHandlingTrack(); - // Required when resuming from style selection. - updateGameplayState(); + // Required to update beatmap/ruleset when resuming from style selection. + updateSelectionState(); } public override bool OnExiting(ScreenExitEvent e) From 06f27277bfe1730e450c188af5a82b7720a4a172 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Feb 2025 22:54:03 +0900 Subject: [PATCH 034/349] Use margins to remove padding from hidden elements If one of these elments is hidden, the following spacing element is expected to be hidden too. Simplest is to use margins. Old implementation already did this. --- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index c0fe78134f..79728fc4b2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -272,11 +272,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, row_padding), new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, row_padding), new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, row_padding), new Dimension(GridSizeMode.AutoSize), }, Content = new[] @@ -287,6 +284,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, Alpha = 0, Children = new Drawable[] { @@ -319,13 +317,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - null, new Drawable[] { userStyleSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, Alpha = 0, Children = new Drawable[] { @@ -338,13 +336,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - null, new Drawable[] { progressSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = row_padding }, Alpha = 0, Direction = FillDirection.Vertical, Children = new Drawable[] @@ -354,7 +352,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - null, new Drawable[] { new OverlinedHeader("Leaderboard") From 65cae7c7aafda7e764a3070958778f901d9f4c74 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 17 Feb 2025 15:02:38 +0900 Subject: [PATCH 035/349] Fix inverted condition --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 79728fc4b2..4ab20f8bb0 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -764,7 +764,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (ExitConfirmed) return true; - if (api.State.Value == APIState.Online) + if (api.State.Value != APIState.Online) return true; bool hasUnsavedChanges = room.RoomID == null && room.Playlist.Count > 0; From 9d7b01bcd47e789d86978508f0bf1148dc9ed066 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 20 Feb 2025 21:20:50 +0800 Subject: [PATCH 036/349] update guest difficulty display to consistent with the web page --- .../API/Requests/Responses/APIBeatmap.cs | 12 +++++ osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 50 +++++++++++++++++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index e5ecfe2c99..c6033e3255 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -103,6 +103,9 @@ namespace osu.Game.Online.API.Requests.Responses public double BPM { get; set; } + [JsonProperty(@"owners")] + public BeatmapOwner[] BeatmapOwners { get; set; } = Array.Empty(); + #region Implementation of IBeatmapInfo public IBeatmapMetadataInfo Metadata => (BeatmapSet as IBeatmapSetInfo)?.Metadata ?? new BeatmapMetadata(); @@ -171,5 +174,14 @@ namespace osu.Game.Online.API.Requests.Responses // ReSharper disable once NonReadonlyMemberInGetHashCode public override int GetHashCode() => OnlineID; } + + public class BeatmapOwner + { + [JsonProperty(@"id")] + public int Id { get; set; } + + [JsonProperty(@"username")] + public string Username { get; set; } = string.Empty; + } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index a7838651a9..8e36b0ed32 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -211,19 +211,59 @@ namespace osu.Game.Overlays.BeatmapSet private void showBeatmap(APIBeatmap? beatmapInfo) { guestMapperContainer.Clear(); + var beatmapOwners = beatmapInfo?.BeatmapOwners; - if (beatmapInfo?.AuthorID != BeatmapSet?.AuthorID) + if (beatmapOwners != null && (beatmapOwners.Length != 1 || beatmapOwners.First().Id != beatmapSet?.AuthorID)) { - APIUser? user = BeatmapSet?.RelatedUsers?.SingleOrDefault(u => u.OnlineID == beatmapInfo?.AuthorID); + APIUser[]? users = BeatmapSet?.RelatedUsers?.Where(u => beatmapOwners.Any(o => o.Id == u.OnlineID)).ToArray(); - if (user != null) + if (users != null) { - guestMapperContainer.AddText("mapped by "); - guestMapperContainer.AddUserLink(user); + formatGuestUser(users); } } version.Text = beatmapInfo?.DifficultyName ?? string.Empty; + return; + + void formatGuestUser(APIUser[] users) + { + int count = users.Length; + + guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); // set string.Empty here because we need link. + + switch (count) + { + case 1: + guestMapperContainer.AddUserLink(users[0]); + break; + + case 2: + guestMapperContainer.AddUserLink(users[0]); + guestMapperContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector); + guestMapperContainer.AddUserLink(users[1]); + break; + + default: + { + for (int i = 0; i < count; i++) + { + guestMapperContainer.AddUserLink(users[i]); + + if (i < count - 2) + { + guestMapperContainer.AddText(CommonStrings.ArrayAndWordsConnector); + } + else if (i == count - 2) + { + guestMapperContainer.AddText(CommonStrings.ArrayAndLastWordConnector); + } + } + + break; + } + } + } } private void updateDifficultyButtons() From cde50bca762d4646ecaec261a06af2bf62325971 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 20 Feb 2025 21:31:04 +0800 Subject: [PATCH 037/349] add test --- .../Online/TestSceneBeatmapSetOverlay.cs | 57 +++++++++++++++---- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 325cb9e0cb..ced95d09d9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -297,6 +297,31 @@ namespace osu.Game.Tests.Visual.Online AddAssert("guest mapper information shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().Any(s => s.Text == "BanchoBot")); } + [Test] + public void TestBeatmapsetWithALotGuestOwner() + { + AddStep("show map with 2 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(2))); + AddStep("move mouse to guest difficulty", () => + { + InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1)); + }); + AddStep("show map with 3 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(3))); + AddStep("move mouse to guest difficulty", () => + { + InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1)); + }); + AddStep("show map with 10 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(20))); + AddStep("move mouse to guest difficulty", () => + { + InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1)); + }); + AddStep("show map with 20 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(20))); + AddStep("move mouse to guest difficulty", () => + { + InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1)); + }); + } + private APIBeatmapSet createManyDifficultiesBeatmapSet() { var set = getBeatmapSet(); @@ -336,22 +361,31 @@ namespace osu.Game.Tests.Visual.Online return beatmapSet; } - private APIBeatmapSet createBeatmapSetWithGuestDifficulty() + private APIBeatmapSet createBeatmapSetWithGuestDifficulty(int guestCount = 1) { var set = getBeatmapSet(); var beatmaps = new List(); + var beatmapOwners = new List(); + var ownersAPIUser = new List(); - var guestUser = new APIUser + for (int i = 0; i < guestCount; i++) { - Username = @"BanchoBot", - Id = 3, - }; + var guestUser = new APIUser + { + Username = @$"BanchoBot{i}", + Id = i + 3, + }; - set.RelatedUsers = new[] - { - set.Author, guestUser - }; + beatmapOwners.Add(new APIBeatmap.BeatmapOwner + { + Username = @$"BanchoBot{i}", + Id = i + 3, + }); + ownersAPIUser.Add(guestUser); + } + + set.RelatedUsers = new[] { set.Author }.Concat(ownersAPIUser).ToArray(); beatmaps.Add(new APIBeatmap { @@ -366,7 +400,7 @@ namespace osu.Game.Tests.Visual.Online Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(), Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(), }, - Status = BeatmapOnlineStatus.Graveyard + Status = BeatmapOnlineStatus.Graveyard, }); beatmaps.Add(new APIBeatmap @@ -382,7 +416,8 @@ namespace osu.Game.Tests.Visual.Online Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(), Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(), }, - Status = BeatmapOnlineStatus.Graveyard + Status = BeatmapOnlineStatus.Graveyard, + BeatmapOwners = beatmapOwners.ToArray(), }); set.Beatmaps = beatmaps.ToArray(); From 4fa0288a20160299b9ecc5c43231f9f606a410a9 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 20 Feb 2025 22:03:57 +0800 Subject: [PATCH 038/349] maybe not a function --- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 76 +++++++++---------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 8e36b0ed32..ab3b8d882e 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -219,51 +219,45 @@ namespace osu.Game.Overlays.BeatmapSet if (users != null) { - formatGuestUser(users); + int count = users.Length; + + guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); // set string.Empty here because we need user link. + + switch (count) + { + case 1: + guestMapperContainer.AddUserLink(users[0]); + break; + + case 2: + guestMapperContainer.AddUserLink(users[0]); + guestMapperContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector); + guestMapperContainer.AddUserLink(users[1]); + break; + + default: + { + for (int i = 0; i < count; i++) + { + guestMapperContainer.AddUserLink(users[i]); + + if (i < count - 2) + { + guestMapperContainer.AddText(CommonStrings.ArrayAndWordsConnector); + } + else if (i == count - 2) + { + guestMapperContainer.AddText(CommonStrings.ArrayAndLastWordConnector); + } + } + + break; + } + } } } version.Text = beatmapInfo?.DifficultyName ?? string.Empty; - return; - - void formatGuestUser(APIUser[] users) - { - int count = users.Length; - - guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); // set string.Empty here because we need link. - - switch (count) - { - case 1: - guestMapperContainer.AddUserLink(users[0]); - break; - - case 2: - guestMapperContainer.AddUserLink(users[0]); - guestMapperContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector); - guestMapperContainer.AddUserLink(users[1]); - break; - - default: - { - for (int i = 0; i < count; i++) - { - guestMapperContainer.AddUserLink(users[i]); - - if (i < count - 2) - { - guestMapperContainer.AddText(CommonStrings.ArrayAndWordsConnector); - } - else if (i == count - 2) - { - guestMapperContainer.AddText(CommonStrings.ArrayAndLastWordConnector); - } - } - - break; - } - } - } } private void updateDifficultyButtons() From 74d9acbede330ca26e1533d657fa5919390eb1eb Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 20 Feb 2025 22:45:14 +0800 Subject: [PATCH 039/349] fix test --- osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index ced95d09d9..822e5f26bd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -289,12 +289,12 @@ namespace osu.Game.Tests.Visual.Online { InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(0)); }); - AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().All(s => s.Text != "BanchoBot")); + AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().All(s => s.Text != "BanchoBot0")); AddStep("move mouse to guest difficulty", () => { InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1)); }); - AddAssert("guest mapper information shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().Any(s => s.Text == "BanchoBot")); + AddAssert("guest mapper information shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().Any(s => s.Text == "BanchoBot0")); } [Test] @@ -310,7 +310,7 @@ namespace osu.Game.Tests.Visual.Online { InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1)); }); - AddStep("show map with 10 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(20))); + AddStep("show map with 10 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(10))); AddStep("move mouse to guest difficulty", () => { InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1)); From fa49b30b5cc077f807f60fd6964bf5416f5ec845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 11:30:52 +0100 Subject: [PATCH 040/349] Attempt to fix spectator list showing other users in multiplayer room even if they're not spectating better Maybe closes https://github.com/ppy/osu/issues/31972. Not sure. I have no reproduction scenario to work with, no solid understanding of how the issue can happen, and if this doesn't fix it, then I'm not even entirely sure how this can ever be fixed client-side. The working theory is that not watching updates to the room provoked a situation wherein the room was temporarily not in a correct state when `WatchingUsers` changed, therefore the collection change callback failed to exclude other players in the room from display. I'm only PRing this because of the `next-release` tag on the issue. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 80 ++++++++++++++++------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 4297c62712..98b3ede874 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -38,8 +38,9 @@ namespace osu.Game.Screens.Play.HUD public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); private BindableList watchingUsers { get; } = new BindableList(); + private BindableList actualSpectators { get; } = new BindableList(); + private Bindable userPlayingState { get; } = new Bindable(); - private int displayedSpectatorCount; private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; @@ -94,7 +95,9 @@ namespace osu.Game.Screens.Play.HUD ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); + watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); + multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom; + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); @@ -104,22 +107,55 @@ namespace osu.Game.Screens.Play.HUD this.FadeInFromZero(200, Easing.OutQuint); } - private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + private void onWatchingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + actualSpectators.Add((SpectatorUser)e.NewItems![i]!); + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + for (int i = 0; i < e.OldItems!.Count; i++) + actualSpectators.Remove((SpectatorUser)e.OldItems![i]!); + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + actualSpectators.Clear(); + break; + } + + default: + throw new NotSupportedException(); + } + + removePlayersFromMultiplayerRoom(); + } + + private void removePlayersFromMultiplayerRoom() + { + if (multiplayerClient.Room == null) + return; + // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. - // - // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions - // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). - // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) - // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component. - var excludedUserIds = new HashSet(); - if (multiplayerClient.Room != null) - excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID)); + var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet(); + actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID)); + } + private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -129,9 +165,6 @@ namespace osu.Game.Screens.Play.HUD var spectator = (SpectatorUser)e.NewItems![i]!; int index = Math.Max(e.NewStartingIndex, 0) + i; - if (excludedUserIds.Contains(spectator.OnlineID)) - continue; - if (index >= max_spectators_displayed) break; @@ -148,10 +181,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (actualSpectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, watchingUsers[i]); + addNewSpectatorToList(i, actualSpectators[i]); } break; @@ -167,8 +200,7 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID)); - header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper(); + header.Text = SpectatorListStrings.SpectatorCount(actualSpectators.Count).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -193,7 +225,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(actualSpectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() @@ -204,6 +236,14 @@ namespace osu.Game.Screens.Play.HUD Width = header.DrawWidth; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (multiplayerClient.IsNotNull()) + multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom; + } + private partial class SpectatorListEntry : PoolableDrawable { public Bindable Current { get; } = new Bindable(); From 8b2582a69d07adf343855b729dd143777abbcbf6 Mon Sep 17 00:00:00 2001 From: finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:54:27 -0800 Subject: [PATCH 041/349] Add tip pressure threshold slider ingame --- .../Settings/Sections/Input/TabletSettings.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 00ffbc1120..2cce6f18ec 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -45,6 +45,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; + private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0, MaxValue = 100 }; + [Resolved] private GameHost host { get; set; } @@ -213,6 +215,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = sizeY, CanBeShown = { BindTarget = enabled } }, + new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Tip Threshold", + Current = pressureThreshold, + CanBeShown = { BindTarget = enabled } + }, } }, }; @@ -267,6 +276,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue)); }); + pressureThreshold.BindTo(tabletHandler.PressureThreshold); + tablet.BindTo(tabletHandler.Tablet); tablet.BindValueChanged(val => Schedule(() => { From 543ad5b2a47591652d04ac66eb8730cafd7e06b9 Mon Sep 17 00:00:00 2001 From: Kunologist <2014709936@qq.com> Date: Mon, 24 Feb 2025 14:16:33 +0800 Subject: [PATCH 042/349] Add alt+wheel volume adjustment on result screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fe0d805cee..8fb3c66054 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -26,6 +26,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Overlays; +using osu.Game.Overlays.Volume; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Expanded.Accuracy; @@ -122,6 +123,7 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + new GlobalScrollAdjustsVolume(), StatisticsPanel = createStatisticsPanel().With(panel => { panel.RelativeSizeAxes = Axes.Both; @@ -503,12 +505,24 @@ namespace osu.Game.Screens.Ranking { } + protected override bool OnScroll(ScrollEvent e) + { + // Match stable behaviour of only alt-scroll adjusting volume. + // This is the same behaviour as the song selection screen. + if (!e.CurrentState.Keyboard.AltPressed) + return true; + + return base.OnScroll(e); + } + protected partial class VerticalScrollContainer : OsuScrollContainer { protected override Container Content => content; private readonly Container content; + protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed; + public VerticalScrollContainer() { Masking = false; From fc2d8bfe5f3b4ed3d1a0f7652dd84601e8115b75 Mon Sep 17 00:00:00 2001 From: finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:25:51 -0800 Subject: [PATCH 043/349] Clamp slider from 0 to 1 --- osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 2cce6f18ec..9d70e49659 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; - private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0, MaxValue = 100 }; + private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0.0f, MaxValue = 1.0f, Precision = 0.005f }; [Resolved] private GameHost host { get; set; } From 13ca8c20f6fa71bd196e30a5987cb112cbc7214f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 21:54:13 +0900 Subject: [PATCH 044/349] Make results screens use tasks to fetch scores --- .../Visual/Ranking/TestSceneResultsScreen.cs | 17 +-- .../Spectate/MultiSpectatorResultsScreen.cs | 7 +- .../Playlists/PlaylistItemResultsScreen.cs | 112 ++++++++++-------- .../PlaylistItemScoreResultsScreen.cs | 5 +- .../PlaylistItemUserBestResultsScreen.cs | 5 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 +++----- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 31 +++-- 7 files changed, 117 insertions(+), 111 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 3a08756090..4acbdb4a76 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -17,7 +17,6 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu; @@ -416,7 +415,7 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } - protected override APIRequest FetchScores(Action> scoresCallback) + protected override Task> FetchScores() { var scores = new List(); @@ -428,9 +427,7 @@ namespace osu.Game.Tests.Visual.Ranking scores.Add(score); } - scoresCallback.Invoke(scores); - - return null; + return Task.FromResult>(scores); } } @@ -446,9 +443,9 @@ namespace osu.Game.Tests.Visual.Ranking this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask; } - protected override APIRequest FetchScores(Action> scoresCallback) + protected override Task> FetchScores() { - Task.Run(async () => + return Task.Run>(async () => { await fetchWaitTask; @@ -461,12 +458,10 @@ namespace osu.Game.Tests.Visual.Ranking scores.Add(score); } - scoresCallback?.Invoke(scores); - Schedule(() => FetchCompleted = true); - }); - return null; + return scores; + }); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs index c240bbea0c..6e2f90e3b5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs @@ -1,9 +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; using System.Collections.Generic; -using osu.Game.Online.API; +using System.Threading.Tasks; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -23,8 +22,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000); } - protected override APIRequest? FetchScores(Action> scoresCallback) => null; + protected override Task> FetchScores() => Task.FromResult>([]); - protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null; + protected override Task> FetchNextPage(int direction) => Task.FromResult>([]); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 13ef5d6f64..ed90b3b1ae 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -76,16 +77,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected abstract APIRequest CreateScoreRequest(); - protected sealed override APIRequest FetchScores(Action> scoresCallback) + protected override async Task> FetchScores() { // This performs two requests: // 1. A request to show the relevant score (and scores around). // 2. If that fails, a request to index the room starting from the highest score. + var requestTaskSource = new TaskCompletionSource(); var userScoreReq = CreateScoreRequest(); + userScoreReq.Success += requestTaskSource.SetResult; + userScoreReq.Failure += requestTaskSource.SetException; + API.Queue(userScoreReq); - userScoreReq.Success += userScore => + try { + var userScore = await requestTaskSource.Task; var allScores = new List { userScore }; // Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date. @@ -113,88 +119,96 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - Schedule(() => - { - PerformSuccessCallback(scoresCallback, allScores); - hideLoadingSpinners(); - }); - }; - - // On failure, fallback to a normal index. - userScoreReq.Failure += _ => API.Queue(createIndexRequest(scoresCallback)); - - return userScoreReq; + return TransformScores(allScores); + } + catch (OperationCanceledException) + { + return []; + } + catch + { + return await fetchScoresAround(); + } + finally + { + Schedule(() => hideLoadingSpinners()); + } } - protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) + protected override async Task> FetchNextPage(int direction) { Debug.Assert(direction == 1 || direction == -1); MultiplayerScores? pivot = direction == -1 ? higherScores : lowerScores; - if (pivot?.Cursor == null) - return null; + return []; - if (pivot == higherScores) - LeftSpinner.Show(); - else - RightSpinner.Show(); + Schedule(() => + { + if (pivot == higherScores) + LeftSpinner.Show(); + else + RightSpinner.Show(); + }); - return createIndexRequest(scoresCallback, pivot); + return await fetchScoresAround(pivot); } /// /// Creates a with an optional score pivot. /// /// Does not queue the request. - /// The callback to perform with the resulting scores. /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score. - /// The indexing . - private APIRequest createIndexRequest(Action> scoresCallback, MultiplayerScores? pivot = null) + private async Task> fetchScoresAround(MultiplayerScores? pivot = null) { + var requestTaskSource = new TaskCompletionSource(); var indexReq = pivot != null ? new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID, pivot.Cursor, pivot.Params) : new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID); + indexReq.Success += requestTaskSource.SetResult; + indexReq.Failure += requestTaskSource.SetException; + API.Queue(indexReq); - indexReq.Success += r => + try { + var index = await requestTaskSource.Task; + if (pivot == lowerScores) { - lowerScores = r; - setPositions(r, pivot, 1); + lowerScores = index; + setPositions(index, pivot, 1); } else { - higherScores = r; - setPositions(r, pivot, -1); + higherScores = index; + setPositions(index, pivot, -1); } - Schedule(() => - { - PerformSuccessCallback(scoresCallback, r.Scores, r); - hideLoadingSpinners(r); - }); - }; - - indexReq.Failure += _ => hideLoadingSpinners(pivot); - - return indexReq; + return TransformScores(index.Scores, index); + } + catch (OperationCanceledException) + { + return []; + } + finally + { + Schedule(() => hideLoadingSpinners(pivot)); + } } /// /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback. /// - /// The callback to invoke with the final s. /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected virtual ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); - - // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already. - callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); - - return scoreInfos; + // Exclude the score provided to this screen since it's added already. + return scores + .Where(s => s.ID != Score?.OnlineID) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)) + .OrderByTotalScore() + .ToArray(); } private void hideLoadingSpinners(MultiplayerScores? pivot = null) @@ -213,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The to set positions on. /// The pivot. /// The amount to increment the pivot position by for each in . - private void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment) + private static void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment) => setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment); /// @@ -222,7 +236,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The to set positions on. /// The pivot position. /// The amount to increment the pivot position by for each in . - private void setPositions(MultiplayerScores scores, int pivotPosition, int increment) + private static void setPositions(MultiplayerScores scores, int pivotPosition, int increment) { foreach (var s in scores.Scores) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 05c03a4b28..c6c10e4d91 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; @@ -31,9 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + var scoreInfos = base.TransformScores(scores, pivot); Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); return scoreInfos; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 5b20496dba..1a0df0291c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; @@ -25,9 +24,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + var scoreInfos = base.TransformScores(scores, pivot); Schedule(() => { diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fe0d805cee..11e90a06b9 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,7 +25,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; -using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Overlays; using osu.Game.Scoring; @@ -60,9 +61,6 @@ namespace osu.Game.Screens.Ranking private bool skipExitTransition; - [Resolved] - private IAPIProvider api { get; set; } = null!; - protected StatisticsPanel StatisticsPanel { get; private set; } = null!; private Drawable bottomPanel = null!; @@ -237,10 +235,7 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - var req = FetchScores(fetchScoresCallback); - - if (req != null) - api.Queue(req); + FetchScores().ContinueWith(t => addScores(t.GetResultSafely())); StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } @@ -251,18 +246,16 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - APIRequest? nextPageRequest = null; + Task> nextPageTask = Task.FromResult>([]); if (ScorePanelList.IsScrolledToStart) - nextPageRequest = FetchNextPage(-1, fetchScoresCallback); + nextPageTask = FetchNextPage(-1); else if (ScorePanelList.IsScrolledToEnd) - nextPageRequest = FetchNextPage(1, fetchScoresCallback); + nextPageTask = FetchNextPage(1); - if (nextPageRequest != null) - { - lastFetchCompleted = false; - api.Queue(nextPageRequest); - } + nextPageTask.ContinueWith(t => addScores(t.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); + + lastFetchCompleted = nextPageTask.IsCompletedSuccessfully; } } @@ -329,17 +322,13 @@ namespace osu.Game.Screens.Ranking /// /// Performs a fetch/refresh of scores to be displayed. /// - /// A callback which should be called when fetching is completed. Scheduling is not required. - /// An responsible for the fetch operation. This will be queued and performed automatically. - protected virtual APIRequest? FetchScores(Action> scoresCallback) => null; + protected virtual Task> FetchScores() => Task.FromResult>([]); /// - /// Performs a fetch of the next page of scores. This is invoked every frame until a non-null is returned. + /// Performs a fetch of the next page of scores. This is invoked every frame. /// /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. - /// A callback which should be called when fetching is completed. Scheduling is not required. - /// An responsible for the fetch operation. This will be queued and performed automatically. - protected virtual APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null; + protected virtual Task> FetchNextPage(int direction) => Task.FromResult>([]); /// /// Creates the to be used to display extended information about scores. @@ -351,10 +340,14 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void fetchScoresCallback(IEnumerable scores) => Schedule(() => + private void addScores(IEnumerable scores) => Schedule(() => { foreach (var s in scores) - addScore(s); + { + var panel = ScorePanelList.AddScore(s); + if (detachedPanel != null) + panel.Alpha = 0; + } // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. Schedule(() => lastFetchCompleted = true); @@ -409,14 +402,6 @@ namespace osu.Game.Screens.Ranking return false; } - private void addScore(ScoreInfo score) - { - var panel = ScorePanelList.AddScore(score); - - if (detachedPanel != null) - panel.Alpha = 0; - } - private ScorePanel? detachedPanel; private void onStatisticsStateChanged(ValueChangedEvent state) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9f7604aa82..0593d5f91f 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -21,26 +23,36 @@ namespace osu.Game.Screens.Ranking [Resolved] private RulesetStore rulesets { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + public SoloResultsScreen(ScoreInfo score) : base(score) { } - protected override APIRequest? FetchScores(Action> scoresCallback) + protected override async Task> FetchScores() { Debug.Assert(Score != null); if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) - return null; + return []; + + var requestTaskSource = new TaskCompletionSource(); getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => + getScoreRequest.Success += requestTaskSource.SetResult; + getScoreRequest.Failure += requestTaskSource.SetException; + api.Queue(getScoreRequest); + + try { + var scores = await requestTaskSource.Task; var toDisplay = new List(); - for (int i = 0; i < r.Scores.Count; ++i) + for (int i = 0; i < scores.Scores.Count; ++i) { - var score = r.Scores[i]; + var score = scores.Scores[i]; int position = i + 1; if (score.MatchesOnlineID(Score)) @@ -58,9 +70,12 @@ namespace osu.Game.Screens.Ranking } } - scoresCallback.Invoke(toDisplay); - }; - return getScoreRequest; + return toDisplay; + } + catch (OperationCanceledException) + { + return []; + } } protected override void Dispose(bool isDisposing) From dfae11101f8b968611a442691b794066a52538c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:37:12 +0900 Subject: [PATCH 045/349] Populate playlists results screen with online beatmaps --- osu.Game/Online/Rooms/MultiplayerScore.cs | 3 +++ .../Playlists/PlaylistItemResultsScreen.cs | 26 ++++++++++++++++--- .../PlaylistItemScoreResultsScreen.cs | 5 ++-- .../PlaylistItemUserBestResultsScreen.cs | 5 ++-- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 2adee26da3..74eaea8dbc 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -80,6 +80,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("ruleset_id")] public int RulesetId { get; set; } + [JsonProperty("beatmap_id")] + public int BeatmapId { get; set; } + public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap) { var ruleset = rulesets.GetRuleset(RulesetId); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index ed90b3b1ae..bba30ec312 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -9,8 +9,11 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -39,6 +42,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] protected RulesetStore Rulesets { get; private set; } = null!; + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { @@ -119,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return TransformScores(allScores); + return await TransformScores(allScores); } catch (OperationCanceledException) { @@ -184,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return TransformScores(index.Scores, index); + return await TransformScores(index.Scores, index); } catch (OperationCanceledException) { @@ -201,12 +207,24 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - protected virtual ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected virtual async Task TransformScores(List scores, MultiplayerScores? pivot = null) { + APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); + + // Minimal data required to get various components in this screen to display correctly. + Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(b!.Difficulty), + DifficultyName = b.DifficultyName, + StarRating = b.StarRating, + Length = b.Length, + BPM = b.BPM + }); + // Exclude the score provided to this screen since it's added already. return scores .Where(s => s.ID != Score?.OnlineID) - .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById.GetValueOrDefault(s.BeatmapId) ?? Beatmap.Value.BeatmapInfo)) .OrderByTotalScore() .ToArray(); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index c6c10e4d91..f74b30c3f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -30,9 +31,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.TransformScores(scores, pivot); + var scoreInfos = await base.TransformScores(scores, pivot); Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); return scoreInfos; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 1a0df0291c..2e763666a7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -24,9 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.TransformScores(scores, pivot); + var scoreInfos = await base.TransformScores(scores, pivot); Schedule(() => { From 8a27b6689edf50cace897a3009640ff1ba8b2e7e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:51:36 +0900 Subject: [PATCH 046/349] Replace virtual async method with better abstraction --- .../Playlists/PlaylistItemResultsScreen.cs | 9 ++++----- .../Playlists/PlaylistItemScoreResultsScreen.cs | 8 +++----- .../Playlists/PlaylistItemUserBestResultsScreen.cs | 14 ++++---------- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++++++++ 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index bba30ec312..e9ba3bdb70 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return await TransformScores(allScores); + return await transformScores(allScores); } catch (OperationCanceledException) { @@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return await TransformScores(index.Scores, index); + return await transformScores(index.Scores); } catch (OperationCanceledException) { @@ -203,11 +203,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback. + /// Transforms returned into s. /// /// The s that were retrieved from s. - /// An optional pivot around which the scores were retrieved. - protected virtual async Task TransformScores(List scores, MultiplayerScores? pivot = null) + private async Task transformScores(List scores) { APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index f74b30c3f7..7f386cd293 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -31,11 +30,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) + protected override void OnScoresAdded(IEnumerable scores) { - var scoreInfos = await base.TransformScores(scores, pivot); - Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); - return scoreInfos; + base.OnScoresAdded(scores); + SelectedScore.Value ??= scores.SingleOrDefault(s => s.OnlineID == scoreId); } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 2e763666a7..faeef93b71 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -25,17 +24,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) + protected override void OnScoresAdded(IEnumerable scores) { - var scoreInfos = await base.TransformScores(scores, pivot); + base.OnScoresAdded(scores); - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault(); - }); - - return scoreInfos; + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value ??= scores.FirstOrDefault(s => s.UserID == userId) ?? scores.FirstOrDefault(); } } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 11e90a06b9..ce86ac0815 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -357,8 +357,18 @@ namespace osu.Game.Screens.Ranking // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); } + + OnScoresAdded(scores); }); + /// + /// Invoked after online scores are fetched and added to the list. + /// + /// The scores that were added. + protected virtual void OnScoresAdded(IEnumerable scores) + { + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From 3b5bf391da57e4ed3efcfd60f6e6fd3724f35b6d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:55:55 +0900 Subject: [PATCH 047/349] Arrays instead of enumerables --- .../Visual/Ranking/TestSceneResultsScreen.cs | 21 +++++++++---------- .../Spectate/MultiSpectatorResultsScreen.cs | 5 ++--- .../Playlists/PlaylistItemResultsScreen.cs | 6 +++--- .../PlaylistItemScoreResultsScreen.cs | 3 +-- .../PlaylistItemUserBestResultsScreen.cs | 3 +-- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++----- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ++-- 7 files changed, 24 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4acbdb4a76..b19288fd99 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NUnit.Framework; @@ -415,19 +414,19 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } - protected override Task> FetchScores() + protected override Task FetchScores() { - var scores = new List(); + var scores = new ScoreInfo[20]; - for (int i = 0; i < 20; i++) + for (int i = 0; i < scores.Length; i++) { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; score.HasOnlineReplay = true; - scores.Add(score); + scores[i] = score; } - return Task.FromResult>(scores); + return Task.FromResult(scores); } } @@ -443,19 +442,19 @@ namespace osu.Game.Tests.Visual.Ranking this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask; } - protected override Task> FetchScores() + protected override Task FetchScores() { - return Task.Run>(async () => + return Task.Run(async () => { await fetchWaitTask; - var scores = new List(); + var scores = new ScoreInfo[20]; - for (int i = 0; i < 20; i++) + for (int i = 0; i < scores.Length; i++) { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; - scores.Add(score); + scores[i] = score; } Schedule(() => FetchCompleted = true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs index 6e2f90e3b5..3cf1661c8d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.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.Threading.Tasks; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -22,8 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000); } - protected override Task> FetchScores() => Task.FromResult>([]); + protected override Task FetchScores() => Task.FromResult([]); - protected override Task> FetchNextPage(int direction) => Task.FromResult>([]); + protected override Task FetchNextPage(int direction) => Task.FromResult([]); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index e9ba3bdb70..0063bcd5f5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected abstract APIRequest CreateScoreRequest(); - protected override async Task> FetchScores() + protected override async Task FetchScores() { // This performs two requests: // 1. A request to show the relevant score (and scores around). @@ -141,7 +141,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } - protected override async Task> FetchNextPage(int direction) + protected override async Task FetchNextPage(int direction) { Debug.Assert(direction == 1 || direction == -1); @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// /// Does not queue the request. /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score. - private async Task> fetchScoresAround(MultiplayerScores? pivot = null) + private async Task fetchScoresAround(MultiplayerScores? pivot = null) { var requestTaskSource = new TaskCompletionSource(); var indexReq = pivot != null diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 7f386cd293..74b12b6d3c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.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.Game.Online.API; using osu.Game.Online.Rooms; @@ -30,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override void OnScoresAdded(IEnumerable scores) + protected override void OnScoresAdded(ScoreInfo[] scores) { base.OnScoresAdded(scores); SelectedScore.Value ??= scores.SingleOrDefault(s => s.OnlineID == scoreId); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index faeef93b71..866b094178 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.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.Game.Online.API; using osu.Game.Online.Rooms; @@ -24,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override void OnScoresAdded(IEnumerable scores) + protected override void OnScoresAdded(ScoreInfo[] scores) { base.OnScoresAdded(scores); diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index ce86ac0815..cfee2aa77d 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -246,7 +246,7 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - Task> nextPageTask = Task.FromResult>([]); + Task nextPageTask = Task.FromResult([]); if (ScorePanelList.IsScrolledToStart) nextPageTask = FetchNextPage(-1); @@ -322,13 +322,13 @@ namespace osu.Game.Screens.Ranking /// /// Performs a fetch/refresh of scores to be displayed. /// - protected virtual Task> FetchScores() => Task.FromResult>([]); + protected virtual Task FetchScores() => Task.FromResult([]); /// /// Performs a fetch of the next page of scores. This is invoked every frame. /// /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. - protected virtual Task> FetchNextPage(int direction) => Task.FromResult>([]); + protected virtual Task FetchNextPage(int direction) => Task.FromResult([]); /// /// Creates the to be used to display extended information about scores. @@ -340,7 +340,7 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void addScores(IEnumerable scores) => Schedule(() => + private void addScores(ScoreInfo[] scores) => Schedule(() => { foreach (var s in scores) { @@ -365,7 +365,7 @@ namespace osu.Game.Screens.Ranking /// Invoked after online scores are fetched and added to the list. /// /// The scores that were added. - protected virtual void OnScoresAdded(IEnumerable scores) + protected virtual void OnScoresAdded(ScoreInfo[] scores) { } diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 0593d5f91f..9fdffce644 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking { } - protected override async Task> FetchScores() + protected override async Task FetchScores() { Debug.Assert(Score != null); @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Ranking } } - return toDisplay; + return toDisplay.ToArray(); } catch (OperationCanceledException) { From 116b5a335a658023e3b58d3ec5caedd78230a3d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:56:38 +0900 Subject: [PATCH 048/349] `ConfigureAwait(false)` everywhere --- .../Playlists/PlaylistItemResultsScreen.cs | 14 +++++++------- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 0063bcd5f5..975cff0b68 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists try { - var userScore = await requestTaskSource.Task; + var userScore = await requestTaskSource.Task.ConfigureAwait(false); var allScores = new List { userScore }; // Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date. @@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return await transformScores(allScores); + return await transformScores(allScores).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -133,7 +133,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } catch { - return await fetchScoresAround(); + return await fetchScoresAround().ConfigureAwait(false); } finally { @@ -157,7 +157,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RightSpinner.Show(); }); - return await fetchScoresAround(pivot); + return await fetchScoresAround(pivot).ConfigureAwait(false); } /// @@ -177,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists try { - var index = await requestTaskSource.Task; + var index = await requestTaskSource.Task.ConfigureAwait(false); if (pivot == lowerScores) { @@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return await transformScores(index.Scores); + return await transformScores(index.Scores).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -208,7 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The s that were retrieved from s. private async Task transformScores(List scores) { - APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); + APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()).ConfigureAwait(false); // Minimal data required to get various components in this screen to display correctly. Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9fdffce644..73bed3383b 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking try { - var scores = await requestTaskSource.Task; + var scores = await requestTaskSource.Task.ConfigureAwait(false); var toDisplay = new List(); for (int i = 0; i < scores.Scores.Count; ++i) From bb457ca8e2fa2283d159fd214f6854046f38cebb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 23:17:02 +0900 Subject: [PATCH 049/349] Clean up completion handling --- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 +++++++++++++---------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index cfee2aa77d..397ad9c0b1 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -10,7 +10,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -66,7 +65,7 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; - private bool lastFetchCompleted; + private Task lastFetchTask = Task.CompletedTask; /// /// Whether the user can retry the beatmap from the results screen. @@ -235,7 +234,7 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - FetchScores().ContinueWith(t => addScores(t.GetResultSafely())); + lastFetchTask = Task.Run(async () => await addScores(await FetchScores().ConfigureAwait(false)).ConfigureAwait(false)); StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } @@ -244,18 +243,17 @@ namespace osu.Game.Screens.Ranking { base.Update(); - if (lastFetchCompleted) + if (lastFetchTask.IsCompleted) { - Task nextPageTask = Task.FromResult([]); + Task? nextPageTask = null; if (ScorePanelList.IsScrolledToStart) nextPageTask = FetchNextPage(-1); else if (ScorePanelList.IsScrolledToEnd) nextPageTask = FetchNextPage(1); - nextPageTask.ContinueWith(t => addScores(t.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); - - lastFetchCompleted = nextPageTask.IsCompletedSuccessfully; + if (nextPageTask != null) + lastFetchTask = Task.Run(async () => await addScores(await nextPageTask).ConfigureAwait(false)); } } @@ -340,26 +338,33 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void addScores(ScoreInfo[] scores) => Schedule(() => + private Task addScores(ScoreInfo[] scores) { - foreach (var s in scores) + var tcs = new TaskCompletionSource(); + + Schedule(() => { - var panel = ScorePanelList.AddScore(s); - if (detachedPanel != null) - panel.Alpha = 0; - } + foreach (var s in scores) + { + var panel = ScorePanelList.AddScore(s); + if (detachedPanel != null) + panel.Alpha = 0; + } - // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. - Schedule(() => lastFetchCompleted = true); + // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. + Schedule(() => tcs.SetResult()); - if (ScorePanelList.IsEmpty) - { - // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. - VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); - } + if (ScorePanelList.IsEmpty) + { + // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. + VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); + } - OnScoresAdded(scores); - }); + OnScoresAdded(scores); + }); + + return tcs.Task; + } /// /// Invoked after online scores are fetched and added to the list. From baf20d84843071e7c1c26418da36d1f4ff5c5a21 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 23:17:23 +0900 Subject: [PATCH 050/349] Fix loading spinners not hiding correctly --- .../Playlists/PlaylistItemResultsScreen.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 975cff0b68..f08b1818ab 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -135,10 +135,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { return await fetchScoresAround().ConfigureAwait(false); } - finally - { - Schedule(() => hideLoadingSpinners()); - } } protected override async Task FetchNextPage(int direction) @@ -196,10 +192,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { return []; } - finally - { - Schedule(() => hideLoadingSpinners(pivot)); - } } /// @@ -228,14 +220,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists .ToArray(); } - private void hideLoadingSpinners(MultiplayerScores? pivot = null) + protected override void OnScoresAdded(ScoreInfo[] scores) { - CentreSpinner.Hide(); + base.OnScoresAdded(scores); - if (pivot == lowerScores) - RightSpinner.Hide(); - else if (pivot == higherScores) - LeftSpinner.Hide(); + CentreSpinner.Hide(); + RightSpinner.Hide(); + LeftSpinner.Hide(); } /// From 65a62d5440b57440b61578981691fd7bb6f2fb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Feb 2025 15:38:48 +0100 Subject: [PATCH 051/349] Attempt to preserve sample control point bank when encoding beatmap This was reported internally in https://discord.com/channels/90072389919997952/1259818301517725707/1343470899357024286. The issue described was that sample specifications on control points in stable disappeared after the beatmap was updated from lazer. The reason why the sample specifications were getting dropped is that they got lost in the logic that attempts to translate per-hitobject samples that lazer has back into stable "green line" type control points. That process only attempted to preserve volume and custom sample bank, but did not keep the standard bank - likely because it's kind of superfluous information *for correct sample playback of the objects*, as the samples get encoded again for each object individually. However dropping this information makes for a subpar editing experience. The choice of which sample to pick the bank from is sort of arbitrary and I'm not sure if there's a correct one to pick. Intuitively picking the normal sample's bank (if there is one) seems most correct. --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 07e88ab956..d80d7e6b09 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -319,11 +319,13 @@ namespace osu.Game.Beatmaps.Formats SampleControlPoint createSampleControlPointFor(double time, IList samples) { int volume = samples.Max(o => o.Volume); + string bank = samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).Select(s => s.Bank).FirstOrDefault() + ?? samples.Select(s => s.Bank).First(); int customIndex = samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo) ? samples.OfType().Max(o => o.CustomSampleBank) : -1; - return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, CustomSampleBank = customIndex }; + return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, SampleBank = bank, CustomSampleBank = customIndex }; } } From 90290997a7b754a2506a4c10a8cc28cb3a0e33bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 14:46:37 +0900 Subject: [PATCH 052/349] Fix score panel difficulty depending on local beatmap This is a very special case where online beatmap/ruleset models are being ferried via `ScoreInfo` in what appear to `BeatmapDifficultyCache` as local `BeatmapInfo`/`RulesetInfo` models. Here, BDC will incorrectly attempt to proceed with calculating true difficulty where it cannot, and return 0. This is fixed locally because `ScoreInfo` is a very weird model, and I'm not sure whether BDC should contain logic to work around this. --- .../Expanded/ExpandedPanelMiddleContent.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 4bc559694a..9bef6a3f3a 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -1,8 +1,6 @@ // 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; using System.Collections.Generic; using System.Linq; @@ -16,6 +14,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -41,10 +40,10 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly List statisticDisplays = new List(); - private RollingCounter scoreCounter; + private RollingCounter scoreCounter = null!; [Resolved] - private ScoreManager scoreManager { get; set; } + private ScoreManager scoreManager { get; set; } = null!; /// /// Creates a new . @@ -63,12 +62,19 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load(BeatmapDifficultyCache beatmapDifficultyCache) + private void load(RealmAccess realmAccess, BeatmapDifficultyCache beatmapDifficultyCache) { var beatmap = score.BeatmapInfo!; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; string creator = metadata.Author.Username; + StarDifficulty starDifficulty = new StarDifficulty(beatmap.StarRating, 0); + + // In some cases, the beatmap ferried through ScoreInfo actually represents an online beatmap. + // If it isn't, we may be able to compute a more accuracy difficulty from the ruleset and mods. + if (realmAccess.Run(r => r.Find(score.BeatmapInfo!.ID)) != null) + starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods).GetResultSafely() ?? starDifficulty; + var topStatistics = new List { new AccuracyStatistic(score.Accuracy), @@ -146,7 +152,7 @@ namespace osu.Game.Screens.Ranking.Expanded Spacing = new Vector2(5, 0), Children = new Drawable[] { - new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default) + new StarRatingDisplay(starDifficulty) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft From 59cfcb3595aa79ea4384bca9af4472b48ace3917 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 14:49:38 +0900 Subject: [PATCH 053/349] Prefer local models where available --- .../Playlists/PlaylistItemResultsScreen.cs | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index f08b1818ab..1be0a7cf81 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -45,6 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { @@ -200,22 +203,43 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The s that were retrieved from s. private async Task transformScores(List scores) { - APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()).ConfigureAwait(false); + int[] allBeatmapIds = scores.Select(s => s.BeatmapId).Distinct().ToArray(); + BeatmapInfo[] localBeatmaps = allBeatmapIds.Select(id => beatmapManager.QueryBeatmap(b => b.OnlineID == id)) + .Where(b => b != null) + .ToArray()!; - // Minimal data required to get various components in this screen to display correctly. - Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo + int[] missingBeatmapIds = allBeatmapIds.Except(localBeatmaps.Select(b => b.OnlineID)).ToArray(); + APIBeatmap[] onlineBeatmaps = (await beatmapLookupCache.GetBeatmapsAsync(missingBeatmapIds).ConfigureAwait(false)).Where(b => b != null).ToArray()!; + + Dictionary beatmapsById = new Dictionary(); + + foreach (var beatmap in localBeatmaps) + beatmapsById[beatmap.OnlineID] = beatmap; + + foreach (var beatmap in onlineBeatmaps) { - Difficulty = new BeatmapDifficulty(b!.Difficulty), - DifficultyName = b.DifficultyName, - StarRating = b.StarRating, - Length = b.Length, - BPM = b.BPM - }); + // Minimal data required to get various components in this screen to display correctly. + beatmapsById[beatmap.OnlineID] = new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(beatmap.Difficulty), + DifficultyName = beatmap.DifficultyName, + StarRating = beatmap.StarRating, + Length = beatmap.Length, + BPM = beatmap.BPM + }; + } + + // Validate that we have all beatmaps we need. + foreach (int id in allBeatmapIds) + { + if (!beatmapsById.ContainsKey(id)) + throw new MissingBeatmapException(PlaylistItem, id); + } // Exclude the score provided to this screen since it's added already. return scores .Where(s => s.ID != Score?.OnlineID) - .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById.GetValueOrDefault(s.BeatmapId) ?? Beatmap.Value.BeatmapInfo)) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById[s.BeatmapId])) .OrderByTotalScore() .ToArray(); } @@ -280,5 +304,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists X = (float)(list.ScrollableExtent - list.Current - panelOffset); } } + + private class MissingBeatmapException : Exception + { + public MissingBeatmapException(PlaylistItem item, int beatmapId) + : base($"Missing beatmap {beatmapId} for playlist item {item.ID}") + { + } + } } } From b7d431fdde61b56f6f1831c366163da54c71d021 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 15:04:43 +0900 Subject: [PATCH 054/349] Include author --- .../OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 1be0a7cf81..53cd81b2a1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; @@ -222,6 +223,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmapsById[beatmap.OnlineID] = new BeatmapInfo { Difficulty = new BeatmapDifficulty(beatmap.Difficulty), + Metadata = + { + Author = new RealmUser + { + Username = beatmap.Metadata.Author.Username, + OnlineID = beatmap.Metadata.Author.OnlineID, + } + }, DifficultyName = beatmap.DifficultyName, StarRating = beatmap.StarRating, Length = beatmap.Length, From c7fd7cf9cd4071123ea83fb479cb8e543cdb1a0c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 17:39:56 +0900 Subject: [PATCH 055/349] Add missing ConfigureAwait --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 397ad9c0b1..26b13d026c 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Ranking nextPageTask = FetchNextPage(1); if (nextPageTask != null) - lastFetchTask = Task.Run(async () => await addScores(await nextPageTask).ConfigureAwait(false)); + lastFetchTask = Task.Run(async () => await addScores(await nextPageTask.ConfigureAwait(false)).ConfigureAwait(false)); } } From 2738c1a8077461b65c892ad0725ca928b1b220c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 16:17:36 +0900 Subject: [PATCH 056/349] Add back right-click-for-new-combo and right-click-delete when in compose mode Requested too many times to count. I'm not sure what to do about the code quality of this. It's a bit weird that there's no way to check the current composition tool from a higher level. Also it was discussed IRL that there should be some kind of hinting that existing notes will be deleted when they are hovered, but I'm not sure how well this will work in normal mapping flows, since it will display even in cases that users aren't intending to delete an object. Still willing to explore this direction though (it's just non-trivial to implement so I haven't yet). --- .../Edit/OsuHitObjectComposer.cs | 14 +++++++++ .../Components/ComposeBlueprintContainer.cs | 2 ++ .../Components/EditorSelectionHandler.cs | 29 +++++++++++++++++-- .../Compose/Components/SelectionHandler.cs | 4 ++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index b3e23daa99..ee386aa366 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit { @@ -351,6 +352,19 @@ namespace osu.Game.Rulesets.Osu.Edit } } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler; + + osuSelectionHandler.SelectionNewComboState.Value = + osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False; + } + + return base.OnMouseDown(e); + } + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Repeat) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 4c57eee971..4414e963bf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -387,6 +387,8 @@ namespace osu.Game.Screens.Edit.Compose.Components currentTool = value; + SelectionHandler.RightClickAlwaysQuickDeletes = currentTool is not SelectTool; + // As per stable editor, when changing tools, we should forcefully commit any pending placement. CommitIfPlacementActive(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index f9e7ef6df8..e90936e38a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -10,16 +10,23 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { public partial class EditorSelectionHandler : SelectionHandler { + /// + /// Whether right click should delete even when shift is not held. + /// + public bool RightClickAlwaysQuickDeletes { get; set; } + /// /// A special bank name that is only used in the editor UI. /// When selected and in placement mode, the bank of the last hit object will always be used. @@ -40,6 +47,14 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectedItems.CollectionChanged += onSelectedItemsChanged; } + protected override bool ShouldQuickDelete(MouseButtonEvent e) + { + if (RightClickAlwaysQuickDeletes && e.Button == MouseButton.Right) + return true; + + return base.ShouldQuickDelete(e); + } + protected override void DeleteItems(IEnumerable items) => EditorBeatmap.RemoveRange(items); #region Selection State @@ -293,7 +308,8 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach ((string bankName, var bindable) in SelectionAdditionBankStates) { - bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank)); + bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), + h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank)); } } @@ -378,14 +394,21 @@ namespace osu.Game.Screens.Edit.Compose.Components return; string normalBank = h.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT; - h.Samples = h.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList(); + h.Samples = h.Samples.Select(s => + s.Name != HitSampleInfo.HIT_NORMAL + ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) + : s) + .ToList(); if (h is IHasRepeats hasRepeats) { for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) { normalBank = hasRepeats.NodeSamples[i].FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT; - hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList(); + hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => + s.Name != HitSampleInfo.HIT_NORMAL + ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) + : s).ToList(); } } }); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 39fff169b7..c1cb8149e7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -263,6 +263,8 @@ namespace osu.Game.Screens.Edit.Compose.Components selectedBlueprints.Remove(blueprint); } + protected virtual bool ShouldQuickDelete(MouseButtonEvent e) => e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right); + /// /// Handle a blueprint requesting selection. /// @@ -271,7 +273,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. internal virtual bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { - if (e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right)) + if (ShouldQuickDelete(e)) { handleQuickDeletion(blueprint); return true; From 896caf4a8df9fb00cb48be52725ef6448f8dc01a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 17:52:43 +0900 Subject: [PATCH 057/349] Update test coverage --- .../Editor/TestSceneEditorPlacement.cs | 4 +- .../Editing/TestScenePlacementBlueprint.cs | 45 ++++++++++++++++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorPlacement.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorPlacement.cs index c523652ae1..0199e98af0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorPlacement.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorPlacement.cs @@ -3,9 +3,7 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Tests.Visual; @@ -31,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor AddStep("hover over first hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType().ElementAt(1))); AddStep("hover over second hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType().ElementAt(0))); AddStep("right click", () => InputManager.Click(MouseButton.Right)); - AddUntilStep("context menu open", () => Editor.ChildrenOfType().Any(menu => menu.State == MenuState.Open)); + AddUntilStep("second hit deleted", () => Editor.ChildrenOfType().Count(), () => Is.EqualTo(1)); } } } diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 4953cf83c9..37caccfa0d 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; @@ -14,8 +15,10 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Beatmaps; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Editing @@ -58,17 +61,47 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestContextMenu() + public void TestRightClickDuringEmptyPlacementTogglesNewCombo() { AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddStep("place circle", () => InputManager.Click(MouseButton.Left)); - AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); - AddStep("delete with right mouse", () => - { - InputManager.Click(MouseButton.Right); - }); + + AddStep("move mouse away from placed circle", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One)); + + AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); + AddAssert("new combo true", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.True)); + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); + AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); + } + + [Test] + public void TestRightClickDuringPlacementDeletes() + { + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); + + AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(0).Items); + AddAssert("circle not selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Exactly(0).Items); + } + + [Test] + public void TestRightClickDuringSelectionShowsContextMenu() + { + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + + AddStep("select selection tool", () => InputManager.Key(Key.Number1)); + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); + AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items); AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items); } From 3dde024650cc1564369dc0f23b462f876871400a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:00:16 +0900 Subject: [PATCH 058/349] Replace error handling with logs - Handling all errors matches master a little bit better. Logging exceptions in any case. - Not throwing when beatmaps are missing simplifies tests. --- .../Playlists/PlaylistItemResultsScreen.cs | 21 +++++++------------ osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 +++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 53cd81b2a1..572bf535f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; 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.Graphics.UserInterface; @@ -131,10 +132,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return await transformScores(allScores).ConfigureAwait(false); } - catch (OperationCanceledException) - { - return []; - } catch { return await fetchScoresAround().ConfigureAwait(false); @@ -192,8 +189,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return await transformScores(index.Scores).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (Exception ex) { + Logger.Log($"Failed to fetch scores (room: {RoomId}, item: {PlaylistItem.ID}): {ex}"); return []; } } @@ -242,7 +240,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists foreach (int id in allBeatmapIds) { if (!beatmapsById.ContainsKey(id)) - throw new MissingBeatmapException(PlaylistItem, id); + { + Logger.Log($"Failed to fetch beatmap {id} to display scores for playlist item {PlaylistItem.ID}"); + beatmapsById[id] = Beatmap.Value.BeatmapInfo; + } } // Exclude the score provided to this screen since it's added already. @@ -313,13 +314,5 @@ namespace osu.Game.Screens.OnlinePlay.Playlists X = (float)(list.ScrollableExtent - list.Current - panelOffset); } } - - private class MissingBeatmapException : Exception - { - public MissingBeatmapException(PlaylistItem item, int beatmapId) - : base($"Missing beatmap {beatmapId} for playlist item {item.ID}") - { - } - } } } diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 73bed3383b..3486d81e8a 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; @@ -72,8 +73,9 @@ namespace osu.Game.Screens.Ranking return toDisplay.ToArray(); } - catch (OperationCanceledException) + catch (Exception ex) { + Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {ex}"); return []; } } From c280c8fa1c463c280aee473b6c987d46a271dd25 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:31:06 +0900 Subject: [PATCH 059/349] Add support to tests Somewhat informal because it isn't super easy to handle. --- .../TestScenePlaylistsResultsScreen.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 33bd573617..dc5fb20e16 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -7,9 +7,12 @@ using System.Linq; using System.Net; using Newtonsoft.Json.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -32,6 +35,9 @@ namespace osu.Game.Tests.Visual.Playlists private const int scores_per_result = 10; private const int real_user_position = 200; + [Cached] + private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache(); + private ResultsScreen resultsScreen = null!; private int lowestScoreId; // Score ID of the lowest score in the list. @@ -41,6 +47,11 @@ namespace osu.Game.Tests.Visual.Playlists private int totalCount; private ScoreInfo userScore = null!; + public TestScenePlaylistsResultsScreen() + { + Add(beatmapLookupCache); + } + [SetUpSteps] public override void SetUpSteps() { @@ -279,6 +290,25 @@ namespace osu.Game.Tests.Visual.Playlists case IndexPlaylistScoresRequest: break; + case GetBeatmapsRequest getBeatmaps: + getBeatmaps.TriggerSuccess(new GetBeatmapsResponse + { + Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap + { + OnlineID = id, + StarRating = id, + DifficultyName = $"Beatmap {id}", + BeatmapSet = new APIBeatmapSet + { + Title = $"Title {id}", + Artist = $"Artist {id}", + AuthorString = $"Author {id}" + } + }).ToList() + }); + + return true; + default: return false; } @@ -346,6 +376,7 @@ namespace osu.Game.Tests.Visual.Playlists Position = real_user_position, MaxCombo = userScore.MaxCombo, User = userScore.User, + BeatmapId = RNG.Next(0, 7), ScoresAround = new MultiplayerScoresAround { Higher = new MultiplayerScores(), @@ -364,6 +395,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, @@ -379,6 +411,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, @@ -396,7 +429,7 @@ namespace osu.Game.Tests.Visual.Playlists return multiplayerUserScore; } - private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false) + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores) { var result = new IndexedMultiplayerScores(); @@ -413,6 +446,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = ScoreRank.X, MaxCombo = 1000, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, From 76bf03b05dd92938290c23631e00f82fc945f631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 10:56:28 +0100 Subject: [PATCH 060/349] Add failing decoder test case for too many combo colours --- .../Formats/LegacyBeatmapDecoderTest.cs | 29 ++++++++ .../Resources/too-many-combo-colours.osu | 73 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 osu.Game.Tests/Resources/too-many-combo-colours.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index adb1755c11..9747b654ae 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -404,6 +404,35 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestComboColourCountIsLimitedToEight() + { + var decoder = new LegacySkinDecoder(); + + using (var resStream = TestResources.OpenResource("too-many-combo-colours.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var comboColors = decoder.Decode(stream).ComboColours; + + Debug.Assert(comboColors != null); + + Color4[] expectedColors = + { + new Color4(142, 199, 255, 255), + new Color4(255, 128, 128, 255), + new Color4(128, 255, 255, 255), + new Color4(128, 255, 128, 255), + new Color4(255, 187, 255, 255), + new Color4(255, 177, 140, 255), + new Color4(100, 100, 100, 255), + new Color4(142, 199, 255, 255), + }; + Assert.AreEqual(expectedColors.Length, comboColors.Count); + for (int i = 0; i < expectedColors.Length; i++) + Assert.AreEqual(expectedColors[i], comboColors[i]); + } + } + [Test] public void TestGetLastObjectTime() { diff --git a/osu.Game.Tests/Resources/too-many-combo-colours.osu b/osu.Game.Tests/Resources/too-many-combo-colours.osu new file mode 100644 index 0000000000..477e362a6d --- /dev/null +++ b/osu.Game.Tests/Resources/too-many-combo-colours.osu @@ -0,0 +1,73 @@ +osu file format v14 + +[General] +AudioFilename: 03. Renatus - Soleily 192kbps.mp3 +AudioLeadIn: 0 +PreviewTime: 164471 +Countdown: 0 +SampleSet: Soft +StackLeniency: 0.7 +Mode: 0 +LetterboxInBreaks: 0 +WidescreenStoryboard: 0 + +[Editor] +Bookmarks: 11505,22054,32604,43153,53703,64252,74802,85351,95901,106450,116999,119637,130186,140735,151285,161834,164471,175020,185570,196119,206669,209306 +DistanceSpacing: 1.8 +BeatDivisor: 4 +GridSize: 4 +TimelineZoom: 2 + +[Metadata] +Title:Renatus +TitleUnicode:Renatus +Artist:Soleily +ArtistUnicode:Soleily +Creator:Gamu +Version:Insane +Source: +Tags:MBC7 Unisphere 地球ヤバイEP Chikyu Yabai +BeatmapID:557821 +BeatmapSetID:241526 + +[Difficulty] +HPDrainRate:6.5 +CircleSize:4 +OverallDifficulty:8 +ApproachRate:9 +SliderMultiplier:1.8 +SliderTickRate:2 + +[Events] +//Background and Video events +0,0,"machinetop_background.jpg",0,0 +//Break Periods +2,122474,140135 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +956,329.67032967033,4,2,0,60,1,0 + + +[Colours] +Combo1:142,199,255 +Combo2:255,128,128 +Combo3:128,255,255 +Combo4:128,255,128 +Combo5:255,187,255 +Combo6:255,177,140 +Combo7:100,100,100 +Combo8:142,199,255 +Combo9:255,128,128 +Combo10:128,255,255 +Combo11:128,255,128 +Combo12:255,187,255 +Combo13:255,177,140 +Combo14:100,100,100 + +[HitObjects] +192,168,956,6,0,P|184:128|200:80,1,90,4|0,1:2|0:0,0:0:0:0: From c2875423eeb264752954ab56f01a8ec2f702510d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:58:29 +0900 Subject: [PATCH 061/349] Cleanup score fetching a bit --- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 ++++++++++++++++------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 26b13d026c..010f7e1a93 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -234,27 +234,19 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - lastFetchTask = Task.Run(async () => await addScores(await FetchScores().ConfigureAwait(false)).ConfigureAwait(false)); - StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); + + fetchScores(null); } protected override void Update() { base.Update(); - if (lastFetchTask.IsCompleted) - { - Task? nextPageTask = null; - - if (ScorePanelList.IsScrolledToStart) - nextPageTask = FetchNextPage(-1); - else if (ScorePanelList.IsScrolledToEnd) - nextPageTask = FetchNextPage(1); - - if (nextPageTask != null) - lastFetchTask = Task.Run(async () => await addScores(await nextPageTask.ConfigureAwait(false)).ConfigureAwait(false)); - } + if (ScorePanelList.IsScrolledToStart) + fetchScores(-1); + else if (ScorePanelList.IsScrolledToEnd) + fetchScores(1); } #region Applause @@ -317,6 +309,37 @@ namespace osu.Game.Screens.Ranking #endregion + /// + /// Fetches the next page of scores in the given direction. + /// + /// The direction, or null to fetch any scores. + private void fetchScores(int? direction) + { + Debug.Assert(direction == null || direction == -1 || direction == 1); + + if (!lastFetchTask.IsCompleted) + return; + + lastFetchTask = Task.Run(async () => + { + ScoreInfo[] scores; + + switch (direction) + { + default: + scores = await FetchScores().ConfigureAwait(false); + break; + + case -1: + case 1: + scores = await FetchNextPage(direction.Value).ConfigureAwait(false); + break; + } + + await addScores(scores).ConfigureAwait(false); + }); + } + /// /// Performs a fetch/refresh of scores to be displayed. /// From e48d36ad1edd2226b5e7afd9e3bc3e397d00d7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:10:33 +0100 Subject: [PATCH 062/349] Add failing encoder test case for too many combo colours --- .../Formats/LegacyBeatmapEncoderTest.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index c8a09786ec..caebf52026 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -28,6 +28,7 @@ using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Resources; using osuTK; +using osuTK.Graphics; namespace osu.Game.Tests.Beatmaps.Formats { @@ -184,6 +185,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5)); } + [Test] + public void TestOnlyEightComboColoursEncoded() + { + var beatmapSkin = new LegacyBeatmapSkin(new BeatmapInfo(), null) + { + Configuration = + { + CustomComboColours = + { + new Color4(1, 1, 1, 255), + new Color4(2, 2, 2, 255), + new Color4(3, 3, 3, 255), + new Color4(4, 4, 4, 255), + new Color4(5, 5, 5, 255), + new Color4(6, 6, 6, 255), + new Color4(7, 7, 7, 255), + new Color4(8, 8, 8, 255), + new Color4(9, 9, 9, 255), + } + } + }; + + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((new Beatmap(), beatmapSkin)), string.Empty); + Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8)); + } + private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b) { // equal to null, no need to SequenceEqual @@ -212,6 +239,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name); + stream.Seek(0, SeekOrigin.Begin); + beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader); return (convert(beatmap), beatmapSkin); } } From 2167c7b8d56bbba00a2167f093a1ddf77d09baf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:13:57 +0100 Subject: [PATCH 063/349] Limit beatmap encoder & decoder to at most 8 combo colours --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 07e88ab956..5529828de2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -349,7 +349,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[Colours]"); - for (int i = 0; i < colours.Count; i++) + for (int i = 0; i < Math.Min(colours.Count, LegacyBeatmapDecoder.MAX_COMBO_COLOUR_COUNT); i++) { var comboColour = colours[i]; diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index ca4fadf458..6c290c4f1c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -18,6 +18,8 @@ namespace osu.Game.Beatmaps.Formats { public const int LATEST_VERSION = 14; + public const int MAX_COMBO_COLOUR_COUNT = 8; + /// /// The .osu format (beatmap) version. /// @@ -126,7 +128,9 @@ namespace osu.Game.Beatmaps.Formats string[] split = pair.Value.Split(','); Color4 colour = convertSettingStringToColor4(split, allowAlpha, pair); - bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal); + bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal) + && int.TryParse(pair.Key[5..], out int comboIndex) + && comboIndex >= 1 && comboIndex <= MAX_COMBO_COLOUR_COUNT; if (isCombo) { From 6b76b8ccdda0ffe4a0b7d47e7fe3ddfd38e70d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:16:37 +0100 Subject: [PATCH 064/349] Do not allow adding more than 8 combo colours in editor --- osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs | 10 ++++++---- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index fad58841e3..258a97d79c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -31,9 +31,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 public LocalisableString Caption { get; init; } public LocalisableString HintText { get; init; } + public BindableBool CanAdd { get; } = new BindableBool(true); + private Box background = null!; private FormFieldCaption caption = null!; private FillFlowContainer flow = null!; + private RoundedButton addButton = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -47,8 +50,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 Masking = true; CornerRadius = 5; - RoundedButton button; - InternalChildren = new Drawable[] { background = new Box @@ -76,7 +77,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, Spacing = new Vector2(5), - Child = button = new RoundedButton + Child = addButton = new RoundedButton { Action = addNewColour, Size = new Vector2(70), @@ -87,7 +88,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, }; - flow.SetLayoutPosition(button, float.MaxValue); + flow.SetLayoutPosition(addButton, float.MaxValue); } protected override void LoadComplete() @@ -99,6 +100,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (args.Action != NotifyCollectionChangedAction.Replace) updateColours(); }, true); + CanAdd.BindValueChanged(_ => addButton.Alpha = CanAdd.Value ? 1 : 0, true); updateState(); } diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 8de7f86523..865fe05c54 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Beatmaps.Formats; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Skinning; @@ -54,6 +55,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapSkin.ComboColours.Clear(); Beatmap.BeatmapSkin.ComboColours.AddRange(comboColours.Colours); + updateAddButtonVisibility(); + syncingColours = false; } }); @@ -68,8 +71,14 @@ namespace osu.Game.Screens.Edit.Setup comboColours.Colours.Clear(); comboColours.Colours.AddRange(Beatmap.BeatmapSkin?.ComboColours); + updateAddButtonVisibility(); + syncingColours = false; }); + + updateAddButtonVisibility(); + + void updateAddButtonVisibility() => comboColours.CanAdd.Value = comboColours.Colours.Count < LegacyBeatmapDecoder.MAX_COMBO_COLOUR_COUNT; } } } From f3632a466fbf88484d2c3be9e461a9e7610e40da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 12:01:30 +0100 Subject: [PATCH 065/349] Prevent closing team chat channels via Ctrl-W As pointed out in https://github.com/ppy/osu/pull/32079#issuecomment-2680297760. The comment suggested putting that logic in `ChannelManager` but honestly I kinda don't see it working out. It'd probably be multiple boolean arguments for `leaveChannel()` (because `sendLeaveRequest` or whatever already exists), and then there's this one usage in tournament client: https://github.com/ppy/osu/blob/31aded69714cf205c215893368d1f148c9a73319/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs#L57-L58 I'm not sure how that would interact with this particular change, but I think there is a nonzero possibility that it would interact badly. So in general I kinda just prefer steering clear of all that and adding a local one-liner. --- osu.Game/Overlays/ChatOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index c49afa3a66..7f4ba3e2e2 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -228,7 +228,8 @@ namespace osu.Game.Overlays return true; case PlatformAction.DocumentClose: - channelManager.LeaveChannel(currentChannel.Value); + if (currentChannel.Value?.Type != ChannelType.Team) + channelManager.LeaveChannel(currentChannel.Value); return true; case PlatformAction.TabRestore: From d3c4afe65d8d86edb8c391d6db96849ef4f48709 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 13:16:51 +0900 Subject: [PATCH 066/349] Fix typo --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 9bef6a3f3a..0190a6f959 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded StarDifficulty starDifficulty = new StarDifficulty(beatmap.StarRating, 0); // In some cases, the beatmap ferried through ScoreInfo actually represents an online beatmap. - // If it isn't, we may be able to compute a more accuracy difficulty from the ruleset and mods. + // If it isn't, we may be able to compute a more accurate difficulty from the ruleset and mods. if (realmAccess.Run(r => r.Find(score.BeatmapInfo!.ID)) != null) starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods).GetResultSafely() ?? starDifficulty; From d31588939c03fb365cf7acd09b6a441a49f100f7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 13:39:16 +0900 Subject: [PATCH 067/349] Disallow attempting to close multiplayer rooms --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 10 +--------- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 +++ .../OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs | 11 +++++++++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 0e08e398a4..30e7b0d31b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -361,14 +360,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge api.Queue(req); } - public void Close(Room room) - { - Debug.Assert(room.RoomID != null); - - var request = new ClosePlaylistRequest(room.RoomID.Value); - request.Success += RefreshRooms; - api.Queue(request); - } + public abstract void Close(Room room); /// /// Push a room as a new subscreen. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 873a9cde88..8f2490f77a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -99,6 +99,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }); } + public override void Close(Room room) + => throw new NotSupportedException("Cannot close multiplayer rooms."); + protected override void OpenNewRoom(Room room) { if (!client.IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 6ed367328c..9de13eb270 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; @@ -74,6 +76,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists api.Queue(joinRoomRequest); } + public override void Close(Room room) + { + Debug.Assert(room.RoomID != null); + + var request = new ClosePlaylistRequest(room.RoomID.Value); + request.Success += RefreshRooms; + api.Queue(request); + } + protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton(); protected override Room CreateNewRoom() From 47ca5c90a5bada5733c89376916236b29c69467f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 14:50:35 +0900 Subject: [PATCH 068/349] Refactor post-join setup to not pass delegates around --- .../Online/Multiplayer/MultiplayerClient.cs | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 636cba719b..1f85aa5d45 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -170,13 +170,23 @@ namespace osu.Game.Online.Multiplayer private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); private CancellationTokenSource? joinCancellationSource; + /// + /// Creates and joins a described by an API . + /// + /// The API describing the room to create. + /// If the current user is already in another room. public async Task CreateRoom(Room room) { if (Room != null) throw new InvalidOperationException("Cannot create a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); + + await joinOrLeaveTaskChain.Add(async () => + { + var multiplayerRoom = await CreateRoomInternal(new MultiplayerRoom(room)).ConfigureAwait(false); + await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); + }, cancellationSource.Token).ConfigureAwait(false); } /// @@ -184,54 +194,61 @@ namespace osu.Game.Online.Multiplayer /// /// The API . /// An optional password to use for the join operation. + /// If the current user is already in another room, or does not represent an active room. public async Task JoinRoom(Room room, string? password = null) { if (Room != null) throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - Debug.Assert(room.RoomID != null); + if (room.RoomID == null) + throw new InvalidOperationException("Cannot join an inactive room."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoomInternal(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); - } - private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) - { await joinOrLeaveTaskChain.Add(async () => { - // Initialise the server-side room. - MultiplayerRoom joinedRoom = await initFunc(room).ConfigureAwait(false); + var multiplayerRoom = await JoinRoomInternal(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false); + await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); + }, cancellationSource.Token).ConfigureAwait(false); + } - // Populate users. - await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); + /// + /// Performs post-join setup of a . + /// + /// The incoming API that was requested to be joined. + /// The resuling that was joined. + /// A token to cancel the process. + private async Task setupJoinedRoom(Room apiRoom, MultiplayerRoom joinedRoom, CancellationToken cancellationToken) + { + // Populate users. + await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); - // Update the stored room (must be done on update thread for thread-safety). - await runOnUpdateThreadAsync(() => - { - Debug.Assert(Room == null); - Debug.Assert(APIRoom == null); + // Update the stored room (must be done on update thread for thread-safety). + await runOnUpdateThreadAsync(() => + { + Debug.Assert(Room == null); + Debug.Assert(APIRoom == null); - Room = joinedRoom; - APIRoom = room; + Room = joinedRoom; + APIRoom = apiRoom; - APIRoom.RoomID = joinedRoom.RoomID; - APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); - APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); - // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. - APIRoom.EndDate = null; + APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); + APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); + // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. + APIRoom.EndDate = null; - Debug.Assert(LocalUser != null); - addUserToAPIRoom(LocalUser); + Debug.Assert(LocalUser != null); + addUserToAPIRoom(LocalUser); - foreach (var user in joinedRoom.Users) - updateUserPlayingState(user.UserID, user.State); + foreach (var user in joinedRoom.Users) + updateUserPlayingState(user.UserID, user.State); - updateLocalRoomSettings(joinedRoom.Settings); + updateLocalRoomSettings(joinedRoom.Settings); - postServerShuttingDownNotification(); + postServerShuttingDownNotification(); - OnRoomJoined(); - }, cancellationToken).ConfigureAwait(false); + OnRoomJoined(); }, cancellationToken).ConfigureAwait(false); } From 0b453772da964dddd2ee73f677367293b26dbf2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Feb 2025 15:14:53 +0900 Subject: [PATCH 069/349] Disable button instead of hiding (and add tooltip) --- .../Graphics/UserInterfaceV2/FormColourPalette.cs | 14 +++++++++++++- osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 5 ++++- .../Overlays/BeatmapSet/Buttons/FavouriteButton.cs | 5 ++--- osu.Game/Overlays/Settings/SettingsButton.cs | 5 +---- .../Screens/OnlinePlay/Components/ReadyButton.cs | 5 ++--- .../Playlists/AddPlaylistToCollectionButton.cs | 5 ++--- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index 258a97d79c..a0348fa27a 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -100,7 +100,19 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (args.Action != NotifyCollectionChangedAction.Replace) updateColours(); }, true); - CanAdd.BindValueChanged(_ => addButton.Alpha = CanAdd.Value ? 1 : 0, true); + CanAdd.BindValueChanged(canAdd => + { + if (canAdd.NewValue) + { + addButton.Enabled.Value = true; + addButton.TooltipText = string.Empty; + } + else + { + addButton.Enabled.Value = false; + addButton.TooltipText = "Maximum combo colours reached"; + } + }, true); updateState(); } diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 6aded3fe32..9b57ebb200 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Backgrounds; @@ -17,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2 { - public partial class RoundedButton : OsuButton, IFilterable + public partial class RoundedButton : OsuButton, IFilterable, IHasTooltip { protected TrianglesV2? Triangles { get; private set; } @@ -107,5 +108,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } public bool FilteringActive { get; set; } + + public virtual LocalisableString TooltipText { get; set; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index cbdb2ea190..eab394c8f6 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; @@ -21,7 +20,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Overlays.BeatmapSet.Buttons { - public partial class FavouriteButton : HeaderButton, IHasTooltip + public partial class FavouriteButton : HeaderButton { public readonly Bindable BeatmapSet = new Bindable(); @@ -32,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly IBindable localUser = new Bindable(); - public LocalisableString TooltipText + public override LocalisableString TooltipText { get { diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 3f5d612eb8..196ddca953 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -6,13 +6,12 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Overlays.Settings { - public partial class SettingsButton : RoundedButton, IHasTooltip, IConditionalFilterable + public partial class SettingsButton : RoundedButton, IConditionalFilterable { public SettingsButton() { @@ -25,8 +24,6 @@ namespace osu.Game.Overlays.Settings public BindableBool CanBeShown { get; } = new BindableBool(true); IBindable IConditionalFilterable.CanBeShown => CanBeShown; - public LocalisableString TooltipText { get; set; } - public override IEnumerable FilterTerms { get diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 2e669fd1b2..56e2719e9c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online; @@ -11,7 +10,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { - public abstract partial class ReadyButton : RoundedButton, IHasTooltip + public abstract partial class ReadyButton : RoundedButton { public new readonly BindableBool Enabled = new BindableBool(); @@ -29,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateState() => base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; - public virtual LocalisableString TooltipText + public override LocalisableString TooltipText { get { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 741173f9a3..47629981f1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Collections; @@ -18,7 +17,7 @@ using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip + public partial class AddPlaylistToCollectionButton : RoundedButton { private readonly Room room; @@ -161,7 +160,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - public LocalisableString TooltipText + public override LocalisableString TooltipText { get { From 5b318edbfbd9aa3ece3a491a9a641d7eee3a4c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Feb 2025 14:57:42 +0100 Subject: [PATCH 070/349] Fix sliders not being selectable if the body is hidden but the head is still visible Closes https://github.com/ppy/osu/issues/31998. Previously: https://github.com/ppy/osu/commit/1648f2efa306f587714178f113e69d8ad8c4ac02, https://github.com/ppy/osu/pull/31923. Oh input handling, how I love ya. --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 39c0681dba..60f335c419 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0)) + if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0 || DrawableObject.HeadCircle.Alpha > 0)) return true; if (ControlPointVisualiser == null) From 09131740992b15ca322054e5c8aee784c6eade79 Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 00:20:58 +0600 Subject: [PATCH 071/349] Fix settings control not visible because of previous search This also makes `SettingsPanel`'s `SearchTextBox` protected from private so that `SettingsOverlay` can access it. --- osu.Game/Overlays/SettingsOverlay.cs | 3 +++ osu.Game/Overlays/SettingsPanel.cs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 1157860e03..8a39d75565 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -68,6 +68,9 @@ namespace osu.Game.Overlays public void ShowAtControl() where T : Drawable { + // if search isn't cleared then the target control won't be visible if it doesn't match the query + SearchTextBox.Current.Value = ""; + Show(); // wait for load of sections diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index df50e0f339..d8b054eaf8 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays public SettingsSectionsContainer SectionsContainer { get; private set; } - private SeekLimitedSearchTextBox searchTextBox; + protected SeekLimitedSearchTextBox SearchTextBox; protected override string PopInSampleName => "UI/settings-pop-in"; protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; @@ -135,7 +135,7 @@ namespace osu.Game.Overlays }, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Child = searchTextBox = new SettingsSearchTextBox + Child = SearchTextBox = new SettingsSearchTextBox { RelativeSizeAxes = Axes.X, Origin = Anchor.TopCentre, @@ -183,8 +183,8 @@ namespace osu.Game.Overlays Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(1, TRANSITION_LENGTH / 2, Easing.OutQuint); - searchTextBox.TakeFocus(); - searchTextBox.HoldFocus = true; + SearchTextBox.TakeFocus(); + SearchTextBox.HoldFocus = true; } protected virtual float ExpandedPosition => 0; @@ -199,8 +199,8 @@ namespace osu.Game.Overlays Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(0, TRANSITION_LENGTH / 2, Easing.OutQuint); - searchTextBox.HoldFocus = false; - if (searchTextBox.HasFocus) + SearchTextBox.HoldFocus = false; + if (SearchTextBox.HasFocus) GetContainingFocusManager()!.ChangeFocus(null); } @@ -208,7 +208,7 @@ namespace osu.Game.Overlays protected override void OnFocus(FocusEvent e) { - searchTextBox.TakeFocus(); + SearchTextBox.TakeFocus(); base.OnFocus(e); } @@ -234,7 +234,7 @@ namespace osu.Game.Overlays loading.Hide(); - searchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchTerm = term.NewValue, true); + SearchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchTerm = term.NewValue, true); loadSidebarButtons(); }); From a659936c57a1f51b917102bc737bfbc22187973e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 13:19:19 +0900 Subject: [PATCH 072/349] Inline some methods --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 4 +--- .../OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index eda3bace40..f74de26f1f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -444,7 +444,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (!ApplyButton.Enabled.Value) return; - hideError(); + ErrorText.FadeOut(50); Debug.Assert(applyingSettingsOperation == null); applyingSettingsOperation = ongoingOperationTracker.BeginOperation(); @@ -480,8 +480,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } } - private void hideError() => ErrorText.FadeOut(50); - private void onSuccess() => Schedule(() => { Debug.Assert(applyingSettingsOperation != null); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index b3d1d577ed..9c0363f40e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -437,7 +437,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!ApplyButton.Enabled.Value) return; - hideError(); + ErrorText.FadeOut(50); room.Name = NameField.Text; room.Availability = AvailabilityPicker.Current.Value; @@ -448,15 +448,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists loadingLayer.Show(); var req = new CreateRoomRequest(room); - req.Success += onSuccess; + req.Success += _ => loadingLayer.Hide(); req.Failure += e => onError(req.Response?.Error ?? e.Message); api.Queue(req); } - private void hideError() => ErrorText.FadeOut(50); - - private void onSuccess(Room room) => loadingLayer.Hide(); - private void onError(string text) { // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. From e1723ec1bbfe40e70754b1971b9e1602eed4a7a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 14:05:49 +0900 Subject: [PATCH 073/349] Adjust preview time display to not conflict with bookmarks --- .../Timelines/Summary/Parts/PreviewTimePart.cs | 5 +++++ .../Components/Timelines/Summary/SummaryTimeline.cs | 13 ++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs index 67bb1ef500..72b58bcb5f 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Extensions; @@ -36,6 +37,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts : base(time) { Alpha = 0.8f; + + // Display as a small circle on the middle line as to not clash with other displays. + RelativeSizeAxes = Axes.None; + Height = Width = 5; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index c01481e840..568137cce1 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -52,13 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }, } }, - new PreviewTimePart - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Height = 0.4f, - }, new BreakPart { Anchor = Anchor.Centre, @@ -85,6 +78,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary RelativeSizeAxes = Axes.Both, Height = 0.4f }, + new PreviewTimePart + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, new MarkerPart { RelativeSizeAxes = Axes.Both }, }; } From 3e8dafa3c51d6c6434d56ac0c51ffe4800c23fd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 14:43:00 +0900 Subject: [PATCH 074/349] Add basic setup for mania legacy barline implementation --- .../Objects/Drawables/DrawableBarLine.cs | 3 +- .../Skinning/Default/DefaultBarLine.cs | 4 ++- .../Skinning/Legacy/LegacyBarLine.cs | 33 +++++++++++++++++++ .../Legacy/ManiaLegacySkinTransformer.cs | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 25fed1a84c..be0f84d7fd 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(barLine) { RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; } [BackgroundDependencyLoader] @@ -36,8 +37,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - - Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true); } protected override void OnApply() diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs index ef75e9df11..05fba1241f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject) { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; // Avoid flickering due to no anti-aliasing of boxes by default. var edgeSmoothness = new Vector2(0.3f); @@ -75,6 +75,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default private void updateMajor(ValueChangedEvent major) { + Height = major.NewValue ? 1.7f : 1.2f; + mainLine.Alpha = major.NewValue ? 0.5f : 0.2f; leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs new file mode 100644 index 0000000000..64ea1df2ae --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning.Legacy +{ + public partial class LegacyBarLine : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 1.2f; + + // Avoid flickering due to no anti-aliasing of boxes by default. + var edgeSmoothness = new Vector2(0.3f); + + AddInternal(new Box + { + Name = "Bar line", + EdgeSmoothness = edgeSmoothness, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 76af569b95..c321fcda87 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new LegacyStageForeground(); case ManiaSkinComponents.BarLine: - return null; // Not yet implemented. + return new LegacyBarLine(); default: throw new UnsupportedSkinComponentException(lookup); From cb29459a1e5c2d97a68a548c592ea3140513632d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 15:13:13 +0900 Subject: [PATCH 075/349] Add support for legacy osu!mania barline height and colour spec --- .../Objects/Drawables/DrawableBarLine.cs | 4 ++-- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs | 9 +++++++-- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 1 + osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 3 +++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ osu.Game/Skinning/LegacySkin.cs | 6 ++++++ 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index be0f84d7fd..c9fc0763a8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(barLine) { RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + Height = 1; } - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load() { AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine()) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs index 64ea1df2ae..ce48c49b2e 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs @@ -5,17 +5,22 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public partial class LegacyBarLine : CompositeDrawable { [BackgroundDependencyLoader] - private void load() + private void load(ISkinSource skin) { + float skinHeight = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BarLineHeight)?.Value ?? 1; + RelativeSizeAxes = Axes.X; - Height = 1.2f; + Height = 1.2f * skinHeight; + Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BarLineColour)?.Value ?? Color4.White; // Avoid flickering due to no anti-aliasing of boxes by default. var edgeSmoothness = new Vector2(0.3f); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index db1f216b6e..1e6fa44e68 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -41,6 +41,7 @@ namespace osu.Game.Skinning public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public float ComboPosition = 111 * POSITION_SCALE_FACTOR; public float ScorePosition = 300 * POSITION_SCALE_FACTOR; + public float BarLineHeight = 1; public bool ShowJudgementLine = true; public bool KeysUnderNotes; public int LightFramePerSecond = 60; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index ee354de68b..e94fb23681 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -70,6 +70,9 @@ namespace osu.Game.Skinning RightStageImage, BottomStageImage, + BarLineHeight, + BarLineColour, + // ReSharper disable once InconsistentNaming Hit300g, diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 09866ef237..2739743387 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -86,6 +86,10 @@ namespace osu.Game.Skinning parseArrayValue(pair.Value, currentConfig.ColumnWidth); break; + case "BarlineHeight": + currentConfig.BarLineHeight = float.Parse(pair.Value, CultureInfo.InvariantCulture); + break; + case "HitPosition": currentConfig.HitPosition = (480 - Math.Clamp(float.Parse(pair.Value, CultureInfo.InvariantCulture), 240, 480)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 08fa068830..51c1473303 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -198,9 +198,15 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ComboBreakColour: return SkinUtils.As(getCustomColour(existing, "ColourBreak")); + case LegacyManiaSkinConfigurationLookups.BarLineColour: + return SkinUtils.As(getCustomColour(existing, "ColourBarline")); + case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth: return SkinUtils.As(new Bindable(existing.MinimumColumnWidth)); + case LegacyManiaSkinConfigurationLookups.BarLineHeight: + return SkinUtils.As(new Bindable(existing.BarLineHeight)); + case LegacyManiaSkinConfigurationLookups.NoteBodyStyle: if (existing.NoteBodyStyle != null) From 306b30cb12238b48e2259d4611185821701d34a9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 15:51:54 +0900 Subject: [PATCH 076/349] Add failing test --- .../TestSceneMultiplayerMatchSubScreen.cs | 23 +++++++++++++++++++ .../OnlinePlay/Match/DrawableMatchRoom.cs | 9 ++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e95209f993..7058532196 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -317,6 +317,29 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); } + [Test] + public void TestChangeSettingsButtonVisibleForHost() + { + AddStep("add playlist item", () => + { + SelectedRoom.Value!.Playlist = + [ + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0)); + AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); + AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID)); + AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0)); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 08bcf32edf..b10e83a05c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -25,12 +25,13 @@ namespace osu.Game.Screens.OnlinePlay.Match set => selectedItem.Current = value; } + public Drawable? ChangeSettingsButton { get; private set; } + [Resolved] private IAPIProvider api { get; set; } = null!; private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private readonly bool allowEdit; - private Drawable? editButton; public DrawableMatchRoom(Room room, bool allowEdit = true) : base(room) @@ -45,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { if (allowEdit) { - ButtonsContainer.Add(editButton = new PurpleRoundedButton + ButtonsContainer.Add(ChangeSettingsButton = new PurpleRoundedButton { RelativeSizeAxes = Axes.Y, Anchor = Anchor.Centre, @@ -73,8 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Match private void updateRoomHost() { - if (editButton != null) - editButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0; + if (ChangeSettingsButton != null) + ChangeSettingsButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0; } protected override UpdateableBeatmapBackgroundSprite CreateBackground() => base.CreateBackground().With(d => From a09ef5d96d0bcd9c56ccd1eb6747fa5ba6d0e449 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 15:52:02 +0900 Subject: [PATCH 077/349] Fix API room host not being populated --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 1f85aa5d45..3c627c7a47 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -222,6 +222,8 @@ namespace osu.Game.Online.Multiplayer { // Populate users. await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); + if (joinedRoom.Host != null) + await PopulateUsers([joinedRoom.Host]).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await runOnUpdateThreadAsync(() => @@ -233,6 +235,7 @@ namespace osu.Game.Online.Multiplayer APIRoom = apiRoom; APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.Host = joinedRoom.Host?.User; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. From 02b950223c055aad3e192cdff99d56f2c5b2c83f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:06:12 +0900 Subject: [PATCH 078/349] Adjust x offsets to work again for keyboard selection --- osu.Game/Screens/SelectV2/PanelBase.cs | 13 ++++++------- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 2 -- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 805cbac8eb..1e47401013 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -23,8 +23,6 @@ namespace osu.Game.Screens.SelectV2 { private const float corner_radius = 10; - private const float left_edge_x_offset = 20f; - private const float keyboard_active_x_offset = 25f; private const float active_x_offset = 50f; private const float duration = 500; @@ -162,6 +160,7 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); Expanded.BindValueChanged(_ => updateDisplay()); + Selected.BindValueChanged(_ => updateDisplay()); KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); } @@ -199,13 +198,13 @@ namespace osu.Game.Screens.SelectV2 private void updateXOffset() { - float x = PanelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; + float x = PanelXOffset; - if (Expanded.Value) - x -= active_x_offset; + if (!Expanded.Value && !Selected.Value) + x += active_x_offset; - if (KeyboardSelected.Value) - x -= keyboard_active_x_offset; + if (!KeyboardSelected.Value) + x += active_x_offset * 0.5f; this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index b27e5cae14..0ce6b1a9a2 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -163,8 +163,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); }, true); - - Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); } protected override void PrepareForUse() From a8fbac0f0dbf628ee284e9b3c27554d00697f1e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:27:18 +0900 Subject: [PATCH 079/349] Add better selection visibility via another tint layer --- osu.Game/Screens/SelectV2/PanelBase.cs | 53 +++++++++++++++++++++----- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 1e47401013..d3132a106e 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -38,6 +38,8 @@ namespace osu.Game.Screens.SelectV2 private Container iconContainer = null!; private Box activationFlash = null!; private Box hoverLayer = null!; + private Box keyboardSelectionLayer = null!; + private Box selectionLayer = null!; public Container TopLevelContent { get; private set; } = null!; @@ -137,6 +139,24 @@ namespace osu.Game.Screens.SelectV2 hoverLayer = new Box { Alpha = 0, + Colour = colours.Blue.Opacity(0.1f), + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + selectionLayer = new Box + { + Alpha = 0, + Colour = ColourInfo.GradientHorizontal(colours.Yellow.Opacity(0), colours.Yellow.Opacity(0.5f)), + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0.7f, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + keyboardSelectionLayer = new Box + { + Alpha = 0, + Colour = colours.Yellow.Opacity(0.1f), Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, }, @@ -151,7 +171,6 @@ namespace osu.Game.Screens.SelectV2 } }; - hoverLayer.Colour = colours.Blue.Opacity(0.1f); backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); } @@ -159,9 +178,27 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateDisplay()); - Selected.BindValueChanged(_ => updateDisplay()); - KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); + Expanded.BindValueChanged(_ => updateDisplay(), true); + + Selected.BindValueChanged(selected => + { + if (selected.NewValue) + selectionLayer.FadeIn(100, Easing.OutQuint); + else + selectionLayer.FadeOut(200, Easing.OutQuint); + + updateXOffset(); + }, true); + + KeyboardSelected.BindValueChanged(selected => + { + if (selected.NewValue) + keyboardSelectionLayer.FadeIn(100, Easing.OutQuint); + else + keyboardSelectionLayer.FadeOut(1000, Easing.OutQuint); + + updateXOffset(); + }, true); } protected override void PrepareForUse() @@ -211,9 +248,7 @@ namespace osu.Game.Screens.SelectV2 private void updateHover() { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) + if (IsHovered) hoverLayer.FadeIn(100, Easing.OutQuint); else hoverLayer.FadeOut(1000, Easing.OutQuint); @@ -221,13 +256,13 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnHover(HoverEvent e) { - updateDisplay(); + updateHover(); return true; } protected override void OnHoverLost(HoverLostEvent e) { - updateDisplay(); + updateHover(); base.OnHoverLost(e); } From 1e46dc6b0a23cf2fa9677104b9101d8f3f94a18d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:27:42 +0900 Subject: [PATCH 080/349] Adjust animation duration to roughly match scroll operations Previous value felt wrong when using keyboard selection for iteration. --- osu.Game/Screens/SelectV2/PanelBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index d3132a106e..2a32b1a95f 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.SelectV2 private const float active_x_offset = 50f; - private const float duration = 500; + private const float duration = 400; protected float PanelXOffset { get; init; } From 51cb0bea1ce61ffd3ca8b3bdb641f8f4840601d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:45:49 +0900 Subject: [PATCH 081/349] Fix carousel taking up too much space on new song select implementation --- osu.Game/Screens/SelectV2/SongSelectV2.cs | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 3943d059f9..23139c8742 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -39,17 +39,32 @@ namespace osu.Game.Screens.SelectV2 { AddRangeInternal(new Drawable[] { - new Container + new GridContainer // used for max width implementation { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, - Child = new BeatmapCarousel + ColumnDimensions = new[] { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - Width = 0.6f, + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750), }, + Content = new[] + { + new[] + { + Empty(), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, + Child = new BeatmapCarousel + { + RelativeSizeAxes = Axes.Both + }, + }, + } + } }, modSelectOverlay, }); From 0e257038e8b49400f5082570d5867c4c7ef23c3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:47:57 +0900 Subject: [PATCH 082/349] Fix status pills displaying wrong --- osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 599d1b380a..7b99ad40de 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps.Drawables { if (Status == BeatmapOnlineStatus.None) { - this.FadeOut(animation_duration, Easing.OutQuint); + Hide(); return; } From 8fc744e9dc7d0045232a6c1eda3c17160c366947 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:55:11 +0900 Subject: [PATCH 083/349] Make `TestSceneSongSelect` work with local database It was pointless before. --- .../SongSelectV2/TestSceneSongSelect.cs | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 33474d7449..6d180c76d9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -9,16 +9,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Online.API; -using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; @@ -29,7 +23,6 @@ using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.SelectV2.Footer; -using osu.Game.Tests.Resources; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -42,8 +35,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached] private readonly OsuLogo logo; - private BeatmapManager beatmapManager = null!; - protected override bool UseOnlineAPI => true; public TestSceneSongSelect() @@ -66,32 +57,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [BackgroundDependencyLoader] - private void load(GameHost host, IAPIProvider onlineAPI) + private void load() { - BeatmapStore beatmapStore; - BeatmapUpdater beatmapUpdater; - BeatmapDifficultyCache difficultyCache; + RealmDetachedBeatmapStore beatmapStore; - // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. - // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(new RealmRulesetStore(Realm)); - Dependencies.Cache(Realm); - Dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, onlineAPI, Audio, Resources, host, Beatmap.Default, difficultyCache)); - Dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(beatmapManager, difficultyCache, onlineAPI, LocalStorage)); - Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); - - beatmapManager.ProcessBeatmap = (set, scope) => beatmapUpdater.Process(set, scope); - - MusicController music; - Dependencies.Cache(music = new MusicController()); - - // required to get bindables attached - Add(difficultyCache); - Add(music); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Add(beatmapStore); - - Dependencies.Cache(new OsuConfigManager(LocalStorage)); } protected override void LoadComplete() @@ -109,7 +80,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); - AddStep("import test beatmap", () => beatmapManager.Import(TestResources.GetTestBeatmapForImport())); } [Test] From 993473c0810e55ce0b1143f0f147e88d10c65396 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 18:40:54 +0900 Subject: [PATCH 084/349] Pass through artist/title in beatmap transform --- .../Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 572bf535f7..184de2f50c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -223,6 +223,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Difficulty = new BeatmapDifficulty(beatmap.Difficulty), Metadata = { + Artist = beatmap.Metadata.Artist, + Title = beatmap.Metadata.Title, Author = new RealmUser { Username = beatmap.Metadata.Author.Username, From ffef6ae1853d84120abf52f3c93382b4863bd556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Feb 2025 13:34:00 +0100 Subject: [PATCH 085/349] Fix possible crash when scaling objects in editor The specific fail case here is when `s.{X,Y}` is 0, and `s{Lower,Upper}Bound` is `Infinity`. Because IEEE math is IEEE math, `0 * Infinity` is `NaN`. `MathHelper.Clamp()` is written the following way: https://github.com/ppy/osuTK/blob/af742f1afd01828efc7bc9fe77536b54aab8b419/src/osuTK/Math/MathHelper.cs#L284-L306 `Math.{Min,Max}` are both documented as reporting `NaN` when any of their operands are `NaN`: https://learn.microsoft.com/en-us/dotnet/api/system.math.min?view=net-8.0#system-math-min(system-single-system-single) https://learn.microsoft.com/en-us/dotnet/api/system.math.max?view=net-8.0#system-math-max(system-single-system-single) which means that if a `NaN` happens to sneak into the bounds, it will start spreading outwards in an uncontrolled manner, and likely crash things. In contrast, the standard library provided `Math.Clamp()` is written like so: https://github.com/dotnet/runtime/blob/577c36cee56480dec4d4610b35605b5d5836888b/src/libraries/System.Private.CoreLib/src/System/Math.cs#L711-L729 With this implementation, if either bound is `NaN`, it will essentially not be checked (because any and all comparisons involving `NaN` return false). This prevents the spread of `NaN`s, all the way to positions of hitobjects, and thus fixes the crash. --- .../Edit/OsuSelectionScaleHandler.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index e3ab95c402..4c3db207f2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -263,12 +263,12 @@ namespace osu.Game.Rulesets.Osu.Edit { case Axes.X: (sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a); - s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound); + s.X = Math.Clamp(s.X, sLowerBound, sUpperBound); break; case Axes.Y: (sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b); - s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound); + s.Y = Math.Clamp(s.Y, sLowerBound, sUpperBound); break; case Axes.Both: @@ -276,11 +276,11 @@ namespace osu.Game.Rulesets.Osu.Edit // Therefore the ratio s.X / s.Y will be maintained (sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y); s.X = s.X < 0 - ? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound) - : MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound); + ? Math.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound) + : Math.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound); s.Y = s.Y < 0 - ? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound) - : MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound); + ? Math.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound) + : Math.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound); break; } From 35b0ff80bb6094a32d9c5c2b93203faf491b68fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Feb 2025 13:41:56 +0100 Subject: [PATCH 086/349] Mark `MathHelper.Clamp()` as banned API See previous commit for partial rationale. There's an argument to be made about the `NaN`-spreading semantics being desirable because at least something will loudly fail in that case, but I'm not so sure about that these days. It feels like either way if `NaN`s are produced, then things are outside of any control, and chances are the game can probably continue without crashing. And, this move reduces our dependence on osuTK, which has already been living on borrowed time for years now and is only awaiting someone brave to go excise it. --- CodeAnalysis/BannedSymbols.txt | 3 +++ .../Beatmaps/PippidonBeatmapConverter.cs | 4 ++-- .../Skinning/Argon/ArgonBananaPiece.cs | 3 ++- .../HitCircles/Components/HitCircleOverlapMarker.cs | 3 ++- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 4 ++-- osu.Game/Overlays/NotificationOverlayToastTray.cs | 2 +- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- osu.Game/Screens/Utility/CircleGameplay.cs | 4 ++-- .../Utility/SampleComponents/LatencyMovableBox.cs | 9 +++++---- osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +- 10 files changed, 23 insertions(+), 17 deletions(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 550f7c8e11..08b79fc2c0 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -18,3 +18,6 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize( M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead. M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead. M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead. +M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead. +M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead. +M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead. diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs index 0a4fa84ce1..dd8337abee 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Pippidon.Objects; using osu.Game.Rulesets.Pippidon.UI; -using osuTK; namespace osu.Game.Rulesets.Pippidon.Beatmaps { @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps }; } - private int getLane(HitObject hitObject) => (int)MathHelper.Clamp( + private int getLane(HitObject hitObject) => (int)Math.Clamp( (getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1); private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X; diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs index 8cdb490922..810dc7eed5 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime; - fadeContent.Alpha = MathHelper.Clamp( + fadeContent.Alpha = Math.Clamp( Interpolation.ValueAt( Time.Current, 1f, 0f, ObjectState.DisplayStartTime + duration * lens_flare_start, diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs index 8ed9d0476a..7a5b01ce79 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -76,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components if (hasReachedObject && showHitMarkers.Value) { float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In); - float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1); + float ringScale = Math.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1); ring.Scale = new Vector2(1 + 0.1f * ringScale); content.Alpha = 0.9f * (1 - alpha); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 39c0681dba..52575bdd67 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -270,14 +270,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (adjustVelocity) { proposedVelocity = proposedDistance / oldDuration; - proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); + proposedDistance = Math.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); } else { double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1; // Add a small amount to the proposed distance to make it easier to snap to the full length of the slider. proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance; - proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); + proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier)) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index ddb2e02fb8..dd60e303f6 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -174,7 +174,7 @@ namespace osu.Game.Overlays } height = toastFlow.DrawHeight + 120; - alpha = MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha; + alpha = Math.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha; } toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 8bfa8dd6ff..19190ac362 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -278,17 +278,17 @@ namespace osu.Game.Screens.Play processDrawables(rulesetComponents); if (lowestTopScreenSpaceRight.HasValue) - TopRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); + TopRightElements.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); else TopRightElements.Y = 0; if (lowestTopScreenSpaceLeft.HasValue) - LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); + LeaderboardFlow.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); else LeaderboardFlow.Y = 0; if (highestBottomScreenSpace.HasValue) - bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); + bottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); else bottomRightElements.Y = 0; diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index 1f970c5121..0f328d04fb 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -201,8 +201,8 @@ namespace osu.Game.Screens.Utility { double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450); - approach.Scale = new Vector2(1 + 4 * (float)MathHelper.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100)); - Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); + approach.Scale = new Vector2(1 + 4 * (float)Math.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100)); + Alpha = (float)Math.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); if (Clock.CurrentTime > HitTime + duration) Expire(); diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs index dcfcf602bf..ef1b848945 100644 --- a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs +++ b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; @@ -55,22 +56,22 @@ namespace osu.Game.Screens.Utility.SampleComponents { case Key.F: case Key.Up: - box.Y = MathHelper.Clamp(box.Y - movementAmount, 0.1f, 0.9f); + box.Y = Math.Clamp(box.Y - movementAmount, 0.1f, 0.9f); break; case Key.J: case Key.Down: - box.Y = MathHelper.Clamp(box.Y + movementAmount, 0.1f, 0.9f); + box.Y = Math.Clamp(box.Y + movementAmount, 0.1f, 0.9f); break; case Key.Z: case Key.Left: - box.X = MathHelper.Clamp(box.X - movementAmount, 0.1f, 0.9f); + box.X = Math.Clamp(box.X - movementAmount, 0.1f, 0.9f); break; case Key.X: case Key.Right: - box.X = MathHelper.Clamp(box.X + movementAmount, 0.1f, 0.9f); + box.X = Math.Clamp(box.X + movementAmount, 0.1f, 0.9f); break; } } diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index 5038c53b4a..c0264f5734 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Utility { double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450); - Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); + Alpha = (float)Math.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); Y = judgement_position - (float)((HitTime - Clock.CurrentTime) / preempt); if (Clock.CurrentTime > HitTime + duration) From 88089fb0144a54d99b2e586f2d1b8e4512494604 Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 19:03:39 +0600 Subject: [PATCH 087/349] make `SettingsPanel.SearchTextBox`'s setter private --- osu.Game/Overlays/SettingsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index d8b054eaf8..9b268c573f 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays public SettingsSectionsContainer SectionsContainer { get; private set; } - protected SeekLimitedSearchTextBox SearchTextBox; + protected SeekLimitedSearchTextBox SearchTextBox { get; private set; } protected override string PopInSampleName => "UI/settings-pop-in"; protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; From 0d7c00ae09d65d7c4a53abd1860d3029e1c004bd Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 19:04:47 +0600 Subject: [PATCH 088/349] use `Bindable.SetDefault` for clearing search text --- osu.Game/Overlays/SettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 8a39d75565..630675a717 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -69,7 +69,7 @@ namespace osu.Game.Overlays where T : Drawable { // if search isn't cleared then the target control won't be visible if it doesn't match the query - SearchTextBox.Current.Value = ""; + SearchTextBox.Current.SetDefault(); Show(); From 8032b6893274a152a12226572e89a000262c5583 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:59:39 +0900 Subject: [PATCH 089/349] Stop using padding for panel x offsets --- osu.Game/Screens/SelectV2/PanelBase.cs | 11 +++++++---- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 2a32b1a95f..1dc645ba53 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -61,6 +61,11 @@ namespace osu.Game.Screens.SelectV2 } } + // content is offset by PanelXOffset, make sure we only handle input at the actual visible + // offset region. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + TopLevelContent.ReceivePositionalInputAt(screenSpacePos); + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -219,8 +224,6 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { - backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint); - var backgroundColour = accentColour ?? Color4.White; var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); @@ -235,7 +238,7 @@ namespace osu.Game.Screens.SelectV2 private void updateXOffset() { - float x = PanelXOffset; + float x = PanelXOffset + corner_radius; if (!Expanded.Value && !Selected.Value) x += active_x_offset; @@ -243,7 +246,7 @@ namespace osu.Game.Screens.SelectV2 if (!KeyboardSelected.Value) x += active_x_offset * 0.5f; - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + TopLevelContent.MoveToX(x, duration, Easing.OutQuint); } private void updateHover() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 0ce6b1a9a2..d4bf3519fa 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.SelectV2 public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = TopLevelContent.DrawRectangle; // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. // @@ -62,7 +62,7 @@ namespace osu.Game.Screens.SelectV2 // larger hit target. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] From 29c35529d27b730847d03896c04c03a9e95efd3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:02:09 +0900 Subject: [PATCH 090/349] Fix activation flash being applied twice (and adjust duration) --- osu.Game/Screens/SelectV2/PanelBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 1dc645ba53..b9d9bbd20a 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -217,7 +217,6 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); carousel?.Activate(Item!); return true; } @@ -287,7 +286,7 @@ namespace osu.Game.Screens.SelectV2 public virtual void Activated() { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); + activationFlash.FadeOutFromOne(1000, Easing.OutQuint); } #endregion From 4beac64bdb6c2dee8492ea8b113498b78ef5f36a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:19:30 +0900 Subject: [PATCH 091/349] Remove unused container level --- osu.Game/Screens/SelectV2/PanelBase.cs | 43 ++++++++++++-------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index b9d9bbd20a..36f4f13a3b 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -32,7 +32,6 @@ namespace osu.Game.Screens.SelectV2 private Box backgroundBorder = null!; private Box backgroundGradient = null!; private Box backgroundAccentGradient = null!; - private Container backgroundLayer = null!; private Container backgroundLayerHorizontalPadding = null!; private Container backgroundContainer = null!; private Container iconContainer = null!; @@ -66,6 +65,9 @@ namespace osu.Game.Screens.SelectV2 public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => TopLevelContent.ReceivePositionalInputAt(screenSpacePos); + [Resolved] + private BeatmapCarousel? carousel { get; set; } + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -102,30 +104,26 @@ namespace osu.Game.Screens.SelectV2 backgroundLayerHorizontalPadding = new Container { RelativeSizeAxes = Axes.Both, - Child = backgroundLayer = new Container + Child = new Container { RelativeSizeAxes = Axes.Both, - Child = new Container + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + backgroundGradient = new Box { - backgroundGradient = new Box - { - RelativeSizeAxes = Axes.Both, - }, - backgroundAccentGradient = new Box - { - RelativeSizeAxes = Axes.Both, - }, - backgroundContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - } - }, + RelativeSizeAxes = Axes.Both, + }, + backgroundAccentGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + } }, } }, @@ -212,9 +210,6 @@ namespace osu.Game.Screens.SelectV2 this.FadeInFromZero(duration, Easing.OutQuint); } - [Resolved] - private BeatmapCarousel? carousel { get; set; } - protected override bool OnClick(ClickEvent e) { carousel?.Activate(Item!); From 38de3566b14b4d08a17c806f2891fa85c82dfafd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:37:18 +0900 Subject: [PATCH 092/349] Adjust set panel display and animations slightly --- .../SelectV2/BeatmapSetPanelBackground.cs | 2 +- osu.Game/Screens/SelectV2/PanelBase.cs | 12 ++++++------ osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 16 +++++++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs index 435a0ad262..798acf62ee 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapSetPanelBackground : ModelBackedDrawable { - protected override bool TransformImmediately => true; + protected override double TransformDuration => 400; public WorkingBeatmap? Beatmap { diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 36f4f13a3b..05a1a55c03 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.SelectV2 private const float active_x_offset = 50f; - private const float duration = 400; + protected const float DURATION = 400; protected float PanelXOffset { get; init; } @@ -207,7 +207,7 @@ namespace osu.Game.Screens.SelectV2 protected override void PrepareForUse() { base.PrepareForUse(); - this.FadeInFromZero(duration, Easing.OutQuint); + this.FadeInFromZero(DURATION, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) @@ -221,10 +221,10 @@ namespace osu.Game.Screens.SelectV2 var backgroundColour = accentColour ?? Color4.White; var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); - backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint); - backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint); + backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), DURATION, Easing.OutQuint); + backgroundBorder.FadeColour(backgroundColour, DURATION, Easing.OutQuint); - TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), DURATION, Easing.OutQuint); updateXOffset(); updateHover(); @@ -240,7 +240,7 @@ namespace osu.Game.Screens.SelectV2 if (!KeyboardSelected.Value) x += active_x_offset * 0.5f; - TopLevelContent.MoveToX(x, duration, Easing.OutQuint); + TopLevelContent.MoveToX(x, DURATION, Easing.OutQuint); } private void updateHover() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 5c38fe8e04..512fbacec1 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.SelectV2 Icon = chevronIcon = new Container { - Size = new Vector2(22), + Size = new Vector2(0, 22), Child = new SpriteIcon { Anchor = Anchor.Centre, @@ -128,10 +128,16 @@ namespace osu.Game.Screens.SelectV2 private void onExpanded() { - const float duration = 500; - - chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + if (Expanded.Value) + { + chevronIcon.ResizeWidthTo(18, 600, Easing.OutElasticQuarter); + chevronIcon.FadeTo(1f, DURATION, Easing.OutQuint); + } + else + { + chevronIcon.ResizeWidthTo(0f, DURATION, Easing.OutQuint); + chevronIcon.FadeTo(0f, DURATION, Easing.OutQuint); + } } protected override void PrepareForUse() From 881534eb7f3d71e817d511c64ca368e0e6eca069 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sat, 1 Mar 2025 01:51:37 +0900 Subject: [PATCH 093/349] Add SFX for kiai/star fountain activation --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 14 +++++++++++++- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index 7978e9fa91..dbbff4a9f5 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Graphics.Containers; @@ -14,8 +16,11 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; + private Sample? sample; + private SampleChannel? sampleChannel; + [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { RelativeSizeAxes = Axes.Both; @@ -34,6 +39,8 @@ namespace osu.Game.Screens.Menu X = -250, }, }; + + sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); } private bool isTriggered; @@ -73,6 +80,11 @@ namespace osu.Game.Screens.Menu rightFountain.Shoot(1); break; } + + // Track sample channel to avoid overlapping playback + sampleChannel?.Stop(); + sampleChannel = sample?.GetChannel(); + sampleChannel?.Play(); } } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index d4e61dc5a0..7e09f50133 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -19,8 +21,11 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; + private Sample? sample; + private SampleChannel? sampleChannel; + [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, AudioManager audio) { kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains); @@ -41,6 +46,8 @@ namespace osu.Game.Screens.Play X = -75, }, }; + + sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); } private bool isTriggered; @@ -66,6 +73,11 @@ namespace osu.Game.Screens.Play { leftFountain.Shoot(1); rightFountain.Shoot(-1); + + // Track sample channel to avoid overlapping playback + sampleChannel?.Stop(); + sampleChannel = sample?.GetChannel(); + sampleChannel?.Play(); } public partial class GameplayStarFountain : StarFountain From ec6ff240f38ef69d37c50437c8f97b5fa3804c90 Mon Sep 17 00:00:00 2001 From: "Giovanni D." Date: Sun, 2 Mar 2025 00:49:04 -0800 Subject: [PATCH 094/349] Add taskbar flashing when a multiplayer game is starting --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 111b453adb..e5bc683d19 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -30,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + private IBindable isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); @@ -142,6 +145,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { + game?.Window?.Flash(); loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } From 35a21b44a698f0cbe84db036f03c1f26202a8d75 Mon Sep 17 00:00:00 2001 From: "Giovanni D." Date: Sun, 2 Mar 2025 20:43:32 -0800 Subject: [PATCH 095/349] Change timing of the flash --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ---- .../OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index e5bc683d19..111b453adb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -30,9 +30,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } = null!; - [Resolved] - private OsuGame? game { get; set; } - private IBindable isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); @@ -145,7 +142,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { - game?.Window?.Flash(); loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 7eb7f6610e..dd9cb56862 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -18,6 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + private Player? player; public MultiplayerPlayerLoader(Func createPlayer) @@ -39,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.OnPlayerLoaded(); + game?.Window?.Flash(); + multiplayerClient.ChangeState(MultiplayerUserState.Loaded) .ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); } From ad9a963bd0fa831c30f7a79abf62a797aa087c3f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:19:19 +0900 Subject: [PATCH 096/349] Exit loop when cancellation requested The following manages to create all hitobjects but proceeds to get stuck in this method: `dotnet run -- difficulty 1607040 -r:2` --- osu.Game/Rulesets/Objects/HitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 9f980769e2..d9e62ccecb 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -114,6 +114,8 @@ namespace osu.Game.Rulesets.Objects { foreach (HitObject hitObject in nestedHitObjects) { + cancellationToken.ThrowIfCancellationRequested(); + if (hitObject is IHasComboInformation n) { n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable); From 52dad09b2011c014b2ec5acb4947aacbc3ba4d90 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:19:38 +0900 Subject: [PATCH 097/349] Cancel slider generation when requested Didn't notice a particular case with this one, just came up as I was looking through code. --- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index f5146d1675..e5e15042ff 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -46,6 +46,8 @@ namespace osu.Game.Rulesets.Objects for (int span = 0; span < spanCount; span++) { + cancellationToken.ThrowIfCancellationRequested(); + double spanStartTime = startTime + span * spanDuration; bool reversed = span % 2 == 1; From 033952029eecd814a62567c58eeafb5fe3fe5c99 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:46:13 +0900 Subject: [PATCH 098/349] Cancel `ApplyDefaults()` when requested Also didn't notice a particular case here, but if all code passes up until we get to the `foreach (var h in nestedHitObjects)` below, then we could end up stuck here for quite a while. --- osu.Game/Rulesets/Objects/HitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index d9e62ccecb..07e07b25d3 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -104,6 +104,8 @@ namespace osu.Game.Rulesets.Objects /// The cancellation token. public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + ApplyDefaultsToSelf(controlPointInfo, difficulty); nestedHitObjects.Clear(); From 47747aed3e9feb09c3b6d9f82703cedda8db3035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 08:40:51 +0100 Subject: [PATCH 099/349] Add guards to prevent clamp calls with invalid bounds --- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 19190ac362..78c602d8f1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -277,17 +277,17 @@ namespace osu.Game.Screens.Play if (rulesetComponents != null) processDrawables(rulesetComponents); - if (lowestTopScreenSpaceRight.HasValue) + if (lowestTopScreenSpaceRight.HasValue && DrawHeight - TopRightElements.DrawHeight > 0) TopRightElements.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); else TopRightElements.Y = 0; - if (lowestTopScreenSpaceLeft.HasValue) + if (lowestTopScreenSpaceLeft.HasValue && DrawHeight - LeaderboardFlow.DrawHeight > 0) LeaderboardFlow.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); else LeaderboardFlow.Y = 0; - if (highestBottomScreenSpace.HasValue) + if (highestBottomScreenSpace.HasValue && DrawHeight - bottomRightElements.DrawHeight > 0) bottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); else bottomRightElements.Y = 0; From 0a50fb1dfac7b0898c134f98c47a459fbbeb769c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 09:32:27 +0100 Subject: [PATCH 100/349] Add failing test case --- .../Beatmaps/BeatmapExtensionsTest.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs diff --git a/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs b/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs new file mode 100644 index 0000000000..1dda2e314d --- /dev/null +++ b/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Tests.Beatmaps +{ + public class BeatmapExtensionsTest + { + [Test] + public void TestLengthCalculations() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 5_000 }, + new HitCircle { StartTime = 300_000 }, + new Spinner { StartTime = 280_000, Duration = 40_000 } + }, + Breaks = + { + new BreakPeriod(50_000, 75_000), + new BreakPeriod(100_000, 150_000), + } + }; + + Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000))); + Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000 + Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(240_000)); // 315_000 - (25_000 + 50_000) = 315_000 - 75_000 + } + + [Test] + public void TestDrainLengthCannotGoNegative() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 5_000 }, + new HitCircle { StartTime = 300_000 }, + new Spinner { StartTime = 280_000, Duration = 40_000 } + }, + Breaks = + { + new BreakPeriod(0, 350_000), + } + }; + + Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000))); + Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000 + Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(0)); // break period encompasses entire beatmap + } + } +} From 87fb8da3517ae0f2d0669dc3afa9b233454c49bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 09:35:46 +0100 Subject: [PATCH 101/349] Fix drain length calculation helper method being able to return negative durations This is the principal failure behind https://github.com/ppy/osu-server-beatmap-submission/issues/40. --- osu.Game/Beatmaps/IBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 826d4e19a7..f95fcefd7e 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -161,7 +161,7 @@ namespace osu.Game.Beatmaps /// /// Find the total milliseconds between the first and last hittable objects, excluding any break time. /// - public static double CalculateDrainLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime; + public static double CalculateDrainLength(this IBeatmap beatmap) => Math.Max(CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime, 0); /// /// Find the timestamps in milliseconds of the start and end of the playable region. From 52860def6c7fb40dcd1d6291f867751c7d08aecb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Mar 2025 18:53:41 +0900 Subject: [PATCH 102/349] Always zoom timeline to centre rather than focus point Closes https://github.com/ppy/osu/issues/32183. --- .../Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 9db14ce4c4..b483f23d1d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (e.AltPressed) { // zoom when holding alt. - AdjustZoomRelatively(e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); + AdjustZoomRelatively(e.ScrollDelta.Y); return true; } From f32a8e8741f4dcd8d915be78a93686ab101d1d74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Mar 2025 18:54:46 +0900 Subject: [PATCH 103/349] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 614f1409bf..e35eaf5645 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 28f9e734f0d3dbf374d90b72e8380e1021aab98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 12:23:52 +0100 Subject: [PATCH 104/349] Add failing test case --- .../TestScenePlaylistsResultsScreen.cs | 103 +++++++++++++----- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index dc5fb20e16..469f7c8b74 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -69,9 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists totalCount = 0; userScore = TestResources.CreateTestScoreInfo(); + userScore.OnlineID = 1; userScore.TotalScore = 0; userScore.Statistics = new Dictionary(); userScore.MaximumStatistics = new Dictionary(); + userScore.Position = real_user_position; // Beatmap is required to be an actual beatmap so the scores can get their scores correctly // calculated for standardised scoring, else the tests that rely on ordering will fall over. @@ -243,6 +245,35 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } + [Test] + public void TestFetchingAllTheWayToFirstNeverDisplaysNegativePosition() + { + AddStep("set user position", () => userScore.Position = 20); + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); + + createResultsWithScore(() => userScore); + waitForDisplay(); + + AddStep("bind delayed handler", () => bindHandler(true)); + + for (int i = 0; i < 2; i++) + { + AddStep("simulate user falling down ranking", () => userScore.Position += 2); + AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); + + AddAssert("left loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); + + waitForDisplay(); + + AddAssert("left loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); + } + + AddAssert("total count is 34", () => this.ChildrenOfType().Count(), () => Is.EqualTo(34)); + AddUntilStep("all panels have non-negative position", () => this.ChildrenOfType().All(p => p.ScorePosition.Value > 0)); + } + private void createResultsWithScore(Func getScore) { AddStep("load results", () => @@ -331,7 +362,7 @@ namespace osu.Game.Tests.Visual.Playlists if (userScore == null) triggerFail(s); else - triggerSuccess(s, createUserResponse(userScore)); + triggerSuccess(s, () => createUserResponse(userScore)); break; @@ -339,12 +370,12 @@ namespace osu.Game.Tests.Visual.Playlists if (userScore == null) triggerFail(u); else - triggerSuccess(u, createUserResponse(userScore)); + triggerSuccess(u, () => createUserResponse(userScore)); break; case IndexPlaylistScoresRequest i: - triggerSuccess(i, createIndexResponse(i, noScores)); + triggerSuccess(i, () => createIndexResponse(i, noScores)); break; } }, delay); @@ -352,11 +383,11 @@ namespace osu.Game.Tests.Visual.Playlists return true; }; - private void triggerSuccess(APIRequest req, T result) + private void triggerSuccess(APIRequest req, Func result) where T : class { requestComplete = true; - req.TriggerSuccess(result); + req.TriggerSuccess(result.Invoke()); } private void triggerFail(APIRequest req) @@ -367,28 +398,13 @@ namespace osu.Game.Tests.Visual.Playlists private MultiplayerScore createUserResponse(ScoreInfo userScore) { - var multiplayerUserScore = new MultiplayerScore - { - ID = highestScoreId, - Accuracy = userScore.Accuracy, - Passed = userScore.Passed, - Rank = userScore.Rank, - Position = real_user_position, - MaxCombo = userScore.MaxCombo, - User = userScore.User, - BeatmapId = RNG.Next(0, 7), - ScoresAround = new MultiplayerScoresAround - { - Higher = new MultiplayerScores(), - Lower = new MultiplayerScores() - } - }; + var multiplayerUserScore = createMultiplayerUserScore(userScore); totalCount++; for (int i = 1; i <= scores_per_result; i++) { - multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore + multiplayerUserScore.ScoresAround!.Lower!.Scores.Add(new MultiplayerScore { ID = getNextLowestScoreId(), Accuracy = userScore.Accuracy, @@ -404,7 +420,7 @@ namespace osu.Game.Tests.Visual.Playlists }, }); - multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore + multiplayerUserScore.ScoresAround!.Higher!.Scores.Add(new MultiplayerScore { ID = getNextHighestScoreId(), Accuracy = userScore.Accuracy, @@ -423,12 +439,32 @@ namespace osu.Game.Tests.Visual.Playlists totalCount += 2; } - addCursor(multiplayerUserScore.ScoresAround.Lower); - addCursor(multiplayerUserScore.ScoresAround.Higher); + addCursor(multiplayerUserScore.ScoresAround!.Lower!); + addCursor(multiplayerUserScore.ScoresAround!.Higher!); return multiplayerUserScore; } + private MultiplayerScore createMultiplayerUserScore(ScoreInfo userScore) + { + return new MultiplayerScore + { + ID = highestScoreId, + Accuracy = userScore.Accuracy, + Passed = userScore.Passed, + Rank = userScore.Rank, + Position = userScore.Position, + MaxCombo = userScore.MaxCombo, + User = userScore.User, + BeatmapId = RNG.Next(0, 7), + ScoresAround = new MultiplayerScoresAround + { + Higher = new MultiplayerScores(), + Lower = new MultiplayerScores() + } + }; + } + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores) { var result = new IndexedMultiplayerScores(); @@ -437,11 +473,21 @@ namespace osu.Game.Tests.Visual.Playlists string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; + bool reachedEnd = false; + for (int i = 1; i <= scores_per_result; i++) { + int nextId = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(); + + if (userScore.OnlineID - nextId >= userScore.Position) + { + reachedEnd = true; + break; + } + result.Scores.Add(new MultiplayerScore { - ID = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(), + ID = nextId, Accuracy = 1, Passed = true, Rank = ScoreRank.X, @@ -458,7 +504,10 @@ namespace osu.Game.Tests.Visual.Playlists totalCount++; } - addCursor(result); + if (!reachedEnd) + addCursor(result); + + result.UserScore = createMultiplayerUserScore(userScore); return result; } From bf4fa58f72c61ff217c2d20a48f86d9aa65a4862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 12:56:28 +0100 Subject: [PATCH 105/349] Fix playlists results screens potentially displaying negative score positions Closes https://github.com/ppy/osu/issues/31434. --- .../Playlists/PlaylistItemResultsScreen.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 184de2f50c..0e539936d8 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -185,6 +185,24 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { higherScores = index; setPositions(index, pivot, -1); + + // when paginating the results, it's possible for the user's score to naturally fall down the rankings. + // unmitigated, this can cause scores at the very top of the rankings to have zero or negative positions + // because the positions are counted backwards from the user's score, which has increased in this case during pagination. + // if this happens, just give the top score the first position. + // note that this isn't 100% correct, but it *is* however the most reliable way to mask the problem. + int smallestPosition = index.Scores.Min(s => s.Position ?? 1); + + if (smallestPosition < 1) + { + int offset = 1 - smallestPosition; + + foreach (var scorePanel in ScorePanelList.GetScorePanels()) + scorePanel.ScorePosition.Value += offset; + + foreach (var score in index.Scores) + score.Position += offset; + } } return await transformScores(index.Scores).ConfigureAwait(false); From d7d5eec58ca4e5438ba22686c7f5f1c1f0a70ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 13:52:10 +0100 Subject: [PATCH 106/349] Update failing assertions Change in behaviour is expected in this case. --- .../Visual/Editing/TestSceneZoomableScrollContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs index 1c8a18e131..2c84e76b2e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs @@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); - AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X)); // Scroll out at 0.25 AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3))); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); - AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X)); AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); } From 23a5d6dc401a9944a544eae923da134fa75a090f Mon Sep 17 00:00:00 2001 From: andy840119 Date: Mon, 3 Mar 2025 22:09:48 +0800 Subject: [PATCH 107/349] This method is not being used anymore. see: https://github.com/ppy/osu/pull/26643 --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 39fff169b7..bfe7fe523f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -151,14 +151,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler(); - /// - /// Handles the selected items being scaled. - /// - /// The delta scale to apply, in local coordinates. - /// The point of reference where the scale is originating from. - /// Whether any items could be scaled. - public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; - /// /// Creates the handler to use for scale operations. /// From cab849b5d91cb1aab055798d1b1e353feba0c598 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 3 Mar 2025 14:23:39 -0800 Subject: [PATCH 108/349] Use web localisable string for team channel label --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index 0a89775cc7..03f6923455 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), FontAwesome.Solid.Bullhorn, false), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), FontAwesome.Solid.Comments, false), selector = new ChannelListItem(ChannelListingChannel), - TeamChannelGroup = new ChannelGroup("TEAM", FontAwesome.Solid.Users, false), // TODO: replace with osu-web localisable string once available + TeamChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleTEAM.ToUpper(), FontAwesome.Solid.Users, false), PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), FontAwesome.Solid.Envelope, true), }, }, From 550ff85550056bb947e67ead816c87004885da91 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 11:22:47 +0900 Subject: [PATCH 109/349] Cancel difficulty calculation after 10 seconds by default --- .../Difficulty/DifficultyCalculator.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 14acc9b908..add24f7866 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -62,6 +62,11 @@ namespace osu.Game.Rulesets.Difficulty /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { + using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + if (!cancellationToken.CanBeCanceled) + cancellationToken = timedCancellationSource.Token; + cancellationToken.ThrowIfCancellationRequested(); preProcess(mods, cancellationToken); @@ -98,6 +103,11 @@ namespace osu.Game.Rulesets.Difficulty /// The set of . public List CalculateTimed([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { + using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + if (!cancellationToken.CanBeCanceled) + cancellationToken = timedCancellationSource.Token; + cancellationToken.ThrowIfCancellationRequested(); preProcess(mods, cancellationToken); @@ -166,15 +176,10 @@ namespace osu.Game.Rulesets.Difficulty /// /// The original list of s. /// The cancellation token. - private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) + private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken) { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - - // Only pass through the cancellation token if it's non-default. - // This allows for the default timeout to be applied for playable beatmap construction. - Beatmap = cancellationToken == default - ? beatmap.GetPlayableBeatmap(ruleset, playableMods) - : beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); + Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); var track = new TrackVirtual(10000); playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); From df25734834b005d4b072e0338aaa892e4f776d1c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 11:36:36 +0900 Subject: [PATCH 110/349] Fix intermittent score panel test --- .../Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 02a321d22f..eade5aaf5d 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(TestResources.CreateTestScoreInfo(beatmap)); }); - AddAssert("pp display faded out", () => + AddUntilStep("pp display faded out", () => { var ppDisplay = this.ChildrenOfType().Single(); return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedBeatmaps; @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(score); }); - AddAssert("pp display faded out", () => + AddUntilStep("pp display faded out", () => { var ppDisplay = this.ChildrenOfType().Single(); return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedMods; @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(score); }); - AddAssert("pp display faded out", () => this.ChildrenOfType().Single().Alpha == 1); + AddUntilStep("pp display faded out", () => this.ChildrenOfType().Single().Alpha == 1); } [Test] From 963df165df34db1c0020c44bb5c0c343fb24cab1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 12:33:33 +0900 Subject: [PATCH 111/349] Add failing test --- .../StatefulMultiplayerClientTest.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 559db16751..a6d715df62 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -6,6 +6,7 @@ using Humanizer; using NUnit.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; +using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -93,6 +94,29 @@ namespace osu.Game.Tests.NonVisual.Multiplayer checkPlayingUserCount(1); } + [Test] + public void TestJoinRoomWithManyUsers() + { + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); + AddUntilStep("wait for room part", () => !RoomJoined); + + AddStep("create room with many users", () => + { + var newRoom = new Room(); + newRoom.CopyFrom(SelectedRoom.Value!); + + newRoom.RoomID = null; + MultiplayerClient.RoomSetupAction = room => + { + room.Users.AddRange(Enumerable.Range(PLAYER_1_ID, 100).Select(id => new MultiplayerRoomUser(id))); + }; + + RoomManager.CreateRoom(newRoom); + }); + + AddUntilStep("wait for room join", () => RoomJoined); + } + private void checkPlayingUserCount(int expectedCount) => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count == expectedCount); From 3024a98658a62a4042d9946a8a72c85c98b8be97 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 12:34:02 +0900 Subject: [PATCH 112/349] Fix unable to join multiplayer rooms with many users --- .../Online/API/Requests/GetUsersRequest.cs | 8 +++--- .../Online/Multiplayer/MultiplayerClient.cs | 27 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs index cd75ff4e31..fe7ba8c33d 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs @@ -13,14 +13,14 @@ namespace osu.Game.Online.API.Requests /// public class GetUsersRequest : APIRequest { - public readonly int[] UserIds; + public const int MAX_IDS_PER_REQUEST = 50; - private const int max_ids_per_request = 50; + public readonly int[] UserIds; public GetUsersRequest(int[] userIds) { - if (userIds.Length > max_ids_per_request) - throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {max_ids_per_request} IDs at once"); + if (userIds.Length > MAX_IDS_PER_REQUEST) + throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {MAX_IDS_PER_REQUEST} IDs at once"); UserIds = userIds; } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 2d445ea25a..9abc013b66 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -815,19 +815,22 @@ namespace osu.Game.Online.Multiplayer /// The s to populate. protected async Task PopulateUsers(IEnumerable multiplayerUsers) { - var request = new GetUsersRequest(multiplayerUsers.Select(u => u.UserID).Distinct().ToArray()); - - await API.PerformAsync(request).ConfigureAwait(false); - - if (request.Response == null) - return; - - Dictionary users = request.Response.Users.ToDictionary(user => user.Id); - - foreach (var multiplayerUser in multiplayerUsers) + foreach (int[] userChunk in multiplayerUsers.Select(u => u.UserID).Distinct().Chunk(GetUsersRequest.MAX_IDS_PER_REQUEST)) { - if (users.TryGetValue(multiplayerUser.UserID, out var user)) - multiplayerUser.User = user; + var request = new GetUsersRequest(userChunk); + + await API.PerformAsync(request).ConfigureAwait(false); + + if (request.Response == null) + return; + + Dictionary users = request.Response.Users.ToDictionary(user => user.Id); + + foreach (var multiplayerUser in multiplayerUsers) + { + if (users.TryGetValue(multiplayerUser.UserID, out var user)) + multiplayerUser.User = user; + } } } From 4a00662092a13cd1e6352400ec76403dff80f657 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:02:45 +0900 Subject: [PATCH 113/349] Fix thread safety when kicking multiplayer users --- .../Online/Multiplayer/MultiplayerClient.cs | 61 ++++++++++--------- .../OnlinePlay/Multiplayer/Multiplayer.cs | 8 +-- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 59a4547e9e..91b4ed448c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -486,18 +486,44 @@ namespace osu.Game.Online.Multiplayer }, false); } - Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) => - handleUserLeft(user, UserLeft); + Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) + { + Scheduler.Add(() => handleUserLeft(user, UserLeft), false); + return Task.CompletedTask; + } Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user) { - if (LocalUser == null) - return Task.CompletedTask; + Scheduler.Add(() => + { + if (LocalUser == null) + return; - if (user.Equals(LocalUser)) - LeaveRoom(); + if (user.Equals(LocalUser)) + LeaveRoom(); - return handleUserLeft(user, UserKicked); + handleUserLeft(user, UserKicked); + }, false); + + return Task.CompletedTask; + } + + private void handleUserLeft(MultiplayerRoomUser user, Action? callback) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + if (Room == null) + return; + + Room.Users.Remove(user); + PlayingUserIds.Remove(user.UserID); + + Debug.Assert(APIRoom != null); + APIRoom.RecentParticipants = APIRoom.RecentParticipants.Where(u => u.Id != user.UserID).ToArray(); + APIRoom.ParticipantCount--; + + callback?.Invoke(user); + RoomUpdated?.Invoke(); } async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) @@ -544,27 +570,6 @@ namespace osu.Game.Online.Multiplayer APIRoom.ParticipantCount++; } - private Task handleUserLeft(MultiplayerRoomUser user, Action? callback) - { - Scheduler.Add(() => - { - if (Room == null) - return; - - Room.Users.Remove(user); - PlayingUserIds.Remove(user.UserID); - - Debug.Assert(APIRoom != null); - APIRoom.RecentParticipants = APIRoom.RecentParticipants.Where(u => u.Id != user.UserID).ToArray(); - APIRoom.ParticipantCount--; - - callback?.Invoke(user); - RoomUpdated?.Invoke(); - }, false); - - return Task.CompletedTask; - } - Task IMultiplayerClient.HostChanged(int userId) { Scheduler.Add(() => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index dfed32aebc..0b06a16d98 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -28,11 +28,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onRoomUpdated() { - if (client.Room == null) + if (client.Room == null || client.LocalUser == null) return; - Debug.Assert(client.LocalUser != null); - // If the user exits gameplay before score submission completes, we'll transition to idle when results has been prepared. if (client.LocalUser.State == MultiplayerUserState.Results && this.IsCurrentScreen()) transitionFromResults(); @@ -62,11 +60,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.OnResuming(e); - if (client.Room == null) + if (client.Room == null || client.LocalUser == null) return; - Debug.Assert(client.LocalUser != null); - if (!(e.Last is MultiplayerPlayerLoader playerLoader)) return; From b73a872b94f1053f57612ad32c1d07c88b8d908c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:11:32 +0900 Subject: [PATCH 114/349] Fix broken test --- .../NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 959f09361f..4019ff6730 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -97,16 +97,12 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("create room with many users", () => { - var newRoom = new Room(); - newRoom.CopyFrom(SelectedRoom.Value!); - - newRoom.RoomID = null; MultiplayerClient.RoomSetupAction = room => { room.Users.AddRange(Enumerable.Range(PLAYER_1_ID, 100).Select(id => new MultiplayerRoomUser(id))); }; - RoomManager.CreateRoom(newRoom); + MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); From 0696cfa4f241516b83face7a526b8626de01930c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 14:40:33 +0900 Subject: [PATCH 115/349] `LoungePollingComponent` -> `LoungeListingPoller` --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- ...ollingComponent.cs => LoungeListingPoller.cs} | 4 ++-- .../Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Screens/OnlinePlay/Lounge/{LoungePollingComponent.cs => LoungeListingPoller.cs} (91%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index a87216287d..ec0117a990 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -805,7 +805,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); - AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); + AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { multiplayerClient.ServerSideRooms[0].Name = "New name"; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs index 420a96cf8a..d92ae7eb6e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs @@ -14,9 +14,9 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Screens.OnlinePlay.Lounge { /// - /// A that polls for the lounge listing. + /// Polls for rooms for the main lounge listing. /// - public partial class LoungePollingComponent : PollingComponent + public partial class LoungeListingPoller : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index e83334eb69..12c0bb12e2 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); private RoomsContainer roomsContainer = null!; - private LoungePollingComponent pollingComponent = null!; + private LoungeListingPoller listingPoller = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private SearchTextBox searchTextBox = null!; @@ -92,7 +92,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - pollingComponent = new LoungePollingComponent + listingPoller = new LoungeListingPoller { RoomsReceived = onListingReceived, Filter = { BindTarget = filter } @@ -187,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { roomsContainer.Rooms.Clear(); hasListingResults.Value = false; - pollingComponent.PollImmediately(); + listingPoller.PollImmediately(); }); updateFilter(); @@ -268,7 +268,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - pollingComponent.PollImmediately(); + listingPoller.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -379,7 +379,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); - public void RefreshRooms() => pollingComponent.PollImmediately(); + public void RefreshRooms() => listingPoller.PollImmediately(); private void updateLoadingLayer() { @@ -392,11 +392,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - pollingComponent.TimeBetweenPolls.Value = 0; + listingPoller.TimeBetweenPolls.Value = 0; else - pollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + listingPoller.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {pollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {listingPoller.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); From 77d5b1d5dd605f94b81205d00fc055697e77a7ef Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:36:54 +0900 Subject: [PATCH 116/349] Fix multiplayer not joining correct chat channel --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + osu.Game/Online/Multiplayer/MultiplayerRoom.cs | 7 +++++++ osu.Game/Online/Rooms/Room.cs | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 59a4547e9e..82836a00f0 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -235,6 +235,7 @@ namespace osu.Game.Online.Multiplayer APIRoom = apiRoom; APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.ChannelId = joinedRoom.ChannelID; APIRoom.Host = joinedRoom.Host?.User; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index f7bd4490ff..b8b90d907f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -59,6 +59,12 @@ namespace osu.Game.Online.Multiplayer [Key(7)] public IList ActiveCountdowns { get; set; } = new List(); + /// + /// The ID of the chat channel for the room. + /// + [Key(8)] + public int ChannelID { get; set; } + [JsonConstructor] [SerializationConstructor] public MultiplayerRoom(long roomId) @@ -69,6 +75,7 @@ namespace osu.Game.Online.Multiplayer public MultiplayerRoom(Room room) { RoomID = room.RoomID ?? 0; + ChannelID = room.ChannelId; Settings = new MultiplayerRoomSettings(room); Host = room.Host != null ? new MultiplayerRoomUser(room.Host.OnlineID) : null; Playlist = room.Playlist.Select(p => new MultiplayerPlaylistItem(p)).ToArray(); diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index c5e292a19d..e965f9c187 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -242,7 +242,7 @@ namespace osu.Game.Online.Rooms public int ChannelId { get => channelId; - private set => SetField(ref channelId, value); + set => SetField(ref channelId, value); } /// From 9e8a6117280fa4ccf1dbe7fb545ca072f397d085 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:05:12 +0900 Subject: [PATCH 117/349] Rename `RoomsContainer` and scope down bindables --- .../TestSceneLoungeRoomsContainer.cs | 4 +-- .../TestScenePlaylistsLoungeSubScreen.cs | 16 +++++----- .../OnlinePlay/DrawableRoomPlaylist.cs | 2 +- .../{RoomsContainer.cs => RoomListing.cs} | 29 ++++++++++++++----- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 14 ++++----- 5 files changed, 39 insertions(+), 26 deletions(-) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{RoomsContainer.cs => RoomListing.cs} (91%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 772eb91174..b43433fe8d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { private BindableList rooms = null!; - private RoomsContainer container = null!; + private RoomListing container = null!; public override void SetUpSteps() { @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, - Child = container = new RoomsContainer + Child = container = new RoomListing { RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 35bf6dc28a..ceb3a32402 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } - private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + private RoomListing roomListing => loungeScreen.ChildrenOfType().First(); [Test] public void TestManyRooms() @@ -41,13 +41,13 @@ namespace osu.Game.Tests.Visual.Playlists createRooms(GenerateRooms(30)); - AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); + AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomListing.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[0])); + AddStep("drag to top", () => InputManager.MoveMouseTo(roomListing.DrawableRooms[0])); AddAssert("first and second room masked", () - => !checkRoomVisible(roomsContainer.DrawableRooms[0]) && - !checkRoomVisible(roomsContainer.DrawableRooms[1])); + => !checkRoomVisible(roomListing.DrawableRooms[0]) && + !checkRoomVisible(roomListing.DrawableRooms[1])); } [Test] @@ -55,10 +55,10 @@ namespace osu.Game.Tests.Visual.Playlists { createRooms(GenerateRooms(30)); - AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); + AddStep("select last room", () => roomListing.DrawableRooms[^1].TriggerClick()); - AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); + AddUntilStep("first room is masked", () => !checkRoomVisible(roomListing.DrawableRooms[0])); + AddUntilStep("last room is not masked", () => checkRoomVisible(roomListing.DrawableRooms[^1])); } private bool checkRoomVisible(DrawableRoom room) => diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 207e0bdf55..c9d8365852 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -204,7 +204,7 @@ namespace osu.Game.Screens.OnlinePlay ScrollContainer.ScrollIntoView(drawableItem); } - #region Key selection logic (shared with BeatmapCarousel and RoomsContainer) + #region Key selection logic (shared with BeatmapCarousel and RoomListing) public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 65f969bc7b..1c3db87aaf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -21,12 +21,25 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public partial class RoomsContainer : CompositeDrawable, IKeyBindingHandler + public partial class RoomListing : CompositeDrawable, IKeyBindingHandler { + /// + /// Rooms which should be displayed. Should be managed externally. + /// public readonly BindableList Rooms = new BindableList(); - public readonly Bindable SelectedRoom = new Bindable(); + + /// + /// The current filter criteria. Should be managed externally. + /// public readonly Bindable Filter = new Bindable(); + /// + /// The currently user-selected room. + /// + public IBindable SelectedRoom => selectedRoom; + + private readonly Bindable selectedRoom = new Bindable(); + public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); private readonly ScrollContainer scroll; @@ -35,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - public RoomsContainer() + public RoomListing() { InternalChild = scroll = new OsuScrollContainer { @@ -158,7 +171,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }; + var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = selectedRoom }; roomFlow.Add(drawableRoom); @@ -177,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // selection may have a lease due to being in a sub screen. if (SelectedRoom.Value == r && !SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; } } @@ -187,13 +200,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // selection may have a lease due to being in a sub screen. if (!SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; } protected override bool OnClick(ClickEvent e) { if (!SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; return base.OnClick(e); } @@ -240,7 +253,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // we already have a valid selection only change selection if we still have a room to switch to. if (room != null) - SelectedRoom.Value = room; + selectedRoom.Value = room; } #endregion diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 12c0bb12e2..c1c65a744a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = roomsContainer.SelectedRoom } + SelectedRoom = { BindTarget = roomListing.SelectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); - private RoomsContainer roomsContainer = null!; + private RoomListing roomListing = null!; private LoungeListingPoller listingPoller = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; @@ -106,7 +106,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = roomsContainer = new RoomsContainer + Child = roomListing = new RoomListing { RelativeSizeAxes = Axes.Both, Filter = { BindTarget = filter }, @@ -185,7 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - roomsContainer.Rooms.Clear(); + roomListing.Rooms.Clear(); hasListingResults.Value = false; listingPoller.PollImmediately(); }); @@ -195,11 +195,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = roomsContainer.Rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = roomListing.Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - roomsContainer.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + roomListing.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -207,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - roomsContainer.Rooms.Add(r); + roomListing.Rooms.Add(r); } hasListingResults.Value = true; From a0888a7f2c5f7839243c7502a755643dddb664d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:51:08 +0900 Subject: [PATCH 118/349] Attempt to fix common editor test failures See https://github.com/ppy/osu/actions/runs/13623586844/job/38143232417?pr=32180 for one example. Arguably the bindable usage in [`ControlPointPart`](https://github.com/ppy/osu/blob/2365b065a4994f38fe67bab7d193e5a09bee538c/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs#L24-L26) is dangerous, but it's only dangerous in tests (because control points aren't mutated outside the editor) so I'm willing to turn a blind eye for now to favour async loading support. --- .../Editing/TestSceneEditorBeatmapCreation.cs | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index b7990b64c1..1413c4f436 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -171,6 +171,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != firstDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -215,6 +217,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != previousDifficultyName; }); + ensureEditorLoaded(); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString()); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add effect points", () => @@ -239,6 +243,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != previousDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -287,6 +293,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != firstDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -367,6 +375,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != originalDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has copy suffix in name", () => EditorBeatmap.BeatmapInfo.DifficultyName == copyDifficultyName); AddAssert("created difficulty has timing point", () => { @@ -377,7 +387,9 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); + ensureEditorLoaded(); AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); + AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); AddStep("save beatmap", () => Editor.Save()); @@ -440,6 +452,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != originalDifficultyName; }); + ensureEditorLoaded(); + AddStep("save without changes", () => Editor.Save()); AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash) @@ -477,6 +491,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != "New Difficulty"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)"); AddAssert("new difficulty persisted", () => { @@ -514,6 +531,10 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != duplicate_difficulty_name; }); + ensureEditorLoaded(); + + ensureEditorLoaded(); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); AddStep("try to save beatmap", () => Editor.Save()); AddAssert("beatmap set not corrupted", () => @@ -540,6 +561,8 @@ namespace osu.Game.Tests.Visual.Editing return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); }); + ensureEditorLoaded(); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo)); AddUntilStep("wait for created", () => @@ -547,7 +570,8 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != duplicate_difficulty_name; }); - AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded); + + ensureEditorLoaded(); AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { @@ -584,6 +608,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName == "New Difficulty"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty persisted", () => { var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); @@ -610,6 +637,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName == "New Difficulty (1)"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty persisted", () => { var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); @@ -735,6 +765,8 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); } + private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded); + private void createNewDifficulty() { string? currentDifficulty = null; @@ -748,13 +780,14 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => { string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != currentDifficulty; }); + ensureEditorLoaded(); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); } @@ -765,7 +798,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep($"switch to difficulty #{index + 1}", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index))); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); + ensureEditorLoaded(); AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); } From 4085ee805a717e2f0869a445b294b08d9730e2e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:29:13 +0900 Subject: [PATCH 119/349] Adjust scale and display of rooms in multiplayer lounge Just a quick pass because the rooms were definitely larger than they should be. --- .../Lounge/Components/RoomListing.cs | 25 ++++- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 104 ++++++++++++------ 2 files changed, 90 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 1c3db87aaf..0276601656 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -45,14 +45,20 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly ScrollContainer scroll; private readonly FillFlowContainer roomFlow; + private const float display_scale = 0.8f; + // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public RoomListing() { - InternalChild = scroll = new OsuScrollContainer + InternalChild = scroll = new Scroll { + Masking = false, RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = display_scale, ScrollbarOverlapsContent = false, Padding = new MarginPadding { Right = 5 }, Child = new OsuContextMenuContainer @@ -64,12 +70,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(10), + Spacing = new Vector2(5), + Margin = new MarginPadding { Vertical = 10 }, } } }; } + private partial class Scroll : OsuScrollContainer + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + } + protected override void LoadComplete() { SelectedRoom.BindValueChanged(onSelectedRoomChanged, true); @@ -171,7 +183,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = selectedRoom }; + var drawableRoom = new DrawableLoungeRoom(room) + { + SelectedRoom = selectedRoom, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(display_scale), + Width = 1 / display_scale, + }; roomFlow.Add(drawableRoom); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index c1c65a744a..c84f49fef6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -8,9 +8,12 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -27,6 +30,7 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge { @@ -85,11 +89,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [BackgroundDependencyLoader(true)] private void load() { + Masking = true; + const float controls_area_height = 25f; if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); + Color4 bg = Color4Extensions.FromHex("#070405"); + InternalChildren = new Drawable[] { listingPoller = new LoungeListingPoller @@ -113,56 +121,80 @@ namespace osu.Game.Screens.OnlinePlay.Lounge } }, loadingLayer = new LoadingLayer(true), - new FillFlowContainer + new Container { - Name = @"Header area flow", + Name = "Header area", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, - Direction = FillDirection.Vertical, Children = new Drawable[] { - new Container + new Box { - RelativeSizeAxes = Axes.X, - Height = Header.HEIGHT, - Child = searchTextBox = new BasicSearchTextBox - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.X, - Width = 0.6f, - }, + Colour = ColourInfo.GradientVertical(bg, bg.Opacity(0.75f)), + RelativeSizeAxes = Axes.Both, + Height = 0.8f, }, - new Container + new Box { + Colour = ColourInfo.GradientVertical(bg.Opacity(0.75f), bg.Opacity(0)), + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Y = 0.8f, + // Intentionally taller than the header for a more gradual fade + Height = 0.5f, + }, + new FillFlowContainer + { + Name = @"Header area flow", RelativeSizeAxes = Axes.X, - Height = controls_area_height, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, + Direction = FillDirection.Vertical, Children = new Drawable[] { - Buttons.WithChild(CreateNewRoomButton().With(d => + new Container { - d.Anchor = Anchor.BottomLeft; - d.Origin = Anchor.BottomLeft; - d.Size = new Vector2(150, 37.5f); - d.Action = () => Open(); - })), - new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => + RelativeSizeAxes = Axes.X, + Height = Header.HEIGHT, + Child = searchTextBox = new BasicSearchTextBox { - d.Anchor = Anchor.TopRight; - d.Origin = Anchor.TopRight; - })) + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + Width = 0.6f, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = controls_area_height, + Children = new Drawable[] + { + Buttons.WithChild(CreateNewRoomButton().With(d => + { + d.Anchor = Anchor.BottomLeft; + d.Origin = Anchor.BottomLeft; + d.Size = new Vector2(150, 37.5f); + d.Action = () => Open(); + })), + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => + { + d.Anchor = Anchor.TopRight; + d.Origin = Anchor.TopRight; + })) + } + } } - } - } - }, + }, + }, + } }, }; } From 4a16b4bd984f8564eec8c940be245ffb3f5014ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 16:15:40 +0900 Subject: [PATCH 120/349] Fix typo in xmldoc --- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 74e85c595c..e9ca0a8ed2 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -411,7 +411,7 @@ namespace osu.Game.Online.Chat } /// - /// Find an existing channel instance for the provided channel. Lookup is performed basd on ID. + /// Find an existing channel instance for the provided channel. Lookup is performed based on ID. /// The provided channel may be used if an existing instance is not found. /// /// A candidate channel to be used for lookup or permanently on lookup failure. From b19c2c7f9faae5025ece2352e5617b29d6f744f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 17:01:41 +0900 Subject: [PATCH 121/349] Update recently-added test --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index c01cb70955..e5e4921a17 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { From 5b0e54a77d712e4b7b924eeb6d2092dc5aa8848a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 17:22:19 +0900 Subject: [PATCH 122/349] Remove duplicated assert --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 1413c4f436..996e87ff8a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -533,8 +533,6 @@ namespace osu.Game.Tests.Visual.Editing ensureEditorLoaded(); - ensureEditorLoaded(); - AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); AddStep("try to save beatmap", () => Editor.Save()); AddAssert("beatmap set not corrupted", () => From f0d6641adf8a4076c886c9dfa321d2281e925361 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 17:44:48 +0900 Subject: [PATCH 123/349] Add basic subclassing and implement beatmap-start flow --- .../SongSelectV2/TestSceneSongSelect.cs | 4 +- .../TestSceneSongSelectNavigation.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 ++++ osu.Game/Screens/SelectV2/SoloSongSelect.cs | 28 +++++++++++++ .../{SongSelectV2.cs => SongSelect.cs} | 41 ++++++++++++------- 5 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/SoloSongSelect.cs rename osu.Game/Screens/SelectV2/{SongSelectV2.cs => SongSelect.cs} (84%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 6d180c76d9..630f3c95ee 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -78,8 +78,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { base.SetUpSteps(); - AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); - AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); + AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect())); + AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs index 5173cb5673..a7ca3cd18c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 base.SetUpSteps(); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddWaitStep("wait", 5); - PushAndConfirm(() => new Screens.SelectV2.SongSelectV2()); + PushAndConfirm(() => new Screens.SelectV2.SoloSongSelect()); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c6bce228dc..7372847402 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.SelectV2 [Cached] public partial class BeatmapCarousel : Carousel { + public Action? RequestPresentBeatmap { private get; init; } + public const float SPACING = 5f; private IBindableList detachedBeatmaps = null!; @@ -128,6 +130,12 @@ namespace osu.Game.Screens.SelectV2 return; case BeatmapInfo beatmapInfo: + if (ReferenceEquals(CurrentSelection, beatmapInfo)) + { + RequestPresentBeatmap?.Invoke(beatmapInfo); + return; + } + CurrentSelection = beatmapInfo; return; } diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs new file mode 100644 index 0000000000..e6ecdc6705 --- /dev/null +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Screens; +using osu.Game.Screens.Play; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class SoloSongSelect : SongSelect + { + protected override bool OnStart() + { + this.Push(new PlayerLoaderV2(() => new SoloPlayer())); + return false; + } + + private partial class PlayerLoaderV2 : PlayerLoader + { + public override bool ShowFooter => true; + + public PlayerLoaderV2(Func createPlayer) + : base(createPlayer) + { + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelect.cs similarity index 84% rename from osu.Game/Screens/SelectV2/SongSelectV2.cs rename to osu.Game/Screens/SelectV2/SongSelect.cs index 23139c8742..5458a02583 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,7 +10,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.Play; +using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2.Footer; namespace osu.Game.Screens.SelectV2 @@ -20,7 +19,7 @@ namespace osu.Game.Screens.SelectV2 /// This screen is intended to house all components introduced in the new song select design to add transitions and examine the overall look. /// This will be gradually built upon and ultimately replace once everything is in place. /// - public partial class SongSelectV2 : OsuScreen + public abstract partial class SongSelect : OsuScreen { private const float logo_scale = 0.4f; @@ -29,6 +28,8 @@ namespace osu.Game.Screens.SelectV2 [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private BeatmapCarousel carousel = null!; + public override bool ShowFooter => true; [Resolved] @@ -58,8 +59,9 @@ namespace osu.Game.Screens.SelectV2 { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, - Child = new BeatmapCarousel + Child = carousel = new BeatmapCarousel { + RequestPresentBeatmap = _ => OnStart(), RelativeSizeAxes = Axes.Both }, }, @@ -141,11 +143,17 @@ namespace osu.Game.Screens.SelectV2 logo.Action = () => { - this.Push(new PlayerLoaderV2(() => new SoloPlayer())); + OnStart(); return false; }; } + /// + /// Called when a selection is made. + /// + /// If a resultant action occurred that takes the user away from SongSelect. + protected abstract bool OnStart(); + protected override void LogoSuspending(OsuLogo logo) { base.LogoSuspending(logo); @@ -160,19 +168,22 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } + /// + /// Set the query to the search text box. + /// + /// The string to search. + public void Search(string query) + { + carousel.Filter(new FilterCriteria + { + // TODO: this should only set the text of the current criteria, not use a completely new criteria. + SearchText = query, + }); + } + private partial class SoloModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; } - - private partial class PlayerLoaderV2 : PlayerLoader - { - public override bool ShowFooter => true; - - public PlayerLoaderV2(Func createPlayer) - : base(createPlayer) - { - } - } } } From 1be3b990e7589b2c1f1ae8e9fec64989f79902c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 18:09:58 +0900 Subject: [PATCH 124/349] Add transition for selecting a beatmap --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 27 +++++++++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 32 ++++++++++---------- osu.Game/Screens/SelectV2/SongSelect.cs | 12 ++++++-- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 7372847402..1c730169eb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; @@ -260,6 +261,32 @@ namespace osu.Game.Screens.SelectV2 #endregion + #region Animation + + /// + /// Moves non-selected beatmaps to the right, hiding off-screen. + /// + public bool VisuallyFocusSelected { get; set; } + + private float selectionFocusOffset; + + protected override void Update() + { + base.Update(); + + selectionFocusOffset = (float)Interpolation.DampContinuously(selectionFocusOffset, VisuallyFocusSelected ? 300 : 0, 100, Time.Elapsed); + + foreach (var panel in Scroll.Panels) + { + var c = (ICarouselPanel)panel; + + if (!c.Selected.Value) + panel.X += selectionFocusOffset; + } + } + + #endregion + #region Filtering public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index e50281e713..1a120e69e7 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The number of items currently actualised into drawables. /// - public int VisibleItems => scroll.Panels.Count; + public int VisibleItems => Scroll.Panels.Count; /// /// The currently selected model. Generally of type T. @@ -185,7 +185,7 @@ namespace osu.Game.Screens.SelectV2 /// The item to find a related drawable representation. /// The drawable representation if it exists. protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => - scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + Scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); /// /// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target. @@ -222,11 +222,11 @@ namespace osu.Game.Screens.SelectV2 #region Initialisation - private readonly CarouselScrollContainer scroll; + protected readonly CarouselScrollContainer Scroll; protected Carousel() { - InternalChild = scroll = new CarouselScrollContainer + InternalChild = Scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, }; @@ -499,13 +499,13 @@ namespace osu.Game.Screens.SelectV2 // If a keyboard selection is currently made, we want to keep the view stable around the selection. // That means that we should offset the immediate scroll position by any change in Y position for the selection. if (prevKeyboard.YPosition != null && currentKeyboardSelection.YPosition != prevKeyboard.YPosition) - scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); + Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); } private void scrollToSelection() { if (currentKeyboardSelection.CarouselItem != null) - scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); + Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); } #endregion @@ -519,12 +519,12 @@ namespace osu.Game.Screens.SelectV2 /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => (float)(scroll.Current + DrawHeight + BleedBottom); + private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom); /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => (float)(scroll.Current - BleedTop); + private float visibleUpperBound => (float)(Scroll.Current - BleedTop); /// /// Half the height of the visible content. @@ -557,7 +557,7 @@ namespace osu.Game.Screens.SelectV2 double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0; - foreach (var panel in scroll.Panels) + foreach (var panel in Scroll.Panels) { var c = (ICarouselPanel)panel; @@ -566,12 +566,12 @@ namespace osu.Game.Screens.SelectV2 continue; float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight); - scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); + Scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); - Vector2 posInScroll = scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); + Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); panel.X = offsetX(dist, visibleHalfHeight); @@ -628,7 +628,7 @@ namespace osu.Game.Screens.SelectV2 toDisplay.RemoveAll(i => !i.IsVisible); // Iterate over all panels which are already displayed and figure which need to be displayed / removed. - foreach (var panel in scroll.Panels) + foreach (var panel in Scroll.Panels) { var carouselPanel = (ICarouselPanel)panel; @@ -658,7 +658,7 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.DrawYPosition = item.CarouselYPosition; carouselPanel.Item = item; - scroll.Add(drawable); + Scroll.Add(drawable); } // Update the total height of all items (to make the scroll container scrollable through the full height even though @@ -666,10 +666,10 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems.Count > 0) { var lastItem = carouselItems[^1]; - scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); + Scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); } else - scroll.SetLayoutHeight(0); + Scroll.SetLayoutHeight(0); } private static void expirePanelImmediately(Drawable panel) @@ -713,7 +713,7 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler + protected partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { public readonly Container Panels; diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 5458a02583..70452de99a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -99,9 +99,13 @@ namespace osu.Game.Screens.SelectV2 base.OnEntering(e); } + private const double fade_duration = 300; + public override void OnResuming(ScreenTransitionEvent e) { - this.FadeIn(); + this.FadeIn(fade_duration, Easing.OutQuint); + + carousel.VisuallyFocusSelected = false; // required due to https://github.com/ppy/osu-framework/issues/3218 modSelectOverlay.SelectedMods.Disabled = false; @@ -112,16 +116,18 @@ namespace osu.Game.Screens.SelectV2 public override void OnSuspending(ScreenTransitionEvent e) { - this.Delay(400).FadeOut(); + this.Delay(100).FadeOut(fade_duration, Easing.OutQuint); modSelectOverlay.SelectedMods.UnbindFrom(Mods); + carousel.VisuallyFocusSelected = true; + base.OnSuspending(e); } public override bool OnExiting(ScreenExitEvent e) { - this.Delay(400).FadeOut(); + this.FadeOut(fade_duration, Easing.OutQuint); return base.OnExiting(e); } From 918315aa65a5d5447d8e7b98c16a81611aa5f7e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 19:19:53 +0900 Subject: [PATCH 125/349] Split out methods so retrieving the room is not a callback function --- .../StatefulMultiplayerClientTest.cs | 3 +- .../TestSceneMultiSpectatorLeaderboard.cs | 3 +- .../TestSceneMultiSpectatorScreen.cs | 4 +- .../TestSceneMultiplayerMatchSongSelect.cs | 5 ++- .../TestSceneMultiplayerParticipantsList.cs | 3 +- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 3 +- .../TestSceneMultiplayerPlaylist.cs | 4 +- .../TestSceneMultiplayerQueueList.cs | 4 +- .../TestSceneMultiplayerSpectateButton.cs | 4 +- .../Multiplayer/MultiplayerTestScene.cs | 44 ++++++++----------- 10 files changed, 43 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 230a996942..8364e58bdc 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -19,7 +19,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 1821c2f3bc..60358dfbc4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -24,7 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); AddStep("reset", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 3fdbe02906..aa98dc59db 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -66,7 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("clear playing users", () => playingUsers.Clear()); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); } [TestCase(1)] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 287d7f5816..9c85bdd57a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -62,7 +62,10 @@ namespace osu.Game.Tests.Visual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); } private void setUp() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index b5655afb8c..ed3fd4a6f8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -32,7 +32,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); createNewParticipantsList(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 1a5be48cad..99bec1e714 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -25,7 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 54932db7c6..7c8691d5d1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -47,7 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create list", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 5eba67bab5..1a7b677798 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -43,7 +43,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create playlist", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index f92721b04b..9e6734ce99 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -47,7 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create button", () => { diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 8150807f4f..ac587d3bb2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.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; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual.OnlinePlay; @@ -24,34 +24,28 @@ namespace osu.Game.Tests.Visual.Multiplayer public bool RoomJoined => MultiplayerClient.RoomJoined; + protected Room CreateDefaultRoom() + { + return new Room + { + Name = "test name", + Type = MatchType.HeadToHead, + Playlist = + [ + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID + } + ] + }; + } + /// /// Creates and joins a basic multiplayer room. /// - /// A callback that may be used to further set up the room. - protected void JoinDefaultRoom(Action? setupFunc = null) - { - AddStep("join room", () => - { - Room room = new Room - { - Name = "test name", - Type = MatchType.HeadToHead, - Playlist = - [ - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID - } - ] - }; + protected void JoinRoom(Room room) => MultiplayerClient.CreateRoom(room).FireAndForget(); - setupFunc?.Invoke(room); - - MultiplayerClient.CreateRoom(room).ConfigureAwait(false); - }); - - AddUntilStep("wait for room join", () => RoomJoined); - } + protected void WaitForJoined() => AddUntilStep("wait for room join", () => RoomJoined); protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } From 21d35f9dae085c6c9bee4369af6e261e98dfc21e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 19:40:31 +0900 Subject: [PATCH 126/349] Use alternative method of offsetting X that conveys flow better --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 11 ++++------- osu.Game/Screens/SelectV2/Carousel.cs | 13 +++++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 1c730169eb..1c1f6fa7fb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -275,14 +275,11 @@ namespace osu.Game.Screens.SelectV2 base.Update(); selectionFocusOffset = (float)Interpolation.DampContinuously(selectionFocusOffset, VisuallyFocusSelected ? 300 : 0, 100, Time.Elapsed); + } - foreach (var panel in Scroll.Panels) - { - var c = (ICarouselPanel)panel; - - if (!c.Selected.Value) - panel.X += selectionFocusOffset; - } + protected override float GetPanelXOffset(Drawable panel) + { + return base.GetPanelXOffset(panel) + (((ICarouselPanel)panel).Selected.Value ? 0 : selectionFocusOffset); } #endregion diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 1a120e69e7..5339b5358b 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -571,10 +571,7 @@ namespace osu.Game.Screens.SelectV2 if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); - Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); - float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); - - panel.X = offsetX(dist, visibleHalfHeight); + panel.X = GetPanelXOffset(panel); c.Selected.Value = c.Item == currentSelection?.CarouselItem; c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; @@ -582,6 +579,14 @@ namespace osu.Game.Screens.SelectV2 } } + protected virtual float GetPanelXOffset(Drawable panel) + { + Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); + float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); + + return offsetX(dist, visibleHalfHeight); + } + /// /// Computes the x-offset of currently visible items. Makes the carousel appear round. /// From b5696f97a072439946e0af495e9f4191d864fad7 Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 03:05:03 +0600 Subject: [PATCH 127/349] Show current beatmap info in window title --- osu.Game/OsuGame.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d23d27c89e..3b55c320b3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -828,6 +828,8 @@ namespace osu.Game { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + + Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; } private void modsChanged(ValueChangedEvent> mods) From c051ff84d293e5c2408e7ff59f55e176f0d1f1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Mar 2025 13:04:23 +0100 Subject: [PATCH 128/349] Add UI for assigning custom tags to beatmaps Visual part for https://github.com/ppy/osu/issues/31913. Opening separately for appropriate visual UI adjustments. Also mostly ready to be hooked up to the results screen, pending merge of https://github.com/ppy/osu-web/pull/11951. --- .../Visual/Ranking/TestSceneUserTagControl.cs | 85 +++ osu.Game/Beatmaps/APIBeatmapTag.cs | 16 + osu.Game/Configuration/SessionStatics.cs | 13 +- .../API/Requests/AddBeatmapTagRequest.cs | 31 + .../Online/API/Requests/ListTagsRequest.cs | 12 + .../API/Requests/RemoveBeatmapTagRequest.cs | 29 + .../API/Requests/Responses/APIBeatmap.cs | 6 + .../Online/API/Requests/Responses/APITag.cs | 19 + .../Requests/Responses/APITagCollection.cs | 14 + osu.Game/Screens/Ranking/UserTagControl.cs | 537 ++++++++++++++++++ 10 files changed, 756 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs create mode 100644 osu.Game/Beatmaps/APIBeatmapTag.cs create mode 100644 osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs create mode 100644 osu.Game/Online/API/Requests/ListTagsRequest.cs create mode 100644 osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APITag.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APITagCollection.cs create mode 100644 osu.Game/Screens/Ranking/UserTagControl.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs new file mode 100644 index 0000000000..ebfd553815 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -0,0 +1,85 @@ +// 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.Cursor; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Tests.Visual.Ranking +{ + public partial class TestSceneUserTagControl : OsuTestScene + { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("set up working beatmap", () => + { + Beatmap.Value.BeatmapInfo.OnlineID = 42; + }); + AddStep("set up network requests", () => + { + dummyAPI.HandleRequest = request => + { + switch (request) + { + case ListTagsRequest listTagsRequest: + { + Scheduler.AddDelayed(() => listTagsRequest.TriggerSuccess(new APITagCollection + { + 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 = 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.", }, + ] + }), 500); + return true; + } + + case GetBeatmapSetRequest getBeatmapSetRequest: + { + var beatmapSet = CreateAPIBeatmapSet(Beatmap.Value.BeatmapInfo); + beatmapSet.Beatmaps.Single().TopTags = + [ + new APIBeatmapTag { TagId = 3, VoteCount = 9 }, + ]; + Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500); + return true; + } + + case AddBeatmapTagRequest: + case RemoveBeatmapTagRequest: + { + Scheduler.AddDelayed(request.TriggerSuccess, 500); + return true; + } + } + + return false; + }; + }); + AddStep("create control", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new UserTagControl + { + Width = 500, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + }); + } + } +} diff --git a/osu.Game/Beatmaps/APIBeatmapTag.cs b/osu.Game/Beatmaps/APIBeatmapTag.cs new file mode 100644 index 0000000000..5f4f9b851d --- /dev/null +++ b/osu.Game/Beatmaps/APIBeatmapTag.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + public class APIBeatmapTag + { + [JsonProperty("tag_id")] + public long TagId { get; set; } + + [JsonProperty("count")] + public int VoteCount { get; set; } + } +} diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index d2069e4027..b816d1a88b 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -1,8 +1,6 @@ // 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 osu.Framework; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -27,11 +25,12 @@ namespace osu.Game.Configuration SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); - SetDefault(Static.SeasonalBackgrounds, null); + SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); - SetDefault(Static.LastLocalUserScore, null); - SetDefault(Static.LastAppliedOffsetScore, null); - SetDefault(Static.UserOnlineActivity, null); + SetDefault(Static.LastLocalUserScore, null); + SetDefault(Static.LastAppliedOffsetScore, null); + SetDefault(Static.UserOnlineActivity, null); + SetDefault(Static.AllBeatmapTags, null); } /// @@ -99,5 +98,7 @@ namespace osu.Game.Configuration /// The activity for the current user to broadcast to other players. /// UserOnlineActivity, + + AllBeatmapTags, } } diff --git a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs new file mode 100644 index 0000000000..4fa02dc569 --- /dev/null +++ b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class AddBeatmapTagRequest : APIRequest + { + public int BeatmapID { get; } + public long TagID { get; } + + public AddBeatmapTagRequest(int beatmapID, long tagID) + { + BeatmapID = beatmapID; + TagID = tagID; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + req.AddParameter(@"tag_id", TagID.ToString(CultureInfo.InvariantCulture), RequestParameterType.Query); + return req; + } + + protected override string Target => $@"beatmaps/{BeatmapID}/tags"; + } +} diff --git a/osu.Game/Online/API/Requests/ListTagsRequest.cs b/osu.Game/Online/API/Requests/ListTagsRequest.cs new file mode 100644 index 0000000000..ac4b1a3e2a --- /dev/null +++ b/osu.Game/Online/API/Requests/ListTagsRequest.cs @@ -0,0 +1,12 @@ +// 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.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class ListTagsRequest : APIRequest + { + protected override string Target => "tags"; + } +} diff --git a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs new file mode 100644 index 0000000000..8090dd2cb0 --- /dev/null +++ b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.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.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class RemoveBeatmapTagRequest : APIRequest + { + public int BeatmapID { get; } + public long TagID { get; } + + public RemoveBeatmapTagRequest(int beatmapID, long tagID) + { + BeatmapID = beatmapID; + TagID = tagID; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Delete; + return req; + } + + protected override string Target => $@"beatmaps/{BeatmapID}/tags/{TagID}"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index e5ecfe2c99..f06d0ef274 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -95,6 +95,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"failtimes")] public APIFailTimes? FailTimes { get; set; } + [JsonProperty(@"top_tag_ids")] + public APIBeatmapTag[]? TopTags { get; set; } + + [JsonProperty(@"own_tag_ids")] + public long[]? OwnTagIds { get; set; } + [JsonProperty(@"max_combo")] public int? MaxCombo { get; set; } diff --git a/osu.Game/Online/API/Requests/Responses/APITag.cs b/osu.Game/Online/API/Requests/Responses/APITag.cs new file mode 100644 index 0000000000..4dd18663af --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITag.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APITag + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("description")] + public string Description { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APITagCollection.cs b/osu.Game/Online/API/Requests/Responses/APITagCollection.cs new file mode 100644 index 0000000000..a177699348 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITagCollection.cs @@ -0,0 +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; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APITagCollection + { + [JsonProperty("tags")] + public APITag[] Tags { get; set; } = Array.Empty(); + } +} diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs new file mode 100644 index 0000000000..6b7d22a7c2 --- /dev/null +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -0,0 +1,537 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Caching; +using osu.Framework.Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +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.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public partial class UserTagControl : CompositeDrawable + { + public override bool HandlePositionalInput => true; + + private readonly Cached layout = new Cached(); + + private FillFlowContainer tagFlow = null!; + private LoadingLayer loadingLayer = null!; + + private BindableList displayedTags { get; } = new BindableList(); + private BindableList extraTags { get; } = new BindableList(); + + private Bindable allTags = null!; + private readonly Bindable apiBeatmap = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private Bindable beatmap { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(SessionStatics sessionStatics) + { + AutoSizeAxes = Axes.Y; + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(8), + Children = new Drawable[] + { + tagFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + LayoutDuration = 300, + LayoutEasing = Easing.OutQuint, + Spacing = new Vector2(4), + }, + new ExtraTagsButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + OnTagSelected = onExtraTagSelected, + ExtraTags = { BindTarget = extraTags }, + }, + }, + }, + loadingLayer = new LoadingLayer + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible } + }, + }; + + allTags = sessionStatics.GetBindable(Static.AllBeatmapTags); + + if (allTags.Value == null) + { + var listTagsRequest = new ListTagsRequest(); + listTagsRequest.Success += tags => allTags.Value = tags.Tags.ToArray(); + api.Queue(listTagsRequest); + } + + var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmap.Value.BeatmapInfo.BeatmapSet!.OnlineID); + getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmap.Value.BeatmapInfo)); + api.Queue(getBeatmapSetRequest); + } + + private void onExtraTagSelected(UserTag tag) + { + loadingLayer.Show(); + extraTags.Remove(tag); + + var req = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, tag.Id); + req.Success += () => + { + tag.Voted.Value = true; + tag.VoteCount.Value += 1; + displayedTags.Add(tag); + loadingLayer.Hide(); + }; + req.Failure += _ => extraTags.Add(tag); + api.Queue(req); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + allTags.BindValueChanged(_ => updateTags()); + apiBeatmap.BindValueChanged(_ => updateTags()); + updateTags(); + + displayedTags.BindCollectionChanged(displayTags, true); + } + + private void updateTags() + { + if (allTags.Value == null || apiBeatmap.Value?.TopTags == null) + return; + + var allTagsById = allTags.Value.ToDictionary(t => t.Id); + var ownTagIds = apiBeatmap.Value.OwnTagIds?.ToHashSet() ?? new HashSet(); + + foreach (var topTag in apiBeatmap.Value.TopTags) + { + if (allTagsById.Remove(topTag.TagId, out var tag)) + { + displayedTags.Add(new UserTag(tag) + { + VoteCount = { Value = topTag.VoteCount }, + Voted = { Value = ownTagIds.Contains(tag.Id) } + }); + } + } + + extraTags.AddRange(allTagsById.Select(t => new UserTag(t.Value))); + + loadingLayer.Hide(); + } + + private void displayTags(object? sender, NotifyCollectionChangedEventArgs e) + { + var oldItems = tagFlow.ToArray(); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + { + var tag = (UserTag)e.NewItems[i]!; + var drawableTag = new DrawableUserTag(tag); + tagFlow.Insert(tagFlow.Count, drawableTag); + tag.VoteCount.BindValueChanged(sortTags, true); + layout.Invalidate(); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + for (int i = 0; i < e.OldItems!.Count; i++) + { + var tag = (UserTag)e.OldItems[i]!; + tag.VoteCount.ValueChanged -= sortTags; + tagFlow.Remove(oldItems[e.OldStartingIndex + i], true); + } + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + tagFlow.Clear(); + break; + } + } + } + + private void sortTags(ValueChangedEvent _) => layout.Invalidate(); + + protected override void Update() + { + base.Update(); + + if (!layout.IsValid && !IsHovered) + { + var sortedTags = new Dictionary( + displayedTags.OrderByDescending(t => t.VoteCount.Value) + .ThenByDescending(t => t.Voted.Value) + .Select((tag, index) => new KeyValuePair(tag, index))); + + foreach (var drawableTag in tagFlow) + tagFlow.SetLayoutPosition(drawableTag, sortedTags[drawableTag.UserTag]); + + layout.Validate(); + } + } + + private partial class DrawableUserTag : OsuAnimatedButton + { + public readonly UserTag UserTag; + + private readonly Bindable voteCount = new Bindable(); + private readonly BindableBool voted = new BindableBool(); + private readonly Bindable confirmed = new BindableBool(); + + private Box mainBackground = null!; + private Box voteBackground = null!; + private OsuSpriteText tagNameText = null!; + private OsuSpriteText voteCountText = null!; + private LoadingSpinner spinner = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private Bindable beatmap { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private APIRequest? requestInFlight; + + public DrawableUserTag(UserTag userTag) + { + UserTag = userTag; + voteCount.BindTo(userTag.VoteCount); + voted.BindTo(userTag.Voted); + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + CornerRadius = 8; + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Colour = colours.Lime1, + Radius = 5, + Type = EdgeEffectType.Glow, + }; + Content.AddRange(new Drawable[] + { + mainBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 6, Right = 3, Vertical = 3, }, + Spacing = new Vector2(5), + Children = new Drawable[] + { + tagNameText = new OsuSpriteText + { + Text = UserTag.Name, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + voteBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + voteCountText = new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 6, Vertical = 3, }, + }, + spinner = new LoadingSpinner(withBox: true) + { + Alpha = 0, + Size = new Vector2(18), + } + } + } + } + } + }); + + TooltipText = UserTag.Description; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const double transition_duration = 300; + + voteCount.BindValueChanged(_ => + { + voteCountText.Text = voteCount.Value.ToLocalisableString(); + confirmed.Value = voteCount.Value >= 10; + }, true); + voted.BindValueChanged(v => + { + if (v.NewValue) + { + voteBackground.FadeColour(colours.Lime3, transition_duration, Easing.OutQuint); + voteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + } + else + { + voteBackground.FadeColour(colours.Gray2, transition_duration, Easing.OutQuint); + voteCountText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + } + }, true); + confirmed.BindValueChanged(c => + { + if (c.NewValue) + { + mainBackground.FadeColour(colours.Lime1, transition_duration, Easing.OutQuint); + tagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0.5f, transition_duration, Easing.OutQuint); + } + else + { + mainBackground.FadeColour(colours.Gray4, transition_duration, Easing.OutQuint); + tagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint); + } + }, true); + FinishTransforms(true); + + Action = () => + { + if (requestInFlight != null) + return; + + spinner.Show(); + + APIRequest request; + + switch (voted.Value) + { + case true: + var removeReq = new RemoveBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id); + removeReq.Success += () => + { + voteCount.Value -= 1; + voted.Value = false; + }; + request = removeReq; + break; + + case false: + var addReq = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id); + addReq.Success += () => + { + voteCount.Value += 1; + voted.Value = true; + }; + request = addReq; + break; + } + + request.Success += () => + { + spinner.Hide(); + requestInFlight = null; + }; + request.Failure += _ => + { + spinner.Hide(); + requestInFlight = null; + }; + api.Queue(requestInFlight = request); + }; + } + } + + private partial class ExtraTagsButton : GrayButton, IHasPopover + { + public BindableList ExtraTags { get; } = new BindableList(); + + public Action? OnTagSelected { get; set; } + + public ExtraTagsButton() + : base(FontAwesome.Solid.Plus) + { + Size = new Vector2(30); + + Action = this.ShowPopover; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ExtraTags.BindCollectionChanged((_, _) => Enabled.Value = ExtraTags.Count > 0, true); + } + + public Popover GetPopover() => new ExtraTagsPopover + { + ExtraTags = { BindTarget = ExtraTags }, + OnSelected = OnTagSelected, + }; + } + + private partial class ExtraTagsPopover : OsuPopover + { + public BindableList ExtraTags { get; } = new BindableList(); + + public Action? OnSelected { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Child = new OsuScrollContainer + { + Width = 250, + Height = 200, + ScrollbarOverlapsContent = false, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 5 }, + Spacing = new Vector2(10), + ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + { + Action = () => + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + }) + } + }; + } + } + + private partial class DrawableExtraTag : OsuAnimatedButton + { + private readonly UserTag tag; + + public DrawableExtraTag(UserTag tag) + { + this.tag = tag; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Anchor = Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Content.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeaFoamDark, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Padding = new MarginPadding(5), + Children = new Drawable[] + { + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = tag.Name, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = tag.Description, + } + } + } + }); + } + } + } + + public record UserTag + { + public long Id { get; } + public string Name { get; } + public string Description { get; set; } + public BindableInt VoteCount { get; } = new BindableInt(); + public BindableBool Voted { get; } = new BindableBool(); + + public UserTag(APITag tag) + { + Id = tag.Id; + Name = tag.Name; + Description = tag.Description; + } + } +} From 2abe75629eefb21f53ab4144210c7e3cf30ce8fc Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 18:28:03 +0600 Subject: [PATCH 129/349] Skip window title update for dummy beatmap --- osu.Game/OsuGame.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3b55c320b3..fb9be8860c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -829,7 +829,11 @@ namespace osu.Game beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); - Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + // prevent weird window title saying please load a beatmap + if (beatmap.NewValue is null or DummyWorkingBeatmap) + Host.Window.Title = Name; + else + Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; } private void modsChanged(ValueChangedEvent> mods) From dff354247eeb9490105f82991a2f98ad8b6efc02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 22:21:36 +0900 Subject: [PATCH 130/349] Change `ModSelectOverlay.ShowPresets` to `init` --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 5 ++++- .../Visual/UserInterface/TestSceneScreenFooter.cs | 9 ++------- .../UserInterface/TestSceneScreenFooterButtonMods.cs | 3 +-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++--- osu.Game/Screens/Select/SongSelect.cs | 10 ++++------ osu.Game/Screens/SelectV2/SongSelect.cs | 10 ++++------ osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs | 3 +-- 7 files changed, 19 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 280497e861..6eb9263c7e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -1030,7 +1030,10 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => true; + public TestModSelectOverlay() + { + ShowPresets = true; + } } private class TestUnimplementedMod : Mod diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index a4cf8a276f..fc8777068d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface { private DependencyProvidingContainer contentContainer = null!; private ScreenFooter screenFooter = null!; - private TestModSelectOverlay modOverlay = null!; + private UserModSelectOverlay modOverlay = null!; [SetUp] public void SetUp() => Schedule(() => @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, Children = new Drawable[] { - modOverlay = new TestModSelectOverlay(), + modOverlay = new UserModSelectOverlay { ShowPresets = true }, new PopoverContainer { RelativeSizeAxes = Axes.Both, @@ -196,11 +196,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("external overlay content still not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); } - private partial class TestModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } - private partial class TestShearedOverlayContainer : ShearedOverlayContainer { public TestShearedOverlayContainer() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs index ba53eb83c4..e86f83ee15 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs @@ -115,11 +115,10 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => true; - public TestModSelectOverlay() : base(OverlayColourScheme.Aquamarine) { + ShowPresets = true; } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index daac925dfb..ac589fbebf 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -35,7 +35,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler + public partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler { public const int BUTTON_WIDTH = 200; @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods /// /// Whether the column with available mod presets should be shown. /// - protected virtual bool ShowPresets => false; + public bool ShowPresets { get; init; } protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false); @@ -125,7 +125,7 @@ namespace osu.Game.Overlays.Mods [Resolved] private ScreenFooter? footer { get; set; } - protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) + public ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) { } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index c20dcb8593..1496eb96f9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -426,7 +426,10 @@ namespace osu.Game.Screens.Select (beatmapOptionsButton = new FooterButtonOptions(), BeatmapOptions) }; - protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); + protected virtual ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay + { + ShowPresets = true, + }; private DependencyContainer dependencies = null!; @@ -1152,10 +1155,5 @@ namespace osu.Game.Screens.Select return base.OnHover(e); } } - - internal partial class SoloModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } } } diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 70452de99a..ad29f846c4 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -23,7 +23,10 @@ namespace osu.Game.Screens.SelectV2 { private const float logo_scale = 0.4f; - private readonly ModSelectOverlay modSelectOverlay = new SoloModSelectOverlay(); + private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay + { + ShowPresets = true, + }; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -186,10 +189,5 @@ namespace osu.Game.Screens.SelectV2 SearchText = query, }); } - - private partial class SoloModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } } } diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index 6908f7f1b4..21d0b8e7a8 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -658,11 +658,10 @@ namespace osu.Game.Tests.Visual.Gameplay private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => false; - public TestModSelectOverlay() : base(OverlayColourScheme.Aquamarine) { + ShowPresets = false; } } } From 14b5c0bf10389b924fa8ca515c2e27457fdcc119 Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 19:56:48 +0600 Subject: [PATCH 131/349] Update window title in input thread --- osu.Game/OsuGame.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fb9be8860c..a80d646e15 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -830,10 +830,11 @@ namespace osu.Game beatmap.NewValue?.BeginAsyncLoad(); // prevent weird window title saying please load a beatmap - if (beatmap.NewValue is null or DummyWorkingBeatmap) - Host.Window.Title = Name; - else - Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + string newTitle = Name; + if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) + newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + + Host.InputThread.Scheduler.AddOnce(s => Host.Window.Title = s, newTitle); } private void modsChanged(ValueChangedEvent> mods) From 8ce6003a3e156cac95f448f40f473d9023c278df Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 20:36:53 +0600 Subject: [PATCH 132/349] Skip updating window title in headless mode --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a80d646e15..2b9e2cb9cd 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -829,6 +829,9 @@ namespace osu.Game beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + if (Host.Window == null) + return; + // prevent weird window title saying please load a beatmap string newTitle = Name; if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) From 9ca12744957f9d660d13a50e13060b2aa772b0e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Mar 2025 13:51:56 +0900 Subject: [PATCH 133/349] Rename test scene to match new `RoomListing` class name --- ...TestSceneLoungeRoomsContainer.cs => TestSceneRoomListing.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneLoungeRoomsContainer.cs => TestSceneRoomListing.cs} (99%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs similarity index 99% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 23e15b0501..27c5758afa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -18,7 +18,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene + public partial class TestSceneRoomListing : OnlinePlayTestScene { private BindableList rooms = null!; private IBindable selectedRoom = null!; From 3661107e4ffc4478ac7fe50e5189877f71b44cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 08:05:15 +0100 Subject: [PATCH 134/349] Update property name in line with web changes --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index f06d0ef274..66e17739a8 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -98,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"top_tag_ids")] public APIBeatmapTag[]? TopTags { get; set; } - [JsonProperty(@"own_tag_ids")] + [JsonProperty(@"current_user_tag_ids")] public long[]? OwnTagIds { get; set; } [JsonProperty(@"max_combo")] From abc4955e8131de912aaec22941d352cb558a7297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:21:47 +0100 Subject: [PATCH 135/349] Add failing test coverage --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 71 ++++++++++++++++--- .../SongSelectComponentsTestScene.cs | 5 +- .../SongSelectV2/TestSceneLeaderboardScore.cs | 66 +++++++++++++++++ 3 files changed, 129 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index c234cc8a9c..23d6725491 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -12,6 +12,8 @@ using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -20,14 +22,16 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Select; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { - public partial class TestSceneBeatmapLeaderboard : OsuTestScene + public partial class TestSceneBeatmapLeaderboard : OsuManualInputManagerTestScene { private readonly FailableLeaderboard leaderboard; @@ -37,6 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect private ScoreManager scoreManager = null!; private RulesetStore rulesetStore = null!; private BeatmapManager beatmapManager = null!; + private PlaySongSelect songSelect = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -45,25 +50,36 @@ namespace osu.Game.Tests.Visual.SongSelect 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.CacheAs(songSelect = new PlaySongSelect()); Dependencies.Cache(Realm); return dependencies; } + [BackgroundDependencyLoader] + private void load() + { + LoadComponent(songSelect); + } + public TestSceneBeatmapLeaderboard() { - AddRange(new Drawable[] + Add(new OsuContextMenuContainer { - dialogOverlay = new DialogOverlay + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Depth = -1 - }, - leaderboard = new FailableLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = BeatmapLeaderboardScope.Global, + dialogOverlay = new DialogOverlay + { + Depth = -1 + }, + leaderboard = new FailableLeaderboard + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + Scope = BeatmapLeaderboardScope.Global, + } } }); } @@ -187,6 +203,39 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); } + [Test] + public void TestUseTheseModsDoesNotCopySystemMods() + { + AddStep(@"set scores", () => leaderboard.SetScores(leaderboard.Scores, new ScoreInfo + { + Position = 999, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + } + })); + AddStep("right click panel", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Right); + }); + AddStep("click use these mods", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("song select received HD", () => songSelect.Mods.Value.Any(m => m is OsuModHidden)); + AddAssert("song select did not receive SV2", () => !songSelect.Mods.Value.Any(m => m is ModScoreV2)); + } + private void showPersonalBestWithNullPosition() { leaderboard.SetScores(leaderboard.Scores, new ScoreInfo diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index b7b0101a7c..8694722acc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -6,16 +6,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Game.Graphics.Cursor; using osu.Game.Overlays; namespace osu.Game.Tests.Visual.SongSelectV2 { - public abstract partial class SongSelectComponentsTestScene : OsuTestScene + public abstract partial class SongSelectComponentsTestScene : OsuManualInputManagerTestScene { [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - protected override Container Content { get; } = new Container + protected override Container Content { get; } = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index a7d0d70c03..26d39c9203 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -7,9 +7,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; @@ -22,6 +24,7 @@ using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -102,6 +105,69 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + [Test] + public void TestUseTheseModsDoesNotCopySystemMods() + { + LeaderboardScoreV2 score = null!; + + AddStep("create content", () => + { + Children = new Drawable[] + { + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = new Vector2(OsuGame.SHEAR, 0) + }, + drawWidthText = new OsuSpriteText(), + }; + + var scoreInfo = new ScoreInfo + { + Position = 999, + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = RNG.Next(1_800_000, 2_000_000), + MaximumStatistics = { { HitResult.Great, 3000 } }, + Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + }, + Date = DateTimeOffset.Now.AddYears(-2), + }; + + fillFlow.Add(score = new LeaderboardScoreV2(scoreInfo) + { + Rank = scoreInfo.Position, + Shear = Vector2.Zero, + }); + + score.Show(); + }); + AddStep("right click panel", () => + { + InputManager.MoveMouseTo(score); + InputManager.Click(MouseButton.Right); + }); + AddStep("click use these mods", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("mods received HD", () => score.SelectedMods.Value.Any(m => m is OsuModHidden)); + AddAssert("mods did not receive SV2", () => !score.SelectedMods.Value.Any(m => m is ModScoreV2)); + } + public override void SetUpSteps() { AddToggleStep("toggle scoring mode", v => config.SetValue(OsuSetting.ScoreDisplayMode, v ? ScoringMode.Classic : ScoringMode.Standardised)); From d9a1dcf9b972af6864b3522e3048a433dbd4ef77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:25:56 +0100 Subject: [PATCH 136/349] Fix "use these mods" option applying to system mods Closes https://github.com/ppy/osu/issues/32229. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 5 ++++- .../Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0db03efb68..ea42c515a6 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -453,7 +453,10 @@ namespace osu.Game.Online.Leaderboards List items = new List(); if (Score.Mods.Length > 0 && songSelect != null) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); + { + // system mods should never be copied across regardless of anything. + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods.Where(m => m.Type != ModType.System).ToArray())); + } if (Score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 978d6eca32..71cc80af49 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -781,7 +781,11 @@ namespace osu.Game.Screens.SelectV2.Leaderboards List items = new List(); if (score.Mods.Length > 0) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); + { + // system mods should never be copied across regardless of anything. + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, + () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray())); + } if (score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); From 097dd701396a476ca5a7a5c03dbbee6b2f623ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:33:33 +0100 Subject: [PATCH 137/349] Add another failing test --- .../DailyChallenge/TestSceneDailyChallenge.cs | 37 +++++++++++++++++++ .../OnlinePlay/TestRoomRequestsHandler.cs | 1 + 2 files changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 0742ed5eb9..c974a852f3 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -6,6 +6,8 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Metadata; @@ -13,9 +15,11 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; +using osuTK.Input; namespace osu.Game.Tests.Visual.DailyChallenge { @@ -57,6 +61,39 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); } + [Test] + public void TestUseTheseModsUnavailableIfNoFreeMods() + { + var room = new Room + { + RoomID = 1234, + Name = "Daily Challenge: June 4, 2024", + Playlist = + [ + new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [] + } + ], + EndDate = DateTimeOffset.Now.AddHours(12), + Category = RoomCategory.DailyChallenge + }; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; + AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + AddUntilStep("wait for pushed", () => screen.IsCurrentScreen()); + AddStep("force transforms to finish", () => FinishTransforms(true)); + AddStep("right click second score", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); + InputManager.Click(MouseButton.Right); + }); + AddAssert("use these mods not present", + () => this.ChildrenOfType().All(m => m.Items.All(item => item.Text.Value != "Use these mods"))); + } + [Test] public void TestNotifications() { diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 0ae3a73e5d..46c1251d42 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -126,6 +126,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay MaxCombo = 100, TotalScore = 200000, User = new APIUser { Username = "worst user" }, + Mods = [new APIMod { Acronym = @"TD" }], Statistics = new Dictionary() }, }, From 0ac3a80406fa295e648e5e83e09d1e8a6c2a7773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:40:11 +0100 Subject: [PATCH 138/349] Fix "use these mods" option showing if it can't do anything Closes https://github.com/ppy/osu/issues/32230. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 10 +++++----- .../SelectV2/Leaderboards/LeaderboardScoreV2.cs | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index ea42c515a6..28b20c0c05 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -452,11 +452,11 @@ namespace osu.Game.Online.Leaderboards { List items = new List(); - if (Score.Mods.Length > 0 && songSelect != null) - { - // system mods should never be copied across regardless of anything. - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods.Where(m => m.Type != ModType.System).ToArray())); - } + // system mods should never be copied across regardless of anything. + var copyableMods = Score.Mods.Where(m => m.Type != ModType.System).ToArray(); + + if (copyableMods.Length > 0 && songSelect != null) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = copyableMods)); if (Score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 71cc80af49..b54f007f38 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -780,12 +780,11 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { List items = new List(); - if (score.Mods.Length > 0) - { - // system mods should never be copied across regardless of anything. - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, - () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray())); - } + // system mods should never be copied across regardless of anything. + var copyableMods = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); + + if (copyableMods.Length > 0) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); if (score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); From 7975c301a846e9c28b02e3ab388912344ff95cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 12:32:58 +0100 Subject: [PATCH 139/349] Try to fix test --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 23d6725491..bfb835cad1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -222,6 +222,7 @@ namespace osu.Game.Tests.Visual.SongSelect CountryCode = CountryCode.ES, } })); + AddUntilStep("wait for scores", () => this.ChildrenOfType().Count(), () => Is.GreaterThan(0)); AddStep("right click panel", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); From f8878463119a4f341346ea856fbee2b29acc1160 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Mar 2025 21:38:04 +0900 Subject: [PATCH 140/349] Reset mods when exiting to lounge To be hoenst, this is all quite abusive but I'm not sure how to do it in any better way. I especially dislike the fact that the screen's bindables are disabled yet the screen itself is allowed to set their value because they're `LeasedBindable`s. But I feel better doing this in a central location like this `ScreenStack` than in the rooms... Otherwise this specific behaviour would have to be replicated in the multiplayer screen when it goes through the same refactoring. --- .../TestSceneOnlinePlaySubScreenStack.cs | 85 +++++++++++++++++++ .../OnlinePlay/OnlinePlaySubScreenStack.cs | 32 +++++-- 2 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs diff --git a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs new file mode 100644 index 0000000000..5eb11b6370 --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Visual.OnlinePlay; + +namespace osu.Game.Tests.OnlinePlay +{ + [HeadlessTest] + public class TestSceneOnlinePlaySubScreenStack : OnlinePlayTestScene + { + private ScreenStack stack = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = stack = new OnlinePlaySubScreenStack + { + RelativeSizeAxes = Axes.Both + }; + }); + + [Test] + public void TestBindablesDisabledWhenRequested() + { + AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False); + + AddStep("push screen that disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(true))); + AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True); + + AddStep("push screen that does not disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(false))); + AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False); + + AddStep("exit one screen", () => stack.Exit()); + AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True); + } + + [Test] + public void TestModsResetWhenExitToLounge() + { + AddStep("push lounge", () => stack.Push(new PlaylistsLoungeSubScreen())); + + AddStep("push screen with mod", () => stack.Push(new ScreenWithMod(new OsuModDoubleTime()))); + AddUntilStep("wait for screen to load", () => ((OsuScreen)stack.CurrentScreen).IsLoaded); + AddAssert("mod set", () => SelectedMods.Value.Count, () => Is.GreaterThan(0)); + + AddStep("exit to lounge", () => stack.Exit()); + AddAssert("mods reset", () => SelectedMods.Value.Count, () => Is.Zero); + } + + private class ScreenWithExternalBindableDisablement : OsuScreen + { + public override bool DisallowExternalBeatmapRulesetChanges { get; } + + public ScreenWithExternalBindableDisablement(bool disableBindables) + { + DisallowExternalBeatmapRulesetChanges = disableBindables; + } + } + + private class ScreenWithMod : OsuScreen + { + private readonly Mod mod; + + public ScreenWithMod(Mod mod) + { + this.mod = mod; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Mods.Value = [mod]; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 6695c97508..c10017f1fe 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.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.Diagnostics; +using System; using osu.Framework.Screens; +using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay { @@ -12,16 +13,31 @@ namespace osu.Game.Screens.OnlinePlay { base.ScreenChanged(prev, next); - // because this is a screen stack within a screen stack, let's manually handle disabled changes to simplify things. - var osuScreen = next as OsuScreen; + if (next is not OsuScreen osuNext) + throw new InvalidOperationException("There must always be an online play subscreen."); - Debug.Assert(osuScreen != null); + // See: OnlinePlayScreen.DisallowExternalBeatmapRulesetChanges. + // + // Bindable leases are held by the OnlinePlayScreen and NOT by the subscreens, + // because PlayerLoader needs to resolve LeasedBindables to function correctly. + // + // An unfortunate consequence of this is we need to manually control bindable + // enablement depending on what effect the subscreens want. + // + // This is a two-part process... - bool disallowChanges = osuScreen.DisallowExternalBeatmapRulesetChanges; + // First, emulate the behaviour of DisallowExternalBeatmapRulesetChanges to disable toolbar buttons. + osuNext.Beatmap.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + osuNext.Ruleset.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + osuNext.Mods.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; - osuScreen.Beatmap.Disabled = disallowChanges; - osuScreen.Ruleset.Disabled = disallowChanges; - osuScreen.Mods.Disabled = disallowChanges; + // Second, when an OsuScreen is exited with DisallowExternalBeatmapRulesetChanges=true, leased bindables + // are normally returned which reverts the mod and ruleset bindables to their original states. + // + // The exact behaiour of the revert is awkward to emulate, but we particularly care about resetting mods + // when returning to the lounge so that they don't stick around if the user then goes to create a new room. + if (next is LoungeSubScreen) + osuNext.Mods.Value = []; } } } From 51f794a56675ee119b57d3b2f43cb012d7f0cf2a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Mar 2025 21:53:49 +0900 Subject: [PATCH 141/349] Block input to screen during gameplay --- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index f762810e9d..df7f86704f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -790,6 +790,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return false; } + // Block all input to this screen during gameplay/etc when the parent screen is no longer current. + // Normally this would be handled by ScreenStack, but we are in a child ScreenStack. + public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && (parentScreen?.IsCurrentScreen() ?? this.IsCurrentScreen()); + + // Block all input to this screen during gameplay/etc when the parent screen is no longer current. + // Normally this would be handled by ScreenStack, but we are in a child ScreenStack. + public override bool PropagateNonPositionalInputSubTree => base.PropagateNonPositionalInputSubTree && (parentScreen?.IsCurrentScreen() ?? this.IsCurrentScreen()); + protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(room.Playlist.FirstOrDefault()) { SelectedItem = { BindTarget = SelectedItem } From 3f461c07348581a313a727e92411769ea8c30c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 14:11:44 +0100 Subject: [PATCH 142/349] Add "discard unsaved changes" operation to beatmap editor Apparently useful in modding workflows when you want to test out a few different variants of a thing. Re-uses `Ctrl-L` binding from stable. Some folks may argue that the dialog makes the hotkey pointless, but I really do want to protect users from accidental data loss, and also if you want to power through it quickly, you can hit the 1 key when the dialog shows, which will bypass the hold-to-activate period (which wasn't intentional, but so many people want a bypass at this point that we're probably keeping that behaviour for power users). --- .../Editor/TestSceneManiaEditorSaving.cs | 4 +-- .../Edit/Setup/ManiaDifficultySection.cs | 2 +- .../Input/Bindings/GlobalActionContainer.cs | 4 +++ osu.Game/Localisation/EditorDialogsStrings.cs | 5 +++ .../GlobalActionKeyBindingStrings.cs | 5 +++ .../Edit/DiscardUnsavedChangesDialog.cs | 33 +++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 29 ++++++++++++++-- ...Dialog.cs => SaveAndReloadEditorDialog.cs} | 4 +-- 8 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs rename osu.Game/Screens/Edit/{ReloadEditorDialog.cs => SaveAndReloadEditorDialog.cs} (86%) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs index d9ba721646..ebaa8bcea2 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { keyCount.Current.Value = 8; }); - AddUntilStep("dialog visible", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog, Is.InstanceOf); + AddUntilStep("dialog visible", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog, Is.InstanceOf); AddStep("refuse", () => InputManager.Key(Key.Number2)); AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5)); @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { keyCount.Current.Value = 8; }); - AddUntilStep("dialog visible", () => Game.ChildrenOfType().Single().CurrentDialog, Is.InstanceOf); + AddUntilStep("dialog visible", () => Game.ChildrenOfType().Single().CurrentDialog, Is.InstanceOf); AddStep("acquiesce", () => InputManager.Key(Key.Number1)); AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8)); } diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index 48e59877df..a5c3c2264c 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup updatingKeyCount = true; - editor.Reload().ContinueWith(t => + editor.SaveAndReload().ContinueWith(t => { if (!t.GetResultSafely()) { diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index e4dc2d503b..6de2dabe2b 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -155,6 +155,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.B }, GlobalAction.EditorRemoveClosestBookmark), new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousBookmark), new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextBookmark), + new KeyBinding(new[] { InputKey.Control, InputKey.L }, GlobalAction.EditorDiscardUnsavedChanges), }; private static IEnumerable editorTestPlayKeyBindings => new[] @@ -502,6 +503,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))] EditorToggleMoveControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges))] + EditorDiscardUnsavedChanges, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/EditorDialogsStrings.cs b/osu.Game/Localisation/EditorDialogsStrings.cs index 94f28c617c..3617dca81f 100644 --- a/osu.Game/Localisation/EditorDialogsStrings.cs +++ b/osu.Game/Localisation/EditorDialogsStrings.cs @@ -54,6 +54,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorReloadDialogHeader => new TranslatableString(getKey(@"editor_reload_dialog_header"), @"The editor must be reloaded to apply this change. The beatmap will be saved."); + /// + /// "Discard all unsaved changes? This cannot be undone." + /// + public static LocalisableString DiscardUnsavedChangesDialogHeader => new TranslatableString(getKey(@"discard_unsaved_changes_dialog_header"), @"Discard all unsaved changes? This cannot be undone."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 5713df57c9..34b9e1fecc 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -459,6 +459,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control"); + /// + /// "Discard unsaved changes" + /// + public static LocalisableString EditorDiscardUnsavedChanges => new TranslatableString(getKey(@"editor_discard_unsaved_changes"), @"Discard unsaved changes"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs b/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs new file mode 100644 index 0000000000..1867b48830 --- /dev/null +++ b/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs @@ -0,0 +1,33 @@ +// 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.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public partial class DiscardUnsavedChangesDialog : PopupDialog + { + public DiscardUnsavedChangesDialog(Action exit) + { + HeaderText = EditorDialogsStrings.DiscardUnsavedChangesDialogHeader; + + Icon = FontAwesome.Solid.Trash; + + Buttons = new PopupDialogButton[] + { + new PopupDialogDangerousButton + { + Text = EditorDialogsStrings.ForgetAllChanges, + Action = exit + }, + new PopupDialogCancelButton + { + Text = EditorDialogsStrings.ContinueEditing, + }, + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 219e14861f..bf254093b3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -164,6 +164,7 @@ namespace osu.Game.Screens.Edit private bool switchingDifficulty; private string lastSavedHash; + private EditorMenuItem discardChangesMenuItem; private ScreenContainer screenContainer; @@ -391,6 +392,10 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, + discardChangesMenuItem = new EditorMenuItem("Discard unsaved changes", MenuItemType.Destructive, DiscardUnsavedChanges) + { + Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) + }, new OsuMenuItemSpacer(), cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) }, copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) }, @@ -607,6 +612,8 @@ namespace osu.Game.Screens.Edit { base.Update(); clock.ProcessFrame(); + + discardChangesMenuItem.Action.Disabled = !HasUnsavedChanges; } public bool OnPressed(KeyBindingPressEvent e) @@ -821,6 +828,10 @@ namespace osu.Game.Screens.Edit case GlobalAction.EditorTestGameplay: bottomBar.TestGameplayButton.TriggerClick(); return true; + + case GlobalAction.EditorDiscardUnsavedChanges: + DiscardUnsavedChanges(); + return true; } return false; @@ -1008,6 +1019,20 @@ namespace osu.Game.Screens.Edit protected void Redo() => changeHandler?.RestoreState(1); + protected void DiscardUnsavedChanges() + { + if (!HasUnsavedChanges) + return; + + // we're not doing this via `changeHandler` because `changeHandler` has limited number of undo actions + // and therefore there's no guarantee that it even *has* the beatmap's last saved state in its history still. + dialogOverlay.Push(new DiscardUnsavedChangesDialog(() => + { + updateLastSavedHash(); // without this a second dialog will show (the standard "save unsaved changes" one that shows on exit). + SwitchToDifficulty(editorBeatmap.BeatmapInfo); + })); + } + protected void SetPreviewPointToCurrentTime() { editorBeatmap.PreviewTime.Value = (int)clock.CurrentTime; @@ -1510,11 +1535,11 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } - public Task Reload() + public Task SaveAndReload() { var tcs = new TaskCompletionSource(); - dialogOverlay.Push(new ReloadEditorDialog( + dialogOverlay.Push(new SaveAndReloadEditorDialog( reload: () => { bool reloadedSuccessfully = attemptMutationOperation(() => diff --git a/osu.Game/Screens/Edit/ReloadEditorDialog.cs b/osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs similarity index 86% rename from osu.Game/Screens/Edit/ReloadEditorDialog.cs rename to osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs index 72a9f81347..b73c7cfff8 100644 --- a/osu.Game/Screens/Edit/ReloadEditorDialog.cs +++ b/osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs @@ -8,9 +8,9 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Edit { - public partial class ReloadEditorDialog : PopupDialog + public partial class SaveAndReloadEditorDialog : PopupDialog { - public ReloadEditorDialog(Action reload, Action cancel) + public SaveAndReloadEditorDialog(Action reload, Action cancel) { HeaderText = EditorDialogsStrings.EditorReloadDialogHeader; From 26daf085d74f5e5be049c51451df72fff6920925 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Mar 2025 23:25:22 +0900 Subject: [PATCH 143/349] Add forgotten partials --- .../OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs index 5eb11b6370..67b0d236ed 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs @@ -15,7 +15,7 @@ using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.OnlinePlay { [HeadlessTest] - public class TestSceneOnlinePlaySubScreenStack : OnlinePlayTestScene + public partial class TestSceneOnlinePlaySubScreenStack : OnlinePlayTestScene { private ScreenStack stack = null!; @@ -56,7 +56,7 @@ namespace osu.Game.Tests.OnlinePlay AddAssert("mods reset", () => SelectedMods.Value.Count, () => Is.Zero); } - private class ScreenWithExternalBindableDisablement : OsuScreen + private partial class ScreenWithExternalBindableDisablement : OsuScreen { public override bool DisallowExternalBeatmapRulesetChanges { get; } @@ -66,7 +66,7 @@ namespace osu.Game.Tests.OnlinePlay } } - private class ScreenWithMod : OsuScreen + private partial class ScreenWithMod : OsuScreen { private readonly Mod mod; From 5feddae6c75f8f8020196362ea40646c7a08460e Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 21:35:24 +0600 Subject: [PATCH 144/349] Revert "Update window title in input thread" This reverts commit 14b5c0bf10389b924fa8ca515c2e27457fdcc119. This is not necessary as the title update is already scheduled on the correct thread by the framework. --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2b9e2cb9cd..e070e89c19 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -837,7 +837,7 @@ namespace osu.Game if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; - Host.InputThread.Scheduler.AddOnce(s => Host.Window.Title = s, newTitle); + Host.Window.Title = newTitle; } private void modsChanged(ValueChangedEvent> mods) From 4ae5f239cb3c342812bef639f43971ccca7d3a71 Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 21:41:11 +0600 Subject: [PATCH 145/349] Remove unnecessary comment --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e070e89c19..abe5ce21c6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -832,7 +832,6 @@ namespace osu.Game if (Host.Window == null) return; - // prevent weird window title saying please load a beatmap string newTitle = Name; if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; From d33a8dfc3b57d3888c09a232e6d8fa3fb70c6dca Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 22:47:39 +0600 Subject: [PATCH 146/349] Skip updating window title for protected mapsets --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index abe5ce21c6..37ff70ccb7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -833,7 +833,7 @@ namespace osu.Game return; string newTitle = Name; - if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) + if (beatmap.NewValue?.BeatmapSetInfo?.Protected == false && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; Host.Window.Title = newTitle; From 02d19eaa55c05fe9149cf7771ca40342bc689bbd Mon Sep 17 00:00:00 2001 From: Zihad Date: Thu, 6 Mar 2025 01:36:59 +0600 Subject: [PATCH 147/349] Update window title changes to match osu! stable It shows beatmap metadata during gameplay, spectating, and watching replays but shows beatmap filename during editng --- osu.Game/OsuGame.cs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 37ff70ccb7..ed71f357a5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -421,6 +421,7 @@ namespace osu.Game SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); + configUserActivity.BindValueChanged(userActivityChanged); applySafeAreaConsiderations = LocalConfig.GetBindable(OsuSetting.SafeAreaConsiderations); applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true); @@ -828,13 +829,41 @@ namespace osu.Game { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + updateWindowTitle(); + } + private void userActivityChanged(ValueChangedEvent userActivity) + { + updateWindowTitle(); + } + + private void updateWindowTitle() + { if (Host.Window == null) return; + if (Beatmap.Value?.BeatmapSetInfo?.Protected != false || Beatmap.Value is DummyWorkingBeatmap) + { + Host.Window.Title = Name; + return; + } + string newTitle = Name; - if (beatmap.NewValue?.BeatmapSetInfo?.Protected == false && beatmap.NewValue is not DummyWorkingBeatmap) - newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + + switch (configUserActivity.Value) + { + case UserActivity.InGame: + case UserActivity.TestingBeatmap: + case UserActivity.WatchingReplay: + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + break; + + case UserActivity.EditingBeatmap: + if (Beatmap.Value.BeatmapInfo.Path != null) + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path}"; + + break; + } Host.Window.Title = newTitle; } From 574f2363fff982d21d7ab42eaf130cc89000f5cb Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Wed, 5 Mar 2025 23:31:35 +0100 Subject: [PATCH 148/349] Add localisation for skin management buttons in settings --- osu.Game/Localisation/CommonStrings.cs | 10 ++++++++++ osu.Game/Overlays/Settings/Sections/SkinSection.cs | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 243a100029..26e344ec71 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -39,11 +39,21 @@ namespace osu.Game.Localisation /// public static LocalisableString Default => new TranslatableString(getKey(@"default"), @"Default"); + /// + /// "Rename" + /// + public static LocalisableString Rename => new TranslatableString(getKey(@"rename"), @"Rename"); + /// /// "Export" /// public static LocalisableString Export => new TranslatableString(getKey(@"export"), @"Export"); + /// + /// "Delete" + /// + public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete"); + /// /// "Width" /// diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index a89d5e2f4a..1f220138de 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Rename"; + Text = CommonStrings.Rename; Action = this.ShowPopover; } @@ -193,7 +193,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Export"; + Text = CommonStrings.Export; Action = export; } @@ -231,7 +231,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Delete"; + Text = CommonStrings.Delete; Action = delete; } From ee2615da53da7f537e5c920869464ce2bd13ffab Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Wed, 5 Mar 2025 23:51:29 +0100 Subject: [PATCH 149/349] Use osu-web delete localisation --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 1f220138de..84767c8619 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -27,6 +27,7 @@ using osu.Game.Screens.Select; using osu.Game.Skinning; using osuTK; using Realms; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Overlays.Settings.Sections { @@ -231,7 +232,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = CommonStrings.Delete; + Text = WebCommonStrings.ButtonsDelete; Action = delete; } From 5c3695673b49fee5570f1fd0bcc0588cc2654d37 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Thu, 6 Mar 2025 00:22:47 +0100 Subject: [PATCH 150/349] Remove delete string from CommonStrings --- osu.Game/Localisation/CommonStrings.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 26e344ec71..f9d0feb5e2 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -49,11 +49,6 @@ namespace osu.Game.Localisation /// public static LocalisableString Export => new TranslatableString(getKey(@"export"), @"Export"); - /// - /// "Delete" - /// - public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete"); - /// /// "Width" /// From 50c4f9098320a6e7b46e5bcea425c96b03f1f07d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 12:55:59 +0900 Subject: [PATCH 151/349] Fix intermittent playlists results screen tests --- .../TestScenePlaylistsResultsScreen.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 469f7c8b74..6b73f1a5f4 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -156,13 +156,13 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); } } @@ -180,26 +180,26 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert("count not increased", () => this.ChildrenOfType().Count() == beforePanelCount); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); AddAssert("no placeholders shown", () => this.ChildrenOfType().Count(), () => Is.Zero); @@ -222,13 +222,13 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); - AddAssert("left loading spinner shown", () => + AddUntilStep("left loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("left loading spinner hidden", () => + AddUntilStep("left loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); } } @@ -242,7 +242,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => bindHandler(noScores: true)); createUserBestResults(); AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType().Single().GetScorePanels().Any()); - AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddUntilStep("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } [Test] @@ -261,12 +261,12 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("simulate user falling down ranking", () => userScore.Position += 2); AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); - AddAssert("left loading spinner shown", () => + AddUntilStep("left loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); waitForDisplay(); - AddAssert("left loading spinner hidden", () => + AddUntilStep("left loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); } From 0bdbaa872ceb6c8d18c68aaf175dcc95548e3a87 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 14:11:41 +0900 Subject: [PATCH 152/349] Simplify user style validation This commit takes half a step back. Rather than trying to preserve the beatmap style across selections, it probably makes more sense to always reset it. It is more likely for playlists to be dominated by unique beatmap sets anyway. Doing this already heavily simplifies the process. Given the above, we can rely on the fact that the user beatmap and ruleset are selected together, and are thus implicitly validated as of exiting the style selection screen. It follows that the only extra validation we need to do is to make sure that the user's ruleset is valid for the playlist item. This even makes the mod structure, which is a bit of an unfortunate scenario, somewhat tenable to look at. The user mods only need to be validated when either the playlist item or the user ruleset changes. If we ever move the ruleset selection into the style selection screen, then we can also remove the latter of the validations with similar reason as the ruleset. --- .../Playlists/PlaylistsRoomSubScreen.cs | 98 ++++++++++++------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index df7f86704f..3b180efbce 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -452,16 +452,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists isIdle.BindValueChanged(_ => updatePollingRate(), true); - beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSelectionState()); + SelectedItem.BindValueChanged(onSelectedItemChanged); - SelectedItem.BindValueChanged(_ => updateSelectionState()); - userBeatmap.BindValueChanged(_ => updateSelectionState()); - userRuleset.BindValueChanged(_ => updateSelectionState()); - userMods.BindValueChanged(_ => updateSelectionState()); + beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); + + userBeatmap.BindValueChanged(_ => updateGameplayState()); + userRuleset.BindValueChanged(_ => + { + validateUserMods(); + updateGameplayState(); + }); + userMods.BindValueChanged(_ => updateGameplayState()); updateSetupState(); - updateSelectionState(); + updateGameplayState(); } /// @@ -519,44 +524,67 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Responds to changes in the selected playlist item or user style (beatmap/ruleset/mods) to validate and update global states in preparation for a gameplay session. + /// Responds to changes in to validate the user style and update the global gameplay state. /// - private void updateSelectionState() + private void onSelectedItemChanged(ValueChangedEvent e) { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (e.NewValue is not PlaylistItem item) return; - // Reset entire user style when disabled. - if (!item.Freestyle) + // Always resetting the user beatmap style when a new item is selected is most intuitive. + userBeatmap.Value = null; + + if (item.Freestyle) { - userBeatmap.Value = null; - userRuleset.Value = null; - } - - // Reset beatmap style when no longer from the same beatmap set. - if (userBeatmap.Value != null && userBeatmap.Value.BeatmapSet!.OnlineID != item.Beatmap.BeatmapSet!.OnlineID) - userBeatmap.Value = null; - - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; - - // Reset ruleset style when no longer valid for the beatmap. - if (userRuleset.Value != null) - { - int beatmapRuleset = gameplayBeatmap.Ruleset.OnlineID; - if (beatmapRuleset > 0 && userRuleset.Value.OnlineID != beatmapRuleset) + // If freestyle is active, attempt to preserve the user ruleset style but only if the online item is from the osu! ruleset + // (i.e. the beatmap is generally always convertible to the current ruleset, excluding custom rulesets). + if (item.RulesetID > 0) userRuleset.Value = null; } + validateUserMods(); + + updateGameplayState(); + } + + /// + /// Lists the s that are valid to be selected for the user mod style. + /// + private Mod[] listAllowedMods() + { + if (SelectedItem.Value is not PlaylistItem item) + return []; + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); - // Remove any user mods that are no longer allowed. - Mod[] allowedMods = item.Freestyle - ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray() - : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - Mod[] newUserMods = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); - if (!newUserMods.SequenceEqual(userMods.Value)) - userMods.Value = newUserMods; + if (item.Freestyle) + return rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray(); + + return item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + } + + /// + /// Validates the user mod style against the selected item and ruleset style. + /// + private void validateUserMods() + { + Mod[] allowedMods = listAllowedMods(); + userMods.Value = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + } + + /// + /// Updates the global states in preparation for a new gameplay session. + /// + private void updateGameplayState() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); + Mod[] allowedMods = listAllowedMods(); // Update global gameplay state to correspond to the new selection. // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info @@ -683,7 +711,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beginHandlingTrack(); // Required to update beatmap/ruleset when resuming from style selection. - updateSelectionState(); + updateGameplayState(); } public override bool OnExiting(ScreenExitEvent e) From 975f4e4c7df982ff2762b0223b2ef51c19d2070e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 15:45:11 +0900 Subject: [PATCH 153/349] Simplify code and don't set title if already correct --- osu.Game/OsuGame.cs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ed71f357a5..4a9154f14b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -421,7 +421,7 @@ namespace osu.Game SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); - configUserActivity.BindValueChanged(userActivityChanged); + configUserActivity.BindValueChanged(_ => updateWindowTitle()); applySafeAreaConsiderations = LocalConfig.GetBindable(OsuSetting.SafeAreaConsiderations); applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true); @@ -832,26 +832,19 @@ namespace osu.Game updateWindowTitle(); } - private void userActivityChanged(ValueChangedEvent userActivity) - { - updateWindowTitle(); - } - private void updateWindowTitle() { if (Host.Window == null) return; - if (Beatmap.Value?.BeatmapSetInfo?.Protected != false || Beatmap.Value is DummyWorkingBeatmap) - { - Host.Window.Title = Name; - return; - } - - string newTitle = Name; + string newTitle; switch (configUserActivity.Value) { + default: + newTitle = Name; + break; + case UserActivity.InGame: case UserActivity.TestingBeatmap: case UserActivity.WatchingReplay: @@ -859,13 +852,12 @@ namespace osu.Game break; case UserActivity.EditingBeatmap: - if (Beatmap.Value.BeatmapInfo.Path != null) - newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path}"; - + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path ?? "new beatmap"}"; break; } - Host.Window.Title = newTitle; + if (newTitle != Host.Window.Title) + Host.Window.Title = newTitle; } private void modsChanged(ValueChangedEvent> mods) From bdd2808fb598360fd710520f92eb5e107ff97cea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 16:05:51 +0900 Subject: [PATCH 154/349] Bump difficulty calculator versions in preparation for release --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 6434adb63c..14a8ff31c5 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty private float halfCatcherWidth; - public override int Version => 20220701; + public override int Version => 20250306; public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 30339fbaa7..eb2cb95972 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { private const double difficulty_multiplier = 0.0675; - public override int Version => 20241007; + public override int Version => 20250306; public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7bc050d2df..e0bc0e177c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private bool isConvert; - public override int Version => 20241007; + public override int Version => 20250306; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) From ba3d6ddc43e0a0dead6b79b83f19585e08865759 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 16:09:05 +0900 Subject: [PATCH 155/349] Add a slew of tests --- .../TestScenePlaylistsRoomSubScreen.cs | 558 ++++++++++++++++++ .../Playlists/PlaylistsRoomSubScreen.cs | 38 +- 2 files changed, 577 insertions(+), 19 deletions(-) create mode 100644 osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs new file mode 100644 index 0000000000..f797f6f2ac --- /dev/null +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -0,0 +1,558 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +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.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.OnlinePlay; + +namespace osu.Game.Tests.Visual.Playlists +{ + public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene + { + private BeatmapManager beatmaps = null!; + private BeatmapSetInfo importedSet = null!; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + BeatmapStore beatmapStore; + + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); + Dependencies.Cache(Realm); + + Add(beatmapStore); + + importedSet = beatmaps.Import(new BeatmapSetInfo + { + OnlineID = TestResources.GetNextTestID(), + Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), + DateAdded = DateTimeOffset.UtcNow, + Beatmaps = + { + new BeatmapInfo + { + OnlineID = 1, + DifficultyName = "Osu 1", + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Ruleset = new OsuRuleset().RulesetInfo, + Metadata = + { + Artist = "Some Artist", + Title = "Some Song", + Author = { Username = "Some Guy" }, + }, + }, + new BeatmapInfo + { + OnlineID = 2, + DifficultyName = "Osu 2", + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Ruleset = new OsuRuleset().RulesetInfo, + Metadata = + { + Artist = "Some Artist", + Title = "Some Song", + Author = { Username = "Some Guy" }, + }, + }, + new BeatmapInfo + { + OnlineID = 3, + DifficultyName = "Taiko 1", + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Ruleset = new TaikoRuleset().RulesetInfo, + Metadata = + { + Artist = "Some Artist", + Title = "Some Song", + Author = { Username = "Some Guy" }, + }, + }, + new BeatmapInfo + { + OnlineID = 4, + DifficultyName = "Taiko 2", + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Ruleset = new TaikoRuleset().RulesetInfo, + Metadata = + { + Artist = "Some Artist", + Title = "Some Song", + Author = { Username = "Some Guy" }, + }, + } + } + })!.PerformRead(s => s.Detach()); + } + + /// + /// Tests that the beatmap and ruleset are adjusted to follow the selected item. + /// + [Test] + public void TestBeatmapAndRuleset_FollowSelection() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + // osu! beatmap + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + // osu! beatmap converted played in taiko + new PlaylistItem(importedSet.Beatmaps[1]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + } + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("select first item", () => screen.SelectedItem.Value = room.Playlist[0]); + AddUntilStep("first beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0])); + AddUntilStep("osu ruleset selected", () => Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("second beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[1])); + AddUntilStep("taiko ruleset selected", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + } + + /// + /// Tests that the beatmap style is reset when the selected item is changed. + /// + [Test] + public void TestBeatmapStyle_Reset_OnSelection() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user beatmap style", () => screen.UserBeatmap.Value = importedSet.Beatmaps[1]); + AddUntilStep("user beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[1])); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user beatmap style reset", () => screen.UserBeatmap.Value == null); + AddUntilStep("second beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0])); + } + + /// + /// Tests that the ruleset style is reset when the selected item is changed and it's no longer valid. + /// + [Test] + public void TestRulesetStyle_Reset_OnSelection_IfNotValid() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user ruleset style", () => screen.UserRuleset.Value = new ManiaRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo)); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user ruleset style reset", () => screen.UserRuleset.Value == null); + AddUntilStep("second ruleset selected", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + } + + /// + /// Tests that the ruleset style is preserved when the selected item is changed and the ruleset is still valid. + /// + [Test] + public void TestRulesetStyle_Preserved_OnSelection_IfStillValid() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user ruleset style", () => screen.UserRuleset.Value = new ManiaRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo)); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user ruleset style preserved", () => screen.UserRuleset.Value!.Equals(new ManiaRuleset().RulesetInfo)); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo)); + } + + /// + /// Tests that mod style is reset when the selected item is changed to another with an inconvertible ruleset. + /// No user style is assumed. + /// + [Test] + public void TestModsReset_OnSelection_DifferentRuleset_NoUserStyle() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user mods", () => screen.UserMods.Value = [new OsuModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mod style reset", () => !screen.UserMods.Value.Any()); + AddUntilStep("mods reset", () => !SelectedMods.Value.Any()); + } + + /// + /// Tests that mod style is preserved when the selected item is changed to another with the same ruleset. + /// No user style is assumed. + /// + [Test] + public void TestModsPreserved_OnSelection_SameRuleset_NoUserStyle() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user mods", () => screen.UserMods.Value = [new OsuModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mod style preserved", () => screen.UserMods.Value.OfType().Any()); + AddUntilStep("mods preserved", () => SelectedMods.Value.OfType().Any()); + } + + /// + /// Tests that mod style is reset when the selected item is changed to another with an inconvertible ruleset. + /// A user beatmap/ruleset style is assumed. + /// + [Test] + public void TestModsReset_OnSelection_DifferentRuleset_WithUserStyle() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user ruleset", () => screen.UserRuleset.Value = new CatchRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new CatchRuleset().RulesetInfo)); + AddStep("set user mods", () => screen.UserMods.Value = [new CatchModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mod style reset", () => !screen.UserMods.Value.Any()); + AddUntilStep("mods reset", () => !SelectedMods.Value.Any()); + } + + /// + /// Tests that mod style is preserved when the selected item is changed to another with the same ruleset. + /// A user beatmap/ruleset style is assumed. + /// + [Test] + public void TestModsPreserved_OnSelection_SameRuleset_WithStyle() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user ruleset", () => screen.UserRuleset.Value = new TaikoRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + AddStep("set user mods", () => screen.UserMods.Value = [new TaikoModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("select second item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mod style preserved", () => screen.UserMods.Value.OfType().Any()); + AddUntilStep("mods preserved", () => SelectedMods.Value.OfType().Any()); + } + + /// + /// Tests that the mod style is revalidated when the ruleset style is changed. + /// + [Test] + public void TestModsValidated_OnRulesetStyleChanged() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + AddStep("set user mods", () => screen.UserMods.Value = [new OsuModDoubleTime()]); + AddUntilStep("user mods selected", () => SelectedMods.Value.OfType().Any()); + + AddStep("set user ruleset", () => screen.UserRuleset.Value = new TaikoRuleset().RulesetInfo); + AddUntilStep("user ruleset selected", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + AddUntilStep("user mods reset", () => !screen.UserMods.Value.Any()); + AddUntilStep("mods reset", () => !SelectedMods.Value.Any()); + } + + private partial class TestPlaylistsScreen : OsuScreen + { + public TestPlaylistsScreen(PlaylistsRoomSubScreen screen) + { + OnlinePlaySubScreenStack stack; + + InternalChildren = new Drawable[] + { + stack = new OnlinePlaySubScreenStack + { + RelativeSizeAxes = Axes.Both + }, + new BackButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + State = { Value = Visibility.Visible }, + Action = () => + { + if (stack.CurrentScreen is not PlaylistsRoomSubScreen) + stack.Exit(); + } + } + }; + + stack.Push(screen); + } + } + + private partial class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen + { + public new Bindable SelectedItem => base.SelectedItem; + public new Bindable UserBeatmap => base.UserBeatmap; + public new Bindable UserRuleset => base.UserRuleset; + public new Bindable> UserMods => base.UserMods; + + public TestPlaylistsRoomSubScreen(Room room) + : base(room) + { + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 3b180efbce..6cb7962c82 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -114,9 +114,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); protected readonly Bindable SelectedItem = new Bindable(); - private readonly Bindable userBeatmap = new Bindable(); - private readonly Bindable userRuleset = new Bindable(); - private readonly Bindable> userMods = new Bindable>(Array.Empty()); + protected readonly Bindable UserBeatmap = new Bindable(); + protected readonly Bindable UserRuleset = new Bindable(); + protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); private readonly IBindable isIdle = new BindableBool(); private readonly Room room; @@ -309,7 +309,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Current = userMods, + Current = UserMods, Scale = new Vector2(0.8f), } } @@ -437,7 +437,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay { SelectedItem = { BindTarget = SelectedItem }, - SelectedMods = { BindTarget = userMods }, + SelectedMods = { BindTarget = UserMods }, IsValidMod = _ => false }); } @@ -457,13 +457,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); - userBeatmap.BindValueChanged(_ => updateGameplayState()); - userRuleset.BindValueChanged(_ => + UserBeatmap.BindValueChanged(_ => updateGameplayState()); + UserRuleset.BindValueChanged(_ => { validateUserMods(); updateGameplayState(); }); - userMods.BindValueChanged(_ => updateGameplayState()); + UserMods.BindValueChanged(_ => updateGameplayState()); updateSetupState(); updateGameplayState(); @@ -532,14 +532,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; // Always resetting the user beatmap style when a new item is selected is most intuitive. - userBeatmap.Value = null; + UserBeatmap.Value = null; if (item.Freestyle) { // If freestyle is active, attempt to preserve the user ruleset style but only if the online item is from the osu! ruleset // (i.e. the beatmap is generally always convertible to the current ruleset, excluding custom rulesets). if (item.RulesetID > 0) - userRuleset.Value = null; + UserRuleset.Value = null; } validateUserMods(); @@ -555,7 +555,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (SelectedItem.Value is not PlaylistItem item) return []; - RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); if (item.Freestyle) @@ -570,7 +570,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void validateUserMods() { Mod[] allowedMods = listAllowedMods(); - userMods.Value = userMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); } /// @@ -581,8 +581,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; - RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap; + RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); Mod[] allowedMods = listAllowedMods(); @@ -592,7 +592,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = gameplayRuleset; - Mods.Value = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); + Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); // Update UI elements to reflect the new selection. bool freemods = allowedMods.Length > 0; @@ -640,8 +640,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; // Required for validation inside the player. - RulesetInfo gameplayRuleset = userRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; - IBeatmapInfo gameplayBeatmap = userBeatmap.Value ?? item.Beatmap; + RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; + IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap; PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional(gameplayBeatmap)); sampleStart?.Play(); @@ -675,8 +675,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists this.Push(new PlaylistsRoomFreestyleSelect(room, item) { - Beatmap = { BindTarget = userBeatmap }, - Ruleset = { BindTarget = userRuleset } + Beatmap = { BindTarget = UserBeatmap }, + Ruleset = { BindTarget = UserRuleset } }); } From 0f0dd58b698df3f30baca8988fac285c5c87401a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Mar 2025 09:45:44 +0100 Subject: [PATCH 156/349] Fix differential submission process crashing when no files have changed Closes https://github.com/ppy/osu/issues/32247. --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 13981bcb69..2ea710d3ab 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -304,7 +304,7 @@ namespace osu.Game.Screens.Edit.Submission Logger.Log($"Beatmap submission failed on upload: {ex}"); allowExit(); }; - patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); + patchRequest.Progressed += (current, total) => uploadStep.SetInProgress(total > 0 ? (float)current / total : null); api.Queue(patchRequest); uploadStep.SetInProgress(); From 05c7b903c360cefda1923bebae729039ad55d03c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 18:35:38 +0900 Subject: [PATCH 157/349] Hide mod select overlay on exit Only discovered this when running multiplayer tests on the upcoming rewrite for that screen. --- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 6cb7962c82..a4e0370ed2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -719,6 +719,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!ensureExitConfirmed()) return true; + // Must hide this overlay because it is added to a global container. + userModsSelectOverlay.Hide(); + if (room.RoomID != null) api.Queue(new PartRoomRequest(room)); From b0fad7e83db35e9f016c11e59cb0e8588e52d440 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 18:37:56 +0900 Subject: [PATCH 158/349] Also hide when suspending --- .../Playlists/PlaylistsRoomSubScreen.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index a4e0370ed2..fd1f35a3fc 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -701,7 +701,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override void OnSuspending(ScreenTransitionEvent e) { - endHandlingTrack(); + onLeaving(); base.OnSuspending(e); } @@ -719,13 +719,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!ensureExitConfirmed()) return true; - // Must hide this overlay because it is added to a global container. - userModsSelectOverlay.Hide(); - if (room.RoomID != null) api.Queue(new PartRoomRequest(room)); - endHandlingTrack(); + onLeaving(); return base.OnExiting(e); } @@ -755,6 +752,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return base.OnBackButton(); } + private void onLeaving() + { + // Must hide this overlay because it is added to a global container. + userModsSelectOverlay.Hide(); + + endHandlingTrack(); + } + /// /// Handles changes in the track to keep it looping while active. /// From 64830e2c31dba046b8753b13aed37fa9596ef413 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 18:51:32 +0900 Subject: [PATCH 159/349] Use localisation --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bf254093b3..80e1a656de 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, - discardChangesMenuItem = new EditorMenuItem("Discard unsaved changes", MenuItemType.Destructive, DiscardUnsavedChanges) + discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) { Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) }, From 6e387761307fe5c03b83c5551f8286a974b4fae4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 18:55:40 +0900 Subject: [PATCH 160/349] Fix initial multiplayer room items not having freestyle --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4c4a3d97f2..3234e28166 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -80,6 +80,7 @@ namespace osu.Game.Online.Rooms PlaylistOrder = item.PlaylistOrder ?? 0; PlayedAt = item.PlayedAt; StarRating = item.Beatmap.StarRating; + Freestyle = item.Freestyle; } } } From d9b7d034ba34e587189adfb5a9c8930b5e1ef8ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 19:34:20 +0900 Subject: [PATCH 161/349] Move to file menu --- osu.Game/Screens/Edit/Editor.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 80e1a656de..f56380a34d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -392,10 +392,6 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, - discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) - { - Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) - }, new OsuMenuItemSpacer(), cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) }, copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) }, @@ -1273,6 +1269,11 @@ namespace osu.Game.Screens.Edit saveRelatedMenuItems.Add(save); yield return save; + yield return discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) + { + Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) + }; + if (RuntimeInfo.OS != RuntimeInfo.Platform.Android) { var export = createExportMenu(); From e39b551b484ea6689ebf581e58a9324361bb894f Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Thu, 6 Mar 2025 22:48:08 +0100 Subject: [PATCH 162/349] Use localisation from osu web for the report button --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 67191f6836..9cdad507a6 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -26,6 +26,7 @@ using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; +using WebUsersStrings = osu.Game.Resources.Localisation.Web.UsersStrings; namespace osu.Game.Overlays.Chat { @@ -178,7 +179,7 @@ namespace osu.Game.Overlays.Chat } if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, ReportRequested)); + items.Add(new OsuMenuItem(WebUsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } From 81d35a7ebfc7a7b144112e62cf4d899522c6a9e5 Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Thu, 6 Mar 2025 23:02:22 +0100 Subject: [PATCH 163/349] Use UsersStrings instead --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 9cdad507a6..83f67d1a8a 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -26,7 +26,6 @@ using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; -using WebUsersStrings = osu.Game.Resources.Localisation.Web.UsersStrings; namespace osu.Game.Overlays.Chat { @@ -179,7 +178,7 @@ namespace osu.Game.Overlays.Chat } if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem(WebUsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); + items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } From 18aa168a00f1bdfb019844e5e024a3b0d606dac3 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Mar 2025 15:45:27 +0900 Subject: [PATCH 164/349] Allow kiai/star-fountain SFX to be skinnable --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 12 ++++++++---- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index dbbff4a9f5..b103d9e573 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,11 +3,12 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Graphics.Containers; +using osu.Game.Skinning; namespace osu.Game.Screens.Menu { @@ -16,11 +17,14 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; - private Sample? sample; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + private ISample? sample; private SampleChannel? sampleChannel; [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { RelativeSizeAxes = Axes.Both; @@ -40,7 +44,7 @@ namespace osu.Game.Screens.Menu }, }; - sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); + sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 7e09f50133..c8dcee2580 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,14 +3,15 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; +using osu.Game.Skinning; namespace osu.Game.Screens.Play { @@ -21,11 +22,14 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; - private Sample? sample; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + private ISample? sample; private SampleChannel? sampleChannel; [BackgroundDependencyLoader] - private void load(OsuConfigManager config, AudioManager audio) + private void load(OsuConfigManager config) { kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains); @@ -47,7 +51,7 @@ namespace osu.Game.Screens.Play }, }; - sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); + sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; From efe1089003c8f1c35c33b9364403b90c13413b76 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Mar 2025 15:46:10 +0900 Subject: [PATCH 165/349] Don't play kiai sfx when game is in background --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index b103d9e573..e62ef31278 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Platform; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Graphics.Containers; @@ -17,6 +18,9 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; + [Resolved] + private GameHost host { get; set; } = null!; + [Resolved] private ISkinSource skin { get; set; } = null!; @@ -85,6 +89,9 @@ namespace osu.Game.Screens.Menu break; } + // Don't play SFX when game is in background + if (!host.IsActive.Value) return; + // Track sample channel to avoid overlapping playback sampleChannel?.Stop(); sampleChannel = sample?.GetChannel(); From 33dccfcec8f9db04b6d098a64ca28048bce2cf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 08:51:55 +0100 Subject: [PATCH 166/349] Add visual test coverage --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index bfb835cad1..62ca8bf831 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -181,6 +181,11 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()).Select(s => + { + s.User.Team = new APITeam(); + return s; + }))); } [Test] @@ -473,7 +478,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.5140, MaxCombo = 244, TotalScore = 1707827, - Date = DateTime.Now.AddMonths(-3), + Date = DateTime.Now.AddMonths(-10), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, BeatmapHash = beatmapInfo.Hash, From 4acdd3365aeac6570b1a420aed34877a028a6ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 08:55:40 +0100 Subject: [PATCH 167/349] Fix leaderboard date text being cut off sometimes Closes https://github.com/ppy/osu/issues/32256. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 28b20c0c05..fb5bb225c0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -190,7 +190,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Width = 114f, + Width = 130f, Masking = true, Children = new Drawable[] { From 6d22502739bf5b69a26692da0a996eac90032a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 09:20:50 +0100 Subject: [PATCH 168/349] Fix precise movement popover crashing if selection bounding box exceeds playfield size Closes https://github.com/ppy/osu/issues/32252. --- .../Edit/PreciseMovementPopover.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index f2cb8794b5..04d6afc925 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -127,8 +128,11 @@ namespace osu.Game.Rulesets.Osu.Edit if (relativeCheckbox.Current.Value) { - (xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X); - (yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y); + xBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.X, 0); + xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - Math.Min(initialSurroundingQuad.BottomRight.X, OsuPlayfield.BASE_SIZE.X); + + yBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.Y, 0); + yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - Math.Min(initialSurroundingQuad.BottomRight.Y, OsuPlayfield.BASE_SIZE.Y); xBindable.Default = yBindable.Default = 0; @@ -146,8 +150,21 @@ namespace osu.Game.Rulesets.Osu.Edit var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size); - (xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X); - (yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y); + if (initialSurroundingQuad.Width < OsuPlayfield.BASE_SIZE.X) + { + xBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.X; + xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X; + } + else + xBindable.MinValue = xBindable.MaxValue = initialPosition.X; + + if (initialSurroundingQuad.Height < OsuPlayfield.BASE_SIZE.Y) + { + yBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.Y; + yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y; + } + else + yBindable.MinValue = yBindable.MaxValue = initialPosition.Y; xBindable.Default = initialPosition.X; yBindable.Default = initialPosition.Y; From 12fa96de252f5d1b186fd7666570dd6c5f21b901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 10:43:22 +0100 Subject: [PATCH 169/349] Ensure that star rating reprocessing does not incur online lookup requests Yesterday after the lazer release there was a bit of a spike in the number of osu-web requests pointed at `/api/v2/beatmaps/lookup` specifically. The most likely reason for this is that prior to this commit, the star rating recalculation was fully performed by the `BeatmapUpdater.Process()` flow. This process does full metadata lookups, and while it *will* attempt to use the local `online.db` metadata cache, it *will* also fall back to API requests if the local metadata fetch fails. While that means that the local cache likely saved us from a doomsday scenario here, it *also* is the case that all of that metadata lookup stuff is *entirely unnecessary* when wanting to just update star ratings. Therefore, this splits out only the part relevant to star ratings as a separate background process, so that it can run completely locally. --- .../Database/BackgroundDataStoreProcessor.cs | 88 ++++++++++++++++--- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 1512b6be93..5053ab9a4c 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -76,8 +76,9 @@ namespace osu.Game.Database { Logger.Log("Beginning background data store processing.."); - checkForOutdatedStarRatings(); - processBeatmapSetsWithMissingMetrics(); + clearOutdatedStarRatings(); + populateMissingStarRatings(); + processOnlineBeatmapSetsWithNoUpdate(); // Note that the previous method will also update these on a fresh run. processBeatmapsWithMissingObjectCounts(); processScoresWithMissingStatistics(); @@ -100,7 +101,7 @@ namespace osu.Game.Database /// Check whether the databased difficulty calculation version matches the latest ruleset provided version. /// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated. /// - private void checkForOutdatedStarRatings() + private void clearOutdatedStarRatings() { foreach (var ruleset in rulesetStore.AvailableRulesets) { @@ -132,7 +133,74 @@ namespace osu.Game.Database } } - private void processBeatmapSetsWithMissingMetrics() + /// + /// This is split out from as a separate process to prevent high server-side load + /// from the firing online requests as part of the update. + /// Star rating recalculations can be ran strictly locally. + /// + private void populateMissingStarRatings() + { + HashSet beatmapIds = new HashSet(); + + Logger.Log("Querying for beatmaps with missing star ratings..."); + + realmAccess.Run(r => + { + foreach (var b in r.All().Where(b => b.StarRating < 0 && b.BeatmapSet != null)) + beatmapIds.Add(b.ID); + }); + + if (beatmapIds.Count == 0) + return; + + Logger.Log($"Found {beatmapIds.Count} beatmaps which require star rating reprocessing."); + + var notification = showProgressNotification(beatmapIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated"); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in beatmapIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, beatmapIds.Count); + + sleepIfRequired(); + + realmAccess.Write(r => + { + var beatmap = r.Find(id); + + if (beatmap == null) + return; + + try + { + var working = beatmapManager.GetWorkingBeatmap(beatmap); + var ruleset = working.BeatmapInfo.Ruleset.CreateInstance(); + + Debug.Assert(ruleset != null); + + var calculator = ruleset.CreateDifficultyCalculator(working); + + beatmap.StarRating = calculator.Calculate().StarRating; + ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); + ++processedCount; + } + catch (Exception e) + { + Logger.Log($"Background processing failed on {beatmap}: {e}"); + ++failedCount; + } + }); + } + + completeNotification(notification, processedCount, beatmapIds.Count, failedCount); + } + + private void processOnlineBeatmapSetsWithNoUpdate() { HashSet beatmapSetIds = new HashSet(); @@ -148,12 +216,7 @@ namespace osu.Game.Database // of other possible ways), but for now avoid queueing if the user isn't logged in at startup. if (api.IsLoggedIn) { - foreach (var b in r.All().Where(b => (b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)) && b.BeatmapSet != null)) - beatmapSetIds.Add(b.BeatmapSet!.ID); - } - else - { - foreach (var b in r.All().Where(b => b.StarRating < 0 && b.BeatmapSet != null)) + foreach (var b in r.All().Where(b => b.OnlineID > 0 && b.LastOnlineUpdate == null && b.BeatmapSet != null)) beatmapSetIds.Add(b.BeatmapSet!.ID); } }); @@ -161,10 +224,9 @@ namespace osu.Game.Database if (beatmapSetIds.Count == 0) return; - Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing."); + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require online updates."); - // Technically this is doing more than just star ratings, but easier for the end user to understand. - var notification = showProgressNotification(beatmapSetIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated"); + var notification = showProgressNotification(beatmapSetIds.Count, "Updating online data for beatmaps", "beatmaps' online data have been updated"); int processedCount = 0; int failedCount = 0; From 1a7774cd196a725cc98e154fe73fbb80e29605a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Mar 2025 21:33:27 +0900 Subject: [PATCH 170/349] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e35eaf5645..1fe29f2a21 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From d220c198858540554724f4fa4408966a3aaea6ce Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 7 Mar 2025 23:52:51 +0900 Subject: [PATCH 171/349] Add failing test --- .../TestScenePlaylistsRoomSubScreen.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index f797f6f2ac..e9c4b56e04 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -17,6 +17,7 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -513,6 +514,63 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("mods reset", () => !SelectedMods.Value.Any()); } + /// + /// Tests that the beatmap and ruleset style are reset when the selected item is changed to one without freestyle, + /// and that the mod selection is re-validated against the item's allowed mods. + /// + [Test] + public void TestUserStyle_Reset_OnFreestyleDisabled() + { + Room room = null!; + + AddStep("add room", () => + { + room = new Room + { + RoomID = 1, + Playlist = + [ + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + Freestyle = true + }, + new PlaylistItem(importedSet.Beatmaps[0]) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = [new APIMod(new OsuModDoubleTime())] + }, + ] + }; + + API.Perform(new CreateRoomRequest(room)); + }); + + TestPlaylistsRoomSubScreen screen = null!; + AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room)))); + AddUntilStep("wait for load", () => screen.IsLoaded); + + // Set beatmap + ruleset, reset by selecting second playlist item + AddStep("set user beatmap/ruleset style", () => + { + screen.UserBeatmap.Value = importedSet.Beatmaps[1]; + screen.UserRuleset.Value = new TaikoRuleset().RulesetInfo; + }); + AddUntilStep("beatmap/ruleset set", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[1]) && Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + AddStep("select second playlist item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user style reset", () => screen.UserBeatmap.Value == null && screen.UserRuleset.Value == null); + AddUntilStep("beatmap/ruleset set", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0]) && Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); + + AddStep("select first playlist item", () => screen.SelectedItem.Value = room.Playlist[0]); + + // Set mods (DT+HR), validate by selecting second playlist item where only DT is allowed. + AddStep("set user mods style", () => screen.UserMods.Value = [new OsuModDoubleTime(), new OsuModHardRock()]); + AddUntilStep("mods set", () => SelectedMods.Value.OfType().Any() && SelectedMods.Value.OfType().Any()); + AddStep("select second playlist item", () => screen.SelectedItem.Value = room.Playlist[1]); + AddUntilStep("user mods validated", () => screen.UserMods.Value.Count == 1 && screen.UserMods.Value.OfType().Any()); + AddUntilStep("mods set", () => SelectedMods.Value.Count == 1 && SelectedMods.Value.OfType().Any()); + } + private partial class TestPlaylistsScreen : OsuScreen { public TestPlaylistsScreen(PlaylistsRoomSubScreen screen) From eb15217a3661ec09bd0aec837450d6fe45467548 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 8 Mar 2025 00:09:32 +0900 Subject: [PATCH 172/349] Fix ruleset style not reset when freestyle disabled --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index fd1f35a3fc..f8dd9cd3d9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -541,6 +541,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (item.RulesetID > 0) UserRuleset.Value = null; } + else + UserRuleset.Value = null; validateUserMods(); From e69ad867990ce7d02396313ab9f4951779e1e513 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 8 Mar 2025 00:32:56 +0900 Subject: [PATCH 173/349] Fix typo --- osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs index 67b0d236ed..d463610034 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneOnlinePlaySubScreenStack.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.OnlinePlay AddStep("push screen that disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(true))); AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True); - AddStep("push screen that does not disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(false))); + AddStep("push screen that does not disable bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(false))); AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False); AddStep("exit one screen", () => stack.Exit()); From dbf571950c3f8b105c17fdcad5495a5f4a4738a1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 8 Mar 2025 00:34:29 +0900 Subject: [PATCH 174/349] Safguard setting states to not depend on screen load timing --- .../OnlinePlay/OnlinePlaySubScreenStack.cs | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index c10017f1fe..65c478308f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -2,13 +2,44 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay { public partial class OnlinePlaySubScreenStack : OsuScreenStack { + private OsuScreenDependencies dependencies = null!; + + // Note - these are required to be unbound on disposal. + private Bindable beatmap = null!; + private Bindable ruleset = null!; + private Bindable> mods = null!; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + // Bindables are leased by the OnlinePlayScreen, but pulled locally in order ot not rely on screen load timings. + // They will all be initially enabled while there is no screen in this stack. + dependencies = new OsuScreenDependencies(true, parent) + { + Beatmap = { Disabled = false }, + Ruleset = { Disabled = false }, + Mods = { Disabled = false } + }; + + beatmap = dependencies.Beatmap; + ruleset = dependencies.Ruleset; + mods = dependencies.Mods; + + return dependencies; + } + protected override void ScreenChanged(IScreen prev, IScreen? next) { base.ScreenChanged(prev, next); @@ -27,9 +58,9 @@ namespace osu.Game.Screens.OnlinePlay // This is a two-part process... // First, emulate the behaviour of DisallowExternalBeatmapRulesetChanges to disable toolbar buttons. - osuNext.Beatmap.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; - osuNext.Ruleset.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; - osuNext.Mods.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + beatmap.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + ruleset.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; + mods.Disabled = osuNext.DisallowExternalBeatmapRulesetChanges; // Second, when an OsuScreen is exited with DisallowExternalBeatmapRulesetChanges=true, leased bindables // are normally returned which reverts the mod and ruleset bindables to their original states. @@ -37,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay // The exact behaiour of the revert is awkward to emulate, but we particularly care about resetting mods // when returning to the lounge so that they don't stick around if the user then goes to create a new room. if (next is LoungeSubScreen) - osuNext.Mods.Value = []; + mods.Value = []; } } } From 2272ca1ae5420f020cf158d5de9418b9cea249fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Mar 2025 22:18:08 +0900 Subject: [PATCH 175/349] Fix namespace --- osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs index 8090dd2cb0..4ac00e28f4 100644 --- a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs +++ b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs @@ -4,7 +4,7 @@ using System.Net.Http; using osu.Framework.IO.Network; -namespace osu.Game.Online.API.Requests.Responses +namespace osu.Game.Online.API.Requests { public class RemoveBeatmapTagRequest : APIRequest { From f845ea19b5dd16369fc1f14bb7f23b759112fc1f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 9 Mar 2025 09:57:04 +0900 Subject: [PATCH 176/349] Fix initial multiplayer room settings not applied --- .../Match/MultiplayerMatchSettingsOverlay.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index f74de26f1f..42d240c60e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -365,8 +365,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateRoomMaxParticipants(); updateRoomAutoStartDuration(); updateRoomPlaylist(); - - drawablePlaylist.Items.BindCollectionChanged((_, __) => room.Playlist = drawablePlaylist.Items.ToArray()); } private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -470,6 +468,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } else { + room.Name = NameField.Text; + room.Password = PasswordTextBox.Text; + room.Type = TypePicker.Current.Value; + room.QueueMode = QueueModeDropdown.Current.Value; + room.AutoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value); + room.AutoSkip = AutoSkipCheckbox.Current.Value; + room.Playlist = drawablePlaylist.Items.ToArray(); + client.CreateRoom(room).ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) @@ -505,10 +511,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match const string not_found_prefix = "beatmaps not found:"; if (message.StartsWith(not_found_prefix, StringComparison.Ordinal)) - { ErrorText.Text = "The selected beatmap is not available online."; - room.Playlist.SingleOrDefault()?.MarkInvalid(); - } else ErrorText.Text = message; From 7fdadbd852ef2dbb3e7a0b09315d2d485414a5e9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 9 Mar 2025 10:16:28 +0900 Subject: [PATCH 177/349] Fix error message on invalid room password --- .../Multiplayer/InvalidPasswordException.cs | 4 ++++ .../Multiplayer/MultiplayerLoungeSubScreen.cs | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index b76a1cc05d..860fb90258 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -9,5 +9,9 @@ namespace osu.Game.Online.Multiplayer [Serializable] public class InvalidPasswordException : HubException { + public InvalidPasswordException() + : base("Invalid password") + { + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 93552670e9..54aa2003fa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -84,12 +84,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer onSuccess(room); else { - const string message = "Failed to join multiplayer room."; + Exception? exception = result.Exception?.AsSingular(); - if (result.Exception != null) - Logger.Error(result.Exception, message); - - onFailure.Invoke(result.Exception?.AsSingular().Message ?? message); + if (exception?.GetHubExceptionMessage() is string message) + onFailure(message); + else + { + const string generic_failure_message = "Failed to join multiplayer room."; + if (result.Exception != null) + Logger.Error(result.Exception, generic_failure_message); + onFailure(generic_failure_message); + } } }); } From 0a6c2121536f0dddcfe840a18c3f1126d8f83aca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 9 Mar 2025 23:47:29 +0900 Subject: [PATCH 178/349] Use `SkinnableSound` to ensure samples track active skin --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 22 +++++-------------- .../Screens/Play/KiaiGameplayFountains.cs | 17 ++++---------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index e62ef31278..b57012eaf7 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Utils; @@ -21,18 +20,14 @@ namespace osu.Game.Screens.Menu [Resolved] private GameHost host { get; set; } = null!; - [Resolved] - private ISkinSource skin { get; set; } = null!; - - private ISample? sample; - private SampleChannel? sampleChannel; + private SkinnableSound? sample; [BackgroundDependencyLoader] private void load() { RelativeSizeAxes = Axes.Both; - Children = new[] + Children = new Drawable[] { leftFountain = new StarFountain { @@ -46,9 +41,8 @@ namespace osu.Game.Screens.Menu Origin = Anchor.BottomRight, X = -250, }, + sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")) }; - - sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; @@ -89,13 +83,9 @@ namespace osu.Game.Screens.Menu break; } - // Don't play SFX when game is in background - if (!host.IsActive.Value) return; - - // Track sample channel to avoid overlapping playback - sampleChannel?.Stop(); - sampleChannel = sample?.GetChannel(); - sampleChannel?.Play(); + // Don't play SFX when game is in background as it can be a bit noisy. + if (host.IsActive.Value) + sample?.Play(); } } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index c8dcee2580..017e66253f 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -22,11 +21,7 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; - [Resolved] - private ISkinSource skin { get; set; } = null!; - - private ISample? sample; - private SampleChannel? sampleChannel; + private SkinnableSound? sample; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -35,7 +30,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; - Children = new[] + Children = new Drawable[] { leftFountain = new GameplayStarFountain { @@ -49,9 +44,8 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, X = -75, }, + sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")) }; - - sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; @@ -78,10 +72,7 @@ namespace osu.Game.Screens.Play leftFountain.Shoot(1); rightFountain.Shoot(-1); - // Track sample channel to avoid overlapping playback - sampleChannel?.Stop(); - sampleChannel = sample?.GetChannel(); - sampleChannel?.Play(); + sample?.Play(); } public partial class GameplayStarFountain : StarFountain From bbd2c33934520e34fd5601b14d1499c3b37daa3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 14:45:36 +0900 Subject: [PATCH 179/349] Allow grid spacing setting up to 256 pixels Addresses https://github.com/ppy/osu/discussions/29713. I think there's valid uses of this apart from just hiding (ie values between 128 and 256) so let's just get this in. --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 6220fa66b1..991d42c7b4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Edit public BindableFloat Spacing { get; } = new BindableFloat(4f) { MinValue = 4f, - MaxValue = 128f, + MaxValue = 256f, Precision = 0.01f, }; From 3cb32c38adaaf66e401a5265eb234b9e22470c22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 15:19:34 +0900 Subject: [PATCH 180/349] Disable user customisation of spectator list font / colour It's all a bit weird so let's just disable it for now. For instance, this is exposed as "text" font / colour but only affects the header. Also, no other headers are cusotmisable in similar components. --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 4 ++-- osu.Game/Screens/Play/HUD/SpectatorList.cs | 13 ++++--------- osu.Game/Skinning/TrianglesSkin.cs | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index bd1e15d06d..1445e872b5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -75,8 +75,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching( spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5); - AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); - AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); + AddStep("change font to venera", () => list.HeaderFont.Value = Typeface.Venera); + AddStep("change font to torus", () => list.HeaderFont.Value = Typeface.Torus); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 4297c62712..0cc4076313 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -13,12 +13,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; -using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Skinning; @@ -31,10 +29,7 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] - public Bindable Font { get; } = new Bindable(Typeface.Torus); - - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + public Bindable HeaderFont { get; } = new Bindable(Typeface.Torus); public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); private BindableList watchingUsers { get; } = new BindableList(); @@ -97,7 +92,7 @@ namespace osu.Game.Screens.Play.HUD watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); - Font.BindValueChanged(_ => updateAppearance()); + HeaderFont.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); FinishTransforms(true); @@ -198,7 +193,7 @@ namespace osu.Game.Screens.Play.HUD private void updateAppearance() { - header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + header.Font = OsuFont.GetFont(HeaderFont.Value, 12, FontWeight.Bold); header.Colour = HeaderColour.Value; Width = header.DrawWidth; diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 06fe1c80ee..a4a967bed9 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -158,7 +158,7 @@ namespace osu.Game.Skinning if (spectatorList != null) { - spectatorList.Font.Value = Typeface.Venera; + spectatorList.HeaderFont.Value = Typeface.Venera; spectatorList.HeaderColour.Value = new OsuColour().BlueLighter; spectatorList.Anchor = Anchor.BottomLeft; spectatorList.Origin = Anchor.BottomLeft; From 3cf0031e16695743a6614ac08938a82e776d3528 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 03:47:05 -0400 Subject: [PATCH 181/349] Add config settings for mania mobile modes --- .../ManiaRulesetConfigManager.cs | 8 +++++- .../ManiaSettingsSubsection.cs | 25 ++++++++++++++++++- .../Localisation/RulesetSettingsStrings.cs | 15 +++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index d9cc224ad1..90bb2e8bd2 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Mania.Configuration SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); + SetDefault(ManiaRulesetSetting.PreferPortraitOnPhone, true); + SetDefault(ManiaRulesetSetting.MobileExtendedColumns, true); + SetDefault(ManiaRulesetSetting.TouchControls, false); #pragma warning disable CS0618 // Although obsolete, this is still required to populate the bindable from the database in case migration is required. @@ -55,6 +58,9 @@ namespace osu.Game.Rulesets.Mania.Configuration ScrollTime, ScrollSpeed, ScrollDirection, - TimingBasedNoteColouring + TimingBasedNoteColouring, + PreferPortraitOnPhone, + MobileExtendedColumns, + TouchControls, } } diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 17add32513..f87e035ecc 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.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 osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -44,8 +45,30 @@ namespace osu.Game.Rulesets.Mania Keywords = new[] { "color" }, LabelText = RulesetSettingsStrings.TimingBasedColouring, Current = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring), - } + }, + new SettingsCheckbox + { + LabelText = RulesetSettingsStrings.ManiaTouchControls, + Current = config.GetBindable(ManiaRulesetSetting.TouchControls), + }, }; + + if (RuntimeInfo.IsMobile) + { + AddRange(new[] + { + new SettingsCheckbox + { + LabelText = RulesetSettingsStrings.ExtendColumnsOnLandscape, + Current = config.GetBindable(ManiaRulesetSetting.MobileExtendedColumns), + }, + new SettingsCheckbox + { + LabelText = RulesetSettingsStrings.PreferPortraitOnPhone, + Current = config.GetBindable(ManiaRulesetSetting.PreferPortraitOnPhone), + } + }); + } } private partial class ManiaScrollSlider : RoundedSliderBar diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index 9434cd53de..e40d771606 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -89,6 +89,21 @@ namespace osu.Game.Localisation /// public static LocalisableString TouchControlScheme => new TranslatableString(getKey(@"touch_control_scheme"), @"Touch control scheme"); + /// + /// "Prefer portrait mode on mobile phones" + /// + public static LocalisableString PreferPortraitOnPhone => new TranslatableString(getKey(@"prefer_portrait_on_phone"), @"Prefer portrait mode on mobile phones"); + + /// + /// "Extend columns on mobile in landscape mode" + /// + public static LocalisableString ExtendColumnsOnLandscape => new TranslatableString(getKey(@"extend_columns_on_landscape"), @"Extend columns on mobile in landscape mode"); + + /// + /// "Enable touch controls instead of touchable columns" + /// + public static LocalisableString ManiaTouchControls => new TranslatableString(getKey(@"mania_touch_controls"), @"Enable touch controls instead of touchable columns"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } From 918e248cd2feaef3287da4d22848345c6695605a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 03:47:51 -0400 Subject: [PATCH 182/349] Hook up portarit setting with behaviour --- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index e33cf092c3..42e481a8dd 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,7 +50,8 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1; + public override bool RequiresPortraitOrientation + => Beatmap.Stages.Count == 1 && Config.Get(ManiaRulesetSetting.PreferPortraitOnPhone); protected override bool RelativeScaleBeatLengths => true; From edcc607f4bb5d12cf2eb4c1ccd08f75537b711a6 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 03:48:12 -0400 Subject: [PATCH 183/349] Bring back touch control under a setting --- .../TestSceneManiaTouchInput.cs | 81 +++++++ osu.Game.Rulesets.Mania/UI/Column.cs | 12 +- .../UI/DrawableManiaRuleset.cs | 2 + .../UI/ManiaTouchInputArea.cs | 223 ++++++++++++++++++ 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs index dc95cd9ca0..364f7240e1 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -3,10 +3,13 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Mania.Tests { @@ -14,6 +17,11 @@ namespace osu.Game.Rulesets.Mania.Tests { protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + [SetUp] + public void SetUp() => Schedule(() => toggleTouchControls(false)); + + #region Without touch controls + [Test] public void TestTouchInput() { @@ -63,6 +71,79 @@ namespace osu.Game.Rulesets.Mania.Tests () => Does.Not.Contain(getColumn(0).Action.Value)); } + #endregion + + #region With touch controls + + [Test] + public void TestTouchAreaNotInitiallyVisible() + { + AddStep("enable touch controls", () => toggleTouchControls(true)); + AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); + } + + [Test] + public void TestPressReceptors() + { + AddStep("enable touch controls", () => toggleTouchControls(true)); + AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); + + for (int i = 0; i < 4; i++) + { + int index = i; + + AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getReceptor(index).Action.Value)); + + AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible); + } + } + + [Test] + public void TestColumnsNotTouchableWithTouchControls() + { + AddStep("enable touch controls", () => toggleTouchControls(true)); + + AddStep("touch receptor 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getReceptor(0).Action.Value)); + + AddStep("release receptor 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible); + + AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f)))); + + AddAssert("action not sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(0).Action.Value)); + + AddStep("release column 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f)))); + + AddAssert("action not sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(0).Action.Value)); + } + + #endregion + + private void toggleTouchControls(bool enabled) + { + var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!; + maniaConfig.SetValue(ManiaRulesetSetting.TouchControls, enabled); + } + + private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); + + private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); + private Column getColumn(int index) => this.ChildrenOfType().ElementAt(index); } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 5425965897..f9f0c21f0d 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -12,6 +12,7 @@ using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Game.Extensions; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Skinning; @@ -57,6 +58,8 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable AccentColour = new Bindable(Color4.Black); + private IBindable touchControls = null!; + public Column(int index, bool isSpecial) { Index = index; @@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.UI private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] - private void load(GameHost host) + private void load(GameHost host, ManiaRulesetConfigManager? rulesetConfig) { SkinnableDrawable keyArea; @@ -115,6 +118,9 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); + + if (rulesetConfig != null) + touchControls = rulesetConfig.GetBindable(ManiaRulesetSetting.TouchControls); } private void onSourceChanged() @@ -193,6 +199,10 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { + // if touch controls are enabled, disallow columns from handling touch directly. + if (touchControls.Value) + return false; + maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); touchActivationCount++; return true; diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 42e481a8dd..c53329599d 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -112,6 +112,8 @@ namespace osu.Game.Rulesets.Mania.UI configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); + + KeyBindingInputManager.Add(new ManiaTouchInputArea(this)); } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs new file mode 100644 index 0000000000..bfa8dcab34 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -0,0 +1,223 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mania.Configuration; +using osuTK; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// An overlay that captures and displays osu!mania mouse and touch input. + /// + public partial class ManiaTouchInputArea : VisibilityContainer + { + private readonly DrawableManiaRuleset drawableRuleset; + + // visibility state affects our child. we always want to handle input. + public override bool PropagatePositionalInputSubTree => true; + public override bool PropagateNonPositionalInputSubTree => true; + + [SettingSource("Spacing", "The spacing between receptors.")] + public BindableFloat Spacing { get; } = new BindableFloat(10) + { + Precision = 1, + MinValue = 0, + MaxValue = 100, + }; + + [SettingSource("Opacity", "The receptor opacity.")] + public BindableFloat Opacity { get; } = new BindableFloat(1) + { + Precision = 0.1f, + MinValue = 0, + MaxValue = 1 + }; + + [Resolved] + private ManiaRulesetConfigManager rulesetConfig { get; set; } = null!; + + private GridContainer gridContainer = null!; + + private readonly BindableBool touchControls = new BindableBool(); + + public ManiaTouchInputArea(DrawableManiaRuleset drawableRuleset) + { + this.drawableRuleset = drawableRuleset; + + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + RelativeSizeAxes = Axes.Both; + Height = 0.5f; + } + + [BackgroundDependencyLoader] + private void load() + { + List receptorGridContent = new List(); + List receptorGridDimensions = new List(); + + bool first = true; + + foreach (var stage in drawableRuleset.Playfield.Stages) + { + foreach (var column in stage.Columns) + { + if (!first) + { + receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } }); + receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize)); + } + + receptorGridContent.Add(new ColumnInputReceptor + { + Action = { BindTarget = column.Action }, + Enabled = { BindTarget = touchControls }, + }); + receptorGridDimensions.Add(new Dimension()); + + first = false; + } + } + + InternalChild = gridContainer = new GridContainer + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Content = new[] { receptorGridContent.ToArray() }, + ColumnDimensions = receptorGridDimensions.ToArray() + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + rulesetConfig.BindWith(ManiaRulesetSetting.TouchControls, touchControls); + Opacity.BindValueChanged(o => Alpha = o.NewValue, true); + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + // Hide whenever the keyboard is used. + Hide(); + return false; + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + if (touchControls.Value) + { + Show(); + return true; + } + + return false; + } + + protected override void PopIn() + { + gridContainer.FadeIn(500, Easing.OutQuint); + } + + protected override void PopOut() + { + gridContainer.FadeOut(300); + } + + public partial class ColumnInputReceptor : CompositeDrawable + { + public readonly IBindable Action = new Bindable(); + public readonly IBindable Enabled = new BindableBool(); + + private readonly Box highlightOverlay; + + [Resolved] + private ManiaInputManager? inputManager { get; set; } + + private bool isPressed; + + public ColumnInputReceptor() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.15f, + }, + highlightOverlay = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Blending = BlendingParameters.Additive, + } + } + } + }; + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + if (Enabled.Value) + { + updateButton(true); + return false; // handled by parent container to show overlay. + } + + return false; + } + + protected override void OnTouchUp(TouchUpEvent e) + { + updateButton(false); + } + + private void updateButton(bool press) + { + if (press == isPressed) + return; + + isPressed = press; + + if (press) + { + inputManager?.KeyBindingContainer.TriggerPressed(Action.Value); + highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); + } + else + { + inputManager?.KeyBindingContainer.TriggerReleased(Action.Value); + highlightOverlay.FadeTo(0, 400, Easing.OutQuint); + } + } + } + + private partial class Gutter : Drawable + { + public readonly IBindable Spacing = new Bindable(); + + public Gutter() + { + Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue)); + } + } + } +} From 7ca9d8392d4854ae9ee21a537eda6692ee35a147 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 17:17:20 +0900 Subject: [PATCH 184/349] Cache ruleset instance to avoid instantiation per beatmap processed --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 5053ab9a4c..5a1c4a4721 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -160,7 +160,17 @@ namespace osu.Game.Database int processedCount = 0; int failedCount = 0; - foreach (var id in beatmapIds) + Dictionary rulesetCache = new Dictionary(); + + Ruleset getRuleset(RulesetInfo rulesetInfo) + { + if (!rulesetCache.TryGetValue(rulesetInfo.ShortName, out var ruleset)) + ruleset = rulesetCache[rulesetInfo.ShortName] = rulesetInfo.CreateInstance(); + + return ruleset; + } + + foreach (Guid id in beatmapIds) { if (notification?.State == ProgressNotificationState.Cancelled) break; @@ -179,7 +189,7 @@ namespace osu.Game.Database try { var working = beatmapManager.GetWorkingBeatmap(beatmap); - var ruleset = working.BeatmapInfo.Ruleset.CreateInstance(); + var ruleset = getRuleset(working.BeatmapInfo.Ruleset); Debug.Assert(ruleset != null); From 27ead5a383dd2bf9884d6a33ac9697909a693592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 09:18:49 +0100 Subject: [PATCH 185/349] Use `CurrentMatchPlayingUserIds` instead of `RoomUpdated` --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 98b3ede874..17e77f5238 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -37,7 +36,8 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - private BindableList watchingUsers { get; } = new BindableList(); + private IBindableList watchingUsers { get; } = new BindableList(); + private IBindableList multiplayerPlayers { get; } = new BindableList(); private BindableList actualSpectators { get; } = new BindableList(); private Bindable userPlayingState { get; } = new Bindable(); @@ -92,11 +92,14 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); + multiplayerPlayers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); + multiplayerPlayers.BindCollectionChanged((_, _) => removePlayersFromMultiplayerRoom()); + + watchingUsers.BindTo(client.WatchingUsers); watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); - multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom; + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); @@ -236,14 +239,6 @@ namespace osu.Game.Screens.Play.HUD Width = header.DrawWidth; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (multiplayerClient.IsNotNull()) - multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom; - } - private partial class SpectatorListEntry : PoolableDrawable { public Bindable Current { get; } = new Bindable(); From 1f749250ac189bc64f90497cc6a3ed927c27612b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 03:49:00 -0400 Subject: [PATCH 186/349] Bring back mobile landscape column extension logic under a setting --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 44 +++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 5614a13a48..fcbe9138eb 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -3,12 +3,17 @@ #nullable disable +using System; +using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -56,15 +61,26 @@ namespace osu.Game.Rulesets.Mania.UI private ISkinSource currentSkin; + private IBindable mobileExtendedColumns = null!; + [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, ManiaRulesetConfigManager rulesetConfig) { currentSkin = skin; + mobileExtendedColumns = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileExtendedColumns); + mobileExtendedColumns.BindValueChanged(_ => updateMobileSizing()); + skin.SourceChanged += onSkinChanged; onSkinChanged(); } + protected override void LoadComplete() + { + base.LoadComplete(); + updateMobileSizing(); + } + private void onSkinChanged() { for (int i = 0; i < stageDefinition.Columns; i++) @@ -89,6 +105,8 @@ namespace osu.Game.Rulesets.Mania.UI columns[i].Width = width.Value; } + + updateMobileSizing(); } /// @@ -101,6 +119,30 @@ namespace osu.Game.Rulesets.Mania.UI Content[column] = columns[column].Child = content; } + private void updateMobileSizing() + { + if (!IsLoaded || !RuntimeInfo.IsMobile || !mobileExtendedColumns.Value) + return; + + // GridContainer+CellContainer containing this stage (gets split up for dual stages). + Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; + + // Will be null in tests. + if (containingCell == null || containingCell.Value.X < containingCell.Value.Y) + return; + + float aspectRatio = containingCell.Value.X / containingCell.Value.Y; + + // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon) + float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns); + // 1.92 is a "reference" mobile screen aspect ratio for phones. + // We should scale it back for cases like tablets which aren't so extreme. + mobileAdjust *= aspectRatio / 1.92f; + + for (int i = 0; i < stageDefinition.Columns; i++) + columns[i].Width *= mobileAdjust; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 857b8637ae26c117d54549f6c069a03b80e1449e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 04:36:51 -0400 Subject: [PATCH 187/349] Replace toggles with a single dropdown --- .../TestSceneManiaTouchInput.cs | 2 +- .../ManiaRulesetConfigManager.cs | 8 ++----- .../ManiaMobilePlayStyle.cs | 20 ++++++++++++++++ .../ManiaSettingsSubsection.cs | 24 +++---------------- osu.Game.Rulesets.Mania/UI/Column.cs | 8 +++---- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 8 +++---- .../UI/DrawableManiaRuleset.cs | 5 ++-- .../UI/ManiaTouchInputArea.cs | 6 ++++- .../Localisation/RulesetSettingsStrings.cs | 17 ++++++++----- 9 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs index 364f7240e1..b33c74d4e9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Mania.Tests private void toggleTouchControls(bool enabled) { var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!; - maniaConfig.SetValue(ManiaRulesetSetting.TouchControls, enabled); + maniaConfig.SetValue(ManiaRulesetSetting.MobilePlayStyle, enabled ? ManiaMobilePlayStyle.TouchControls : ManiaMobilePlayStyle.TouchableColumns); } private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 90bb2e8bd2..10a3236178 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -24,9 +24,7 @@ namespace osu.Game.Rulesets.Mania.Configuration SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); - SetDefault(ManiaRulesetSetting.PreferPortraitOnPhone, true); - SetDefault(ManiaRulesetSetting.MobileExtendedColumns, true); - SetDefault(ManiaRulesetSetting.TouchControls, false); + SetDefault(ManiaRulesetSetting.MobilePlayStyle, ManiaMobilePlayStyle.TouchableColumns); #pragma warning disable CS0618 // Although obsolete, this is still required to populate the bindable from the database in case migration is required. @@ -59,8 +57,6 @@ namespace osu.Game.Rulesets.Mania.Configuration ScrollSpeed, ScrollDirection, TimingBasedNoteColouring, - PreferPortraitOnPhone, - MobileExtendedColumns, - TouchControls, + MobilePlayStyle, } } diff --git a/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs b/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs new file mode 100644 index 0000000000..e6b1224fd3 --- /dev/null +++ b/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.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.Framework.Localisation; +using osu.Game.Localisation; + +namespace osu.Game.Rulesets.Mania +{ + public enum ManiaMobilePlayStyle + { + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.TouchableColumns))] + TouchableColumns, + + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.TouchControls))] + TouchControls, + + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ExtendedColumns))] + ExtendedColumns, + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index f87e035ecc..f558d30ee0 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.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 osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -46,29 +45,12 @@ namespace osu.Game.Rulesets.Mania LabelText = RulesetSettingsStrings.TimingBasedColouring, Current = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring), }, - new SettingsCheckbox + new SettingsEnumDropdown { - LabelText = RulesetSettingsStrings.ManiaTouchControls, - Current = config.GetBindable(ManiaRulesetSetting.TouchControls), + LabelText = RulesetSettingsStrings.MobilePlayStyle, + Current = config.GetBindable(ManiaRulesetSetting.MobilePlayStyle), }, }; - - if (RuntimeInfo.IsMobile) - { - AddRange(new[] - { - new SettingsCheckbox - { - LabelText = RulesetSettingsStrings.ExtendColumnsOnLandscape, - Current = config.GetBindable(ManiaRulesetSetting.MobileExtendedColumns), - }, - new SettingsCheckbox - { - LabelText = RulesetSettingsStrings.PreferPortraitOnPhone, - Current = config.GetBindable(ManiaRulesetSetting.PreferPortraitOnPhone), - } - }); - } } private partial class ManiaScrollSlider : RoundedSliderBar diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index f9f0c21f0d..ebd8efe124 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable AccentColour = new Bindable(Color4.Black); - private IBindable touchControls = null!; + private IBindable mobilePlayStyle = null!; public Column(int index, bool isSpecial) { @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); if (rulesetConfig != null) - touchControls = rulesetConfig.GetBindable(ManiaRulesetSetting.TouchControls); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); } private void onSourceChanged() @@ -199,8 +199,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - // if touch controls are enabled, disallow columns from handling touch directly. - if (touchControls.Value) + // if touch controls are selected, disallow columns from handling touch directly. + if (mobilePlayStyle.Value == ManiaMobilePlayStyle.TouchControls) return false; maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index fcbe9138eb..61c70bcf14 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -61,15 +61,15 @@ namespace osu.Game.Rulesets.Mania.UI private ISkinSource currentSkin; - private IBindable mobileExtendedColumns = null!; + private IBindable mobilePlayStyle = null!; [BackgroundDependencyLoader] private void load(ISkinSource skin, ManiaRulesetConfigManager rulesetConfig) { currentSkin = skin; - mobileExtendedColumns = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileExtendedColumns); - mobileExtendedColumns.BindValueChanged(_ => updateMobileSizing()); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); + mobilePlayStyle.BindValueChanged(_ => updateMobileSizing()); skin.SourceChanged += onSkinChanged; onSkinChanged(); @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.UI private void updateMobileSizing() { - if (!IsLoaded || !RuntimeInfo.IsMobile || !mobileExtendedColumns.Value) + if (!IsLoaded || !RuntimeInfo.IsMobile || mobilePlayStyle.Value != ManiaMobilePlayStyle.ExtendedColumns) return; // GridContainer+CellContainer containing this stage (gets split up for dual stages). diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index c53329599d..2dcbcacf93 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,8 +50,9 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - public override bool RequiresPortraitOrientation - => Beatmap.Stages.Count == 1 && Config.Get(ManiaRulesetSetting.PreferPortraitOnPhone); + private bool playsWithTouchableColumns => Config.Get(ManiaRulesetSetting.MobilePlayStyle) == ManiaMobilePlayStyle.TouchableColumns; + + public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && playsWithTouchableColumns; protected override bool RelativeScaleBeatLengths => true; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index bfa8dcab34..48b362b051 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -97,11 +97,15 @@ namespace osu.Game.Rulesets.Mania.UI }; } + private IBindable mobilePlayStyle; + protected override void LoadComplete() { base.LoadComplete(); - rulesetConfig.BindWith(ManiaRulesetSetting.TouchControls, touchControls); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); + mobilePlayStyle.BindValueChanged(p => touchControls.Value = p.NewValue == ManiaMobilePlayStyle.TouchControls, true); + Opacity.BindValueChanged(o => Alpha = o.NewValue, true); } diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index e40d771606..527707b011 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -90,19 +90,24 @@ namespace osu.Game.Localisation public static LocalisableString TouchControlScheme => new TranslatableString(getKey(@"touch_control_scheme"), @"Touch control scheme"); /// - /// "Prefer portrait mode on mobile phones" + /// "Mobile play style" /// - public static LocalisableString PreferPortraitOnPhone => new TranslatableString(getKey(@"prefer_portrait_on_phone"), @"Prefer portrait mode on mobile phones"); + public static LocalisableString MobilePlayStyle => new TranslatableString(getKey(@"mobile_play_style"), @"Mobile play style"); /// - /// "Extend columns on mobile in landscape mode" + /// "Touchable columns" /// - public static LocalisableString ExtendColumnsOnLandscape => new TranslatableString(getKey(@"extend_columns_on_landscape"), @"Extend columns on mobile in landscape mode"); + public static LocalisableString TouchableColumns => new TranslatableString(getKey(@"touchable_columns"), @"Touchable columns"); /// - /// "Enable touch controls instead of touchable columns" + /// "Touch controls" /// - public static LocalisableString ManiaTouchControls => new TranslatableString(getKey(@"mania_touch_controls"), @"Enable touch controls instead of touchable columns"); + public static LocalisableString TouchControls => new TranslatableString(getKey(@"touch_controls"), @"Touch controls"); + + /// + /// "Extended columns" + /// + public static LocalisableString ExtendedColumns => new TranslatableString(getKey(@"extended_columns"), @"Extended columns"); private static string getKey(string key) => $@"{prefix}:{key}"; } From 25108beae3fb470c123af1d2ffb1cc7fcf808269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 09:47:44 +0100 Subject: [PATCH 188/349] Actually use the proper list --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 17e77f5238..6479956601 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -145,16 +145,12 @@ namespace osu.Game.Screens.Play.HUD private void removePlayersFromMultiplayerRoom() { - if (multiplayerClient.Room == null) - return; - // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. - var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet(); - actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID)); + actualSpectators.RemoveAll(s => multiplayerPlayers.Contains(s.OnlineID)); } private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) From 6b1472b0705486a250fe3d84320ac57f2560e6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:36:19 +0100 Subject: [PATCH 189/349] Pull actual diffcalc out of realm transaction --- .../Database/BackgroundDataStoreProcessor.cs | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 5a1c4a4721..4e813fa2c7 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -179,32 +179,34 @@ namespace osu.Game.Database sleepIfRequired(); - realmAccess.Write(r => + var beatmap = realmAccess.Run(r => r.Find(id)?.Detach()); + + if (beatmap == null) + return; + + try { - var beatmap = r.Find(id); + var working = beatmapManager.GetWorkingBeatmap(beatmap); + var ruleset = getRuleset(working.BeatmapInfo.Ruleset); - if (beatmap == null) - return; + Debug.Assert(ruleset != null); - try + var calculator = ruleset.CreateDifficultyCalculator(working); + + double starRating = calculator.Calculate().StarRating; + realmAccess.Write(r => { - var working = beatmapManager.GetWorkingBeatmap(beatmap); - var ruleset = getRuleset(working.BeatmapInfo.Ruleset); - - Debug.Assert(ruleset != null); - - var calculator = ruleset.CreateDifficultyCalculator(working); - - beatmap.StarRating = calculator.Calculate().StarRating; - ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); - ++processedCount; - } - catch (Exception e) - { - Logger.Log($"Background processing failed on {beatmap}: {e}"); - ++failedCount; - } - }); + if (r.Find(id) is BeatmapInfo liveBeatmapInfo) + liveBeatmapInfo.StarRating = starRating; + }); + ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); + ++processedCount; + } + catch (Exception e) + { + Logger.Log($"Background processing failed on {beatmap}: {e}"); + ++failedCount; + } } completeNotification(notification, processedCount, beatmapIds.Count, failedCount); From 3d4dd8507723fcd3a048442834336486debb9732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:44:22 +0100 Subject: [PATCH 190/349] Move back tag to extra if reached zero votes --- osu.Game/Screens/Ranking/UserTagControl.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 6b7d22a7c2..b11dc1588b 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.Ranking var tag = (UserTag)e.NewItems[i]!; var drawableTag = new DrawableUserTag(tag); tagFlow.Insert(tagFlow.Count, drawableTag); - tag.VoteCount.BindValueChanged(sortTags, true); + tag.VoteCount.BindValueChanged(voteCountChanged, true); layout.Invalidate(); } @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Ranking for (int i = 0; i < e.OldItems!.Count; i++) { var tag = (UserTag)e.OldItems[i]!; - tag.VoteCount.ValueChanged -= sortTags; + tag.VoteCount.ValueChanged -= voteCountChanged; tagFlow.Remove(oldItems[e.OldStartingIndex + i], true); } @@ -199,7 +199,18 @@ namespace osu.Game.Screens.Ranking } } - private void sortTags(ValueChangedEvent _) => layout.Invalidate(); + private void voteCountChanged(ValueChangedEvent _) + { + var tagsWithNoVotes = displayedTags.Where(t => t.VoteCount.Value == 0).ToArray(); + + foreach (var tag in tagsWithNoVotes) + { + displayedTags.Remove(tag); + extraTags.Add(tag); + } + + layout.Invalidate(); + } protected override void Update() { From 00127b363d532ed4f51ec03de13f06e0478d5920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:52:24 +0100 Subject: [PATCH 191/349] Add search box to user tag control --- osu.Game/Screens/Ranking/UserTagControl.cs | 56 ++++++++++++++++------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index b11dc1588b..57b05f078c 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; @@ -447,6 +448,9 @@ namespace osu.Game.Screens.Ranking private partial class ExtraTagsPopover : OsuPopover { + private SearchTextBox searchBox = null!; + private SearchContainer searchContainer = null!; + public BindableList ExtraTags { get; } = new BindableList(); public Action? OnSelected { get; set; } @@ -457,28 +461,43 @@ namespace osu.Game.Screens.Ranking Child = new OsuScrollContainer { Width = 250, - Height = 200, + Height = 250, ScrollbarOverlapsContent = false, - Child = new FillFlowContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 5 }, - Spacing = new Vector2(10), - ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + searchBox = new SearchTextBox { - Action = () => + RelativeSizeAxes = Axes.X, + }, + searchContainer = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Right = 5, Top = 50, }, + Spacing = new Vector2(10), + ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) { - OnSelected?.Invoke(tag); - this.HidePopover(); - } - }) - } + Action = () => + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + }) + } + }, }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); + } } - private partial class DrawableExtraTag : OsuAnimatedButton + private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable { private readonly UserTag tag; @@ -527,6 +546,15 @@ namespace osu.Game.Screens.Ranking } }); } + + public IEnumerable FilterTerms => [tag.Name, tag.Description]; + + public bool MatchingFilter + { + set => Alpha = value ? 1 : 0; + } + + public bool FilteringActive { set { } } } } From 905afd57140df38b1a473167e0ff691cc50e543e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 06:49:52 -0400 Subject: [PATCH 192/349] Fix code quality error --- osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 48b362b051..7cb6b3b96f 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI }; } - private IBindable mobilePlayStyle; + private IBindable mobilePlayStyle = null!; protected override void LoadComplete() { From afad2cf278cdbc142a8edd56e9b5a69f98cd3acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 11:52:09 +0100 Subject: [PATCH 193/349] Apply more granular copying from database when retrieving working beatmap --- osu.Game/Beatmaps/WorkingBeatmap.cs | 17 ++++++++++++----- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index fd40097c4e..8df57fd0c8 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -235,11 +235,18 @@ namespace osu.Game.Beatmaps // Todo: Handle cancellation during beatmap parsing var b = GetBeatmap() ?? new Beatmap(); - // The original beatmap version needs to be preserved as the database doesn't contain it - BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; - - // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) - b.BeatmapInfo = BeatmapInfo; + // Copy across values of key properties for which the database-backed model has data that the decoded beatmap isn't going to. + b.BeatmapInfo.ID = BeatmapInfo.ID; + b.BeatmapInfo.UserSettings = BeatmapInfo.UserSettings; + b.BeatmapInfo.BeatmapSet = BeatmapInfo.BeatmapSet; + b.BeatmapInfo.Status = BeatmapInfo.Status; + b.BeatmapInfo.OnlineID = BeatmapInfo.OnlineID; + b.BeatmapInfo.OnlineMD5Hash = BeatmapInfo.OnlineMD5Hash; + b.BeatmapInfo.LastLocalUpdate = BeatmapInfo.LastLocalUpdate; + b.BeatmapInfo.LastOnlineUpdate = BeatmapInfo.LastOnlineUpdate; + b.BeatmapInfo.LastPlayed = BeatmapInfo.LastPlayed; + b.BeatmapInfo.EditorTimestamp = BeatmapInfo.EditorTimestamp; + b.BeatmapInfo.StarRating = BeatmapInfo.StarRating; // this could be recomputed in the decoding process but it's a bit annoying to do. return b; }, loadCancellationSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 8af74d11d8..352012106a 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -21,6 +21,7 @@ using osu.Framework.Statistics; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -152,14 +153,28 @@ namespace osu.Game.Beatmaps return null; } - if (stream.ComputeMD5Hash() != BeatmapInfo.MD5Hash) + string streamMD5 = stream.ComputeMD5Hash(); + string streamSHA2 = stream.ComputeSHA2Hash(); + + if (streamMD5 != BeatmapInfo.MD5Hash) { Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} does not have the expected hash).", level: LogLevel.Error); return null; } using (var reader = new LineBufferedReader(stream)) - return Decoder.GetDecoder(reader).Decode(reader); + { + var beatmap = Decoder.GetDecoder(reader).Decode(reader); + + beatmap.BeatmapInfo.MD5Hash = streamMD5; + beatmap.BeatmapInfo.Hash = streamSHA2; + beatmap.BeatmapInfo.Length = beatmap.CalculatePlayableLength(); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + beatmap.BeatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); + beatmap.BeatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + + return beatmap; + } } catch (Exception e) { From a78868712ca0ea25d60826f3e0c9fcb78fb95fd6 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 10 Mar 2025 16:09:18 -0300 Subject: [PATCH 194/349] Change amount from 0.9f to 0.6f --- .../Compose/Components/Timeline/TimelineBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 011ff17b30..0f1d3716e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull(); - placementBlueprint.Colour = OsuColour.Gray(0.9f); + placementBlueprint.Colour = OsuColour.Gray(0.6f); // TODO: this is out of order, causing incorrect stacking height. SelectionBlueprints.Add(placementBlueprint); From d5e06102d4879d832c72ee7e6088e33241ae2f0b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Mar 2025 22:40:24 -0400 Subject: [PATCH 195/349] Fix failing tests --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 61c70bcf14..9f2cc2d10f 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -1,9 +1,8 @@ // 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; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -59,16 +58,16 @@ namespace osu.Game.Rulesets.Mania.UI columns.Add(new Container { RelativeSizeAxes = Axes.Y }); } - private ISkinSource currentSkin; + private ISkinSource currentSkin = null!; - private IBindable mobilePlayStyle = null!; + private readonly Bindable mobilePlayStyle = new Bindable(); [BackgroundDependencyLoader] - private void load(ISkinSource skin, ManiaRulesetConfigManager rulesetConfig) + private void load(ISkinSource skin, ManiaRulesetConfigManager? rulesetConfig) { currentSkin = skin; - mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); + rulesetConfig?.BindWith(ManiaRulesetSetting.MobilePlayStyle, mobilePlayStyle); mobilePlayStyle.BindValueChanged(_ => updateMobileSizing()); skin.SourceChanged += onSkinChanged; From 3e71a969e64e06049290c18189a45099e70ed733 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 15:41:08 +0900 Subject: [PATCH 196/349] Apply commenting adjustments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 65c478308f..b52789f535 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -17,14 +17,14 @@ namespace osu.Game.Screens.OnlinePlay { private OsuScreenDependencies dependencies = null!; - // Note - these are required to be unbound on disposal. + // Note - these bindables must be stored to fields of this component to be correctly unbound on disposal. private Bindable beatmap = null!; private Bindable ruleset = null!; private Bindable> mods = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - // Bindables are leased by the OnlinePlayScreen, but pulled locally in order ot not rely on screen load timings. + // Bindables are leased by the OnlinePlayScreen, but pulled locally in order to not rely on screen load timings. // They will all be initially enabled while there is no screen in this stack. dependencies = new OsuScreenDependencies(true, parent) { From 75bd101c9e9f822b6dadb9904185516fc8aeab8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 15:47:45 +0900 Subject: [PATCH 197/349] Ensure realm database file is touched on startup Closes https://github.com/ppy/osu/discussions/32304. --- osu.Game/Database/RealmAccess.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 5cc143f4e2..3212e17b7b 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -315,6 +315,17 @@ namespace osu.Game.Database attemptRecoverFromFile(newerVersionFilename); } + try + { + // Some platforms' realm implementation (including windows) don't update modified time on open. + // Let's do this explicitly as some users may depend on it roughly aligning to usage expectations. + string fullPath = storage.GetFullPath(Filename); + var fi = new FileInfo(fullPath); + if (fi.Exists) + fi.LastWriteTime = DateTime.Now; + } + catch { } + try { return getRealmInstance(); From c962210b4f250fab62c31f082fedf34dbe26a8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 13:21:27 +0100 Subject: [PATCH 198/349] Fix placement blueprint tests --- .../Editor/CatchPlacementBlueprintTestScene.cs | 2 ++ .../Editor/ManiaPlacementBlueprintTestScene.cs | 2 ++ .../Editor/TestSceneHitCirclePlacementBlueprint.cs | 1 + .../Editor/TestSceneSliderPlacementBlueprint.cs | 2 ++ .../Editor/TestSceneSpinnerPlacementBlueprint.cs | 2 ++ osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs | 4 +++- 6 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index a327e6d4c9..a5713feda3 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new CatchRuleset(); + protected const double TIME_SNAP = 100; protected DrawableCatchHitObject LastObject; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 0f913a6a7d..83070c3e29 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { public abstract partial class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new ManiaRuleset(); + private readonly Column column; [Cached(typeof(IReadOnlyList))] diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index a105d860bf..5bce97d7b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject); protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint(); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 5831cc0a8a..8835254c48 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); + [SetUp] public void Setup() => Schedule(() => { diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index d7b5cc73be..18834ef847 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); + protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject); protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint(); diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index baf614d1c8..a644936a16 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -51,7 +51,9 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap GetPlayableBeatmap() { - var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + var rulesetInfo = CreateRuleset()!.RulesetInfo; + var playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo); + playable.BeatmapInfo.Ruleset = rulesetInfo; playable.Difficulty.CircleSize = 2; return playable; } From d4f0fc0fdee85accf84de96d6388e0a9ba2ecd0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:12:32 +0900 Subject: [PATCH 199/349] Disallow adjusting slider repeats with more lenient check condition --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 6c0d5af247..f60d1b023b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -441,7 +441,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); int proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1); - if (proposedCount == repeatHitObject.RepeatCount || Precision.AlmostEquals(lengthOfOneRepeat, 0)) + if (proposedCount == repeatHitObject.RepeatCount || Precision.AlmostEquals(lengthOfOneRepeat, 0, 1)) return; repeatHitObject.RepeatCount = proposedCount; From 23891b1994c296d7e6761010deeb334f6c1af103 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:17:44 +0900 Subject: [PATCH 200/349] Fix edge case allowing almost-zero-length sliders to be placed during distance snapping --- .../Edit/Blueprints/Components/SelectionEditablePath.cs | 2 +- .../Sliders/Components/PathControlPointVisualiser.cs | 2 +- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 +- osu.Game/Rulesets/Objects/SliderPath.cs | 5 ++++- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index 26b26641d3..654ef006a5 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { base.UpdateHitObjectFromPath(hitObject); - if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength) + if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement) EditorBeatmap?.Remove(hitObject); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index bc3d27fd68..5ae9b194be 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -484,7 +484,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // Snap the path to the current beat divisor before checking length validity. hitObject.SnapTo(distanceSnapProvider); - if (!hitObject.Path.HasValidLength) + if (!hitObject.Path.HasValidLengthForPlacement) { for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) hitObject.Path.ControlPoints[i].Position = oldControlPoints[i]; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index a747d4fce8..d934eb5a9e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; - protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; + protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement; public SliderPlacementBlueprint() : base(new Slider()) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 9978c46027..d6150f85db 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -476,7 +476,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.SnapTo(distanceSnapProvider); // If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted - if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) + if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLengthForPlacement) { placementHandler?.Delete(HitObject); return; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 4c3db207f2..9a5d3c3bc1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Edit Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); - if (xInBounds && yInBounds && slider.Path.HasValidLength) + if (xInBounds && yInBounds && slider.Path.HasValidLengthForPlacement) return; for (int i = 0; i < slider.Path.ControlPoints.Count; i++) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 5550815370..eb591ec530 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -31,7 +31,10 @@ namespace osu.Game.Rulesets.Objects /// public readonly Bindable ExpectedDistance = new Bindable(); - public bool HasValidLength => Precision.DefinitelyBigger(Distance, 0); + /// + /// Should be used to check whether placement can continue after a user editor operation. + /// + public bool HasValidLengthForPlacement => Precision.DefinitelyBigger(Distance, 0, 1); /// /// The control points of the path. From 5ef2479e24d8001ee82b32c7bde832347b981747 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:29:47 +0900 Subject: [PATCH 201/349] Remove previous version of local cache lookup handling --- .../LocalCachedBeatmapMetadataSource.cs | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index a1744f74b3..1412d3234c 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -104,11 +104,6 @@ namespace osu.Game.Beatmaps switch (getCacheVersion(db)) { - case 1: - // will eventually become irrelevant due to the monthly recycling of local caches - // can be removed 20250221 - return queryCacheVersion1(db, beatmapInfo, out onlineMetadata); - case 2: return queryCacheVersion2(db, beatmapInfo, out onlineMetadata); } @@ -270,42 +265,6 @@ namespace osu.Game.Beatmaps } } - private bool queryCacheVersion1(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) - { - Debug.Assert(beatmapInfo.BeatmapSet != null); - - using var cmd = db.CreateCommand(); - - cmd.CommandText = - @"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR filename = @Path"; - - cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); - - using var reader = cmd.ExecuteReader(); - - if (reader.Read()) - { - logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 1)."); - - onlineMetadata = new OnlineBeatmapMetadata - { - BeatmapSetID = reader.GetInt32(0), - BeatmapID = reader.GetInt32(1), - BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2), - BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2), - AuthorID = reader.GetInt32(3), - MD5Hash = reader.GetString(4), - LastUpdated = reader.GetDateTimeOffset(5), - // TODO: DateSubmitted and DateRanked are not provided by local cache in this version. - }; - return true; - } - - onlineMetadata = null; - return false; - } - private bool queryCacheVersion2(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) { Debug.Assert(beatmapInfo.BeatmapSet != null); From 8d83dfede7a2c7a2818da4f5cc97165524f4237b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:37:46 +0900 Subject: [PATCH 202/349] Ensure only ranked/approved/loved lookups occur on local cached source --- osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 1412d3234c..0b4f4f1700 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -277,7 +277,11 @@ namespace osu.Game.Beatmaps FROM `osu_beatmaps` AS `b` JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id` WHERE `b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path + AND `b`.`approved` in (1, 2, 4) """; + // approved conditional can theoretically be removed as it was fixed in + // https://github.com/ppy/osu-onlinedb-generator/commit/489ac000775c3ff63bc914efb83cad0f6fbde261 + // but it's also safe to leave it (should not affect performance). cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); From be5c89c2e40321a1c10d80abb3e523686d7734f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 17:03:06 +0900 Subject: [PATCH 203/349] Add basic helper method to update beatmap statistics --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 13 +++++++++++++ osu.Game/Beatmaps/BeatmapUpdater.cs | 7 ++----- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 6 +----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index 16b4b04ce4..25f98c812c 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,15 +1,28 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Select; namespace osu.Game.Beatmaps { public static class BeatmapInfoExtensions { + /// + /// Given an , update length, BPM and object counts. + /// + public static void UpdateStatisticsFromBeatmap(this BeatmapInfo beatmapInfo, IBeatmap beatmap) + { + beatmapInfo.Length = beatmap.CalculatePlayableLength(); + beatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); + beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + } + /// /// A user-presentable display title representing this beatmap. /// diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index efb432b84e..64ac69bb07 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -51,7 +51,7 @@ namespace osu.Game.Beatmaps if (lookupScope != MetadataLookupScope.None) metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst); - foreach (var beatmap in beatmapSet.Beatmaps) + foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps) { difficultyCache.Invalidate(beatmap); @@ -63,10 +63,7 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(working); beatmap.StarRating = calculator.Calculate().StarRating; - beatmap.Length = working.Beatmap.CalculatePlayableLength(); - beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength(); - beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration); - beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count; + beatmap.UpdateStatisticsFromBeatmap(working.Beatmap); } // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 352012106a..fdeb840977 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -21,7 +21,6 @@ using osu.Framework.Statistics; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -168,10 +167,7 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo.MD5Hash = streamMD5; beatmap.BeatmapInfo.Hash = streamSHA2; - beatmap.BeatmapInfo.Length = beatmap.CalculatePlayableLength(); - beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); - beatmap.BeatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); - beatmap.BeatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + beatmap.BeatmapInfo.UpdateStatisticsFromBeatmap(beatmap); return beatmap; } From 148fe5ca16e92f6ea783f71cffc12ffe86135486 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 17:08:52 +0900 Subject: [PATCH 204/349] Fix missing base dependencies --- osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index b52789f535..79baa490ac 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay { // Bindables are leased by the OnlinePlayScreen, but pulled locally in order to not rely on screen load timings. // They will all be initially enabled while there is no screen in this stack. - dependencies = new OsuScreenDependencies(true, parent) + dependencies = new OsuScreenDependencies(true, base.CreateChildDependencies(parent)) { Beatmap = { Disabled = false }, Ruleset = { Disabled = false }, From 4565c271dd46ab35af1177302e965aa7bf93c920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 17:23:09 +0900 Subject: [PATCH 205/349] Fix multiple code inspections --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 25 ++++++++++++------------ 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 9f2cc2d10f..6a1949e978 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Beatmaps; @@ -58,15 +58,14 @@ namespace osu.Game.Rulesets.Mania.UI columns.Add(new Container { RelativeSizeAxes = Axes.Y }); } - private ISkinSource currentSkin = null!; + [Resolved] + private ISkinSource skin { get; set; } = null!; private readonly Bindable mobilePlayStyle = new Bindable(); [BackgroundDependencyLoader] - private void load(ISkinSource skin, ManiaRulesetConfigManager? rulesetConfig) + private void load(ManiaRulesetConfigManager? rulesetConfig) { - currentSkin = skin; - rulesetConfig?.BindWith(ManiaRulesetSetting.MobilePlayStyle, mobilePlayStyle); mobilePlayStyle.BindValueChanged(_ => updateMobileSizing()); @@ -86,16 +85,16 @@ namespace osu.Game.Rulesets.Mania.UI { if (i > 0) { - float spacing = currentSkin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1)) - ?.Value ?? Stage.COLUMN_SPACING; + float spacing = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1)) + ?.Value ?? Stage.COLUMN_SPACING; columns[i].Margin = new MarginPadding { Left = spacing }; } - float? width = currentSkin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) - ?.Value; + float? width = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) + ?.Value; bool isSpecialColumn = stageDefinition.IsSpecialColumn(i); @@ -146,8 +145,8 @@ namespace osu.Game.Rulesets.Mania.UI { base.Dispose(isDisposing); - if (currentSkin != null) - currentSkin.SourceChanged -= onSkinChanged; + if (skin.IsNotNull()) + skin.SourceChanged -= onSkinChanged; } } } From 338328b9113c3ebbcf67ad01561e0bef31368a77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 17:56:58 +0900 Subject: [PATCH 206/349] Fix loading layer not showing when closing room --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index e5f4b6087a..6aa366dbc5 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -217,10 +217,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { roomListing.Rooms.Clear(); - hasListingResults.Value = false; - listingPoller.PollImmediately(); + RefreshRooms(); }); + updateLoadingLayer(); updateFilter(); } @@ -410,7 +410,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); - public void RefreshRooms() => listingPoller.PollImmediately(); + public void RefreshRooms() + { + hasListingResults.Value = false; + listingPoller.PollImmediately(); + } private void updateLoadingLayer() { From 914a230446da83db38d46752381f66e37fe272ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:17:18 +0900 Subject: [PATCH 207/349] Add brackets to ensure correct lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 0b4f4f1700..d876ba55b2 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -276,7 +276,7 @@ namespace osu.Game.Beatmaps SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date` FROM `osu_beatmaps` AS `b` JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id` - WHERE `b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path + WHERE (`b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path) AND `b`.`approved` in (1, 2, 4) """; // approved conditional can theoretically be removed as it was fixed in From 770291b4623f7818ea76c6928de7e69768389ca8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:35:10 +0900 Subject: [PATCH 208/349] Show border instead of adjusting dim --- .../Components/Timeline/TimelineBlueprintContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 0f1d3716e2..c149a8f73a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -18,6 +18,7 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -90,7 +91,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull(); - placementBlueprint.Colour = OsuColour.Gray(0.6f); + // just to show the border. using the selection state doesn't seem to backfire. + // if it does then we'll probably want to just make `new` object above rather than rely on `CreateBlueprintFor`. + placementBlueprint.State = SelectionState.Selected; // TODO: this is out of order, causing incorrect stacking height. SelectionBlueprints.Add(placementBlueprint); From ee723aef6811656fe09aa4f670a98163c648e5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:43:02 +0100 Subject: [PATCH 209/349] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d4b49e492a..8f219ea426 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 d10a3d649a..8045009621 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From e2e2383c504282a9ef29e7c4803b185d9eb5d2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Mar 2025 15:02:18 +0100 Subject: [PATCH 210/349] Adjust text flow usages to framework changes --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 13 +++++++++---- osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs | 1 - osu.Game/Overlays/Changelog/ChangelogEntry.cs | 1 - osu.Game/Overlays/Chat/ChatLine.cs | 2 +- osu.Game/Overlays/Music/PlaylistItem.cs | 1 - .../Header/Components/PreviousUsernamesDisplay.cs | 1 - 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index aa72996fff..6022ea6bd6 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -134,9 +134,14 @@ namespace osu.Game.Graphics.Containers protected virtual DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new DrawableLinkCompiler(textPart); - // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. - // However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation. - // Since the compilers don't display any content and don't affect the layout, it's simplest to exclude them from the flow. - public override IEnumerable FlowingChildren => base.FlowingChildren.Where(c => !(c is DrawableLinkCompiler)); + protected override FillFlowContainer CreateFlow() => new LinkFlow(); + + private partial class LinkFlow : InnerFlow + { + // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. + // However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation. + // Since the compilers don't display any content and don't affect the layout, it's simplest to exclude them from the flow. + public override IEnumerable FlowingChildren => base.FlowingChildren.Where(c => !(c is DrawableLinkCompiler)); + } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs index d18e1c93c9..c9783d42dc 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs @@ -39,7 +39,6 @@ namespace osu.Game.Overlays.BeatmapSet }, textContainer = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: 14)) { - Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(10), diff --git a/osu.Game/Overlays/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs index 9c40440778..d6021972c6 100644 --- a/osu.Game/Overlays/Changelog/ChangelogEntry.cs +++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs @@ -82,7 +82,6 @@ namespace osu.Game.Overlays.Changelog }, title = new LinkFlowContainer { - Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.BottomLeft, diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index e386f2ac09..20c3b26b8b 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat } } - public IReadOnlyCollection DrawableContentFlow => drawableContentFlow; + public IEnumerable DrawableContentFlow => drawableContentFlow.Children; private const float font_size = 13; diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 90fdfd0491..01b0472172 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -63,7 +63,6 @@ namespace osu.Game.Overlays.Music { sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); sprite.Colour = colours.Gray9; - sprite.Padding = new MarginPadding { Top = 1 }; }); SelectedSet.BindValueChanged(set => updateSelectionState(set.NewValue)); diff --git a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs index dce5c84d12..1cd09566fb 100644 --- a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs @@ -85,7 +85,6 @@ namespace osu.Game.Overlays.Profile.Header.Components { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, // Prevents the tooltip of having a sudden size reduction and flickering when the text is being faded out. // Also prevents a potential OnHover/HoverLost feedback loop. AlwaysPresent = true, From 749df665d161fd27b253247980f7e441a528f6ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:47:16 +0900 Subject: [PATCH 211/349] Focus search box immediately --- osu.Game/Screens/Ranking/UserTagControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 57b05f078c..a643bd6206 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -467,6 +467,7 @@ namespace osu.Game.Screens.Ranking { searchBox = new SearchTextBox { + HoldFocus = true, RelativeSizeAxes = Axes.X, }, searchContainer = new SearchContainer From 345f565b90b947eb6d353381a2cc5fc1d7a38a7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:47:28 +0900 Subject: [PATCH 212/349] Allow using `Enter` key to select a single match --- osu.Game/Screens/Ranking/UserTagControl.cs | 39 ++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index a643bd6206..2e559ff534 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -30,6 +31,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Ranking { @@ -479,32 +481,49 @@ namespace osu.Game.Screens.Ranking Spacing = new Vector2(10), ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) { - Action = () => - { - OnSelected?.Invoke(tag); - this.HidePopover(); - } + Action = () => select(tag) }) } }, }; } + private void select(UserTag tag) + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + protected override void LoadComplete() { base.LoadComplete(); searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } + + protected override bool OnKeyDown(KeyDownEvent e) + { + var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + + if (e.Key == Key.Enter) + { + if (visibleItems.Length == 1) + select(visibleItems.Single().Tag); + + return true; + } + + return base.OnKeyDown(e); + } } private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable { - private readonly UserTag tag; + public readonly UserTag Tag; public DrawableExtraTag(UserTag tag) { - this.tag = tag; + Tag = tag; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -535,20 +554,20 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = tag.Name, + Text = Tag.Name, }, new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = tag.Description, + Text = Tag.Description, } } } }); } - public IEnumerable FilterTerms => [tag.Name, tag.Description]; + public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; public bool MatchingFilter { From e6fe6206475106d73801c9801498119189566022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:51:12 +0100 Subject: [PATCH 213/349] Improve tip threshold for click slider copy & tooltip --- osu.Game/Localisation/TabletSettingsStrings.cs | 5 +++++ osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/TabletSettingsStrings.cs b/osu.Game/Localisation/TabletSettingsStrings.cs index 6c2e3c1f9c..ff0ced457f 100644 --- a/osu.Game/Localisation/TabletSettingsStrings.cs +++ b/osu.Game/Localisation/TabletSettingsStrings.cs @@ -59,6 +59,11 @@ namespace osu.Game.Localisation /// public static LocalisableString LockAspectRatio => new TranslatableString(getKey(@"lock_aspect_ratio"), @"Lock aspect ratio"); + /// + /// "Tip pressure for click" + /// + public static LocalisableString TipPressureForClick => new TranslatableString(getKey(@"tip_pressure_for_click"), "Tip pressure for click"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 9d70e49659..e104bb7e39 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -215,10 +215,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = sizeY, CanBeShown = { BindTarget = enabled } }, - new SettingsSlider + new SettingsPercentageSlider { TransferValueOnCommit = true, - LabelText = "Tip Threshold", + LabelText = TabletSettingsStrings.TipPressureForClick, Current = pressureThreshold, CanBeShown = { BindTarget = enabled } }, From 61e1234e0aeaa3fc30902d593a283ff786ed0d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:52:03 +0100 Subject: [PATCH 214/349] Fix compile failure --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 5ca08e0bba..95a134e204 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -135,6 +135,7 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaSize { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); + public BindableFloat PressureThreshold { get; } = new BindableFloat(); public IBindable Tablet => tablet; From bcdc49e248b826b6dce387242d84c4710762dd1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:54:17 +0900 Subject: [PATCH 215/349] Adjust naming and subclassing --- osu.Game/Screens/Ranking/UserTag.cs | 25 ++++ osu.Game/Screens/Ranking/UserTagControl.cs | 144 +++++++++------------ 2 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 osu.Game/Screens/Ranking/UserTag.cs diff --git a/osu.Game/Screens/Ranking/UserTag.cs b/osu.Game/Screens/Ranking/UserTag.cs new file mode 100644 index 0000000000..d44e531330 --- /dev/null +++ b/osu.Game/Screens/Ranking/UserTag.cs @@ -0,0 +1,25 @@ +// 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.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Ranking +{ + public record UserTag + { + public long Id { get; } + public string Name { get; } + public string Description { get; } + + public BindableInt VoteCount { get; } = new BindableInt(); + public BindableBool Voted { get; } = new BindableBool(); + + public UserTag(APITag tag) + { + Id = tag.Id; + Name = tag.Name; + Description = tag.Description; + } + } +} diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 2e559ff534..7600d0aaae 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -79,12 +79,12 @@ namespace osu.Game.Screens.Ranking LayoutEasing = Easing.OutQuint, Spacing = new Vector2(4), }, - new ExtraTagsButton + new AddTagsButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, OnTagSelected = onExtraTagSelected, - ExtraTags = { BindTarget = extraTags }, + AvailableTags = { BindTarget = extraTags }, }, }, }, @@ -420,13 +420,13 @@ namespace osu.Game.Screens.Ranking } } - private partial class ExtraTagsButton : GrayButton, IHasPopover + private partial class AddTagsButton : GrayButton, IHasPopover { - public BindableList ExtraTags { get; } = new BindableList(); + public BindableList AvailableTags { get; } = new BindableList(); public Action? OnTagSelected { get; set; } - public ExtraTagsButton() + public AddTagsButton() : base(FontAwesome.Solid.Plus) { Size = new Vector2(30); @@ -438,22 +438,22 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - ExtraTags.BindCollectionChanged((_, _) => Enabled.Value = ExtraTags.Count > 0, true); + AvailableTags.BindCollectionChanged((_, _) => Enabled.Value = AvailableTags.Count > 0, true); } - public Popover GetPopover() => new ExtraTagsPopover + public Popover GetPopover() => new AddTagsPopover { - ExtraTags = { BindTarget = ExtraTags }, + AvailableTags = { BindTarget = AvailableTags }, OnSelected = OnTagSelected, }; } - private partial class ExtraTagsPopover : OsuPopover + private partial class AddTagsPopover : OsuPopover { private SearchTextBox searchBox = null!; private SearchContainer searchContainer = null!; - public BindableList ExtraTags { get; } = new BindableList(); + public BindableList AvailableTags { get; } = new BindableList(); public Action? OnSelected { get; set; } @@ -479,7 +479,7 @@ namespace osu.Game.Screens.Ranking Direction = FillDirection.Vertical, Padding = new MarginPadding { Right = 5, Top = 50, }, Spacing = new Vector2(10), - ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + ChildrenEnumerable = AvailableTags.Select(tag => new DrawableAddableTag(tag) { Action = () => select(tag) }) @@ -488,12 +488,6 @@ namespace osu.Game.Screens.Ranking }; } - private void select(UserTag tag) - { - OnSelected?.Invoke(tag); - this.HidePopover(); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -503,7 +497,7 @@ namespace osu.Game.Screens.Ranking protected override bool OnKeyDown(KeyDownEvent e) { - var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); if (e.Key == Key.Enter) { @@ -515,82 +509,68 @@ namespace osu.Game.Screens.Ranking return base.OnKeyDown(e); } - } - private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable - { - public readonly UserTag Tag; - - public DrawableExtraTag(UserTag tag) + private void select(UserTag tag) { - Tag = tag; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Anchor = Origin = Anchor.Centre; + OnSelected?.Invoke(tag); + this.HidePopover(); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private partial class DrawableAddableTag : OsuAnimatedButton, IFilterable { - Content.AddRange(new Drawable[] + public readonly UserTag Tag; + + public DrawableAddableTag(UserTag tag) { - new Box + Tag = tag; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Anchor = Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Content.AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoamDark, - Depth = float.MaxValue, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - Padding = new MarginPadding(5), - Children = new Drawable[] + new Box { - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeaFoamDark, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Padding = new MarginPadding(5), + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = Tag.Name, - }, - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = Tag.Description, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = Tag.Name, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = Tag.Description, + } } } - } - }); + }); + } + + public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; + + public bool MatchingFilter { set => Alpha = value ? 1 : 0; } + public bool FilteringActive { set { } } } - - public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; - - public bool MatchingFilter - { - set => Alpha = value ? 1 : 0; - } - - public bool FilteringActive { set { } } - } - } - - public record UserTag - { - public long Id { get; } - public string Name { get; } - public string Description { get; set; } - public BindableInt VoteCount { get; } = new BindableInt(); - public BindableBool Voted { get; } = new BindableBool(); - - public UserTag(APITag tag) - { - Id = tag.Id; - Name = tag.Name; - Description = tag.Description; } } } From 1a8aa861fd4c6157ee35418bc967d1916071f83f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 19:11:41 +0900 Subject: [PATCH 216/349] Use presence in user panel context menu --- osu.Game/Users/UserPanel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 09a5cb414f..1f72cbccbf 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -22,6 +22,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osu.Game.Localisation; +using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -76,6 +77,9 @@ namespace osu.Game.Users [Resolved] private MultiplayerClient? multiplayerClient { get; set; } + [Resolved] + private MetadataClient? metadataClient { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -153,7 +157,7 @@ namespace osu.Game.Users chatOverlay?.Show(); })); - if (User.IsOnline) + if (metadataClient?.GetPresence(User.OnlineID) != null) { items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => { From 1f4cfa74dcb7c1b41ee76ab1a368a198cde515c6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 19:13:10 +0900 Subject: [PATCH 217/349] Initial rework of FriendDisplay to use presence It's at a very simple stage for now: - Tab control is no longer recreated when friends change, counts are updated as bindables. - All async logic removed temporarily. - Sort and filtering happens in realtime without panel reload. - Display modes removed for now. Need to think about this one a bit more, and whether to retry the async path or look for a separate solution. - Real time user presence is now considered. Not considered for sorting by last visit time yet. --- .../Visual/Online/TestSceneFriendDisplay.cs | 2 +- .../TestSceneFriendsOnlineStatusControl.cs | 36 +- .../Changelog/ChangelogUpdateStreamItem.cs | 12 +- .../Dashboard/Friends/FriendDisplay.cs | 312 ++++++++++-------- .../Friends/FriendOnlineStreamControl.cs | 43 ++- .../Dashboard/Friends/FriendStream.cs | 10 +- .../Friends/FriendsOnlineStatusItem.cs | 21 +- osu.Game/Overlays/OverlayStreamItem.cs | 66 +++- 8 files changed, 284 insertions(+), 218 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 7925b252b6..010a261d4c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOffline() { - AddStep("Populate with offline test users", () => display.Users = getUsers()); + // AddStep("Populate with offline test users", () => display.Users = getUsers()); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index c75c2a7877..548f3067a7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -1,14 +1,9 @@ // 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.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; @@ -19,37 +14,14 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private FriendOnlineStreamControl control; - [SetUp] - public void SetUp() => Schedule(() => Child = control = new FriendOnlineStreamControl + public void SetUp() => Schedule(() => Child = new FriendOnlineStreamControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, + CountAll = { Value = 15 }, + CountOnline = { Value = 10 }, + CountOffline = { Value = 5 } }); - - [Test] - public void Populate() - { - AddStep("Populate", () => control.Populate(new List - { - new APIUser - { - IsOnline = true - }, - new APIUser - { - IsOnline = false - }, - new APIUser - { - IsOnline = false - } - })); - - AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.All)?.Count == 3); - AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Online)?.Count == 1); - AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Offline)?.Count == 2); - } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 30273d2405..df1ea6c283 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -1,8 +1,6 @@ // 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 Humanizer; using osu.Framework.Localisation; using osu.Game.Graphics; @@ -18,14 +16,12 @@ namespace osu.Game.Overlays.Changelog { if (stream.IsFeatured) Width *= 2; + + MainText = Value.DisplayName; + AdditionalText = Value.LatestBuild.DisplayVersion; + InfoText = Value.UserCount > 0 ? $"{"user".ToQuantity(Value.UserCount, "N0")} online" : default(LocalisableString); } - protected override LocalisableString MainText => Value.DisplayName; - - protected override LocalisableString AdditionalText => Value.LatestBuild.DisplayVersion; - - protected override LocalisableString InfoText => Value.UserCount > 0 ? $"{"user".ToQuantity(Value.UserCount, "N0")} online" : null; - protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 3e393ced01..2938be732c 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -1,21 +1,19 @@ // 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.Collections.Generic; +using System.Collections.Specialized; using System.Linq; -using System.Threading; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Resources.Localisation.Web; using osu.Game.Users; using osuTK; @@ -24,32 +22,22 @@ namespace osu.Game.Overlays.Dashboard.Friends { public partial class FriendDisplay : CompositeDrawable { - private List users = new List(); - - public List Users - { - get => users; - set - { - users = value; - onlineStreamControl.Populate(value); - } - } - - private CancellationTokenSource cancellationToken; - - [CanBeNull] - private SearchContainer currentContent; - - private FriendOnlineStreamControl onlineStreamControl; - private Box background; - private Box controlBackground; - private UserListToolbar userListToolbar; - private Container itemsPlaceholder; - private LoadingLayer loading; - private BasicSearchTextBox searchTextBox; - private readonly IBindableList apiFriends = new BindableList(); + private readonly IBindableDictionary friendPresences = new BindableDictionary(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + private FriendOnlineStreamControl streamControl = null!; + private Box background = null!; + private Box controlBackground = null!; + private UserListToolbar userListToolbar = null!; + private LoadingLayer loading = null!; + private BasicSearchTextBox searchTextBox = null!; + private FriendsSearchContainer panelsContainer = null!; public FriendDisplay() { @@ -57,8 +45,8 @@ namespace osu.Game.Overlays.Dashboard.Friends AutoSizeAxes = Axes.Y; } - [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider colourProvider, IAPIProvider api) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { InternalChild = new FillFlowContainer { @@ -86,7 +74,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Top = 20, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - FriendsOnlineStatusItem.PADDING }, - Child = onlineStreamControl = new FriendOnlineStreamControl(), + Child = streamControl = new FriendOnlineStreamControl(), } } }, @@ -157,11 +145,14 @@ namespace osu.Game.Overlays.Dashboard.Friends AutoSizeAxes = Axes.Y, Children = new Drawable[] { - itemsPlaceholder = new Container + panelsContainer = new FriendsSearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING } + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, + // Todo: Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), + Spacing = new Vector2(10), + SortCriteria = { BindTarget = userListToolbar.SortCriteria } }, loading = new LoadingLayer(true) } @@ -175,127 +166,180 @@ namespace osu.Game.Overlays.Dashboard.Friends background.Colour = colourProvider.Background4; controlBackground.Colour = colourProvider.Background5; - - apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged((_, _) => Schedule(() => Users = apiFriends.Select(f => f.TargetUser).ToList()), true); } protected override void LoadComplete() { base.LoadComplete(); - onlineStreamControl.Current.BindValueChanged(_ => recreatePanels()); - userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); - userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); - searchTextBox.Current.BindValueChanged(_ => - { - if (currentContent.IsNotNull()) - currentContent.SearchTerm = searchTextBox.Current.Value; - }); + apiFriends.BindTo(api.Friends); + apiFriends.BindCollectionChanged(onFriendsChanged, true); + + friendPresences.BindTo(metadataClient.FriendPresences); + friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); + + searchTextBox.Current.BindValueChanged(onSearchChanged); + streamControl.Current.BindValueChanged(onFriendsStreamChanged); } - private void recreatePanels() + private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - if (!users.Any()) - return; - - cancellationToken?.Cancel(); - - if (itemsPlaceholder.Any()) - loading.Show(); - - var sortedUsers = sortUsers(getUsersInCurrentGroup()); - - LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); - } - - private List getUsersInCurrentGroup() - { - switch (onlineStreamControl.Current.Value?.Status) + switch (e.Action) { - default: - case OnlineStatus.All: - return users; - - case OnlineStatus.Offline: - return users.Where(u => !u.IsOnline).ToList(); - - case OnlineStatus.Online: - return users.Where(u => u.IsOnline).ToList(); - } - } - - private void addContentToPlaceholder(SearchContainer content) - { - loading.Hide(); - - var lastContent = currentContent; - - if (lastContent != null) - { - lastContent.FadeOut(100, Easing.OutQuint).Expire(); - lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y); - } - - itemsPlaceholder.Add(currentContent = content); - currentContent.FadeIn(200, Easing.OutQuint); - } - - private SearchContainer createTable(List users) - { - var style = userListToolbar.DisplayStyle.Value; - - return new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), - Children = users.Select(u => createUserPanel(u, style)).ToList(), - SearchTerm = searchTextBox.Current.Value, - }; - } - - private UserPanel createUserPanel(APIUser user, OverlayPanelDisplayStyle style) - { - switch (style) - { - default: - case OverlayPanelDisplayStyle.Card: - return new UserGridPanel(user).With(panel => + case NotifyCollectionChangedAction.Add: + foreach (APIRelation relation in e.NewItems!.OfType()) { - panel.Anchor = Anchor.TopCentre; - panel.Origin = Anchor.TopCentre; - panel.Width = 290; - }); + panelsContainer.Add(new FilterableUserPanel(new UserGridPanel(relation.TargetUser!).With(panel => + { + panel.Anchor = Anchor.TopCentre; + panel.Origin = Anchor.TopCentre; + panel.Width = 290; + }))); + } - case OverlayPanelDisplayStyle.List: - return new UserListPanel(user); + break; - case OverlayPanelDisplayStyle.Brick: - return new UserBrickPanel(user); + case NotifyCollectionChangedAction.Remove: + foreach (APIRelation relation in e.OldItems!.OfType()) + panelsContainer.RemoveAll(panel => panel.User.Equals(relation.TargetUser), true); + + break; } + + updateStatusCounts(); } - private List sortUsers(List unsorted) + private void onFriendPresencesChanged(object? sender, NotifyDictionaryChangedEventArgs e) { - switch (userListToolbar.SortCriteria.Value) + switch (e.Action) { - default: - case UserSortCriteria.LastVisit: - return unsorted.OrderByDescending(u => u.LastVisit).ToList(); - - case UserSortCriteria.Rank: - return unsorted.OrderByDescending(u => u.Statistics.GlobalRank.HasValue).ThenBy(u => u.Statistics.GlobalRank ?? 0).ToList(); - - case UserSortCriteria.Username: - return unsorted.OrderBy(u => u.Username).ToList(); + case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Remove: + updatePanelVisibilities(); + updateStatusCounts(); + break; } } - protected override void Dispose(bool isDisposing) + private void onFriendsStreamChanged(ValueChangedEvent stream) { - cancellationToken?.Cancel(); - base.Dispose(isDisposing); + updatePanelVisibilities(); + } + + private void onSearchChanged(ValueChangedEvent search) + { + panelsContainer.SearchTerm = search.NewValue; + } + + private void updatePanelVisibilities() + { + foreach (var panel in panelsContainer) + { + switch (streamControl.Current.Value) + { + case OnlineStatus.All: + panel.CanBeShown.Value = true; + break; + + case OnlineStatus.Online: + panel.CanBeShown.Value = friendPresences.ContainsKey(panel.User.OnlineID); + break; + + case OnlineStatus.Offline: + panel.CanBeShown.Value = !friendPresences.ContainsKey(panel.User.OnlineID); + break; + } + } + } + + private void updateStatusCounts() + { + int countOnline = 0; + int countOffline = 0; + + foreach (var user in apiFriends) + { + if (friendPresences.TryGetValue(user.TargetID, out _)) + countOnline++; + else + countOffline++; + } + + streamControl.CountAll.Value = apiFriends.Count; + streamControl.CountOnline.Value = countOnline; + streamControl.CountOffline.Value = countOffline; + } + + private class FriendsSearchContainer : SearchContainer + { + public readonly IBindable SortCriteria = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + SortCriteria.BindValueChanged(_ => InvalidateLayout(), true); + } + + public override IEnumerable FlowingChildren + { + get + { + IEnumerable panels = base.FlowingChildren.OfType(); + + switch (SortCriteria.Value) + { + default: + case UserSortCriteria.LastVisit: + return panels.OrderByDescending(panel => panel.User.LastVisit); + + case UserSortCriteria.Rank: + return panels.OrderByDescending(panel => panel.User.Statistics.GlobalRank.HasValue).ThenBy(panel => panel.User.Statistics.GlobalRank ?? 0); + + case UserSortCriteria.Username: + return panels.OrderBy(panel => panel.User.Username); + } + } + } + } + + private class FilterableUserPanel : CompositeDrawable, IConditionalFilterable + { + public readonly Bindable CanBeShown = new Bindable(); + + public APIUser User => panel.User; + + private readonly UserPanel panel; + + public FilterableUserPanel(UserPanel panel) + { + this.panel = panel; + + Anchor = panel.Anchor; + Origin = panel.Origin; + RelativeSizeAxes = panel.RelativeSizeAxes; + AutoSizeAxes = panel.AutoSizeAxes; + Width = panel.Width; + Height = panel.Height; + + InternalChild = panel; + } + + IBindable IConditionalFilterable.CanBeShown => CanBeShown; + + IEnumerable IHasFilterTerms.FilterTerms => panel.FilterTerms; + + bool IFilterable.MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + + bool IFilterable.FilteringActive { set { } } } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs index 9f429c23d8..25b29e8d16 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs @@ -1,30 +1,43 @@ // 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.Collections.Generic; -using System.Linq; -using osu.Game.Online.API.Requests.Responses; +using System; +using osu.Framework.Bindables; namespace osu.Game.Overlays.Dashboard.Friends { - public partial class FriendOnlineStreamControl : OverlayStreamControl + public partial class FriendOnlineStreamControl : OverlayStreamControl { - protected override OverlayStreamItem CreateStreamItem(FriendStream value) => new FriendsOnlineStatusItem(value); + public readonly BindableInt CountAll = new BindableInt(); + public readonly BindableInt CountOnline = new BindableInt(); + public readonly BindableInt CountOffline = new BindableInt(); - public void Populate(List users) + public FriendOnlineStreamControl() { - Clear(); + Items = + [ + OnlineStatus.All, + OnlineStatus.Online, + OnlineStatus.Offline + ]; + } - int userCount = users.Count; - int onlineUsersCount = users.Count(user => user.IsOnline); + protected override OverlayStreamItem CreateStreamItem(OnlineStatus value) + { + switch (value) + { + case OnlineStatus.All: + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountAll } }; - AddItem(new FriendStream(OnlineStatus.All, userCount)); - AddItem(new FriendStream(OnlineStatus.Online, onlineUsersCount)); - AddItem(new FriendStream(OnlineStatus.Offline, userCount - onlineUsersCount)); + case OnlineStatus.Online: + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountOnline } }; - Current.Value = Items.FirstOrDefault(); + case OnlineStatus.Offline: + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountOffline } }; + + default: + throw new ArgumentException(nameof(value)); + } } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs index 4abece9a8d..f791e34c8f 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs @@ -1,18 +1,18 @@ // 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; + namespace osu.Game.Overlays.Dashboard.Friends { public class FriendStream { - public OnlineStatus Status { get; } + public readonly BindableInt UserCount = new BindableInt(); + public readonly OnlineStatus Status; - public int Count { get; } - - public FriendStream(OnlineStatus status, int count) + public FriendStream(OnlineStatus status) { Status = status; - Count = count; } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index 2aea631b7c..459592085b 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs @@ -2,27 +2,32 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Localisation; using osu.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Overlays.Dashboard.Friends { - public partial class FriendsOnlineStatusItem : OverlayStreamItem + public partial class FriendsOnlineStatusItem : OverlayStreamItem { - public FriendsOnlineStatusItem(FriendStream value) + public readonly IBindable UserCount = new Bindable(); + + public FriendsOnlineStatusItem(OnlineStatus value) : base(value) { + MainText = value.GetLocalisableDescription(); } - protected override LocalisableString MainText => Value.Status.GetLocalisableDescription(); - - protected override LocalisableString AdditionalText => Value.Count.ToString(); + protected override void LoadComplete() + { + base.LoadComplete(); + UserCount.BindValueChanged(count => AdditionalText = count.NewValue.ToString(), true); + } protected override Color4 GetBarColour(OsuColour colours) { - switch (Value.Status) + switch (Value) { case OnlineStatus.All: return Color4.White; @@ -34,7 +39,7 @@ namespace osu.Game.Overlays.Dashboard.Friends return Color4.Black; default: - throw new ArgumentException($@"{Value.Status} status does not provide a colour in {nameof(GetBarColour)}."); + throw new ArgumentException($@"{Value} status does not provide a colour in {nameof(GetBarColour)}."); } } } diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs index f0ae0b41fc..ec04a130cf 100644 --- a/osu.Game/Overlays/OverlayStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -1,8 +1,6 @@ // 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 osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Graphics.UserInterface; @@ -22,7 +20,9 @@ namespace osu.Game.Overlays { public abstract partial class OverlayStreamItem : TabItem { - public readonly Bindable SelectedItem = new Bindable(); + public const float PADDING = 5; + + public readonly Bindable SelectedItem = new Bindable(); private bool userHoveringArea; @@ -38,10 +38,12 @@ namespace osu.Game.Overlays } } - private FillFlowContainer text; - private ExpandingBar expandingBar; - - public const float PADDING = 5; + private FillFlowContainer text = null!; + private ExpandingBar expandingBar = null!; + private Sample selectSample = null!; + private OsuSpriteText? mainTextPiece; + private OsuSpriteText? additionalTextPiece; + private OsuSpriteText? infoTextPiece; protected OverlayStreamItem(T value) : base(value) @@ -51,8 +53,6 @@ namespace osu.Game.Overlays Margin = new MarginPadding(PADDING); } - private Sample selectSample; - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours, AudioManager audio) { @@ -65,17 +65,17 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Top = 6 }, Children = new[] { - new OsuSpriteText + mainTextPiece = new OsuSpriteText { Text = MainText, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), }, - new OsuSpriteText + additionalTextPiece = new OsuSpriteText { Text = AdditionalText, Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), }, - new OsuSpriteText + infoTextPiece = new OsuSpriteText { Text = InfoText, Font = OsuFont.GetFont(size: 10), @@ -99,11 +99,47 @@ namespace osu.Game.Overlays SelectedItem.BindValueChanged(_ => updateState(), true); } - protected abstract LocalisableString MainText { get; } + private LocalisableString mainText; - protected abstract LocalisableString AdditionalText { get; } + protected LocalisableString MainText + { + get => mainText; + set + { + mainText = value; - protected virtual LocalisableString InfoText => string.Empty; + if (mainTextPiece != null) + mainTextPiece.Text = value; + } + } + + private LocalisableString additionalText; + + protected LocalisableString AdditionalText + { + get => additionalText; + set + { + additionalText = value; + + if (additionalTextPiece != null) + additionalTextPiece.Text = value; + } + } + + private LocalisableString infoText; + + protected LocalisableString InfoText + { + get => infoText; + set + { + infoText = value; + + if (infoTextPiece != null) + infoTextPiece.Text = value; + } + } protected abstract Color4 GetBarColour(OsuColour colours); From 1a5a47347659a079924a4767f74d5f086d9b11c2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 19:28:59 +0900 Subject: [PATCH 218/349] Update panel visibility when friends are added/removed --- osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 2938be732c..308be32cf9 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -206,6 +206,7 @@ namespace osu.Game.Overlays.Dashboard.Friends break; } + updatePanelVisibilities(); updateStatusCounts(); } From 6378c8ed754daac6c621cc58061dfd805515f666 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Mar 2025 19:29:10 +0900 Subject: [PATCH 219/349] Simplify condition --- osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 308be32cf9..4bd79188c8 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -260,7 +260,7 @@ namespace osu.Game.Overlays.Dashboard.Friends foreach (var user in apiFriends) { - if (friendPresences.TryGetValue(user.TargetID, out _)) + if (friendPresences.ContainsKey(user.TargetID)) countOnline++; else countOffline++; From 2af3bebff38b13220f04df04301d8ca1b3d881b1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 11 Mar 2025 08:26:45 -0400 Subject: [PATCH 220/349] Add draw size invalidation handling to column extension logic Also indirectly makes the setting effective during gameplay as requested. --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 93 ++++++++++++------------ 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 6a1949e978..46a7a70f27 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Skinning; @@ -38,6 +39,8 @@ namespace osu.Game.Rulesets.Mania.UI set => base.Masking = value; } + private readonly LayoutValue columnSizeLayout = new LayoutValue(Invalidation.DrawSize); + public ColumnFlow(StageDefinition stageDefinition) { this.stageDefinition = stageDefinition; @@ -56,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < stageDefinition.Columns; i++) columns.Add(new Container { RelativeSizeAxes = Axes.Y }); + + AddLayout(columnSizeLayout); } [Resolved] @@ -67,20 +72,54 @@ namespace osu.Game.Rulesets.Mania.UI private void load(ManiaRulesetConfigManager? rulesetConfig) { rulesetConfig?.BindWith(ManiaRulesetSetting.MobilePlayStyle, mobilePlayStyle); - mobilePlayStyle.BindValueChanged(_ => updateMobileSizing()); - skin.SourceChanged += onSkinChanged; - onSkinChanged(); + mobilePlayStyle.BindValueChanged(_ => updateColumnSize()); + skin.SourceChanged += updateColumnSize; } - protected override void LoadComplete() + protected override void Update() { - base.LoadComplete(); - updateMobileSizing(); + base.Update(); + + if (!columnSizeLayout.IsValid) + { + updateColumnSize(); + columnSizeLayout.Validate(); + } } - private void onSkinChanged() + /// + /// Sets the content of one of the columns of this . + /// + /// The index of the column to set the content of. + /// The content. + public void SetContentForColumn(int column, TContent content) { + Content[column] = columns[column].Child = content; + } + + private void updateColumnSize() + { + float mobileAdjust = 1f; + + if (mobilePlayStyle.Value == ManiaMobilePlayStyle.ExtendedColumns) + { + // GridContainer+CellContainer containing this stage (gets split up for dual stages). + Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; + + // Will be null in tests. + if (containingCell != null && containingCell.Value.X >= containingCell.Value.Y) + { + float aspectRatio = containingCell.Value.X / containingCell.Value.Y; + + // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon) + mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns); + // 1.92 is a "reference" mobile screen aspect ratio for phones. + // We should scale it back for cases like tablets which aren't so extreme. + mobileAdjust *= aspectRatio / 1.92f; + } + } + for (int i = 0; i < stageDefinition.Columns; i++) { if (i > 0) @@ -101,44 +140,8 @@ namespace osu.Game.Rulesets.Mania.UI // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; - columns[i].Width = width.Value; + columns[i].Width = width.Value * mobileAdjust; } - - updateMobileSizing(); - } - - /// - /// Sets the content of one of the columns of this . - /// - /// The index of the column to set the content of. - /// The content. - public void SetContentForColumn(int column, TContent content) - { - Content[column] = columns[column].Child = content; - } - - private void updateMobileSizing() - { - if (!IsLoaded || !RuntimeInfo.IsMobile || mobilePlayStyle.Value != ManiaMobilePlayStyle.ExtendedColumns) - return; - - // GridContainer+CellContainer containing this stage (gets split up for dual stages). - Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; - - // Will be null in tests. - if (containingCell == null || containingCell.Value.X < containingCell.Value.Y) - return; - - float aspectRatio = containingCell.Value.X / containingCell.Value.Y; - - // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon) - float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns); - // 1.92 is a "reference" mobile screen aspect ratio for phones. - // We should scale it back for cases like tablets which aren't so extreme. - mobileAdjust *= aspectRatio / 1.92f; - - for (int i = 0; i < stageDefinition.Columns; i++) - columns[i].Width *= mobileAdjust; } protected override void Dispose(bool isDisposing) @@ -146,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); if (skin.IsNotNull()) - skin.SourceChanged -= onSkinChanged; + skin.SourceChanged -= updateColumnSize; } } } From c99448939258b8d7ec7c39b3d1f17b71d5dec38b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 21:28:35 +0900 Subject: [PATCH 221/349] Fix silly test failures --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 95a134e204..9f0dc75f84 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -135,7 +135,13 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaSize { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); - public BindableFloat PressureThreshold { get; } = new BindableFloat(); + + public BindableFloat PressureThreshold { get; } = new BindableFloat + { + MinValue = 0f, + MaxValue = 1f, + Precision = 0.005f, + }; public IBindable Tablet => tablet; From b64e69d581a08d0be9d5e705f9bb1078faf3050f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 11 Mar 2025 09:04:07 -0400 Subject: [PATCH 222/349] Apply suggested renames --- .../TestSceneManiaTouchInput.cs | 2 +- .../Configuration/ManiaRulesetConfigManager.cs | 4 ++-- ...iaMobilePlayStyle.cs => ManiaMobileLayout.cs} | 14 +++++++------- .../ManiaSettingsSubsection.cs | 15 ++++++++++----- osu.Game.Rulesets.Mania/UI/Column.cs | 8 ++++---- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 8 ++++---- .../UI/DrawableManiaRuleset.cs | 2 +- .../UI/ManiaTouchInputArea.cs | 6 +++--- osu.Game/Localisation/RulesetSettingsStrings.cs | 16 ++++++++-------- 9 files changed, 40 insertions(+), 35 deletions(-) rename osu.Game.Rulesets.Mania/{ManiaMobilePlayStyle.cs => ManiaMobileLayout.cs} (62%) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs index b33c74d4e9..fc495a5ab0 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Mania.Tests private void toggleTouchControls(bool enabled) { var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!; - maniaConfig.SetValue(ManiaRulesetSetting.MobilePlayStyle, enabled ? ManiaMobilePlayStyle.TouchControls : ManiaMobilePlayStyle.TouchableColumns); + maniaConfig.SetValue(ManiaRulesetSetting.MobileLayout, enabled ? ManiaMobileLayout.LandscapeWithOverlay : ManiaMobileLayout.Portrait); } private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 10a3236178..5242b6685c 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Configuration SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); - SetDefault(ManiaRulesetSetting.MobilePlayStyle, ManiaMobilePlayStyle.TouchableColumns); + SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait); #pragma warning disable CS0618 // Although obsolete, this is still required to populate the bindable from the database in case migration is required. @@ -57,6 +57,6 @@ namespace osu.Game.Rulesets.Mania.Configuration ScrollSpeed, ScrollDirection, TimingBasedNoteColouring, - MobilePlayStyle, + MobileLayout, } } diff --git a/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs b/osu.Game.Rulesets.Mania/ManiaMobileLayout.cs similarity index 62% rename from osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs rename to osu.Game.Rulesets.Mania/ManiaMobileLayout.cs index e6b1224fd3..7d70dba092 100644 --- a/osu.Game.Rulesets.Mania/ManiaMobilePlayStyle.cs +++ b/osu.Game.Rulesets.Mania/ManiaMobileLayout.cs @@ -6,15 +6,15 @@ using osu.Game.Localisation; namespace osu.Game.Rulesets.Mania { - public enum ManiaMobilePlayStyle + public enum ManiaMobileLayout { - [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.TouchableColumns))] - TouchableColumns, + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.PortraitExpandedColumns))] + Portrait, - [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.TouchControls))] - TouchControls, + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeExpandedColumns))] + Landscape, - [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ExtendedColumns))] - ExtendedColumns, + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeTouchOverlay))] + LandscapeWithOverlay, } } diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index f558d30ee0..5ae7ec9480 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.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 osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -45,12 +46,16 @@ namespace osu.Game.Rulesets.Mania LabelText = RulesetSettingsStrings.TimingBasedColouring, Current = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring), }, - new SettingsEnumDropdown - { - LabelText = RulesetSettingsStrings.MobilePlayStyle, - Current = config.GetBindable(ManiaRulesetSetting.MobilePlayStyle), - }, }; + + if (RuntimeInfo.IsMobile) + { + Add(new SettingsEnumDropdown + { + LabelText = RulesetSettingsStrings.MobileLayout, + Current = config.GetBindable(ManiaRulesetSetting.MobileLayout), + }); + } } private partial class ManiaScrollSlider : RoundedSliderBar diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index ebd8efe124..cb825761d1 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable AccentColour = new Bindable(Color4.Black); - private IBindable mobilePlayStyle = null!; + private IBindable mobilePlayStyle = null!; public Column(int index, bool isSpecial) { @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); if (rulesetConfig != null) - mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileLayout); } private void onSourceChanged() @@ -199,8 +199,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - // if touch controls are selected, disallow columns from handling touch directly. - if (mobilePlayStyle.Value == ManiaMobilePlayStyle.TouchControls) + // if touch overlay is visible, disallow columns from handling touch directly. + if (mobilePlayStyle.Value == ManiaMobileLayout.LandscapeWithOverlay) return false; maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 46a7a70f27..3b19e90b60 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -66,14 +66,14 @@ namespace osu.Game.Rulesets.Mania.UI [Resolved] private ISkinSource skin { get; set; } = null!; - private readonly Bindable mobilePlayStyle = new Bindable(); + private readonly Bindable mobileLayout = new Bindable(); [BackgroundDependencyLoader] private void load(ManiaRulesetConfigManager? rulesetConfig) { - rulesetConfig?.BindWith(ManiaRulesetSetting.MobilePlayStyle, mobilePlayStyle); + rulesetConfig?.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout); - mobilePlayStyle.BindValueChanged(_ => updateColumnSize()); + mobileLayout.BindValueChanged(_ => updateColumnSize()); skin.SourceChanged += updateColumnSize; } @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.UI { float mobileAdjust = 1f; - if (mobilePlayStyle.Value == ManiaMobilePlayStyle.ExtendedColumns) + if (RuntimeInfo.IsMobile && mobileLayout.Value == ManiaMobileLayout.Landscape) { // GridContainer+CellContainer containing this stage (gets split up for dual stages). Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 2dcbcacf93..bac62d2b66 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - private bool playsWithTouchableColumns => Config.Get(ManiaRulesetSetting.MobilePlayStyle) == ManiaMobilePlayStyle.TouchableColumns; + private bool playsWithTouchableColumns => Config.Get(ManiaRulesetSetting.MobileLayout) == ManiaMobileLayout.Portrait; public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && playsWithTouchableColumns; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 7cb6b3b96f..e9489d4c06 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -97,14 +97,14 @@ namespace osu.Game.Rulesets.Mania.UI }; } - private IBindable mobilePlayStyle = null!; + private IBindable mobilePlayStyle = null!; protected override void LoadComplete() { base.LoadComplete(); - mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobilePlayStyle); - mobilePlayStyle.BindValueChanged(p => touchControls.Value = p.NewValue == ManiaMobilePlayStyle.TouchControls, true); + mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileLayout); + mobilePlayStyle.BindValueChanged(p => touchControls.Value = p.NewValue == ManiaMobileLayout.LandscapeWithOverlay, true); Opacity.BindValueChanged(o => Alpha = o.NewValue, true); } diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index 527707b011..fc4fb58e26 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -90,24 +90,24 @@ namespace osu.Game.Localisation public static LocalisableString TouchControlScheme => new TranslatableString(getKey(@"touch_control_scheme"), @"Touch control scheme"); /// - /// "Mobile play style" + /// "Mobile layout" /// - public static LocalisableString MobilePlayStyle => new TranslatableString(getKey(@"mobile_play_style"), @"Mobile play style"); + public static LocalisableString MobileLayout => new TranslatableString(getKey(@"mobile_layout"), @"Mobile layout"); /// - /// "Touchable columns" + /// "Portrait (expanded columns)" /// - public static LocalisableString TouchableColumns => new TranslatableString(getKey(@"touchable_columns"), @"Touchable columns"); + public static LocalisableString PortraitExpandedColumns => new TranslatableString(getKey(@"portrait_expanded_columns"), @"Portrait (expanded columns)"); /// - /// "Touch controls" + /// "Landscape (expanded columns)" /// - public static LocalisableString TouchControls => new TranslatableString(getKey(@"touch_controls"), @"Touch controls"); + public static LocalisableString LandscapeExpandedColumns => new TranslatableString(getKey(@"landscape_expanded_columns"), @"Landscape (expanded columns)"); /// - /// "Extended columns" + /// "Landscape (touch overlay)" /// - public static LocalisableString ExtendedColumns => new TranslatableString(getKey(@"extended_columns"), @"Extended columns"); + public static LocalisableString LandscapeTouchOverlay => new TranslatableString(getKey(@"landscape_touch_overlay"), @"Landscape (touch overlay)"); private static string getKey(string key) => $@"{prefix}:{key}"; } From b3ea63598e892ba860d21992f3dcceb4f2244712 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 11 Mar 2025 09:17:19 -0400 Subject: [PATCH 223/349] Fix touch input area not handling settings changes --- .../UI/DrawableManiaRuleset.cs | 27 +++++++++++++++--- .../UI/ManiaTouchInputArea.cs | 28 +++---------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index bac62d2b66..66400b0a55 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,9 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - private bool playsWithTouchableColumns => Config.Get(ManiaRulesetSetting.MobileLayout) == ManiaMobileLayout.Portrait; - - public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && playsWithTouchableColumns; + public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && mobileLayout.Value == ManiaMobileLayout.Portrait; protected override bool RelativeScaleBeatLengths => true; @@ -60,6 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Bindable configDirection = new Bindable(); private readonly BindableDouble configScrollSpeed = new BindableDouble(); + private readonly Bindable mobileLayout = new Bindable(); private double currentTimeRange; protected double TargetTimeRange; @@ -114,7 +113,27 @@ namespace osu.Game.Rulesets.Mania.UI TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); - KeyBindingInputManager.Add(new ManiaTouchInputArea(this)); + Config.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout); + mobileLayout.BindValueChanged(_ => updateMobileLayout(), true); + } + + private ManiaTouchInputArea? touchInputArea; + + private void updateMobileLayout() + { + switch (mobileLayout.Value) + { + case ManiaMobileLayout.LandscapeWithOverlay: + KeyBindingInputManager.Add(touchInputArea = new ManiaTouchInputArea(this)); + break; + + default: + if (touchInputArea != null) + KeyBindingInputManager.Remove(touchInputArea, true); + + touchInputArea = null; + break; + } } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index e9489d4c06..1df05bf350 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -46,8 +46,6 @@ namespace osu.Game.Rulesets.Mania.UI private GridContainer gridContainer = null!; - private readonly BindableBool touchControls = new BindableBool(); - public ManiaTouchInputArea(DrawableManiaRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; @@ -80,7 +78,6 @@ namespace osu.Game.Rulesets.Mania.UI receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action }, - Enabled = { BindTarget = touchControls }, }); receptorGridDimensions.Add(new Dimension()); @@ -97,15 +94,9 @@ namespace osu.Game.Rulesets.Mania.UI }; } - private IBindable mobilePlayStyle = null!; - protected override void LoadComplete() { base.LoadComplete(); - - mobilePlayStyle = rulesetConfig.GetBindable(ManiaRulesetSetting.MobileLayout); - mobilePlayStyle.BindValueChanged(p => touchControls.Value = p.NewValue == ManiaMobileLayout.LandscapeWithOverlay, true); - Opacity.BindValueChanged(o => Alpha = o.NewValue, true); } @@ -118,13 +109,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - if (touchControls.Value) - { - Show(); - return true; - } - - return false; + Show(); + return true; } protected override void PopIn() @@ -140,7 +126,6 @@ namespace osu.Game.Rulesets.Mania.UI public partial class ColumnInputReceptor : CompositeDrawable { public readonly IBindable Action = new Bindable(); - public readonly IBindable Enabled = new BindableBool(); private readonly Box highlightOverlay; @@ -180,13 +165,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - if (Enabled.Value) - { - updateButton(true); - return false; // handled by parent container to show overlay. - } - - return false; + updateButton(true); + return false; // handled by parent container to show overlay. } protected override void OnTouchUp(TouchUpEvent e) From 54d7a91cabc04e63873e53ec8ccefd69662e36f1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 12 Mar 2025 00:36:28 -0400 Subject: [PATCH 224/349] Fix osu!taiko mobile scaling not accurate --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 6a9e5789de..07fda13c8c 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -60,19 +59,7 @@ namespace osu.Game.Rulesets.Taiko.UI // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions. relativeHeight = Math.Min(relativeHeight, 1f / 3f); - Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f)); - - // on mobile platforms where the base aspect ratio is wider, the taiko playfield - // needs to be scaled down to remain playable. - if (RuntimeInfo.IsMobile && osuGame != null) - { - const float base_aspect_ratio = 1024f / 768f; - float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y; - // this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases. - const float magic_scale = 1.25f; - Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio); - } - + Scale = new Vector2(Parent!.ChildSize.Y / 768f * (relativeHeight / base_relative_height)); Width = 1 / Scale.X; } From 65cdcb469603ca22ec36872d20e07ad8f9fd563f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 14:01:11 +0900 Subject: [PATCH 225/349] Fix default beatmap not being correctly set after aborting new beatmap creation Closes https://github.com/ppy/osu/issues/32337. --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 ++ osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 996e87ff8a..2758954907 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -94,6 +94,8 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull().ID)?.Value.DeletePending == true); + + AddUntilStep("wait for default beatmap", () => Editor.Beatmap.Value is DummyWorkingBeatmap); } [Test] diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index fdeb840977..bd125deddf 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap([CanBeNull] BeatmapInfo beatmapInfo) { - if (beatmapInfo?.BeatmapSet == null) + if (beatmapInfo?.ID == DefaultBeatmap.BeatmapInfo.ID || beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; lock (workingCache) From 7f4f92dedf35e6933a2a3f242484eb81c2279e88 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 12 Mar 2025 01:14:31 -0400 Subject: [PATCH 226/349] Remove unnecessary DI property --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 07fda13c8c..9f821ee93d 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Taiko.Beatmaps; @@ -20,9 +19,6 @@ namespace osu.Game.Rulesets.Taiko.UI public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); - [Resolved] - private OsuGame? osuGame { get; set; } - public TaikoPlayfieldAdjustmentContainer() { RelativeSizeAxes = Axes.X; From 72854d0ae64c0c867b9b9bf2a601bfba6c115a1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 14:20:13 +0900 Subject: [PATCH 227/349] Fix storyboard letterbox hiding HUD elements Addresses https://github.com/ppy/osu/discussions/29788. --- osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs | 2 +- osu.Game/Screens/Play/BreakOverlay.cs | 8 +------- osu.Game/Screens/Play/Player.cs | 9 ++++++++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 21b6495865..9fc1ce3027 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, }, breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset())) + breakOverlay = new BreakOverlay(new ScoreProcessor(new OsuRuleset())) { ProcessCustomClock = false, BreakTracker = breakTracker, diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 550d29965f..49b7067c8d 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play private readonly IBindable currentPeriod = new Bindable(); - public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) + public BreakOverlay(ScoreProcessor scoreProcessor) { this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; @@ -63,12 +63,6 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new LetterboxOverlay - { - Alpha = letterboxing ? 1 : 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, new CircularContainer { Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 92c483b24a..29b54c8699 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,6 +34,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -450,6 +451,12 @@ namespace osu.Game.Screens.Play Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), + new LetterboxOverlay + { + Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { HoldToQuit = @@ -468,7 +475,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre }, - BreakOverlay = new BreakOverlay(working.Beatmap.LetterboxInBreaks, ScoreProcessor) + BreakOverlay = new BreakOverlay(ScoreProcessor) { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, From 5a5246a40710491fc1d29ee095cd54a20ac4d40e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 12 Mar 2025 03:04:06 -0400 Subject: [PATCH 228/349] Improve handling for top-anchored judgement positions in osu!mania --- .../Skinning/Argon/ArgonJudgementPiece.cs | 6 ++++- .../Legacy/LegacyManiaJudgementPiece.cs | 15 ++++++++---- .../UI/DefaultManiaJudgementPiece.cs | 6 ++++- .../UI/DrawableManiaJudgement.cs | 23 +++++++------------ 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index 6098459f6b..bef8625ea2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -53,7 +53,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon } } - private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + private void onDirectionChanged() + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + } protected override SpriteText CreateJudgementText() => new OsuSpriteText diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 3752c5f27a..5ece5df66f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy this.result = result; this.animation = animation; - Anchor = Anchor.BottomCentre; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -53,10 +52,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; - float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; - float finalPosition = scorePosition - absoluteHitPosition; + float hitPositionFromTop = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; - Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition; + if (scorePosition > hitPositionFromTop / 2f) + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Y = direction.Value == ScrollingDirection.Up ? hitPositionFromTop - scorePosition : scorePosition - hitPositionFromTop; + } + else + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.BottomCentre : Anchor.TopCentre; + Y = direction.Value == ScrollingDirection.Up ? -scorePosition : scorePosition; + } } public void PlayAnimation() diff --git a/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs index f0af6085d0..4872fe5049 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs @@ -29,7 +29,11 @@ namespace osu.Game.Rulesets.Mania.UI direction.BindValueChanged(_ => onDirectionChanged(), true); } - private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + private void onDirectionChanged() + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + } protected override void LoadComplete() { diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 40fef1a56a..20248ab6bc 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -3,30 +3,23 @@ #nullable disable -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.UI.Scrolling; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { public partial class DrawableManiaJudgement : DrawableJudgement { - private IBindable direction; - - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) + public DrawableManiaJudgement() { - direction = scrollingInfo.Direction.GetBoundCopy(); - direction.BindValueChanged(_ => onDirectionChanged(), true); - } - - private void onDirectionChanged() - { - Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; - Origin = Anchor.Centre; + // Extend the dimensions of this drawable to the entire parenting container. + // This allows skin implementations (i.e. LegacyManiaJudgementPiece) to freely choose the anchor based on skin settings. + Anchor = Anchor.TopLeft; + Origin = Anchor.TopLeft; + RelativeSizeAxes = Axes.Both; + Size = new Vector2(1f); } protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); From a492e070fbf939b9b70022c8ed501a5cf7180a9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 15:57:53 +0900 Subject: [PATCH 229/349] Add failing test cases that came up in review --- .../Editing/TestScenePlacementBlueprint.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 37caccfa0d..ae20f5e5cf 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -73,8 +73,11 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); AddAssert("new combo true", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.True)); + AddAssert("context menu not visible", () => !Editor.ChildrenOfType().Any(c => c.IsPresent)); + AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); + AddAssert("context menu not visible", () => !Editor.ChildrenOfType().Any(c => c.IsPresent)); } [Test] @@ -89,6 +92,8 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(0).Items); AddAssert("circle not selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Exactly(0).Items); + AddAssert("context menu not visible", () => !Editor.ChildrenOfType().Any(c => c.IsPresent)); + AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); } [Test] @@ -97,13 +102,22 @@ namespace osu.Game.Tests.Visual.Editing AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddStep("place circle", () => InputManager.Click(MouseButton.Left)); - AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + + // ensure the circle we're selecting is not a new combo so we can assert + // new combo doesn't happen to get toggled by right click. + AddStep("seek forward", () => EditorClock.Seek(1000)); + AddStep("place second circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("two circles added", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items); + AddAssert("context menu not visible", () => !Editor.ChildrenOfType().Any(c => c.IsPresent)); AddStep("select selection tool", () => InputManager.Key(Key.Number1)); AddStep("click right mouse", () => InputManager.Click(MouseButton.Right)); - AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items); + AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items); AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items); + AddAssert("context menu visible", () => Editor.ChildrenOfType().Any(c => c.IsPresent)); + AddAssert("new combo false", () => this.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(TernaryState.False)); } [Test] From db7d1f32d7801099caef27aa92272a2715657d38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 16:42:49 +0900 Subject: [PATCH 230/349] Fix quick delete still propagating right click mouse input upwards --- .../Edit/Compose/Components/BlueprintContainer.cs | 14 ++++++++------ .../Edit/Compose/Components/SelectionHandler.cs | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index dc04561242..872c4c2465 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -115,19 +115,21 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - bool selectionPerformed = performMouseDownActions(e); + var selectionBefore = SelectionHandler.SelectedItems.ToArray(); + bool handled = performMouseDownActions(e); + bool selectionChanged = !SelectionHandler.SelectedItems.SequenceEqual(selectionBefore); bool movementPossible = prepareSelectionMovement(e); - // check if selection has occurred - if (selectionPerformed) + if (selectionChanged) { - // only unmodified right click should show context menu + // if the selection changed and there are no modifiers pressed, don't block so the context menu still shows. bool shouldShowContextMenu = e.Button == MouseButton.Right && !e.ShiftPressed && !e.AltPressed && !e.SuperPressed; - - // stop propagation if not showing context menu return !shouldShowContextMenu; } + if (handled) + return true; + // even if a selection didn't occur, a drag event may still move the selection. return e.Button == MouseButton.Left && movementPossible; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 59b64ad192..758b712fef 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -262,7 +262,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The blueprint. /// The mouse event responsible for selection. - /// Whether a selection was performed. + /// Whether an action was performed. internal virtual bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (ShouldQuickDelete(e)) From 6a7a83415199b17a1371a9c6c791246e075f7221 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 16:52:51 +0900 Subject: [PATCH 231/349] Fix new combo being toggled when selection is made --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index ee386aa366..8917a68efa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -358,8 +358,12 @@ namespace osu.Game.Rulesets.Osu.Edit { var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler; - osuSelectionHandler.SelectionNewComboState.Value = - osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False; + if (!osuSelectionHandler.SelectedItems.Any()) + { + osuSelectionHandler.SelectionNewComboState.Value = + osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False; + return true; + } } return base.OnMouseDown(e); From c2de43f6777126e57034a4e02f07408e0aa93d1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 16:56:16 +0900 Subject: [PATCH 232/349] Add explanation of why the logic is in such a bad place --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 8917a68efa..ed3fc34d94 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -354,6 +354,18 @@ namespace osu.Game.Rulesets.Osu.Edit protected override bool OnMouseDown(MouseDownEvent e) { + // Why is this logic here and not in `OsuSelectionHandler`? + // Because we only want to handle this toggle after all other right-click handling completes. + // + // Consider that input is handled from the most nested child first: + // + // ComposeScreen + // |- OsuContextMenuContainer // right click for context + // |- TimelineBlueprintContainer + // |- TimelineSelectionHandler + // |- (Osu)HitObjectComposer // right click for toggle new combo + // |- (Osu)EditorBlueprintContainer // right click for select + // |- (Osu)EditorSelectionHandler // right click for delete if (e.Button == MouseButton.Right) { var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler; From aeb55ee25da10960e7aa98f60ea7a7d0438299c6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Mar 2025 17:07:23 +0900 Subject: [PATCH 233/349] Don't use `is` for null-checks --- .../Playlists/PlaylistsRoomSubScreen.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index f8dd9cd3d9..31d5c7ea33 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -526,19 +526,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// /// Responds to changes in to validate the user style and update the global gameplay state. /// - private void onSelectedItemChanged(ValueChangedEvent e) + private void onSelectedItemChanged(ValueChangedEvent item) { - if (e.NewValue is not PlaylistItem item) + if (item.NewValue == null) return; // Always resetting the user beatmap style when a new item is selected is most intuitive. UserBeatmap.Value = null; - if (item.Freestyle) + if (item.NewValue.Freestyle) { // If freestyle is active, attempt to preserve the user ruleset style but only if the online item is from the osu! ruleset // (i.e. the beatmap is generally always convertible to the current ruleset, excluding custom rulesets). - if (item.RulesetID > 0) + if (item.NewValue.RulesetID > 0) UserRuleset.Value = null; } else @@ -554,9 +554,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private Mod[] listAllowedMods() { - if (SelectedItem.Value is not PlaylistItem item) + if (SelectedItem.Value == null) return []; + PlaylistItem item = SelectedItem.Value; + RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); @@ -580,9 +582,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private void updateGameplayState() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (!this.IsCurrentScreen() || SelectedItem.Value == null) return; + PlaylistItem item = SelectedItem.Value; + IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap; RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = gameplayRuleset.CreateInstance(); @@ -638,9 +642,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private void startPlay() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (!this.IsCurrentScreen() || SelectedItem.Value == null) return; + PlaylistItem item = SelectedItem.Value; + // Required for validation inside the player. RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!; IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap; @@ -661,7 +667,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private void showUserModSelect() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) + if (!this.IsCurrentScreen() || SelectedItem.Value == null) return; userModsSelectOverlay.Show(); @@ -672,10 +678,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// private void showUserStyleSelect() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (!this.IsCurrentScreen() || SelectedItem.Value == null) return; - this.Push(new PlaylistsRoomFreestyleSelect(room, item) + this.Push(new PlaylistsRoomFreestyleSelect(room, SelectedItem.Value) { Beatmap = { BindTarget = UserBeatmap }, Ruleset = { BindTarget = UserRuleset } From 16afd5f1179f02a791949e3e35dafa8773c1e9fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:18:30 +0900 Subject: [PATCH 234/349] Use reference check rather than `Guid` comparison --- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index bd125deddf..30bbbbc1fe 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap([CanBeNull] BeatmapInfo beatmapInfo) { - if (beatmapInfo?.ID == DefaultBeatmap.BeatmapInfo.ID || beatmapInfo?.BeatmapSet == null) + if (beatmapInfo == null || ReferenceEquals(beatmapInfo, DefaultBeatmap.BeatmapInfo)) return DefaultBeatmap; lock (workingCache) From 77aac2922f5135b9a9eac5a163bb79aae1c95391 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:31:27 +0900 Subject: [PATCH 235/349] Fix `LetterboxOverlay` not handling its own visibility --- .../Visual/Gameplay/TestSceneBreakTracker.cs | 7 +- .../Gameplay/TestSceneLetterboxOverlay.cs | 24 ------ .../Screens/Play/Break/LetterboxOverlay.cs | 42 ---------- osu.Game/Screens/Play/BreakOverlay.cs | 4 +- osu.Game/Screens/Play/LetterboxOverlay.cs | 82 +++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 6 +- 6 files changed, 93 insertions(+), 72 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs delete mode 100644 osu.Game/Screens/Play/Break/LetterboxOverlay.cs create mode 100644 osu.Game/Screens/Play/LetterboxOverlay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 9fc1ce3027..844f5cba01 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Gameplay { ProcessCustomClock = false, BreakTracker = breakTracker, - } + }, + new LetterboxOverlay + { + ProcessCustomClock = false, + BreakTracker = breakTracker, + }, }; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs deleted file mode 100644 index ce93837925..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Screens.Play.Break; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public partial class TestSceneLetterboxOverlay : OsuTestScene - { - public TestSceneLetterboxOverlay() - { - AddRange(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both - }, - new LetterboxOverlay() - }); - } - } -} diff --git a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs deleted file mode 100644 index 9308a02b07..0000000000 --- a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osuTK.Graphics; - -namespace osu.Game.Screens.Play.Break -{ - public partial class LetterboxOverlay : CompositeDrawable - { - private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); - - public LetterboxOverlay() - { - const int height = 150; - - RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, - Height = height, - Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), - }, - new Box - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = height, - Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), - } - }; - } - } -} diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 49b7067c8d..2ae66a6dc4 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Play public override bool RemoveCompletedTransforms => false; - public BreakTracker BreakTracker { get; init; } = null!; + public required BreakTracker BreakTracker { get; init; } private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Play if (currentPeriod.Value == null) return; - float timeBoxTargetWidth = (float)Math.Max(0, (remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration)); + float timeBoxTargetWidth = (float)Math.Max(0, remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration); remainingTimeBox.ResizeWidthTo(timeBoxTargetWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint); } diff --git a/osu.Game/Screens/Play/LetterboxOverlay.cs b/osu.Game/Screens/Play/LetterboxOverlay.cs new file mode 100644 index 0000000000..168c707c3b --- /dev/null +++ b/osu.Game/Screens/Play/LetterboxOverlay.cs @@ -0,0 +1,82 @@ +// 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.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Utils; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + public partial class LetterboxOverlay : CompositeDrawable + { + public required BreakTracker BreakTracker { get; init; } + + private readonly Container fadeContainer; + + private readonly IBindable currentPeriod = new Bindable(); + + private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); + + public LetterboxOverlay() + { + const int letterbox_height = 150; + + RelativeSizeAxes = Axes.Both; + + InternalChild = fadeContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + Height = letterbox_height, + Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), + }, + new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = letterbox_height, + Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + currentPeriod.BindTo(BreakTracker.CurrentPeriod); + currentPeriod.BindValueChanged(updateDisplay, true); + } + + private void updateDisplay(ValueChangedEvent period) + { + FinishTransforms(true); + Scheduler.CancelDelayedTasks(); + + if (period.NewValue == null) + return; + + var b = period.NewValue.Value; + + using (BeginAbsoluteSequence(b.Start)) + { + fadeContainer.FadeIn(BreakOverlay.BREAK_FADE_DURATION); + using (BeginDelayedSequence(b.Duration - BreakOverlay.BREAK_FADE_DURATION)) + fadeContainer.FadeOut(BreakOverlay.BREAK_FADE_DURATION); + } + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 29b54c8699..b27e0b7477 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,7 +34,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -453,9 +452,10 @@ namespace osu.Game.Screens.Play DimmableStoryboard.OverlayLayerContainer.CreateProxy(), new LetterboxOverlay { + Clock = DrawableRuleset.FrameStableClock, + ProcessCustomClock = false, + BreakTracker = breakTracker, Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, }, HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { From 8ef5a01bc15e5ec88c43020cdd5cf78a32e699bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:46:05 +0900 Subject: [PATCH 236/349] Adjust visuals to match stable Was never a huge fan of the gradient we had. --- osu.Game/Screens/Play/LetterboxOverlay.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/LetterboxOverlay.cs b/osu.Game/Screens/Play/LetterboxOverlay.cs index 168c707c3b..4c934f56cd 100644 --- a/osu.Game/Screens/Play/LetterboxOverlay.cs +++ b/osu.Game/Screens/Play/LetterboxOverlay.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Utils; @@ -23,9 +22,8 @@ namespace osu.Game.Screens.Play public LetterboxOverlay() { - const int letterbox_height = 150; - RelativeSizeAxes = Axes.Both; + const float letterbox_height = 0.125f; InternalChild = fadeContainer = new Container { @@ -37,17 +35,17 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Height = letterbox_height, - Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), + Colour = Color4.Black, }, new Box { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Height = letterbox_height, - Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), + Colour = Color4.Black, } } }; From 5b911ad8d0d8a72d58d01dedf052b9378c0f6271 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 18:25:56 +0900 Subject: [PATCH 237/349] Fix context menu not working when selection hasn't changed --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 872c4c2465..b49dee279e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -115,14 +115,12 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - var selectionBefore = SelectionHandler.SelectedItems.ToArray(); bool handled = performMouseDownActions(e); - bool selectionChanged = !SelectionHandler.SelectedItems.SequenceEqual(selectionBefore); bool movementPossible = prepareSelectionMovement(e); - if (selectionChanged) + if (SelectedItems.Any()) { - // if the selection changed and there are no modifiers pressed, don't block so the context menu still shows. + // if there is a selection and there are no modifiers pressed, don't block so the context menu still shows. bool shouldShowContextMenu = e.Button == MouseButton.Right && !e.ShiftPressed && !e.AltPressed && !e.SuperPressed; return !shouldShowContextMenu; } From 70693c8bb80a3830512f2e4a964155d01fd4a398 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 18:33:20 +0900 Subject: [PATCH 238/349] Fix weird implementation of layout validation --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 3b19e90b60..cee43b300a 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.UI set => base.Masking = value; } - private readonly LayoutValue columnSizeLayout = new LayoutValue(Invalidation.DrawSize); + private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize); public ColumnFlow(StageDefinition stageDefinition) { @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < stageDefinition.Columns; i++) columns.Add(new Container { RelativeSizeAxes = Axes.Y }); - AddLayout(columnSizeLayout); + AddLayout(layout); } [Resolved] @@ -73,18 +73,18 @@ namespace osu.Game.Rulesets.Mania.UI { rulesetConfig?.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout); - mobileLayout.BindValueChanged(_ => updateColumnSize()); - skin.SourceChanged += updateColumnSize; + mobileLayout.BindValueChanged(_ => invalidateLayout()); + skin.SourceChanged += invalidateLayout; } protected override void Update() { base.Update(); - if (!columnSizeLayout.IsValid) + if (!layout.IsValid) { updateColumnSize(); - columnSizeLayout.Validate(); + layout.Validate(); } } @@ -98,6 +98,8 @@ namespace osu.Game.Rulesets.Mania.UI Content[column] = columns[column].Child = content; } + private void invalidateLayout() => layout.Invalidate(); + private void updateColumnSize() { float mobileAdjust = 1f; @@ -149,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); if (skin.IsNotNull()) - skin.SourceChanged -= updateColumnSize; + skin.SourceChanged -= invalidateLayout; } } } From 92d374a5bb61aa8d0f2a4f3248c12b4439c15d53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 18:36:02 +0900 Subject: [PATCH 239/349] Remove unused thing --- osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 1df05bf350..2a2faf0cf7 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Configuration; -using osu.Game.Rulesets.Mania.Configuration; using osuTK; namespace osu.Game.Rulesets.Mania.UI @@ -41,9 +40,6 @@ namespace osu.Game.Rulesets.Mania.UI MaxValue = 1 }; - [Resolved] - private ManiaRulesetConfigManager rulesetConfig { get; set; } = null!; - private GridContainer gridContainer = null!; public ManiaTouchInputArea(DrawableManiaRuleset drawableRuleset) From 9c0f3f9bef31fb3e541eb37c4b0fc0e290bf55c3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Mar 2025 19:34:34 +0900 Subject: [PATCH 240/349] Describe special case of user mod validation --- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 31d5c7ea33..ae31e55da5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -458,12 +458,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState()); UserBeatmap.BindValueChanged(_ => updateGameplayState()); + UserMods.BindValueChanged(_ => updateGameplayState()); UserRuleset.BindValueChanged(_ => { + // The user mod selection overlay is separate from the beatmap/ruleset style selection screen, + // and so the validity of mods has to be confirmed separately after the ruleset is changed. validateUserMods(); updateGameplayState(); }); - UserMods.BindValueChanged(_ => updateGameplayState()); updateSetupState(); updateGameplayState(); @@ -545,7 +547,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists UserRuleset.Value = null; validateUserMods(); - updateGameplayState(); } From 0ffb23379629176aac71a2ae7e9e7ea59a4e815f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 11:29:31 +0100 Subject: [PATCH 241/349] Rewrite `OverallRanking` to interface directly with `UserStatisticsWatcher` --- .../Visual/Ranking/TestSceneOverallRanking.cs | 18 ++++++--- .../Ranking/TestSceneStatisticsPanel.cs | 38 ------------------- .../Ranking/Statistics/User/OverallRanking.cs | 38 ++++++++++++++----- .../Ranking/Statistics/UserStatisticsPanel.cs | 25 +----------- 4 files changed, 42 insertions(+), 77 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs index b406ea369f..f96d272e40 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online; using osu.Game.Scoring; @@ -12,7 +13,7 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneOverallRanking : OsuTestScene { - private OverallRanking overallRanking = null!; + private readonly Bindable statisticsUpdate = new Bindable(); [Test] public void TestUpdatePending() @@ -104,14 +105,19 @@ namespace osu.Game.Tests.Visual.Ranking displayUpdate(statistics, statistics); } - private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking + private void createDisplay() => AddStep("create display", () => { - Width = 400, - Anchor = Anchor.Centre, - Origin = Anchor.Centre + statisticsUpdate.Value = null; + Child = new OverallRanking(new ScoreInfo()) + { + Width = 400, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + DisplayedUpdate = { BindTarget = statisticsUpdate } + }; }); private void displayUpdate(UserStatistics before, UserStatistics after) => - AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after)); + AddStep("display update", () => statisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after)); } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index c12b9d29bc..c075e75c87 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -155,44 +155,6 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, Score = { Value = score }, - DisplayedUserStatisticsUpdate = - { - Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics - { - Level = new UserStatistics.LevelInfo - { - Current = 5, - Progress = 20, - }, - GlobalRank = 38000, - CountryRank = 12006, - PP = 2134, - RankedScore = 21123849, - Accuracy = 0.985, - PlayCount = 13375, - PlayTime = 354490, - TotalScore = 128749597, - TotalHits = 0, - MaxCombo = 1233, - }, new UserStatistics - { - Level = new UserStatistics.LevelInfo - { - Current = 5, - Progress = 30, - }, - GlobalRank = 36000, - CountryRank = 12000, - PP = (decimal)2134.5, - RankedScore = 23897015, - Accuracy = 0.984, - PlayCount = 13376, - PlayTime = 35789, - TotalScore = 132218497, - TotalHits = 0, - MaxCombo = 1233, - }) - } }; }); diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 9f5afea6f0..9d0a511f5a 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -5,8 +5,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Statistics.User { @@ -14,13 +16,21 @@ namespace osu.Game.Screens.Ranking.Statistics.User { private const float transition_duration = 300; - public Bindable StatisticsUpdate { get; } = new Bindable(); + public Bindable DisplayedUpdate { get; } = new Bindable(); + private readonly IBindable latestGlobalStatisticsUpdate = new Bindable(); + + private readonly ScoreInfo scoreInfo; private LoadingLayer loadingLayer = null!; private GridContainer content = null!; + public OverallRanking(ScoreInfo scoreInfo) + { + this.scoreInfo = scoreInfo; + } + [BackgroundDependencyLoader] - private void load() + private void load(UserStatisticsWatcher? userStatisticsWatcher) { AutoSizeAxes = Axes.Y; AutoSizeEasing = Easing.OutQuint; @@ -55,34 +65,44 @@ namespace osu.Game.Screens.Ranking.Statistics.User { new Drawable[] { - new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, new SimpleStatisticTable.Spacer(), - new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, }, [], new Drawable[] { - new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, new SimpleStatisticTable.Spacer(), - new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new AccuracyChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, }, [], new Drawable[] { - new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, new SimpleStatisticTable.Spacer(), - new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } }, } } } }; + + if (userStatisticsWatcher != null) + { + latestGlobalStatisticsUpdate.BindTo(userStatisticsWatcher.LatestUpdate); + latestGlobalStatisticsUpdate.BindValueChanged(update => + { + if (update.NewValue?.Score.MatchesOnlineID(scoreInfo) == true) + DisplayedUpdate.Value = update.NewValue; + }, true); + } } protected override void LoadComplete() { base.LoadComplete(); - StatisticsUpdate.BindValueChanged(onUpdateReceived, true); + DisplayedUpdate.BindValueChanged(onUpdateReceived, true); FinishTransforms(true); } diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs index 86fed4a9bb..de31c234c4 100644 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs @@ -3,12 +3,8 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Extensions; -using osu.Game.Online; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics.User; @@ -18,29 +14,11 @@ namespace osu.Game.Screens.Ranking.Statistics { private readonly ScoreInfo achievedScore; - internal readonly Bindable DisplayedUserStatisticsUpdate = new Bindable(); - - private IBindable latestGlobalStatisticsUpdate = null!; - public UserStatisticsPanel(ScoreInfo achievedScore) { this.achievedScore = achievedScore; } - [BackgroundDependencyLoader] - private void load(UserStatisticsWatcher? userStatisticsWatcher) - { - if (userStatisticsWatcher != null) - { - latestGlobalStatisticsUpdate = userStatisticsWatcher.LatestUpdate.GetBoundCopy(); - latestGlobalStatisticsUpdate.BindValueChanged(update => - { - if (update.NewValue?.Score.MatchesOnlineID(achievedScore) == true) - DisplayedUserStatisticsUpdate.Value = update.NewValue; - }, true); - } - } - protected override ICollection CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap) { var items = base.CreateStatisticItems(newScore, playableBeatmap); @@ -50,12 +28,11 @@ namespace osu.Game.Screens.Ranking.Statistics && newScore.OnlineID > 0 && newScore.OnlineID == achievedScore.OnlineID) { - items = items.Append(new StatisticItem("Overall Ranking", () => new OverallRanking + items = items.Append(new StatisticItem("Overall Ranking", () => new OverallRanking(newScore) { RelativeSizeAxes = Axes.X, Anchor = Anchor.Centre, Origin = Anchor.Centre, - StatisticsUpdate = { BindTarget = DisplayedUserStatisticsUpdate } })).ToArray(); } From 5f0451eb791c83af12b5896a05f8b3d5c93f14ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 11:39:07 +0100 Subject: [PATCH 242/349] Remove inheritance in `StatisticsPanel` --- .../Ranking/TestSceneStatisticsPanel.cs | 8 ++-- osu.Game/Screens/Ranking/ResultsScreen.cs | 15 ++----- .../Ranking/Statistics/StatisticsPanel.cs | 31 ++++++++++++-- .../Ranking/Statistics/UserStatisticsPanel.cs | 42 ------------------- 4 files changed, 37 insertions(+), 59 deletions(-) delete mode 100644 osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index c075e75c87..b117e90260 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -137,11 +137,12 @@ namespace osu.Game.Tests.Visual.Ranking { CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)], RelativeSizeAxes = Axes.Both, - Child = new UserStatisticsPanel(score) + Child = new StatisticsPanel { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, - Score = { Value = score, } + Score = { Value = score, }, + AchievedScore = score, } }); AddUntilStep("overall ranking present", () => this.ChildrenOfType().Any()); @@ -150,11 +151,12 @@ namespace osu.Game.Tests.Visual.Ranking private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new UserStatisticsPanel(score) + Child = new StatisticsPanel { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, Score = { Value = score }, + AchievedScore = score, }; }); diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 24b40968d6..6f9bbd0cfb 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -121,7 +121,10 @@ namespace osu.Game.Screens.Ranking Children = new Drawable[] { new GlobalScrollAdjustsVolume(), - StatisticsPanel = createStatisticsPanel().With(panel => + StatisticsPanel = new StatisticsPanel + { + AchievedScore = ShowUserStatistics && Score != null ? Score : null + }.With(panel => { panel.RelativeSizeAxes = Axes.Both; panel.Score.BindTarget = SelectedScore; @@ -353,16 +356,6 @@ namespace osu.Game.Screens.Ranking /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. protected virtual Task FetchNextPage(int direction) => Task.FromResult([]); - /// - /// Creates the to be used to display extended information about scores. - /// - private StatisticsPanel createStatisticsPanel() - { - return ShowUserStatistics && Score != null - ? new UserStatisticsPanel(Score) - : new StatisticsPanel(); - } - private Task addScores(ScoreInfo[] scores) { var tcs = new TaskCompletionSource(); diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index f9f5254bc2..639600391f 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics.User; using osuTK; namespace osu.Game.Screens.Ranking.Statistics @@ -28,6 +29,13 @@ namespace osu.Game.Screens.Ranking.Statistics public readonly Bindable Score = new Bindable(); + /// + /// The score which was achieved by the local user. + /// If this is set to a non-null score, an component will be displayed showing changes to the local user's ranking & statistics + /// when a statistics update related to this score is received from spectator server. + /// + public ScoreInfo? AchievedScore { get; init; } + protected override bool StartHidden => true; [Resolved] @@ -97,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics bool hitEventsAvailable = newScore.HitEvents.Count != 0; Container container; - var statisticItems = CreateStatisticItems(newScore, task.GetResultSafely()); + var statisticItems = CreateStatisticItems(newScore, task.GetResultSafely()).ToArray(); if (!hitEventsAvailable && statisticItems.All(c => c.RequiresHitEvents)) { @@ -199,8 +207,25 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The score to create the rows for. /// The beatmap on which the score was set. - protected virtual ICollection CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap) - => newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap); + protected virtual IEnumerable CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap) + { + foreach (var statistic in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + yield return statistic; + + if (AchievedScore != null + && newScore.UserID > 1 + && newScore.UserID == AchievedScore.UserID + && newScore.OnlineID > 0 + && newScore.OnlineID == AchievedScore.OnlineID) + { + yield return new StatisticItem("Overall Ranking", () => new OverallRanking(newScore) + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } protected override bool OnClick(ClickEvent e) { diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs deleted file mode 100644 index de31c234c4..0000000000 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking.Statistics.User; - -namespace osu.Game.Screens.Ranking.Statistics -{ - public partial class UserStatisticsPanel : StatisticsPanel - { - private readonly ScoreInfo achievedScore; - - public UserStatisticsPanel(ScoreInfo achievedScore) - { - this.achievedScore = achievedScore; - } - - protected override ICollection CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap) - { - var items = base.CreateStatisticItems(newScore, playableBeatmap); - - if (newScore.UserID > 1 - && newScore.UserID == achievedScore.UserID - && newScore.OnlineID > 0 - && newScore.OnlineID == achievedScore.OnlineID) - { - items = items.Append(new StatisticItem("Overall Ranking", () => new OverallRanking(newScore) - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - })).ToArray(); - } - - return items; - } - } -} From d1ce64b3f62113e8f5597c81b10e48cbb7a9d9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 11:43:23 +0100 Subject: [PATCH 243/349] Add user tag control to results screen's statistics panel --- .../Visual/Ranking/TestSceneUserTagControl.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 13 ++++++------ .../Ranking/Statistics/StatisticsPanel.cs | 21 +++++++++++++++++++ osu.Game/Screens/Ranking/UserTagControl.cs | 14 ++++++++----- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index ebfd553815..d622df8d76 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Ranking Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new UserTagControl + Child = new UserTagControl(Beatmap.Value.BeatmapInfo) { Width = 500, Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 6f9bbd0cfb..fcf90a3e28 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -85,6 +85,8 @@ namespace osu.Game.Screens.Ranking /// public bool ShowUserStatistics { get; init; } + public bool ShowUserTagControl { get; init; } + private Sample? popInSample; protected ResultsScreen(ScoreInfo? score) @@ -123,12 +125,11 @@ namespace osu.Game.Screens.Ranking new GlobalScrollAdjustsVolume(), StatisticsPanel = new StatisticsPanel { - AchievedScore = ShowUserStatistics && Score != null ? Score : null - }.With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), + RelativeSizeAxes = Axes.Both, + Score = { BindTarget = SelectedScore }, + AchievedScore = ShowUserStatistics && Score != null ? Score : null, + ShowUserTagControl = ShowUserTagControl, + }, ScorePanelList = new ScorePanelList { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 639600391f..b974b2f515 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics.User; @@ -36,11 +37,19 @@ namespace osu.Game.Screens.Ranking.Statistics /// public ScoreInfo? AchievedScore { get; init; } + /// + /// Whether to show a control that allows to assign user tags to the played beatmap. + /// + public bool ShowUserTagControl { get; init; } + protected override bool StartHidden => true; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + private readonly Container content; private readonly LoadingSpinner spinner; @@ -225,6 +234,18 @@ namespace osu.Game.Screens.Ranking.Statistics Origin = Anchor.Centre, }); } + + if (ShowUserTagControl + && newScore.BeatmapInfo!.OnlineID > 0 + && api.IsLoggedIn) + { + yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 7600d0aaae..3817f662eb 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -37,6 +37,8 @@ namespace osu.Game.Screens.Ranking { public partial class UserTagControl : CompositeDrawable { + private readonly BeatmapInfo beatmapInfo; + public override bool HandlePositionalInput => true; private readonly Cached layout = new Cached(); @@ -53,8 +55,10 @@ namespace osu.Game.Screens.Ranking [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private Bindable beatmap { get; set; } = null!; + public UserTagControl(BeatmapInfo beatmapInfo) + { + this.beatmapInfo = beatmapInfo; + } [BackgroundDependencyLoader] private void load(SessionStatics sessionStatics) @@ -104,8 +108,8 @@ namespace osu.Game.Screens.Ranking api.Queue(listTagsRequest); } - var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmap.Value.BeatmapInfo.BeatmapSet!.OnlineID); - getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmap.Value.BeatmapInfo)); + var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmapInfo.BeatmapSet!.OnlineID); + getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmapInfo)); api.Queue(getBeatmapSetRequest); } @@ -114,7 +118,7 @@ namespace osu.Game.Screens.Ranking loadingLayer.Show(); extraTags.Remove(tag); - var req = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, tag.Id); + var req = new AddBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id); req.Success += () => { tag.Voted.Value = true; From e70ad146472e3bfeeaa150b03503a2fac36c9554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 11:49:31 +0100 Subject: [PATCH 244/349] Show user tag control only following local user gameplay --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 5 +++++ .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 +++- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 1 + osu.Game/Screens/Play/Player.cs | 6 +----- osu.Game/Screens/Play/SubmittingPlayer.cs | 8 ++++++++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 4377cc6219..60cb3ba07c 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -13,7 +14,9 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osu.Game.Users; namespace osu.Game.Screens.Edit.GameplayTest @@ -228,5 +231,7 @@ namespace osu.Game.Screens.Edit.GameplayTest editor.RestoreState(editorState); return base.OnExiting(e); } + + protected override ResultsScreen CreateResults(ScoreInfo score) => throw new NotSupportedException(); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 111b453adb..67a67cf271 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -199,10 +199,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores) { ShowUserStatistics = true, + ShowUserTagControl = true, } : new MultiplayerResultsScreen(score, Room.RoomID.Value, PlaylistItem) { - ShowUserStatistics = true + ShowUserStatistics = true, + ShowUserTagControl = true, }; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index b82c2404ab..80b378bdcf 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -60,6 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { AllowRetry = true, ShowUserStatistics = true, + ShowUserTagControl = true, }; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b27e0b7477..a738a40993 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1276,11 +1276,7 @@ namespace osu.Game.Screens.Play /// /// The to be displayed in the results screen. /// The . - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) - { - AllowRetry = true, - ShowUserStatistics = true, - }; + protected abstract ResultsScreen CreateResults(ScoreInfo score); private void fadeOut() { diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index b667963a70..04cf473173 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -21,6 +21,7 @@ using osu.Game.Online.Rooms; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { @@ -323,5 +324,12 @@ namespace osu.Game.Screens.Play api.Queue(request); return scoreSubmissionSource.Task; } + + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) + { + AllowRetry = true, + ShowUserStatistics = true, + ShowUserTagControl = true, + }; } } From 266682d5854d31b0f176d62205477cd13195ef45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 12:10:48 +0100 Subject: [PATCH 245/349] Adjust colour of user tag popover control For some reason in actual gameplay there seems to be an `OverlayColourProvider` cached that's nowhere to be seen in tests, and without this change things look bad. Dunno, not looking for it. --- osu.Game/Screens/Ranking/UserTagControl.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 3817f662eb..3ae6501b36 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -30,6 +30,7 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osuTK; using osuTK.Input; @@ -534,14 +535,14 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider? colourProvider) { Content.AddRange(new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoamDark, + Colour = colourProvider?.Background3 ?? colours.GreySeaFoamDark, Depth = float.MaxValue, }, new FillFlowContainer From 6f74d8ad507a182a5e91863c556c93a88897555e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 12:16:54 +0100 Subject: [PATCH 246/349] Add visual test coverage of user tag control on results --- .../Ranking/TestSceneStatisticsPanel.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index b117e90260..a64fd488bc 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -18,6 +19,9 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -36,6 +40,8 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneStatisticsPanel : OsuTestScene { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + [Test] public void TestScoreWithPositionStatistics() { @@ -149,6 +155,71 @@ namespace osu.Game.Tests.Visual.Ranking AddUntilStep("loading spinner not visible", () => this.ChildrenOfType().All(l => l.State.Value == Visibility.Hidden)); } + [Test] + public void TestTagging() + { + var score = TestResources.CreateTestScoreInfo(); + + AddStep("set up network requests", () => + { + dummyAPI.HandleRequest = request => + { + switch (request) + { + case ListTagsRequest listTagsRequest: + { + Scheduler.AddDelayed(() => listTagsRequest.TriggerSuccess(new APITagCollection + { + 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 = 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.", }, + ] + }), 500); + return true; + } + + case GetBeatmapSetRequest getBeatmapSetRequest: + { + var beatmapSet = CreateAPIBeatmapSet(score.BeatmapInfo); + beatmapSet.Beatmaps.Single().TopTags = + [ + new APIBeatmapTag { TagId = 3, VoteCount = 9 }, + ]; + Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500); + return true; + } + + case AddBeatmapTagRequest: + case RemoveBeatmapTagRequest: + { + Scheduler.AddDelayed(request.TriggerSuccess, 500); + return true; + } + } + + 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, + ShowUserTagControl = true, + } + }; + }); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new StatisticsPanel From 73fba15adf1cf2a5d8be1d48fb0cff6de322d1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 12:38:55 +0100 Subject: [PATCH 247/349] Fix xmldoc --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index b974b2f515..16de00fcf1 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The score which was achieved by the local user. - /// If this is set to a non-null score, an component will be displayed showing changes to the local user's ranking & statistics + /// If this is set to a non-null score, an component will be displayed showing changes to the local user's ranking and statistics /// when a statistics update related to this score is received from spectator server. /// public ScoreInfo? AchievedScore { get; init; } From 17f964dc6bcf31edae1dd40cb6dc2e8401a47d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Mar 2025 13:21:04 +0100 Subject: [PATCH 248/349] Fix `OsuTextFlowContainer.AddArbitraryDrawable()` not aligning the drawable correctly Closes https://github.com/ppy/osu/issues/32348. --- osu.Game/Graphics/Containers/OsuTextFlowContainer.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs index d3bbc2e80b..d5cce1a10a 100644 --- a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs +++ b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs @@ -21,8 +21,18 @@ namespace osu.Game.Graphics.Containers protected override SpriteText CreateSpriteText() => new OsuSpriteText(); - public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(drawable.Yield())); + public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(new ArbitraryDrawableWrapper { Child = drawable }.Yield())); public ITextPart AddIcon(IconUsage icon, Action creationParameters = null) => AddText(icon.Icon.ToString(), creationParameters); + + private partial class ArbitraryDrawableWrapper : Container, IHasLineBaseHeight + { + public float LineBaseHeight => DrawHeight; + + public ArbitraryDrawableWrapper() + { + AutoSizeAxes = Axes.Both; + } + } } } From 0906983f6fda3c43d23e17542d72e9d3532ca614 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 00:45:57 +0900 Subject: [PATCH 249/349] Add reload on display mode change --- .../Dashboard/Friends/FriendDisplay.cs | 161 +++---------- .../Overlays/Dashboard/Friends/FriendsList.cs | 212 ++++++++++++++++++ 2 files changed, 243 insertions(+), 130 deletions(-) create mode 100644 osu.Game/Overlays/Dashboard/Friends/FriendsList.cs diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 4bd79188c8..6cc56d5915 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -1,22 +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 System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Resources.Localisation.Web; using osu.Game.Users; -using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { @@ -35,9 +33,11 @@ namespace osu.Game.Overlays.Dashboard.Friends private Box background = null!; private Box controlBackground = null!; private UserListToolbar userListToolbar = null!; + private Container listContainer = null!; private LoadingLayer loading = null!; private BasicSearchTextBox searchTextBox = null!; - private FriendsSearchContainer panelsContainer = null!; + + private CancellationTokenSource? listLoadCancellation; public FriendDisplay() { @@ -145,14 +145,11 @@ namespace osu.Game.Overlays.Dashboard.Friends AutoSizeAxes = Axes.Y, Children = new Drawable[] { - panelsContainer = new FriendsSearchContainer + listContainer = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, - // Todo: Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), - Spacing = new Vector2(10), - SortCriteria = { BindTarget = userListToolbar.SortCriteria } + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING } }, loading = new LoadingLayer(true) } @@ -178,35 +175,12 @@ namespace osu.Game.Overlays.Dashboard.Friends friendPresences.BindTo(metadataClient.FriendPresences); friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); - searchTextBox.Current.BindValueChanged(onSearchChanged); - streamControl.Current.BindValueChanged(onFriendsStreamChanged); + userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList()); } private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - foreach (APIRelation relation in e.NewItems!.OfType()) - { - panelsContainer.Add(new FilterableUserPanel(new UserGridPanel(relation.TargetUser!).With(panel => - { - panel.Anchor = Anchor.TopCentre; - panel.Origin = Anchor.TopCentre; - panel.Width = 290; - }))); - } - - break; - - case NotifyCollectionChangedAction.Remove: - foreach (APIRelation relation in e.OldItems!.OfType()) - panelsContainer.RemoveAll(panel => panel.User.Equals(relation.TargetUser), true); - - break; - } - - updatePanelVisibilities(); + reloadList(); updateStatusCounts(); } @@ -216,40 +190,39 @@ namespace osu.Game.Overlays.Dashboard.Friends { case NotifyDictionaryChangedAction.Add: case NotifyDictionaryChangedAction.Remove: - updatePanelVisibilities(); updateStatusCounts(); break; } } - private void onFriendsStreamChanged(ValueChangedEvent stream) + private void reloadList() { - updatePanelVisibilities(); - } + listLoadCancellation?.Cancel(); + var cancellationSource = listLoadCancellation = new CancellationTokenSource(); - private void onSearchChanged(ValueChangedEvent search) - { - panelsContainer.SearchTerm = search.NewValue; - } - - private void updatePanelVisibilities() - { - foreach (var panel in panelsContainer) + FriendsList? currentList = listContainer.SingleOrDefault(); + FriendsList newList = new FriendsList(userListToolbar.DisplayStyle.Value, apiFriends.Select(f => f.TargetUser!).ToArray()) { - switch (streamControl.Current.Value) + OnlineStream = { BindTarget = streamControl.Current }, + SortCriteria = { BindTarget = userListToolbar.SortCriteria }, + SearchText = { BindTarget = searchTextBox.Current } + }; + + loading.Show(); + LoadComponentAsync(newList, finishLoad, cancellationSource.Token); + + void finishLoad(FriendsList list) + { + loading.Hide(); + + if (currentList != null) { - case OnlineStatus.All: - panel.CanBeShown.Value = true; - break; - - case OnlineStatus.Online: - panel.CanBeShown.Value = friendPresences.ContainsKey(panel.User.OnlineID); - break; - - case OnlineStatus.Offline: - panel.CanBeShown.Value = !friendPresences.ContainsKey(panel.User.OnlineID); - break; + currentList.FadeOut(100, Easing.OutQuint).Expire(); + currentList.Delay(25).Schedule(() => currentList.BypassAutoSizeAxes = Axes.Y); } + + listContainer.Add(newList); + newList.FadeIn(200, Easing.OutQuint); } } @@ -270,77 +243,5 @@ namespace osu.Game.Overlays.Dashboard.Friends streamControl.CountOnline.Value = countOnline; streamControl.CountOffline.Value = countOffline; } - - private class FriendsSearchContainer : SearchContainer - { - public readonly IBindable SortCriteria = new Bindable(); - - protected override void LoadComplete() - { - base.LoadComplete(); - SortCriteria.BindValueChanged(_ => InvalidateLayout(), true); - } - - public override IEnumerable FlowingChildren - { - get - { - IEnumerable panels = base.FlowingChildren.OfType(); - - switch (SortCriteria.Value) - { - default: - case UserSortCriteria.LastVisit: - return panels.OrderByDescending(panel => panel.User.LastVisit); - - case UserSortCriteria.Rank: - return panels.OrderByDescending(panel => panel.User.Statistics.GlobalRank.HasValue).ThenBy(panel => panel.User.Statistics.GlobalRank ?? 0); - - case UserSortCriteria.Username: - return panels.OrderBy(panel => panel.User.Username); - } - } - } - } - - private class FilterableUserPanel : CompositeDrawable, IConditionalFilterable - { - public readonly Bindable CanBeShown = new Bindable(); - - public APIUser User => panel.User; - - private readonly UserPanel panel; - - public FilterableUserPanel(UserPanel panel) - { - this.panel = panel; - - Anchor = panel.Anchor; - Origin = panel.Origin; - RelativeSizeAxes = panel.RelativeSizeAxes; - AutoSizeAxes = panel.AutoSizeAxes; - Width = panel.Width; - Height = panel.Height; - - InternalChild = panel; - } - - IBindable IConditionalFilterable.CanBeShown => CanBeShown; - - IEnumerable IHasFilterTerms.FilterTerms => panel.FilterTerms; - - bool IFilterable.MatchingFilter - { - set - { - if (value) - Show(); - else - Hide(); - } - } - - bool IFilterable.FilteringActive { set { } } - } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs new file mode 100644 index 0000000000..ed87a58ff4 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -0,0 +1,212 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Dashboard.Friends +{ + public class FriendsList : CompositeDrawable + { + public readonly IBindable OnlineStream = new Bindable(); + public readonly IBindable SortCriteria = new Bindable(); + public readonly IBindable SearchText = new Bindable(); + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + private readonly IBindableDictionary friendPresences = new BindableDictionary(); + private readonly OverlayPanelDisplayStyle style; + private readonly APIUser[] friends; + + private FriendsSearchContainer searchContainer = null!; + + public FriendsList(OverlayPanelDisplayStyle style, APIUser[] friends) + { + this.style = style; + this.friends = friends; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = searchContainer = new FriendsSearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, + Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), + SortCriteria = { BindTarget = SortCriteria }, + ChildrenEnumerable = friends.Select(createUserPanel) + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + friendPresences.BindTo(metadataClient.FriendPresences); + friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); + + SearchText.BindValueChanged(onSearchTextChanged, true); + OnlineStream.BindValueChanged(onFriendsStreamChanged, true); + } + + private void onFriendPresencesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + { + switch (e.Action) + { + case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Remove: + updatePanelVisibilities(); + break; + } + } + + private void onSearchTextChanged(ValueChangedEvent search) + { + searchContainer.SearchTerm = search.NewValue; + } + + private void onFriendsStreamChanged(ValueChangedEvent stream) + { + updatePanelVisibilities(); + } + + private void updatePanelVisibilities() + { + foreach (var panel in searchContainer) + { + switch (OnlineStream.Value) + { + case OnlineStatus.All: + panel.CanBeShown.Value = true; + break; + + case OnlineStatus.Online: + panel.CanBeShown.Value = friendPresences.ContainsKey(panel.User.OnlineID); + break; + + case OnlineStatus.Offline: + panel.CanBeShown.Value = !friendPresences.ContainsKey(panel.User.OnlineID); + break; + } + } + } + + private FilterableUserPanel createUserPanel(APIUser user) + { + UserPanel panel; + + switch (style) + { + default: + case OverlayPanelDisplayStyle.Card: + panel = new UserGridPanel(user); + panel.Anchor = Anchor.TopCentre; + panel.Origin = Anchor.TopCentre; + panel.Width = 290; + break; + + case OverlayPanelDisplayStyle.List: + panel = new UserListPanel(user); + break; + + case OverlayPanelDisplayStyle.Brick: + panel = new UserBrickPanel(user); + break; + } + + return new FilterableUserPanel(panel); + } + + private class FriendsSearchContainer : SearchContainer + { + public readonly IBindable SortCriteria = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + SortCriteria.BindValueChanged(_ => InvalidateLayout(), true); + } + + public override IEnumerable FlowingChildren + { + get + { + IEnumerable panels = base.FlowingChildren.OfType(); + + switch (SortCriteria.Value) + { + default: + case UserSortCriteria.LastVisit: + return panels.OrderByDescending(panel => panel.User.LastVisit); + + case UserSortCriteria.Rank: + return panels.OrderByDescending(panel => panel.User.Statistics.GlobalRank.HasValue).ThenBy(panel => panel.User.Statistics.GlobalRank ?? 0); + + case UserSortCriteria.Username: + return panels.OrderBy(panel => panel.User.Username); + } + } + } + } + + private class FilterableUserPanel : CompositeDrawable, IConditionalFilterable + { + public readonly Bindable CanBeShown = new Bindable(); + + public APIUser User => panel.User; + + private readonly UserPanel panel; + + public FilterableUserPanel(UserPanel panel) + { + this.panel = panel; + + Anchor = panel.Anchor; + Origin = panel.Origin; + RelativeSizeAxes = panel.RelativeSizeAxes; + AutoSizeAxes = panel.AutoSizeAxes; + + if (!AutoSizeAxes.HasFlagFast(Axes.X)) + Width = panel.Width; + + if (!AutoSizeAxes.HasFlagFast(Axes.Y)) + Height = panel.Height; + + InternalChild = panel; + } + + IBindable IConditionalFilterable.CanBeShown => CanBeShown; + + IEnumerable IHasFilterTerms.FilterTerms => panel.FilterTerms; + + bool IFilterable.MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + + bool IFilterable.FilteringActive { set { } } + } + } +} From 9ff6c44559a6455ea91e363f4da9bb726203098e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 01:36:24 +0900 Subject: [PATCH 250/349] Handle stream counts internally --- .../Dashboard/Friends/FriendDisplay.cs | 47 +------------- .../Friends/FriendOnlineStreamControl.cs | 64 +++++++++++++++++-- 2 files changed, 59 insertions(+), 52 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 6cc56d5915..223bcdf2d9 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.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.Specialized; using System.Linq; using System.Threading; using osu.Framework.Allocation; @@ -12,23 +11,17 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Metadata; using osu.Game.Resources.Localisation.Web; -using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { public partial class FriendDisplay : CompositeDrawable { private readonly IBindableList apiFriends = new BindableList(); - private readonly IBindableDictionary friendPresences = new BindableDictionary(); [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private MetadataClient metadataClient { get; set; } = null!; - private FriendOnlineStreamControl streamControl = null!; private Box background = null!; private Box controlBackground = null!; @@ -170,31 +163,11 @@ namespace osu.Game.Overlays.Dashboard.Friends base.LoadComplete(); apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged(onFriendsChanged, true); - - friendPresences.BindTo(metadataClient.FriendPresences); - friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); + apiFriends.BindCollectionChanged((_, _) => reloadList()); userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList()); } - private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - reloadList(); - updateStatusCounts(); - } - - private void onFriendPresencesChanged(object? sender, NotifyDictionaryChangedEventArgs e) - { - switch (e.Action) - { - case NotifyDictionaryChangedAction.Add: - case NotifyDictionaryChangedAction.Remove: - updateStatusCounts(); - break; - } - } - private void reloadList() { listLoadCancellation?.Cancel(); @@ -225,23 +198,5 @@ namespace osu.Game.Overlays.Dashboard.Friends newList.FadeIn(200, Easing.OutQuint); } } - - private void updateStatusCounts() - { - int countOnline = 0; - int countOffline = 0; - - foreach (var user in apiFriends) - { - if (friendPresences.ContainsKey(user.TargetID)) - countOnline++; - else - countOffline++; - } - - streamControl.CountAll.Value = apiFriends.Count; - streamControl.CountOnline.Value = countOnline; - streamControl.CountOffline.Value = countOffline; - } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs index 25b29e8d16..763571f605 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs @@ -2,15 +2,28 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; +using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { public partial class FriendOnlineStreamControl : OverlayStreamControl { - public readonly BindableInt CountAll = new BindableInt(); - public readonly BindableInt CountOnline = new BindableInt(); - public readonly BindableInt CountOffline = new BindableInt(); + private readonly IBindableDictionary friendPresences = new BindableDictionary(); + private readonly IBindableList apiFriends = new BindableList(); + private readonly BindableInt countAll = new BindableInt(); + private readonly BindableInt countOnline = new BindableInt(); + private readonly BindableInt countOffline = new BindableInt(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; public FriendOnlineStreamControl() { @@ -22,18 +35,57 @@ namespace osu.Game.Overlays.Dashboard.Friends ]; } + protected override void LoadComplete() + { + base.LoadComplete(); + + apiFriends.BindTo(api.Friends); + apiFriends.BindCollectionChanged((_, _) => updateCounts()); + + friendPresences.BindTo(metadataClient.FriendPresences); + friendPresences.BindCollectionChanged(onFriendPresencesChanged); + + updateCounts(); + } + + private void onFriendPresencesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + { + switch (e.Action) + { + case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Remove: + updateCounts(); + break; + } + } + + private void updateCounts() + { + countAll.Value = apiFriends.Count; + countOnline.Value = 0; + countOffline.Value = 0; + + foreach (var user in apiFriends) + { + if (friendPresences.ContainsKey(user.TargetID)) + countOnline.Value++; + else + countOffline.Value++; + } + } + protected override OverlayStreamItem CreateStreamItem(OnlineStatus value) { switch (value) { case OnlineStatus.All: - return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountAll } }; + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = countAll } }; case OnlineStatus.Online: - return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountOnline } }; + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = countOnline } }; case OnlineStatus.Offline: - return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = CountOffline } }; + return new FriendsOnlineStatusItem(value) { UserCount = { BindTarget = countOffline } }; default: throw new ArgumentException(nameof(value)); From 4b54b8c0d843855305c25dc0ad1e33ba8e60978f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 01:36:32 +0900 Subject: [PATCH 251/349] Update tests --- .../Visual/Online/TestSceneFriendDisplay.cs | 151 ++++++++++++++++-- .../TestSceneFriendsOnlineStatusControl.cs | 96 ++++++++++- .../Overlays/Dashboard/Friends/FriendsList.cs | 2 +- 3 files changed, 233 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 010a261d4c..2e0f86c622 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -5,46 +5,179 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; +using osu.Game.Tests.Visual.Metadata; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { public partial class TestSceneFriendDisplay : OsuTestScene { - protected override bool UseOnlineAPI => true; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private FriendDisplay display; + private TestMetadataClient metadataClient; [SetUp] public void Setup() => Schedule(() => { - Child = new BasicScrollContainer + Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - Child = display = new FriendDisplay() + CachedDependencies = + [ + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], + Children = new Drawable[] + { + metadataClient, + new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FriendDisplay() + } + } }; }); [Test] - public void TestOffline() + public void TestAddAndRemoveFriends() { - // AddStep("Populate with offline test users", () => display.Users = getUsers()); + AddStep("set friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(getUsers().Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + waitForLoad(3); + + AddStep("remove one friend", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.RemoveAt(0); + }); + + waitForLoad(2); + + AddStep("add one friend", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.AddRange(getUsers().Take(1).Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + waitForLoad(3); + + void waitForLoad(int expectedPanels) + { + AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); + AddAssert($"{expectedPanels} panels in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(expectedPanels)); + } } [Test] - public void TestOnline() + public void TestChangeDisplayStyle() { - // No need to do anything, fetch is performed automatically. + AddStep("set friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(getUsers().Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + waitForLoad(); + + AddStep("set list style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.List); + waitForLoad(); + + AddStep("set brick style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.Brick); + waitForLoad(); + + void waitForLoad() + { + AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); + AddAssert($"3 {typeof(T).ReadableName()} in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(3)); + } + } + + [Test] + public void TestOnlinePresence() + { + AddStep("set friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(getUsers().Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); + assertVisible(3); + + AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); + assertVisible(0); + + AddStep("bring a friend online", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + metadataClient.FriendPresenceUpdated(api.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online }); + }); + + assertVisible(1); + + AddStep("change to offline stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Offline); + assertVisible(2); + + AddStep("bring a friend online", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online }); + }); + + assertVisible(1); + + AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); + assertVisible(2); + + AddStep("change to all stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.All); + assertVisible(3); + + void assertVisible(int expectedPanels) + { + AddAssert($"{expectedPanels} panels visible", + () => this.ChildrenOfType().Count(p => p.Alpha > 0), + () => Is.EqualTo(expectedPanels)); + } } private List getUsers() => new List diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 548f3067a7..c08e7b7b0f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -1,11 +1,18 @@ // 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.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; +using osu.Game.Tests.Visual.Metadata; +using osu.Game.Users; namespace osu.Game.Tests.Visual.UserInterface { @@ -14,14 +21,91 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private TestMetadataClient metadataClient = null!; + [SetUp] - public void SetUp() => Schedule(() => Child = new FriendOnlineStreamControl + public void SetUp() => Schedule(() => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - CountAll = { Value = 15 }, - CountOnline = { Value = 10 }, - CountOffline = { Value = 5 } + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], + Children = new Drawable[] + { + metadataClient, + new FriendOnlineStreamControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; }); + + [Test] + public void TestChangeFriends() + { + AddStep("set 10 friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = i, + TargetUser = new APIUser { Id = i }, + })); + }); + + AddStep("set 20 friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(Enumerable.Range(1, 20).Select(i => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = i, + TargetUser = new APIUser { Id = i }, + })); + }); + } + + [Solo] + [Test] + public void TestChangeOnlineStates() + { + AddStep("set 10 friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = i, + TargetUser = new APIUser { Id = i }, + })); + }); + + AddStep("make users 1-5 online", () => + { + for (int i = 1; i <= 5; i++) + metadataClient.FriendPresenceUpdated(i, new UserPresence { Status = UserStatus.Online }); + }); + + AddStep("make users 1-5 DnD", () => + { + for (int i = 1; i <= 5; i++) + metadataClient.FriendPresenceUpdated(i, new UserPresence { Status = UserStatus.DoNotDisturb }); + }); + + AddStep("make users 1-5 offline", () => + { + for (int i = 1; i <= 5; i++) + metadataClient.FriendPresenceUpdated(i, null); + }); + } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index ed87a58ff4..d1ae4b2f46 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Dashboard.Friends } } - private class FilterableUserPanel : CompositeDrawable, IConditionalFilterable + public class FilterableUserPanel : CompositeDrawable, IConditionalFilterable { public readonly Bindable CanBeShown = new Bindable(); From e4ade7acd1b4c807702350cbb3a5f579aef2e15e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 01:44:20 +0900 Subject: [PATCH 252/349] Prevent spectate/invite callbacks on invalid states --- osu.Game/Users/UserPanel.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 1f72cbccbf..1010234e1f 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -157,20 +157,28 @@ namespace osu.Game.Users chatOverlay?.Show(); })); - if (metadataClient?.GetPresence(User.OnlineID) != null) + if (isUserOnline()) { items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => { - performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))); + if (isUserOnline()) + performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))); })); - if (multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true) + if (canInviteUser()) { - items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(User.Id))); + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => + { + if (canInviteUser()) + multiplayerClient!.InvitePlayer(User.Id); + })); } } return items.ToArray(); + + bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null; + bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true; } } From bfdc7c1688d30a8c53f4860dfe9289b9dd1af948 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 01:54:36 +0900 Subject: [PATCH 253/349] Fix no display if friends already loaded --- .../Visual/Online/TestSceneFriendDisplay.cs | 40 +++++++++++++++++++ .../Dashboard/Friends/FriendDisplay.cs | 2 +- .../Overlays/Dashboard/Friends/FriendsList.cs | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 2e0f86c622..06e51f97fc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -180,6 +180,46 @@ namespace osu.Game.Tests.Visual.Online } } + [Test] + public void TestLoadFriendsBeforeDisplay() + { + AddStep("set friends", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Friends.Clear(); + api.Friends.AddRange(getUsers().Select(u => new APIRelation + { + RelationType = RelationType.Friend, + TargetID = u.OnlineID, + TargetUser = u + })); + }); + + AddStep("load new display", () => + { + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], + Children = new Drawable[] + { + metadataClient, + new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FriendDisplay() + } + } + }; + }); + + AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); + AddAssert("3 panels in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(3)); + } + private List getUsers() => new List { new APIUser diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 223bcdf2d9..19fcb44be7 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Dashboard.Friends apiFriends.BindTo(api.Friends); apiFriends.BindCollectionChanged((_, _) => reloadList()); - userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList()); + userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList(), true); } private void reloadList() diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index d1ae4b2f46..826db945c0 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Dashboard.Friends base.LoadComplete(); friendPresences.BindTo(metadataClient.FriendPresences); - friendPresences.BindCollectionChanged(onFriendPresencesChanged, true); + friendPresences.BindCollectionChanged(onFriendPresencesChanged); SearchText.BindValueChanged(onSearchTextChanged, true); OnlineStream.BindValueChanged(onFriendsStreamChanged, true); From 09c8cf9a83fdce08ba5343c280e555dc7662d9ea Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 02:17:23 +0900 Subject: [PATCH 254/349] Partial classes --- osu.Game/Overlays/Dashboard/Friends/FriendsList.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index 826db945c0..4107bdfb73 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsList : CompositeDrawable + public partial class FriendsList : CompositeDrawable { public readonly IBindable OnlineStream = new Bindable(); public readonly IBindable SortCriteria = new Bindable(); @@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Dashboard.Friends return new FilterableUserPanel(panel); } - private class FriendsSearchContainer : SearchContainer + private partial class FriendsSearchContainer : SearchContainer { public readonly IBindable SortCriteria = new Bindable(); @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Dashboard.Friends } } - public class FilterableUserPanel : CompositeDrawable, IConditionalFilterable + public partial class FilterableUserPanel : CompositeDrawable, IConditionalFilterable { public readonly Bindable CanBeShown = new Bindable(); From 04bb1c13f858b0ff51eb1df02ae48b72dc7eb14e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 02:23:24 +0900 Subject: [PATCH 255/349] Cancel + dispose CTS --- osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 19fcb44be7..941d293d9d 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -198,5 +198,13 @@ namespace osu.Game.Overlays.Dashboard.Friends newList.FadeIn(200, Easing.OutQuint); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + listLoadCancellation?.Cancel(); + listLoadCancellation?.Dispose(); + } } } From 22e4527118c9c30685ac988611465ba2fc036427 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 02:24:30 +0900 Subject: [PATCH 256/349] Fix doubled padding --- osu.Game/Overlays/Dashboard/Friends/FriendsList.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index 4107bdfb73..91256cae17 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -47,7 +47,6 @@ namespace osu.Game.Overlays.Dashboard.Friends { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), SortCriteria = { BindTarget = SortCriteria }, ChildrenEnumerable = friends.Select(createUserPanel) From db0676d6158e55b44c1b99f6cef4ccad96a17eff Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 12 Mar 2025 19:10:41 +0000 Subject: [PATCH 257/349] Remove JSON property attributes from non-databased taiko difficulty attributes --- .../Difficulty/TaikoDifficultyAttributes.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index b43468ab18..b8051054e7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -13,25 +13,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// /// The difficulty corresponding to the rhythm skill. /// - [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } /// /// The difficulty corresponding to the reading skill. /// - [JsonProperty("reading_difficulty")] public double ReadingDifficulty { get; set; } /// /// The difficulty corresponding to the colour skill. /// - [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } /// /// The difficulty corresponding to the stamina skill. /// - [JsonProperty("stamina_difficulty")] public double StaminaDifficulty { get; set; } /// @@ -40,13 +36,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("mono_stamina_factor")] public double MonoStaminaFactor { get; set; } - [JsonProperty("rhythm_difficult_strains")] public double RhythmTopStrains { get; set; } - [JsonProperty("colour_difficult_strains")] public double ColourTopStrains { get; set; } - [JsonProperty("stamina_difficult_strains")] public double StaminaTopStrains { get; set; } public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() From a075a5641aaa0eb44f2c9cc455a8d8e20d4bf84b Mon Sep 17 00:00:00 2001 From: Nuno <146981906+Nunolin@users.noreply.github.com> Date: Thu, 13 Mar 2025 01:02:34 +0000 Subject: [PATCH 258/349] Remove end note conversion in mania invert mod --- osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index ef9154d180..d1912e3690 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -42,8 +42,7 @@ namespace osu.Game.Rulesets.Mania.Mods var locations = column.OfType().Select(n => (startTime: n.StartTime, samples: n.Samples)) .Concat(column.OfType().SelectMany(h => new[] { - (startTime: h.StartTime, samples: h.GetNodeSamples(0)), - (startTime: h.EndTime, samples: h.GetNodeSamples(1)) + (startTime: h.StartTime, samples: h.GetNodeSamples(0)) })) .OrderBy(h => h.startTime).ToList(); From e0a23000be9fe569cd79d2715314daa0aec3ad86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 12:37:57 +0900 Subject: [PATCH 259/349] 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 260/349] 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 f00d2cbfa282ee630340fb89928e6c1895c51e78 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 12:55:00 +0900 Subject: [PATCH 261/349] Fix tests --- .../Visual/Online/TestSceneFriendDisplay.cs | 69 ++++++++++--------- .../TestSceneFriendsOnlineStatusControl.cs | 2 - 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 06e51f97fc..8ed61fe028 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; @@ -66,7 +67,8 @@ namespace osu.Game.Tests.Visual.Online })); }); - waitForLoad(3); + waitForLoad(); + assertPanels(3); AddStep("remove one friend", () => { @@ -74,7 +76,8 @@ namespace osu.Game.Tests.Visual.Online api.Friends.RemoveAt(0); }); - waitForLoad(2); + waitForLoad(); + assertPanels(2); AddStep("add one friend", () => { @@ -87,13 +90,8 @@ namespace osu.Game.Tests.Visual.Online })); }); - waitForLoad(3); - - void waitForLoad(int expectedPanels) - { - AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); - AddAssert($"{expectedPanels} panels in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(expectedPanels)); - } + waitForLoad(); + assertPanels(3); } [Test] @@ -111,19 +109,18 @@ namespace osu.Game.Tests.Visual.Online })); }); - waitForLoad(); + waitForLoad(); + assertPanels(3); AddStep("set list style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.List); - waitForLoad(); + + waitForLoad(); + assertPanels(3); AddStep("set brick style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.Brick); - waitForLoad(); - void waitForLoad() - { - AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); - AddAssert($"3 {typeof(T).ReadableName()} in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(3)); - } + waitForLoad(); + assertPanels(3); } [Test] @@ -141,11 +138,11 @@ namespace osu.Game.Tests.Visual.Online })); }); - AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); - assertVisible(3); + waitForLoad(); + assertPanels(3); AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); - assertVisible(0); + assertPanels(0); AddStep("bring a friend online", () => { @@ -153,10 +150,10 @@ namespace osu.Game.Tests.Visual.Online metadataClient.FriendPresenceUpdated(api.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online }); }); - assertVisible(1); + assertPanels(1); AddStep("change to offline stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Offline); - assertVisible(2); + assertPanels(2); AddStep("bring a friend online", () => { @@ -164,20 +161,13 @@ namespace osu.Game.Tests.Visual.Online metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online }); }); - assertVisible(1); + assertPanels(1); AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); - assertVisible(2); + assertPanels(2); AddStep("change to all stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.All); - assertVisible(3); - - void assertVisible(int expectedPanels) - { - AddAssert($"{expectedPanels} panels visible", - () => this.ChildrenOfType().Count(p => p.Alpha > 0), - () => Is.EqualTo(expectedPanels)); - } + assertPanels(3); } [Test] @@ -216,8 +206,19 @@ namespace osu.Game.Tests.Visual.Online }; }); - AddUntilStep("wait for friends to load", () => this.ChildrenOfType().LastOrDefault()?.IsLoaded == true); - AddAssert("3 panels in list", () => this.ChildrenOfType().Last().ChildrenOfType().Count(), () => Is.EqualTo(3)); + waitForLoad(); + assertPanels(3); + } + + private void waitForLoad() + => AddUntilStep("wait for panels to load", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + + private void assertPanels(int expectedVisible) + where T : UserPanel + { + AddAssert($"{typeof(T).ReadableName()}s in list", () => this.ChildrenOfType().Last().ChildrenOfType().All(p => p is T)); + AddAssert($"{expectedVisible} panels visible", () => this.ChildrenOfType().Last().ChildrenOfType().Count(p => p.IsPresent), + () => Is.EqualTo(expectedVisible)); } private List getUsers() => new List diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index c08e7b7b0f..c7e2a0ed4b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Testing; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; @@ -73,7 +72,6 @@ namespace osu.Game.Tests.Visual.UserInterface }); } - [Solo] [Test] public void TestChangeOnlineStates() { From 6b786a6ab476f6c3fdcdef92727dc2022df799ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 13:45:02 +0900 Subject: [PATCH 262/349] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8f219ea426..b6ab7dc712 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 8045009621..486979487b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From d840fcfb99ea886ac52793c5c4bac5b5b500bb15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 13:41:01 +0900 Subject: [PATCH 263/349] 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 264/349] 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 da71e7a3633ebdd363c577a0584d004ff1b44e08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 14:24:10 +0900 Subject: [PATCH 265/349] Fix enter to select tag not working in results scren --- osu.Game/Screens/Ranking/UserTagControl.cs | 35 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 3ae6501b36..ae4a918ae5 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -27,6 +27,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -500,21 +501,45 @@ namespace osu.Game.Screens.Ranking searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } + public override bool OnPressed(KeyBindingPressEvent e) + { + if (base.OnPressed(e)) + return true; + + if (e.Repeat) + return false; + + if (State.Value == Visibility.Hidden) + return false; + + if (e.Action == GlobalAction.Select) + { + attemptSelect(); + return true; + } + + return false; + } + protected override bool OnKeyDown(KeyDownEvent e) { - var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); - if (e.Key == Key.Enter) { - if (visibleItems.Length == 1) - select(visibleItems.Single().Tag); - + attemptSelect(); return true; } return base.OnKeyDown(e); } + private void attemptSelect() + { + var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + + if (visibleItems.Length == 1) + select(visibleItems.Single().Tag); + } + private void select(UserTag tag) { OnSelected?.Invoke(tag); From 829dfedfdc58866ed3c6628d4bf4362bca295607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 15:28:04 +0900 Subject: [PATCH 266/349] Add test coverage of friend going offline --- .../Visual/Online/TestSceneFriendDisplay.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 8ed61fe028..f7fd95a6e1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); AddStep("remove one friend", () => { @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(2); + assertVisiblePanelCount(2); AddStep("add one friend", () => { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); } [Test] @@ -110,17 +110,17 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); AddStep("set list style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.List); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); AddStep("set brick style", () => this.ChildrenOfType().Single().DisplayStyle.Value = OverlayPanelDisplayStyle.Brick); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); } [Test] @@ -139,10 +139,10 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); - assertPanels(0); + assertVisiblePanelCount(0); AddStep("bring a friend online", () => { @@ -150,10 +150,10 @@ namespace osu.Game.Tests.Visual.Online metadataClient.FriendPresenceUpdated(api.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online }); }); - assertPanels(1); + assertVisiblePanelCount(1); AddStep("change to offline stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Offline); - assertPanels(2); + assertVisiblePanelCount(2); AddStep("bring a friend online", () => { @@ -161,13 +161,20 @@ namespace osu.Game.Tests.Visual.Online metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online }); }); - assertPanels(1); + assertVisiblePanelCount(1); AddStep("change to online stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.Online); - assertPanels(2); + assertVisiblePanelCount(2); + + AddStep("take friend offline", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, null); + }); + assertVisiblePanelCount(1); AddStep("change to all stream", () => this.ChildrenOfType().Single().Current.Value = OnlineStatus.All); - assertPanels(3); + assertVisiblePanelCount(3); } [Test] @@ -207,13 +214,13 @@ namespace osu.Game.Tests.Visual.Online }); waitForLoad(); - assertPanels(3); + assertVisiblePanelCount(3); } private void waitForLoad() => AddUntilStep("wait for panels to load", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); - private void assertPanels(int expectedVisible) + private void assertVisiblePanelCount(int expectedVisible) where T : UserPanel { AddAssert($"{typeof(T).ReadableName()}s in list", () => this.ChildrenOfType().Last().ChildrenOfType().All(p => p is T)); From d74dd262f532f2922a6fcd90655c35a9a4947fe0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Mar 2025 16:04:27 +0900 Subject: [PATCH 267/349] Add inline TODOs regarding sorting modes --- osu.Game/Overlays/Dashboard/Friends/FriendsList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index 91256cae17..955c2c046e 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -152,9 +152,11 @@ namespace osu.Game.Overlays.Dashboard.Friends { default: case UserSortCriteria.LastVisit: + // Todo: Last visit time is not currently updated according to realtime user presence. return panels.OrderByDescending(panel => panel.User.LastVisit); case UserSortCriteria.Rank: + // Todo: Statistics are not currently updated according to realtime user statistics, but it's also not currently displayed in the panels. return panels.OrderByDescending(panel => panel.User.Statistics.GlobalRank.HasValue).ThenBy(panel => panel.User.Statistics.GlobalRank ?? 0); case UserSortCriteria.Username: From 3e71d04bd076926d794c0a066c900515c05fcbf9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 15:07:15 +0900 Subject: [PATCH 268/349] Standardise sizing of placeholder messages on beatmap scores overlay --- .../Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs | 6 ++---- osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs index 7cb119bf32..36f71be606 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs @@ -3,11 +3,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osuTK; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; +using osuTK; namespace osu.Game.Overlays.BeatmapSet.Scores { @@ -30,7 +29,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = BeatmapsetsStrings.ShowScoreboardSupporterOnly, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), }, text = new LinkFlowContainer(t => t.Font = t.Font.With(size: 11)) { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index b53b7826f3..bd61992dbf 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -158,6 +158,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + Margin = new MarginPadding { Vertical = 10 }, Alpha = 0, }, new FillFlowContainer From 9af3c8351d27c743d623268a836b13d8225b2b14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 15:08:12 +0900 Subject: [PATCH 269/349] Add helper methods to check whether beatmap leaderboard scope requires supporter --- osu.Game/Extensions/ModelExtensions.cs | 16 ++++++++++++++++ .../BeatmapSet/Scores/ScoresContainer.cs | 3 ++- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index eef9b63b62..ec6b5ac6de 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -9,6 +9,7 @@ using osu.Game.IO; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Extensions @@ -164,5 +165,20 @@ namespace osu.Game.Extensions /// that function does not have per-platform considerations (and is only made to work on windows). /// public static string GetValidFilename(this string filename) => invalid_filename_chars.Replace(filename, "_"); + + public static bool RequiresSupporter(this BeatmapLeaderboardScope scope, bool filterMods) + { + switch (scope) + { + case BeatmapLeaderboardScope.Local: + return false; + + case BeatmapLeaderboardScope.Country: + case BeatmapLeaderboardScope.Friend: + return true; + } + + return filterMods; + } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index bd61992dbf..b54750c5c3 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -248,7 +249,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; } - if ((scope.Value != BeatmapLeaderboardScope.Global || modSelector.SelectedMods.Count > 0) && !userIsSupporter) + if (scope.Value.RequiresSupporter(modSelector.SelectedMods.Count > 0) && !userIsSupporter) { Scores = null; notSupporterPlaceholder.Show(); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 57fe22aa59..9bf517cb77 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (!api.LocalUser.Value.IsSupporter && (Scope != BeatmapLeaderboardScope.Global || filterMods)) + if (Scope.RequiresSupporter(filterMods) && !api.LocalUser.Value.IsSupporter) { SetErrorState(LeaderboardState.NotSupporter); return null; From 249bbd0b5975510725637529c86c76325d4aab27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 16:26:54 +0900 Subject: [PATCH 270/349] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 21a3ddad0e..5b5482b3c7 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From b0cf5e8bff79e25333783ffa53f91041ca476576 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Mar 2025 15:08:23 +0900 Subject: [PATCH 271/349] Add support for team beatmap leaderboards --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 1 + osu.Game/Localisation/LeaderboardStrings.cs | 5 +++ osu.Game/Online/Leaderboards/Leaderboard.cs | 3 ++ .../Online/Leaderboards/LeaderboardState.cs | 1 + .../BeatmapSet/LeaderboardScopeSelector.cs | 1 + .../BeatmapSet/Scores/NoScoresPlaceholder.cs | 4 +++ .../BeatmapSet/Scores/NoTeamPlaceholder.cs | 34 +++++++++++++++++++ .../BeatmapSet/Scores/ScoresContainer.cs | 16 +++++++++ .../Select/Leaderboards/BeatmapLeaderboard.cs | 7 ++++ .../Leaderboards/BeatmapLeaderboardScope.cs | 4 +++ .../Screens/Select/PlayBeatmapDetailArea.cs | 10 +++++- 11 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/BeatmapSet/Scores/NoTeamPlaceholder.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 62ca8bf831..474d2ee6e3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -201,6 +201,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("ensure no scores displayed", () => leaderboard.SetScores(null)); AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure)); + AddStep(@"No team", () => leaderboard.SetErrorState(LeaderboardState.NoTeam)); AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter)); AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn)); AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable)); diff --git a/osu.Game/Localisation/LeaderboardStrings.cs b/osu.Game/Localisation/LeaderboardStrings.cs index 8e53f8e88c..816406bf31 100644 --- a/osu.Game/Localisation/LeaderboardStrings.cs +++ b/osu.Game/Localisation/LeaderboardStrings.cs @@ -44,6 +44,11 @@ namespace osu.Game.Localisation /// public static LocalisableString PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard => new TranslatableString(getKey(@"please_invest_in_an_osu_supporter_tag_to_view_this_leaderboard"), @"Please invest in an osu!supporter tag to view this leaderboard!"); + /// + /// "You are not on a team. Maybe you should join one!" + /// + public static LocalisableString NoTeam => new TranslatableString(getKey(@"no_team"), @"You are not on a team. Maybe you should join one!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 3c25d6f789..021a2b3959 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -356,6 +356,9 @@ namespace osu.Game.Online.Leaderboards case LeaderboardState.NotSupporter: return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard); + case LeaderboardState.NoTeam: + return new MessagePlaceholder(LeaderboardStrings.NoTeam); + case LeaderboardState.Retrieving: return null; diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs index 6b07500a98..dbd982acf2 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -14,5 +14,6 @@ namespace osu.Game.Online.Leaderboards NoScores, NotLoggedIn, NotSupporter, + NoTeam } } diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs index 5cfe4a35b3..12fbc4c790 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays.BeatmapSet AddItem(BeatmapLeaderboardScope.Global); AddItem(BeatmapLeaderboardScope.Country); AddItem(BeatmapLeaderboardScope.Friend); + AddItem(BeatmapLeaderboardScope.Team); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs index 29a696593d..b161ee49c6 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs @@ -41,6 +41,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores case BeatmapLeaderboardScope.Country: text.Text = BeatmapsetsStrings.ShowScoreboardNoScoresCountry; break; + + case BeatmapLeaderboardScope.Team: + text.Text = BeatmapsetsStrings.ShowScoreboardNoScoresTeam; + break; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NoTeamPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NoTeamPlaceholder.cs new file mode 100644 index 0000000000..0bd4a1334f --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/Scores/NoTeamPlaceholder.cs @@ -0,0 +1,34 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osuTK; + +namespace osu.Game.Overlays.BeatmapSet.Scores +{ + public partial class NoTeamPlaceholder : Container + { + public NoTeamPlaceholder() + { + AutoSizeAxes = Axes.Both; + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = LeaderboardStrings.NoTeam, + }, + } + }; + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index b54750c5c3..9b9661f83d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -41,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly LeaderboardModSelector modSelector; private readonly NoScoresPlaceholder noScoresPlaceholder; private readonly NotSupporterPlaceholder notSupporterPlaceholder; + private readonly NoTeamPlaceholder noTeamPlaceholder; [Resolved] private IAPIProvider api { get; set; } @@ -155,6 +156,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores AlwaysPresent = true, Margin = new MarginPadding { Vertical = 10 } }, + noTeamPlaceholder = new NoTeamPlaceholder + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Vertical = 10 }, + Alpha = 0, + }, notSupporterPlaceholder = new NotSupporterPlaceholder { Anchor = Anchor.TopCentre, @@ -249,6 +257,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; } + if ((scope.Value == BeatmapLeaderboardScope.Team) && user.Value.Team == null) + { + Scores = null; + noTeamPlaceholder.Show(); + return; + } + if (scope.Value.RequiresSupporter(modSelector.SelectedMods.Count > 0) && !userIsSupporter) { Scores = null; @@ -256,6 +271,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; } + noTeamPlaceholder.Hide(); notSupporterPlaceholder.Hide(); Show(); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 9bf517cb77..ec1ef33387 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -9,6 +9,7 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; @@ -144,6 +145,12 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } + if (Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) + { + SetErrorState(LeaderboardState.NoTeam); + return null; + } + IReadOnlyList? requestMods = null; if (filterMods && !mods.Value.Any()) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index e2e3404877..aec22cc007 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs @@ -20,5 +20,9 @@ namespace osu.Game.Screens.Select.Leaderboards [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardFriend))] Friend, + + // TODO: localise once localisations are updated + [Description("Team Ranking")] + Team, } } diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index deb1100dfc..5b62d5e8d7 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -83,6 +83,7 @@ namespace osu.Game.Screens.Select new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global), new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country), new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend), + new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Team), }).ToArray(); private BeatmapDetailAreaTabItem getTabItemFromTabType(TabType type) @@ -104,6 +105,9 @@ namespace osu.Game.Screens.Select case TabType.Friends: return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend); + case TabType.Team: + return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Team); + default: throw new ArgumentOutOfRangeException(nameof(type)); } @@ -131,6 +135,9 @@ namespace osu.Game.Screens.Select case BeatmapLeaderboardScope.Friend: return TabType.Friends; + case BeatmapLeaderboardScope.Team: + return TabType.Team; + default: throw new ArgumentOutOfRangeException(nameof(item)); } @@ -146,7 +153,8 @@ namespace osu.Game.Screens.Select Local, Country, Global, - Friends + Friends, + Team } } } From 3515f9a8f8c95b577d8ac79e6802df4547b1d0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 08:36:19 +0100 Subject: [PATCH 272/349] Remove unused using --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index ec1ef33387..46705aaa28 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -9,7 +9,6 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; From c18764d764f2f18abfad5a4f3e98f1c0b6357055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 08:37:02 +0100 Subject: [PATCH 273/349] Use localisable string --- .../Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index aec22cc007..a3687d9586 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs @@ -21,8 +21,7 @@ namespace osu.Game.Screens.Select.Leaderboards [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardFriend))] Friend, - // TODO: localise once localisations are updated - [Description("Team Ranking")] + [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardTeam))] Team, } } From f78e371b1b5194cd7ee74e79ef54926d6ac7f3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 10:10:09 +0100 Subject: [PATCH 274/349] Simplify boolean flags --- .../Visual/Ranking/TestSceneResultsScreen.cs | 2 +- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 1 - .../OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 6 ++---- .../Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 3 +-- osu.Game/Screens/Play/SubmittingPlayer.cs | 3 +-- osu.Game/Screens/Ranking/ResultsScreen.cs | 12 ++++-------- .../Screens/Ranking/Statistics/StatisticsPanel.cs | 7 +------ 7 files changed, 10 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index b19288fd99..4758b70526 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -404,7 +404,7 @@ namespace osu.Game.Tests.Visual.Ranking : base(score) { AllowRetry = true; - ShowUserStatistics = true; + IsLocalPlay = true; } protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index a64fd488bc..02d30d12e6 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -214,7 +214,6 @@ namespace osu.Game.Tests.Visual.Ranking State = { Value = Visibility.Visible }, Score = { Value = score }, AchievedScore = score, - ShowUserTagControl = true, } }; }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 67a67cf271..3d4b46f49e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -198,13 +198,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return multiplayerLeaderboard.TeamScores.Count == 2 ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores) { - ShowUserStatistics = true, - ShowUserTagControl = true, + IsLocalPlay = true, } : new MultiplayerResultsScreen(score, Room.RoomID.Value, PlaylistItem) { - ShowUserStatistics = true, - ShowUserTagControl = true, + IsLocalPlay = true, }; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 80b378bdcf..dc4078cb1f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -59,8 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem) { AllowRetry = true, - ShowUserStatistics = true, - ShowUserTagControl = true, + IsLocalPlay = true, }; } diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 04cf473173..dc3e5f08ac 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -328,8 +328,7 @@ namespace osu.Game.Screens.Play protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) { AllowRetry = true, - ShowUserStatistics = true, - ShowUserTagControl = true, + IsLocalPlay = true, }; } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fcf90a3e28..6da731588f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -79,13 +79,10 @@ namespace osu.Game.Screens.Ranking public bool AllowWatchingReplay { get; init; } = true; /// - /// Whether the user's personal statistics should be shown on the extended statistics panel - /// after clicking the score panel associated with the being presented. - /// Requires to be present. + /// Whether the provided score is for a local user's play. + /// This will trigger elements like the user's ranking to display. /// - public bool ShowUserStatistics { get; init; } - - public bool ShowUserTagControl { get; init; } + public bool IsLocalPlay { get; init; } private Sample? popInSample; @@ -127,8 +124,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, Score = { BindTarget = SelectedScore }, - AchievedScore = ShowUserStatistics && Score != null ? Score : null, - ShowUserTagControl = ShowUserTagControl, + AchievedScore = IsLocalPlay && Score != null ? Score : null, }, ScorePanelList = new ScorePanelList { diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 16de00fcf1..0cee01cf77 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -37,11 +37,6 @@ namespace osu.Game.Screens.Ranking.Statistics /// public ScoreInfo? AchievedScore { get; init; } - /// - /// Whether to show a control that allows to assign user tags to the played beatmap. - /// - public bool ShowUserTagControl { get; init; } - protected override bool StartHidden => true; [Resolved] @@ -235,7 +230,7 @@ namespace osu.Game.Screens.Ranking.Statistics }); } - if (ShowUserTagControl + if (AchievedScore != null && newScore.BeatmapInfo!.OnlineID > 0 && api.IsLoggedIn) { From 5b1a4c8ed14bbf17baa11c87325226540a5fe170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 10:13:22 +0100 Subject: [PATCH 275/349] Require high enough score rank for tagging beatmap --- .../Ranking/TestSceneStatisticsPanel.cs | 22 +++++++++++++++ .../Ranking/Statistics/StatisticsPanel.cs | 27 +++++++++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 02d30d12e6..168410fe4a 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -219,6 +219,28 @@ namespace osu.Game.Tests.Visual.Ranking }); } + [Test] + public void TestTaggingWhenRankTooLow() + { + var score = TestResources.CreateTestScoreInfo(); + score.Rank = ScoreRank.D; + + 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 diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 0cee01cf77..758eabcf2e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -234,12 +235,28 @@ namespace osu.Game.Screens.Ranking.Statistics && newScore.BeatmapInfo!.OnlineID > 0 && api.IsLoggedIn) { - yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) + if ( + // We may want to iterate on this condition + AchievedScore.Rank >= ScoreRank.C + ) { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); + yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + else + { + yield return new StatisticItem("Tag the beatmap!", () => new OsuTextFlowContainer(cp => cp.Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.Centre, + Text = "Set a better score to contribute to beatmap tags!", + }); + } } } From 723a22130d61061b3f0b9062ba49b89e4ff4c0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 12:06:27 +0100 Subject: [PATCH 276/349] Fix test --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 168410fe4a..df65023303 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -152,7 +152,9 @@ namespace osu.Game.Tests.Visual.Ranking } }); AddUntilStep("overall ranking present", () => this.ChildrenOfType().Any()); - AddUntilStep("loading spinner not visible", () => this.ChildrenOfType().All(l => l.State.Value == Visibility.Hidden)); + AddUntilStep("loading spinner not visible", + () => this.ChildrenOfType().Single() + .ChildrenOfType().All(l => l.State.Value == Visibility.Hidden)); } [Test] From 607c83abd69c09c8d5313f18ec67734151bc576a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Mar 2025 12:07:19 +0100 Subject: [PATCH 277/349] Switch add beatmap tag request to using path paramx See https://github.com/ppy/osu-web/pull/11999. --- osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs index 4fa02dc569..2dd9303841 100644 --- a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs +++ b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.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.Globalization; using System.Net.Http; using osu.Framework.IO.Network; @@ -22,10 +21,9 @@ namespace osu.Game.Online.API.Requests { var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; - req.AddParameter(@"tag_id", TagID.ToString(CultureInfo.InvariantCulture), RequestParameterType.Query); return req; } - protected override string Target => $@"beatmaps/{BeatmapID}/tags"; + protected override string Target => $@"beatmaps/{BeatmapID}/tags/{TagID}"; } } From 17b17f4d714bcbfe9fbbc20776b26f4d79af3f36 Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 12 Mar 2025 17:41:16 +0600 Subject: [PATCH 278/349] Highlight diff attribute changes in mod select --- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 2670c20d26..dedd1e336e 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -177,15 +177,18 @@ namespace osu.Game.Overlays.Mods bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate); BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); + BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(originalDifficulty); foreach (var mod in Mods.Value.OfType()) - mod.ApplyToDifficulty(originalDifficulty); + mod.ApplyToDifficulty(adjustedDifficulty); Ruleset ruleset = GameRuleset.Value.CreateInstance(); - BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); + adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(adjustedDifficulty, rate); TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); + circleSizeDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.CircleSize, adjustedDifficulty.CircleSize); + drainRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.DrainRate, adjustedDifficulty.DrainRate); approachRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate); overallDifficultyDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty); From 993be4da41d2c63659beedd14c0c59aed0178278 Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Thu, 13 Mar 2025 23:41:41 +0100 Subject: [PATCH 279/349] Add copy version context menu --- osu.Game/Overlays/Settings/SettingsFooter.cs | 36 ++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 4e9d4c0d28..ce32dc1a85 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -1,13 +1,18 @@ // 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 osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -47,10 +52,21 @@ namespace osu.Game.Overlays.Settings Text = game.Name, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), }, - new BuildDisplay(game.Version) + new OsuContextMenuContainer() { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + + Children = new Drawable[] + { + new BuildDisplay(game.Version) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } } }; @@ -76,10 +92,13 @@ namespace osu.Game.Overlays.Settings } } - private partial class BuildDisplay : OsuAnimatedButton + private partial class BuildDisplay : OsuAnimatedButton, IHasContextMenu { private readonly string version; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -108,6 +127,19 @@ namespace osu.Game.Overlays.Settings Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White, }); } + + public MenuItem[] ContextMenuItems + { + get + { + List menuItems = new List() + { + new OsuMenuItem("Copy Version", MenuItemType.Standard, () => clipboard.SetText(version)) + }; + + return menuItems.ToArray(); + } + } } } } From b6b115e2690ca261bbbc02926f8b61d3882f299a Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Fri, 14 Mar 2025 09:50:53 +0100 Subject: [PATCH 280/349] Move the context menu container to a higher level --- osu.Game/Overlays/Settings/SettingsFooter.cs | 14 +------------- osu.Game/Overlays/SettingsOverlay.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index ce32dc1a85..07dc2ea230 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -12,7 +12,6 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -52,21 +51,10 @@ namespace osu.Game.Overlays.Settings Text = game.Name, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), }, - new OsuContextMenuContainer() + new BuildDisplay(game.Version) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - - Children = new Drawable[] - { - new BuildDisplay(game.Version) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - } - } } }; diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 630675a717..a498f2fe1f 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Graphics; +using osu.Game.Graphics.Cursor; using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; @@ -56,7 +57,13 @@ namespace osu.Game.Overlays private SettingsSubPanel lastOpenedSubPanel; protected override Drawable CreateHeader() => new SettingsHeader(Title, Description); - protected override Drawable CreateFooter() => new SettingsFooter(); + + protected override Drawable CreateFooter() => new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new SettingsFooter() + }; public SettingsOverlay() : base(false) From f72de39e4ded4ec79810b873170adb92b27988d2 Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Fri, 14 Mar 2025 10:02:41 +0100 Subject: [PATCH 281/349] Change CopyUrlToClipboard to CopyStringToClipboard --- osu.Game/Graphics/UserInterface/ExternalLinkButton.cs | 2 +- osu.Game/Localisation/ToastStrings.cs | 4 ++-- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- .../Overlays/OSD/{CopyUrlToast.cs => CopyStringToast.cs} | 6 +++--- .../Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) rename osu.Game/Overlays/OSD/{CopyUrlToast.cs => CopyStringToast.cs} (61%) diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index b3ffd15816..2bc5ba91fa 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -89,7 +89,7 @@ namespace osu.Game.Graphics.UserInterface { if (Link == null) return; - game?.CopyUrlToClipboard(Link); + game?.CopyStringToClipboard(Link); } } } diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index 49e8d00371..000f01ebca 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -45,9 +45,9 @@ namespace osu.Game.Localisation public static LocalisableString SkinSaved => new TranslatableString(getKey(@"skin_saved"), @"Skin saved"); /// - /// "Link copied to clipboard" + /// "Copied to clipboard" /// - public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"Link copied to clipboard"); + public static LocalisableString StringCopied => new TranslatableString(getKey(@"string_copied"), @"Copied to clipboard"); /// /// "Speed changed to {0:N2}x" diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4a9154f14b..c461c0fcc5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -519,10 +519,10 @@ namespace osu.Game } }); - public void CopyUrlToClipboard(string url) => waitForReady(() => onScreenDisplay, _ => + public void CopyStringToClipboard(string url) => waitForReady(() => onScreenDisplay, _ => { dependencies.Get().SetText(url); - onScreenDisplay.Display(new CopyUrlToast()); + onScreenDisplay.Display(new CopyStringToast()); }); public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) => waitForReady(() => externalLinkOpener, _ => externalLinkOpener.OpenUrlExternally(url, warnMode)); diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 0d566174bb..2f1b7054e2 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -420,7 +420,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}"); - onScreenDisplay?.Display(new CopyUrlToast()); + onScreenDisplay?.Display(new CopyStringToast()); } private void toggleReply() diff --git a/osu.Game/Overlays/OSD/CopyUrlToast.cs b/osu.Game/Overlays/OSD/CopyStringToast.cs similarity index 61% rename from osu.Game/Overlays/OSD/CopyUrlToast.cs rename to osu.Game/Overlays/OSD/CopyStringToast.cs index 2c5a9179f2..34f85dc9cb 100644 --- a/osu.Game/Overlays/OSD/CopyUrlToast.cs +++ b/osu.Game/Overlays/OSD/CopyStringToast.cs @@ -5,10 +5,10 @@ using osu.Game.Localisation; namespace osu.Game.Overlays.OSD { - public partial class CopyUrlToast : Toast + public partial class CopyStringToast : Toast { - public CopyUrlToast() - : base(CommonStrings.General, ToastStrings.UrlCopied, "") + public CopyStringToast() + : base(CommonStrings.General, ToastStrings.StringCopied, "") { } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index de5813ce0d..d18e00d643 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -355,7 +355,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { items.AddRange([ new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value))), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value))) + new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyStringToClipboard(formatRoomUrl(Room.RoomID.Value))) ]); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 4451cfcf32..055cb53d24 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -301,7 +301,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); if (beatmapInfo.GetOnlineURL(api, ruleset.Value) is string url) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyUrlToClipboard(url))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyStringToClipboard(url))); if (manager != null) items.Add(new OsuMenuItem("Mark as played", MenuItemType.Standard, () => manager.MarkPlayed(beatmapInfo))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 996d9ea0ab..55b2e68209 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -301,7 +301,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); if (beatmapSet.GetOnlineURL(api, ruleset.Value) is string url) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyUrlToClipboard(url))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyStringToClipboard(url))); if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); From c31fcb946b609018c9875d4cfc54d9e171c5d2c6 Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Fri, 14 Mar 2025 10:06:28 +0100 Subject: [PATCH 282/349] Rename parameter --- osu.Game/OsuGame.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c461c0fcc5..87f6d58d02 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -519,9 +519,9 @@ namespace osu.Game } }); - public void CopyStringToClipboard(string url) => waitForReady(() => onScreenDisplay, _ => + public void CopyStringToClipboard(string value) => waitForReady(() => onScreenDisplay, _ => { - dependencies.Get().SetText(url); + dependencies.Get().SetText(value); onScreenDisplay.Display(new CopyStringToast()); }); From c671eef26031770c4bf0f4e2e1eaac22b0a7716c Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Fri, 14 Mar 2025 10:09:10 +0100 Subject: [PATCH 283/349] Use CopyStringToClipboard --- osu.Game/Overlays/Settings/SettingsFooter.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 07dc2ea230..70cb73581c 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -90,6 +90,9 @@ namespace osu.Game.Overlays.Settings [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OsuGame game { get; set; } = null!; + public BuildDisplay(string version) { this.version = version; @@ -122,7 +125,7 @@ namespace osu.Game.Overlays.Settings { List menuItems = new List() { - new OsuMenuItem("Copy Version", MenuItemType.Standard, () => clipboard.SetText(version)) + new OsuMenuItem("Copy version", MenuItemType.Standard, () => game?.CopyStringToClipboard(version)) }; return menuItems.ToArray(); From 7715a3dd9f65049a140e450e59dbdec0560ae152 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Mar 2025 19:11:15 +0900 Subject: [PATCH 284/349] Add failing test --- .../Visual/Multiplayer/TestSceneRoomListing.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 27c5758afa..45f1ff1acb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Tests.Visual.OnlinePlay; @@ -198,6 +199,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } + [Test] + public void TestFreestyleBypassesRulesetFilter() + { + AddStep("apply taiko filter", () => container.Filter.Value = new FilterCriteria { Ruleset = new TaikoRuleset().RulesetInfo }); + + AddStep("add osu + freestyle room", () => + { + var room = GenerateRooms(1, new OsuRuleset().RulesetInfo)[0]; + room.Playlist[0].Freestyle = true; + room.CurrentPlaylistItem = room.Playlist[0]; + rooms.Add(room); + }); + + AddAssert("room visible", () => container.DrawableRooms.Any()); + } + private bool checkRoomSelected(Room? room) => selectedRoom.Value == room; private Room? getRoomInFlow(int index) => From 145cff50911e5c48cc1fb7d11f5e92751f0faa66 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Mar 2025 19:11:32 +0900 Subject: [PATCH 285/349] Filter freestyle rooms against all rulesets --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 0276601656..9835802fae 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; + matchingFilter &= criteria.Ruleset == null || r.Room.CurrentPlaylistItem?.Freestyle == true || r.Room.PlaylistItemStats?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; matchingFilter &= matchPermissions(r, criteria.Permissions); // Room name isn't translatable, so ToString() is used here for simplicity. From 8d10fe8d324567d65142b02b9d9b6a14a6f50d2c Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Fri, 14 Mar 2025 12:42:42 +0100 Subject: [PATCH 286/349] Fix failing test --- osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index df0fc8de57..970949280f 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -161,6 +161,9 @@ namespace osu.Game.Tests.Visual.Settings }); Dependencies.CacheAs(dialogOverlay); + + var osuGame = new OsuGame(); + Dependencies.CacheAs(osuGame); } } } From d5074bb30f785eaada7de720959dab84a6528702 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 14 Mar 2025 20:47:11 +0900 Subject: [PATCH 287/349] Improve SFX playback behaviour of rapid kiai/star fountain activations --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 10 +-- osu.Game/Screens/Menu/StarFountainSfx.cs | 74 +++++++++++++++++++ .../Screens/Play/KiaiGameplayFountains.cs | 8 +- 3 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Screens/Menu/StarFountainSfx.cs diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index b57012eaf7..7baf18d526 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -6,9 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Utils; -using osu.Game.Audio; using osu.Game.Graphics.Containers; -using osu.Game.Skinning; namespace osu.Game.Screens.Menu { @@ -20,7 +18,7 @@ namespace osu.Game.Screens.Menu [Resolved] private GameHost host { get; set; } = null!; - private SkinnableSound? sample; + private StarFountainSfx sfx = null!; [BackgroundDependencyLoader] private void load() @@ -41,7 +39,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.BottomRight, X = -250, }, - sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")) + sfx = new StarFountainSfx() }; } @@ -83,9 +81,9 @@ namespace osu.Game.Screens.Menu break; } - // Don't play SFX when game is in background as it can be a bit noisy. + // Don't play SFX when game is in background, as it can be a bit noisy. if (host.IsActive.Value) - sample?.Play(); + sfx.Trigger(); } } } diff --git a/osu.Game/Screens/Menu/StarFountainSfx.cs b/osu.Game/Screens/Menu/StarFountainSfx.cs new file mode 100644 index 0000000000..91337d6959 --- /dev/null +++ b/osu.Game/Screens/Menu/StarFountainSfx.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; +using osu.Game.Audio; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Menu +{ + public partial class StarFountainSfx : Container + { + private const int shoot_retrigger_delay = 500; + private const int loop_fade_duration = 500; + + private double? lastPlayback; + + private SkinnableSound? shootSample; + private PausableSkinnableSound? loopSample; + + private ScheduledDelegate? loopFadeDelegate; + private ScheduledDelegate? loopStopDelegate; + + public void Trigger() + { + loopFadeDelegate?.Cancel(); + loopStopDelegate?.Cancel(); + + // Only play 'shootSample' if enough time has passed since last `Trigger()` call. + if (lastPlayback == null || Time.Current - lastPlayback > shoot_retrigger_delay) + { + loopSample?.Stop(); + shootSample?.Play(); + lastPlayback = Time.Current; + + return; + } + + if (loopSample == null) return; + + // Only call `Play()` if `loopSample` is not already playing, to prevent restarting the sample each time. + if (!loopSample.RequestedPlaying) + { + loopSample.Volume.Value = 1f; + loopSample.Play(); + } + + // Schedule a volume fadeout, followed by a `Stop()`. + loopFadeDelegate = Scheduler.AddDelayed(() => + { + this.TransformBindableTo(loopSample.Volume, 0, loop_fade_duration); + + loopStopDelegate = Scheduler.AddDelayed(() => + { + loopSample?.Stop(); + }, loop_fade_duration); + }, shoot_retrigger_delay); + + lastPlayback = Time.Current; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + shootSample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")), + loopSample = new PausableSkinnableSound(new SampleInfo("Gameplay/fountain-loop")) { Looping = true }, + }; + } + } +} diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 017e66253f..cdeb2a0700 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -6,11 +6,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; -using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; -using osu.Game.Skinning; namespace osu.Game.Screens.Play { @@ -21,7 +19,7 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; - private SkinnableSound? sample; + private StarFountainSfx sfx = null!; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -44,7 +42,7 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, X = -75, }, - sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")) + sfx = new StarFountainSfx(), }; } @@ -72,7 +70,7 @@ namespace osu.Game.Screens.Play leftFountain.Shoot(1); rightFountain.Shoot(-1); - sample?.Play(); + sfx.Trigger(); } public partial class GameplayStarFountain : StarFountain From f37f60b22d043e459c5cada4e17e8f15e06b4d73 Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Fri, 14 Mar 2025 13:23:10 +0100 Subject: [PATCH 288/349] Fix code quality --- osu.Game/Overlays/Settings/SettingsFooter.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 70cb73581c..41ea266af8 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.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 osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Graphics; @@ -9,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -84,9 +82,6 @@ namespace osu.Game.Overlays.Settings { private readonly string version; - [Resolved] - private Clipboard clipboard { get; set; } = null!; - [Resolved] private OsuColour colours { get; set; } = null!; @@ -119,18 +114,10 @@ namespace osu.Game.Overlays.Settings }); } - public MenuItem[] ContextMenuItems + public MenuItem[] ContextMenuItems => new MenuItem[] { - get - { - List menuItems = new List() - { - new OsuMenuItem("Copy version", MenuItemType.Standard, () => game?.CopyStringToClipboard(version)) - }; - - return menuItems.ToArray(); - } - } + new OsuMenuItem("Copy version", MenuItemType.Standard, () => game?.CopyStringToClipboard(version)) + }; } } } From f2b57e39546075481071236bf685134eb468e528 Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Fri, 14 Mar 2025 13:33:07 +0100 Subject: [PATCH 289/349] Fix more code quality errors --- osu.Game/Overlays/Settings/SettingsFooter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 41ea266af8..52abd4fa65 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -86,7 +86,7 @@ namespace osu.Game.Overlays.Settings private OsuColour colours { get; set; } = null!; [Resolved] - private OsuGame game { get; set; } = null!; + private OsuGame? game { get; set; } public BuildDisplay(string version) { From ed61f87eed779733d123f91c28e95a24d653a645 Mon Sep 17 00:00:00 2001 From: wezwery Date: Sat, 15 Mar 2025 17:12:11 +0200 Subject: [PATCH 290/349] 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 0bde11b504394f915a64ca61f4d11477f5c4843e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Mar 2025 01:21:00 +0900 Subject: [PATCH 291/349] Allow spectator button to work without begin play requests --- osu.Game/Online/Spectator/SpectatorClient.cs | 12 ---- .../Dashboard/CurrentlyOnlineDisplay.cs | 60 +++++-------------- osu.Game/Users/ExtendedUserPanel.cs | 1 + 3 files changed, 15 insertions(+), 58 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 91f009b76f..76e5cb0404 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -47,11 +47,6 @@ namespace osu.Game.Online.Spectator /// public IBindableList WatchingUsers => watchingUsers; - /// - /// A global list of all players currently playing. - /// - public IBindableList PlayingUsers => playingUsers; - /// /// Whether the local user is playing. /// @@ -91,7 +86,6 @@ namespace osu.Game.Online.Spectator private readonly BindableDictionary watchedUserStates = new BindableDictionary(); private readonly BindableList watchingUsers = new BindableList(); - private readonly BindableList playingUsers = new BindableList(); private readonly SpectatorState currentState = new SpectatorState(); private IBeatmap? currentBeatmap; @@ -134,7 +128,6 @@ namespace osu.Game.Online.Spectator } else { - playingUsers.Clear(); watchedUserStates.Clear(); watchingUsers.Clear(); } @@ -145,9 +138,6 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - if (!playingUsers.Contains(userId)) - playingUsers.Add(userId); - if (watchedUsersRefCounts.ContainsKey(userId)) watchedUserStates[userId] = state; @@ -161,8 +151,6 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - playingUsers.Remove(userId); - if (watchedUsersRefCounts.ContainsKey(userId)) watchedUserStates[userId] = state; diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 2fb1ebc050..fce73f0198 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -16,10 +14,8 @@ using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; -using osu.Game.Online.Spectator; using osu.Game.Resources.Localisation.Web; using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -34,19 +30,12 @@ namespace osu.Game.Overlays.Dashboard private const float search_textbox_height = 40; private const float padding = 10; - private readonly IBindableList playingUsers = new BindableList(); private readonly IBindableDictionary onlineUserPresences = new BindableDictionary(); private readonly Dictionary userPanels = new Dictionary(); private SearchContainer userFlow = null!; private BasicSearchTextBox searchTextBox = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - - [Resolved] - private SpectatorClient spectatorClient { get; set; } = null!; - [Resolved] private MetadataClient metadataClient { get; set; } = null!; @@ -106,9 +95,6 @@ namespace osu.Game.Overlays.Dashboard onlineUserPresences.BindTo(metadataClient.UserPresences); onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true); - - playingUsers.BindTo(spectatorClient.PlayingUsers); - playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } protected override void OnFocus(FocusEvent e) @@ -152,53 +138,27 @@ namespace osu.Game.Overlays.Dashboard } }); - private void onPlayingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Debug.Assert(e.NewItems != null); - - foreach (int userId in e.NewItems) - { - if (userPanels.TryGetValue(userId, out var panel)) - panel.CanSpectate.Value = userId != api.LocalUser.Value.Id; - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(e.OldItems != null); - - foreach (int userId in e.OldItems) - { - if (userPanels.TryGetValue(userId, out var panel)) - panel.CanSpectate.Value = false; - } - - break; - } - } - private OnlineUserPanel createUserPanel(APIUser user) => new OnlineUserPanel(user).With(panel => { panel.Anchor = Anchor.TopCentre; panel.Origin = Anchor.TopCentre; - panel.CanSpectate.Value = playingUsers.Contains(user.Id); }); public partial class OnlineUserPanel : CompositeDrawable, IFilterable { public readonly APIUser User; - public BindableBool CanSpectate { get; } = new BindableBool(); + private PurpleRoundedButton spectateButton = null!; public IEnumerable FilterTerms { get; } [Resolved] private IPerformFromScreenRunner? performer { get; set; } + [Resolved] + private MetadataClient? metadataClient { get; set; } + public bool FilteringActive { set; get; } public bool MatchingFilter @@ -221,6 +181,15 @@ namespace osu.Game.Overlays.Dashboard AutoSizeAxes = Axes.Both; } + protected override void Update() + { + base.Update(); + + // TODO: we probably don't want to do this every frame. + var activity = metadataClient?.GetPresence(User.Id)?.Activity; + spectateButton.Enabled.Value = activity is UserActivity.InSoloGame or UserActivity.InMultiplayerGame or UserActivity.InPlaylistGame; + } + [BackgroundDependencyLoader] private void load() { @@ -240,14 +209,13 @@ namespace osu.Game.Overlays.Dashboard Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - new PurpleRoundedButton + spectateButton = new PurpleRoundedButton { RelativeSizeAxes = Axes.X, Text = "Spectate", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))), - Enabled = { BindTarget = CanSpectate } } } }, diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index b6fa4bbac6..0185165b36 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -90,6 +90,7 @@ namespace osu.Game.Users private void updatePresence() { + // TODO: we probably don't want to do this every frame. UserPresence? presence = metadata?.GetPresence(User.OnlineID); UserStatus status = presence?.Status ?? UserStatus.Offline; UserActivity? activity = presence?.Activity; From ef8448caaa2efec765951e9e74da3d037d9044d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Mar 2025 12:09:10 +0900 Subject: [PATCH 292/349] Use switch statement class type matching --- .../Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index fce73f0198..39df3ba22c 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -187,7 +187,19 @@ namespace osu.Game.Overlays.Dashboard // TODO: we probably don't want to do this every frame. var activity = metadataClient?.GetPresence(User.Id)?.Activity; - spectateButton.Enabled.Value = activity is UserActivity.InSoloGame or UserActivity.InMultiplayerGame or UserActivity.InPlaylistGame; + + switch (activity) + { + default: + spectateButton.Enabled.Value = false; + break; + + case UserActivity.InSoloGame: + case UserActivity.InMultiplayerGame: + case UserActivity.InPlaylistGame: + spectateButton.Enabled.Value = true; + break; + } } [BackgroundDependencyLoader] From 3e368201177985198d2b91b901d955a914c6405e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Mar 2025 13:28:57 +0900 Subject: [PATCH 293/349] Fix failing test --- .../Visual/Online/TestSceneCurrentlyOnlineDisplay.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs index 2e53ec2ba4..5c03325470 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs @@ -88,13 +88,12 @@ namespace osu.Game.Tests.Visual.Online { IDisposable token = null!; - AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); - AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); - AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); + AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() })); + AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == streamingUser.Id); AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.True); - AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id)); + AddStep("User finished playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); AddStep("End watching user presence", () => token.Dispose()); From 4d54f98c552d3acb23275ea1cb51441526fb5b48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Mar 2025 14:10:57 +0900 Subject: [PATCH 294/349] Fix one more test (sorry) --- .../Visual/Online/TestSceneCurrentlyOnlineDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs index 5c03325470..a1d0d40811 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs @@ -72,10 +72,10 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); - AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); + AddStep("User began playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() })); AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.True); - AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id)); + AddStep("User finished playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); From b00be5477f2dbb029466bd51dd057b14f4728d7d Mon Sep 17 00:00:00 2001 From: wezwery Date: Sun, 16 Mar 2025 15:30:27 +0200 Subject: [PATCH 295/349] 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 f9078b98dd80b6e69a5a459eb10b9e89fb0b9770 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Mar 2025 12:20:37 +0900 Subject: [PATCH 296/349] Fix tag add request using wrong method --- osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs index 2dd9303841..911c4fa5f1 100644 --- a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs +++ b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs @@ -20,7 +20,7 @@ namespace osu.Game.Online.API.Requests protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); - req.Method = HttpMethod.Post; + req.Method = HttpMethod.Put; return req; } From e8d245e9f1fc8d3c4aab64084d38741a46d45f63 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 17 Mar 2025 02:16:09 -0400 Subject: [PATCH 297/349] 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 298/349] 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 299/349] 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 64c726334234ca01b9b2c2bad501a54f8e849e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Mar 2025 08:39:05 +0100 Subject: [PATCH 300/349] Fix broken text alignment in medal display Bit unfortunate that this is code that can be written and do stupid things. Unsure if additional API protections against this are desired framework-side. --- osu.Game/Overlays/MedalSplash/DrawableMedal.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index 2beed6645a..6b7ffbd1db 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -107,12 +107,7 @@ namespace osu.Game.Overlays.MedalSplash }, }; - description.AddText(medal.Description, s => - { - s.Anchor = Anchor.TopCentre; - s.Origin = Anchor.TopCentre; - s.Font = s.Font.With(size: 16); - }); + description.AddText(medal.Description, s => s.Font = s.Font.With(size: 16)); medalContainer.OnLoadComplete += _ => { From 427f75a7035cb9444e6d9382675ad476b6bfe5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Mar 2025 08:49:32 +0100 Subject: [PATCH 301/349] Fix broken text alignment in supporter display See previous commit. --- osu.Game/Screens/Menu/SupporterDisplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Menu/SupporterDisplay.cs b/osu.Game/Screens/Menu/SupporterDisplay.cs index 6639300f4a..be50a54619 100644 --- a/osu.Game/Screens/Menu/SupporterDisplay.cs +++ b/osu.Game/Screens/Menu/SupporterDisplay.cs @@ -100,7 +100,6 @@ namespace osu.Game.Screens.Menu t.Padding = new MarginPadding { Left = 5, Top = 1 }; t.Font = t.Font.With(size: font_size); - t.Origin = Anchor.Centre; t.Colour = colours.Pink; Schedule(() => From 3954d8f3bea48ae8995544c308454e1957a42f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Mar 2025 08:53:53 +0100 Subject: [PATCH 302/349] Fix baseline misalignment in drawable comment link section `CommentReportButton` is pretty cursed but I guess I can see why it is like it is. Short of inlining it into `DrawableComment` this is probably the best escape hatch (which I wouldn't be against doing, by the way, just not sure it's the most productive use of time unless review feedback comes in saying that would be a better path forward). --- osu.Game/Graphics/Containers/OsuTextFlowContainer.cs | 2 +- osu.Game/Overlays/Comments/CommentReportButton.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs index d5cce1a10a..8da8b7ed7d 100644 --- a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs +++ b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs @@ -27,7 +27,7 @@ namespace osu.Game.Graphics.Containers private partial class ArbitraryDrawableWrapper : Container, IHasLineBaseHeight { - public float LineBaseHeight => DrawHeight; + public float LineBaseHeight => (Child as IHasLineBaseHeight)?.LineBaseHeight ?? DrawHeight; public ArbitraryDrawableWrapper() { diff --git a/osu.Game/Overlays/Comments/CommentReportButton.cs b/osu.Game/Overlays/Comments/CommentReportButton.cs index e4d4d671da..09c0fd32d0 100644 --- a/osu.Game/Overlays/Comments/CommentReportButton.cs +++ b/osu.Game/Overlays/Comments/CommentReportButton.cs @@ -1,13 +1,16 @@ // 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; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -19,7 +22,7 @@ using osuTK; namespace osu.Game.Overlays.Comments { - public partial class CommentReportButton : CompositeDrawable, IHasPopover + public partial class CommentReportButton : CompositeDrawable, IHasPopover, IHasLineBaseHeight { private readonly Comment comment; @@ -88,5 +91,7 @@ namespace osu.Game.Overlays.Comments api.Queue(request); } + + public float LineBaseHeight => link.ChildrenOfType().FirstOrDefault()?.LineBaseHeight ?? DrawHeight; } } From e430619e0e7ae05e880179fd01272875f3691249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Mar 2025 12:34:20 +0100 Subject: [PATCH 303/349] Make logic clearer & fix issues --- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index ab3b8d882e..5b856181f5 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -212,47 +212,49 @@ namespace osu.Game.Overlays.BeatmapSet { guestMapperContainer.Clear(); var beatmapOwners = beatmapInfo?.BeatmapOwners; + bool isHostDifficulty = beatmapOwners?.Length == 1 && beatmapOwners.First().Id == beatmapSet?.AuthorID; - if (beatmapOwners != null && (beatmapOwners.Length != 1 || beatmapOwners.First().Id != beatmapSet?.AuthorID)) + if (beatmapOwners != null && !isHostDifficulty) { - APIUser[]? users = BeatmapSet?.RelatedUsers?.Where(u => beatmapOwners.Any(o => o.Id == u.OnlineID)).ToArray(); + APIUser[] users = BeatmapSet?.RelatedUsers?.Where(u => beatmapOwners.Any(o => o.Id == u.OnlineID)).ToArray() ?? []; + int count = users.Length; - if (users != null) + switch (count) { - int count = users.Length; + case 0: + break; - guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); // set string.Empty here because we need user link. + case 1: + guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); + guestMapperContainer.AddUserLink(users[0]); + break; - switch (count) + case 2: + guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); + guestMapperContainer.AddUserLink(users[0]); + guestMapperContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector); + guestMapperContainer.AddUserLink(users[1]); + break; + + default: { - case 1: - guestMapperContainer.AddUserLink(users[0]); - break; + guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); - case 2: - guestMapperContainer.AddUserLink(users[0]); - guestMapperContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector); - guestMapperContainer.AddUserLink(users[1]); - break; - - default: + for (int i = 0; i < count; i++) { - for (int i = 0; i < count; i++) + guestMapperContainer.AddUserLink(users[i]); + + if (i < count - 2) { - guestMapperContainer.AddUserLink(users[i]); - - if (i < count - 2) - { - guestMapperContainer.AddText(CommonStrings.ArrayAndWordsConnector); - } - else if (i == count - 2) - { - guestMapperContainer.AddText(CommonStrings.ArrayAndLastWordConnector); - } + guestMapperContainer.AddText(CommonStrings.ArrayAndWordsConnector); + } + else if (i == count - 2) + { + guestMapperContainer.AddText(CommonStrings.ArrayAndLastWordConnector); } - - break; } + + break; } } } From 87932f707da54ef20c9feaced488fe0d5ee72db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Mar 2025 12:52:30 +0100 Subject: [PATCH 304/349] Use link flow at a higher level to fix broken layout --- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 120 ++++++------------ .../BeatmapSet/BeatmapSetHeaderContent.cs | 2 +- 2 files changed, 41 insertions(+), 81 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 5b856181f5..eea0b087eb 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Extensions; @@ -31,9 +32,7 @@ namespace osu.Game.Overlays.BeatmapSet private const float tile_icon_padding = 7; private const float tile_spacing = 2; - private readonly OsuSpriteText version, starRating, starRatingText; - private readonly LinkFlowContainer guestMapperContainer; - private readonly FillFlowContainer starRatingContainer; + private readonly LinkFlowContainer infoContainer; private readonly Statistic plays, favourites; public readonly DifficultiesContainer Difficulties; @@ -53,6 +52,9 @@ namespace osu.Game.Overlays.BeatmapSet } } + [Resolved] + private OsuColour colours { get; set; } = null!; + public BeatmapPicker() { RelativeSizeAxes = Axes.X; @@ -72,59 +74,13 @@ namespace osu.Game.Overlays.BeatmapSet RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Left = -(tile_icon_padding + tile_spacing / 2), Bottom = 10 }, - OnLostHover = () => - { - showBeatmap(Beatmap.Value); - starRatingContainer.FadeOut(100); - }, + OnLostHover = () => showBeatmap(Beatmap.Value, withStarRating: false), }, - new FillFlowContainer + infoContainer = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11)) { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5f), - Children = new Drawable[] - { - version = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold) - }, - guestMapperContainer = new LinkFlowContainer(s => - s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11)) - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Bottom = 1 }, - }, - starRatingContainer = new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Alpha = 0, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(2f, 0), - Margin = new MarginPadding { Bottom = 1 }, - Children = new[] - { - starRatingText = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold), - Text = BeatmapsetsStrings.ShowStatsStars, - }, - starRating = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold), - Text = string.Empty, - }, - } - }, - }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.BottomLeft, }, new FillFlowContainer { @@ -144,7 +100,7 @@ namespace osu.Game.Overlays.BeatmapSet Beatmap.ValueChanged += b => { - showBeatmap(b.NewValue); + showBeatmap(b.NewValue, withStarRating: Difficulties.Any(d => d.IsHovered)); updateDifficultyButtons(); }; } @@ -153,10 +109,8 @@ namespace osu.Game.Overlays.BeatmapSet private IBindable ruleset { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - starRating.Colour = colours.Yellow; - starRatingText.Colour = colours.Yellow; updateDisplay(); } @@ -185,16 +139,12 @@ namespace osu.Game.Overlays.BeatmapSet State = DifficultySelectorState.NotSelected, OnHovered = beatmap => { - showBeatmap(beatmap); - starRating.Text = beatmap.StarRating.FormatStarRating(); - starRatingContainer.FadeIn(100); + showBeatmap(beatmap, withStarRating: true); }, OnClicked = beatmap => { Beatmap.Value = beatmap; }, }); } - starRatingContainer.FadeOut(100); - // If a selection is already made, try and maintain it. if (Beatmap.Value != null) Beatmap.Value = Difficulties.FirstOrDefault(b => b.Beatmap.OnlineID == Beatmap.Value.OnlineID)?.Beatmap; @@ -208,9 +158,13 @@ namespace osu.Game.Overlays.BeatmapSet updateDifficultyButtons(); } - private void showBeatmap(APIBeatmap? beatmapInfo) + private void showBeatmap(APIBeatmap? beatmapInfo, bool withStarRating) { - guestMapperContainer.Clear(); + infoContainer.Clear(); + + infoContainer.AddText(beatmapInfo?.DifficultyName ?? string.Empty, s => s.Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold)); + infoContainer.AddArbitraryDrawable(Empty().With(e => e.Width = 5)); + var beatmapOwners = beatmapInfo?.BeatmapOwners; bool isHostDifficulty = beatmapOwners?.Length == 1 && beatmapOwners.First().Id == beatmapSet?.AuthorID; @@ -225,33 +179,29 @@ namespace osu.Game.Overlays.BeatmapSet break; case 1: - guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); - guestMapperContainer.AddUserLink(users[0]); + infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); + infoContainer.AddUserLink(users[0]); break; case 2: - guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); - guestMapperContainer.AddUserLink(users[0]); - guestMapperContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector); - guestMapperContainer.AddUserLink(users[1]); + infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); + infoContainer.AddUserLink(users[0]); + infoContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector); + infoContainer.AddUserLink(users[1]); break; default: { - guestMapperContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); + infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty)); for (int i = 0; i < count; i++) { - guestMapperContainer.AddUserLink(users[i]); + infoContainer.AddUserLink(users[i]); if (i < count - 2) - { - guestMapperContainer.AddText(CommonStrings.ArrayAndWordsConnector); - } + infoContainer.AddText(CommonStrings.ArrayAndWordsConnector); else if (i == count - 2) - { - guestMapperContainer.AddText(CommonStrings.ArrayAndLastWordConnector); - } + infoContainer.AddText(CommonStrings.ArrayAndLastWordConnector); } break; @@ -259,7 +209,17 @@ namespace osu.Game.Overlays.BeatmapSet } } - version.Text = beatmapInfo?.DifficultyName ?? string.Empty; + if (withStarRating) + { + infoContainer.AddArbitraryDrawable(Empty().With(e => e.Width = 5)); + infoContainer.AddText( + LocalisableString.Interpolate($"{BeatmapsetsStrings.ShowStatsStars} {beatmapInfo?.StarRating.FormatStarRating()}"), + t => + { + t.Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold); + t.Colour = colours.Yellow; + }); + } } private void updateDifficultyButtons() diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index a50043f0f0..c72c2a6698 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -98,7 +98,7 @@ namespace osu.Game.Overlays.BeatmapSet { Vertical = BeatmapSetOverlay.Y_PADDING, Left = WaveOverlayContainer.HORIZONTAL_PADDING, - Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, + Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH + 10, }, Children = new Drawable[] { From 613c46d3756fa84b5bea7271be717f4f961c50cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Mar 2025 14:52:09 +0100 Subject: [PATCH 305/349] Add the most basic (yet failing) test possible --- osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index d1782da25f..e38ae0cbc9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -208,5 +208,11 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7)); AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7)); } + + [Test] + public void TestBeatmapVersionPopulatedCorrectly() + { + AddAssert("beatmap version is populated", () => EditorBeatmap.BeatmapInfo.BeatmapVersion > 0); + } } } From 91f3be5feaab0c73c17e1a8c270516aa9bee1e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Mar 2025 14:56:41 +0100 Subject: [PATCH 306/349] Move `BeatmapVersion` from `BeatmapInfo` to `IBeatmap` Closes https://github.com/ppy/osu/issues/32420. The failure cause here is that in editor the beatmap version for the beatmap affected (or... any beatmap, really), is 0 (ZERO). That is probably a regression from https://github.com/ppy/osu/pull/32315, but like... can we universally agree that calling that change "a regression" in any capacity is dumb? Like what was that code *doing* playing dumb reference games and copying stuff into an arbitrary instance that could get or not get used later on? And now you have a 50/50 chance of accessing the *correct* model's field, depending on whether you go via `BeatmapInfo` or `Beatmap.BeatmapInfo`? Moving the field to `IBeatmap`, i.e. what is by now - by consensus, since https://github.com/ppy/osu/pull/28473 - supposed to be the "decoded and materialised" beatmap, fixes this issue. I probably should have done this as part of https://github.com/ppy/osu/pull/28473 but it slipped my mind. Probably for the better too because this change has rather large chances of breaking stuff so maybe better to examine it in isolation (via diffcalc runs or whatever). For added humour points, you'd say that the field on `BeatmapInfo` was not `[Ignore]`d, so this is a realm schema change, right? No. As far as I can tell, it's not. I opened realm studio and `BeatmapVersion` *is not a listed column` on `Beatmap` models. I'm also not gonna get into the fact that I think `EditorBeatmap` doing dumb games with juggling two `BeatmapInfo` references since https://github.com/ppy/osu/pull/15075 is bad, because I don't think I have the mental capacity to hotfix this by going down that train of thought. --- .../Beatmaps/CatchBeatmapConverter.cs | 2 +- .../TestSceneLegacyHitPolicy.cs | 4 ++-- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 10 ++++------ .../Beatmaps/Formats/LegacyScoreDecoderTest.cs | 11 ++++------- .../Visual/Editing/TestSceneEditorSaving.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 3 +++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 1 + osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 +- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 9 +++++++++ 17 files changed, 34 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index f5c5ffb529..756376edf8 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y, // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // this results in more (or less) ticks being generated in = 6) + if (beatmap.BeatmapVersion >= 6) applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1); else applyStackingOld(beatmap, hitObjects); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 010b1f0a7a..b784fd181f 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps double osuVelocity = taikoVelocity * (1000f / beatLength); // osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8 - if (beatmap.BeatmapInfo.BeatmapVersion >= 8) + if (beatmap.BeatmapVersion >= 8) beatLength = timingPoint.BeatLength; // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 9747b654ae..17153a3ff2 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -42,9 +42,8 @@ namespace osu.Game.Tests.Beatmaps.Formats var decoder = Decoder.GetDecoder(stream); var working = new TestWorkingBeatmap(decoder.Decode(stream)); - Assert.AreEqual(6, working.BeatmapInfo.BeatmapVersion); - Assert.AreEqual(6, working.Beatmap.BeatmapInfo.BeatmapVersion); - Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty()).BeatmapInfo.BeatmapVersion); + Assert.AreEqual(6, working.Beatmap.BeatmapVersion); + Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty()).BeatmapVersion); } } @@ -59,9 +58,8 @@ namespace osu.Game.Tests.Beatmaps.Formats ((LegacyBeatmapDecoder)decoder).ApplyOffsets = applyOffsets; var working = new TestWorkingBeatmap(decoder.Decode(stream)); - Assert.AreEqual(4, working.BeatmapInfo.BeatmapVersion); - Assert.AreEqual(4, working.Beatmap.BeatmapInfo.BeatmapVersion); - Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty()).BeatmapInfo.BeatmapVersion); + Assert.AreEqual(4, working.Beatmap.BeatmapVersion); + Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty()).BeatmapVersion); Assert.AreEqual(-1, working.BeatmapInfo.Metadata.PreviewTime); } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 713f2f3fb1..de07e2be01 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -155,10 +155,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); var beatmap = new TestBeatmap(ruleset) { - BeatmapInfo = - { - BeatmapVersion = beatmapVersion - } + BeatmapVersion = beatmapVersion }; var score = new Score @@ -633,14 +630,14 @@ namespace osu.Game.Tests.Beatmaps.Formats MD5Hash = md5Hash, Ruleset = new OsuRuleset().RulesetInfo, Difficulty = new BeatmapDifficulty(), - BeatmapVersion = beatmapVersion, }, - // needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die + // needs to have at least one object so that `StandardisedScoreMigrationTools` doesn't die // when trying to recompute total score. HitObjects = { new HitCircle() - } + }, + BeatmapVersion = beatmapVersion, }); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index e38ae0cbc9..2e7b55ab49 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -212,7 +212,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestBeatmapVersionPopulatedCorrectly() { - AddAssert("beatmap version is populated", () => EditorBeatmap.BeatmapInfo.BeatmapVersion > 0); + AddAssert("beatmap version is populated", () => EditorBeatmap.BeatmapVersion > 0); } } } diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 8ea6fa1f51..155ded5747 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -9,6 +9,7 @@ using System.Linq; using osu.Game.Beatmaps.ControlPoints; using Newtonsoft.Json; using osu.Framework.Lists; +using osu.Game.Beatmaps.Formats; using osu.Game.IO.Serialization.Converters; namespace osu.Game.Beatmaps @@ -141,6 +142,8 @@ namespace osu.Game.Beatmaps public int[] Bookmarks { get; set; } = Array.Empty(); + public int BeatmapVersion { get; set; } = LegacyBeatmapEncoder.FIRST_LAZER_VERSION; + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 0cf10c659b..f0cb6d0484 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -86,6 +86,7 @@ namespace osu.Game.Beatmaps beatmap.Countdown = original.Countdown; beatmap.CountdownOffset = original.CountdownOffset; beatmap.Bookmarks = original.Bookmarks; + beatmap.BeatmapVersion = original.BeatmapVersion; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 333ec89eab..487b578317 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -231,8 +231,6 @@ namespace osu.Game.Beatmaps [Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")] public int? MaxCombo { get; set; } - public int BeatmapVersion; - public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone(); public override string ToString() => this.GetDisplayTitle(); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index b0aabe3787..729daf5b0a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -79,7 +79,7 @@ namespace osu.Game.Beatmaps.Formats protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap) { this.beatmap = beatmap; - this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; + this.beatmap.BeatmapVersion = FormatVersion; parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion); ApplyLegacyDefaults(this.beatmap); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index f95fcefd7e..482bc73742 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -109,6 +109,8 @@ namespace osu.Game.Beatmaps int[] Bookmarks { get; internal set; } + int BeatmapVersion { get; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index add24f7866..5c840a8357 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -344,6 +344,7 @@ namespace osu.Game.Rulesets.Difficulty public double TotalBreakTime => baseBeatmap.TotalBreakTime; public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); + public int BeatmapVersion => baseBeatmap.BeatmapVersion; public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); public double AudioLeadIn diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 6ad118547b..2eec12ac28 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -96,7 +96,7 @@ namespace osu.Game.Scoring.Legacy scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. - beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; + beatmapOffset = currentBeatmap.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; /* score.HpGraphString = */ sr.ReadString(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 0f00cce080..b575c02337 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -142,7 +142,7 @@ namespace osu.Game.Scoring.Legacy StringBuilder replayData = new StringBuilder(); // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. - double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; + double offset = beatmap?.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; int lastTime = 0; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 254336e963..91ae4593dd 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -13,6 +13,7 @@ using osu.Framework.Bindables; using osu.Framework.Lists; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Edit; @@ -133,6 +134,8 @@ namespace osu.Game.Screens.Edit BeatmapInfo.Metadata.PreviewTime = s.NewValue; EndChange(); }); + + BeatmapVersion = PlayableBeatmap.BeatmapVersion; } /// @@ -286,6 +289,8 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.Bookmarks = value; } + public int BeatmapVersion { get; set; } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; @@ -456,6 +461,10 @@ namespace osu.Game.Screens.Edit if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0) return; + // if the user is doing edits to this beatmaps via this flow, we better bump the beatmap version + // because the beatmap encoder can only output this specific beatmap version anyway, + // so *not* bumping it could lead to results that look misleading at best. + BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION; beatmapProcessor.PreProcess(); foreach (var h in batchPendingDeletes) processHitObject(h); From 4d00591df00b922927c61dcd068ae105c48526f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Mar 2025 00:06:02 +0900 Subject: [PATCH 307/349] Remove silly cache --- osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 970949280f..df0fc8de57 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -161,9 +161,6 @@ namespace osu.Game.Tests.Visual.Settings }); Dependencies.CacheAs(dialogOverlay); - - var osuGame = new OsuGame(); - Dependencies.CacheAs(osuGame); } } } From 43d791854868ad6939eed372ffffe2633699aec1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Mar 2025 00:06:39 +0900 Subject: [PATCH 308/349] Rename localised string to something slightly more correct --- osu.Game/Graphics/UserInterface/ExternalLinkButton.cs | 2 +- osu.Game/Localisation/ToastStrings.cs | 2 +- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- .../OSD/{CopyStringToast.cs => CopiedToClipboardToast.cs} | 6 +++--- osu.Game/Overlays/Settings/SettingsFooter.cs | 2 +- .../Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) rename osu.Game/Overlays/OSD/{CopyStringToast.cs => CopiedToClipboardToast.cs} (58%) diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index 2bc5ba91fa..e5a4e807b5 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -89,7 +89,7 @@ namespace osu.Game.Graphics.UserInterface { if (Link == null) return; - game?.CopyStringToClipboard(Link); + game?.CopyToClipboard(Link); } } } diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index 000f01ebca..b520511d8f 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -47,7 +47,7 @@ namespace osu.Game.Localisation /// /// "Copied to clipboard" /// - public static LocalisableString StringCopied => new TranslatableString(getKey(@"string_copied"), @"Copied to clipboard"); + public static LocalisableString CopiedToClipboard => new TranslatableString(getKey(@"copied_to_clipboard"), @"Copied to clipboard"); /// /// "Speed changed to {0:N2}x" diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 87f6d58d02..3381553970 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -519,10 +519,10 @@ namespace osu.Game } }); - public void CopyStringToClipboard(string value) => waitForReady(() => onScreenDisplay, _ => + public void CopyToClipboard(string value) => waitForReady(() => onScreenDisplay, _ => { dependencies.Get().SetText(value); - onScreenDisplay.Display(new CopyStringToast()); + onScreenDisplay.Display(new CopiedToClipboardToast()); }); public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) => waitForReady(() => externalLinkOpener, _ => externalLinkOpener.OpenUrlExternally(url, warnMode)); diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 2f1b7054e2..805d997998 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -420,7 +420,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}"); - onScreenDisplay?.Display(new CopyStringToast()); + onScreenDisplay?.Display(new CopiedToClipboardToast()); } private void toggleReply() diff --git a/osu.Game/Overlays/OSD/CopyStringToast.cs b/osu.Game/Overlays/OSD/CopiedToClipboardToast.cs similarity index 58% rename from osu.Game/Overlays/OSD/CopyStringToast.cs rename to osu.Game/Overlays/OSD/CopiedToClipboardToast.cs index 34f85dc9cb..4059a274ad 100644 --- a/osu.Game/Overlays/OSD/CopyStringToast.cs +++ b/osu.Game/Overlays/OSD/CopiedToClipboardToast.cs @@ -5,10 +5,10 @@ using osu.Game.Localisation; namespace osu.Game.Overlays.OSD { - public partial class CopyStringToast : Toast + public partial class CopiedToClipboardToast : Toast { - public CopyStringToast() - : base(CommonStrings.General, ToastStrings.StringCopied, "") + public CopiedToClipboardToast() + : base(CommonStrings.General, ToastStrings.CopiedToClipboard, "") { } } diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 52abd4fa65..307d88e712 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Settings public MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuMenuItem("Copy version", MenuItemType.Standard, () => game?.CopyStringToClipboard(version)) + new OsuMenuItem("Copy version", MenuItemType.Standard, () => game?.CopyToClipboard(version)) }; } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index d18e00d643..491d8071f1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -355,7 +355,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { items.AddRange([ new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value))), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyStringToClipboard(formatRoomUrl(Room.RoomID.Value))) + new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyToClipboard(formatRoomUrl(Room.RoomID.Value))) ]); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 055cb53d24..b99f046f4b 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -301,7 +301,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); if (beatmapInfo.GetOnlineURL(api, ruleset.Value) is string url) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyStringToClipboard(url))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard(url))); if (manager != null) items.Add(new OsuMenuItem("Mark as played", MenuItemType.Standard, () => manager.MarkPlayed(beatmapInfo))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 55b2e68209..c410cb7d69 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -301,7 +301,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); if (beatmapSet.GetOnlineURL(api, ruleset.Value) is string url) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyStringToClipboard(url))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard(url))); if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); From bf3caacf51ef67b93121d454a6f0fdba69e70087 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 18 Mar 2025 14:31:37 +0900 Subject: [PATCH 309/349] Make freestyle not bypass ruleset filter once more --- .../Visual/Multiplayer/TestSceneRoomListing.cs | 17 ----------------- .../OnlinePlay/Lounge/Components/RoomListing.cs | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 45f1ff1acb..27c5758afa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -11,7 +11,6 @@ using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Taiko; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Tests.Visual.OnlinePlay; @@ -199,22 +198,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } - [Test] - public void TestFreestyleBypassesRulesetFilter() - { - AddStep("apply taiko filter", () => container.Filter.Value = new FilterCriteria { Ruleset = new TaikoRuleset().RulesetInfo }); - - AddStep("add osu + freestyle room", () => - { - var room = GenerateRooms(1, new OsuRuleset().RulesetInfo)[0]; - room.Playlist[0].Freestyle = true; - room.CurrentPlaylistItem = room.Playlist[0]; - rooms.Add(room); - }); - - AddAssert("room visible", () => container.DrawableRooms.Any()); - } - private bool checkRoomSelected(Room? room) => selectedRoom.Value == room; private Room? getRoomInFlow(int index) => diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 9835802fae..0276601656 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= criteria.Ruleset == null || r.Room.CurrentPlaylistItem?.Freestyle == true || r.Room.PlaylistItemStats?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; + matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; matchingFilter &= matchPermissions(r, criteria.Permissions); // Room name isn't translatable, so ToString() is used here for simplicity. From a55abdb9b3cc5d198bb8cf68bfc3bab0e7d1d8c9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 18 Mar 2025 15:55:02 +0900 Subject: [PATCH 310/349] Fix multiplayer join errors potentially not being logged --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 6 +++++- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 7 +------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 6aa366dbc5..c455020f9a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -351,7 +351,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { joiningRoomOperation?.Dispose(); joiningRoomOperation = null; - onFailure?.Invoke(error); + + if (onFailure != null) + onFailure(error); + else + Logger.Log(error, level: LogLevel.Error); }); }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 56b82cdaee..51c135f042 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -88,12 +88,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (exception?.GetHubExceptionMessage() is string message) onFailure(message); else - { - const string generic_failure_message = "Failed to join multiplayer room."; - if (result.Exception != null) - Logger.Error(result.Exception, generic_failure_message); - onFailure(generic_failure_message); - } + onFailure($"Failed to join multiplayer room: {exception?.Message}"); } }); } From 2630d9437c60f56ea203d861c1b4e0abe226e205 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 18 Mar 2025 16:19:52 +0900 Subject: [PATCH 311/349] 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 533b0e0f887553f107d35257c136ba36344958be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Mar 2025 16:35:11 +0900 Subject: [PATCH 312/349] Allow testing fountain sound effects in tests --- .../Visual/Menus/TestSceneStarFountain.cs | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index 0d981014b8..396d2e9027 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -50,30 +50,17 @@ namespace osu.Game.Tests.Visual.Menus [Test] public void TestGameplay() { + KiaiGameplayFountains fountains = null!; + AddStep("make fountains", () => { Children = new[] { - new KiaiGameplayFountains.GameplayStarFountain - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - X = 75, - }, - new KiaiGameplayFountains.GameplayStarFountain - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - X = -75, - }, + fountains = new KiaiGameplayFountains(), }; }); - AddStep("activate fountains", () => - { - ((StarFountain)Children[0]).Shoot(1); - ((StarFountain)Children[1]).Shoot(-1); - }); + AddStep("activate fountains", () => fountains.Shoot()); } [Test] From a4ced55640bdf3ed9846750a4c50e1da9bb65a1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Mar 2025 16:49:27 +0900 Subject: [PATCH 313/349] Code quality pass --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 6 +- osu.Game/Screens/Menu/StarFountainSfx.cs | 74 ------------------- osu.Game/Screens/Menu/StarFountainSounds.cs | 71 ++++++++++++++++++ .../Screens/Play/KiaiGameplayFountains.cs | 6 +- 4 files changed, 77 insertions(+), 80 deletions(-) delete mode 100644 osu.Game/Screens/Menu/StarFountainSfx.cs create mode 100644 osu.Game/Screens/Menu/StarFountainSounds.cs diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index 7baf18d526..6e0351f922 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Menu [Resolved] private GameHost host { get; set; } = null!; - private StarFountainSfx sfx = null!; + private StarFountainSounds sounds = null!; [BackgroundDependencyLoader] private void load() @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.BottomRight, X = -250, }, - sfx = new StarFountainSfx() + sounds = new StarFountainSounds() }; } @@ -83,7 +83,7 @@ namespace osu.Game.Screens.Menu // Don't play SFX when game is in background, as it can be a bit noisy. if (host.IsActive.Value) - sfx.Trigger(); + sounds.Play(); } } } diff --git a/osu.Game/Screens/Menu/StarFountainSfx.cs b/osu.Game/Screens/Menu/StarFountainSfx.cs deleted file mode 100644 index 91337d6959..0000000000 --- a/osu.Game/Screens/Menu/StarFountainSfx.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Threading; -using osu.Game.Audio; -using osu.Game.Skinning; - -namespace osu.Game.Screens.Menu -{ - public partial class StarFountainSfx : Container - { - private const int shoot_retrigger_delay = 500; - private const int loop_fade_duration = 500; - - private double? lastPlayback; - - private SkinnableSound? shootSample; - private PausableSkinnableSound? loopSample; - - private ScheduledDelegate? loopFadeDelegate; - private ScheduledDelegate? loopStopDelegate; - - public void Trigger() - { - loopFadeDelegate?.Cancel(); - loopStopDelegate?.Cancel(); - - // Only play 'shootSample' if enough time has passed since last `Trigger()` call. - if (lastPlayback == null || Time.Current - lastPlayback > shoot_retrigger_delay) - { - loopSample?.Stop(); - shootSample?.Play(); - lastPlayback = Time.Current; - - return; - } - - if (loopSample == null) return; - - // Only call `Play()` if `loopSample` is not already playing, to prevent restarting the sample each time. - if (!loopSample.RequestedPlaying) - { - loopSample.Volume.Value = 1f; - loopSample.Play(); - } - - // Schedule a volume fadeout, followed by a `Stop()`. - loopFadeDelegate = Scheduler.AddDelayed(() => - { - this.TransformBindableTo(loopSample.Volume, 0, loop_fade_duration); - - loopStopDelegate = Scheduler.AddDelayed(() => - { - loopSample?.Stop(); - }, loop_fade_duration); - }, shoot_retrigger_delay); - - lastPlayback = Time.Current; - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - shootSample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")), - loopSample = new PausableSkinnableSound(new SampleInfo("Gameplay/fountain-loop")) { Looping = true }, - }; - } - } -} diff --git a/osu.Game/Screens/Menu/StarFountainSounds.cs b/osu.Game/Screens/Menu/StarFountainSounds.cs new file mode 100644 index 0000000000..842e718c48 --- /dev/null +++ b/osu.Game/Screens/Menu/StarFountainSounds.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; +using osu.Game.Audio; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Menu +{ + public partial class StarFountainSounds : CompositeComponent + { + private const int shoot_retrigger_delay = 500; + private const int loop_fade_duration = 500; + + private double? lastPlayback; + + private SkinnableSound shootSample = null!; + private PausableSkinnableSound loopSample = null!; + + private ScheduledDelegate? loopFadeDelegate; + private ScheduledDelegate? loopStopDelegate; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + shootSample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")), + loopSample = new PausableSkinnableSound(new SampleInfo("Gameplay/fountain-loop")) { Looping = true }, + }; + } + + public void Play() + { + loopFadeDelegate?.Cancel(); + loopStopDelegate?.Cancel(); + + try + { + // Only play 'shootSample' if enough time has passed since last `Play()` call. + if (lastPlayback == null || Time.Current - lastPlayback > shoot_retrigger_delay) + { + loopSample.Stop(); + shootSample.Play(); + return; + } + + // Only call `Play()` if `loopSample` is not already playing, to prevent restarting the sample each time. + if (!loopSample.RequestedPlaying) + { + this.TransformBindableTo(loopSample.Volume, 1); + loopSample.Play(); + } + + // Schedule a volume fadeout, followed by a `Stop()`. + loopFadeDelegate = Scheduler.AddDelayed(() => + { + this.TransformBindableTo(loopSample.Volume, 0, loop_fade_duration); + loopStopDelegate = Scheduler.AddDelayed(() => loopSample.Stop(), loop_fade_duration); + }, shoot_retrigger_delay); + } + finally + { + lastPlayback = Time.Current; + } + } + } +} diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index cdeb2a0700..826c60c6cf 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; - private StarFountainSfx sfx = null!; + private StarFountainSounds sounds = null!; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, X = -75, }, - sfx = new StarFountainSfx(), + sounds = new StarFountainSounds(), }; } @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Play leftFountain.Shoot(1); rightFountain.Shoot(-1); - sfx.Trigger(); + sounds.Play(); } public partial class GameplayStarFountain : StarFountain From 2be450c01016680ee1eed81921f179c4349884d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Mar 2025 17:10:13 +0900 Subject: [PATCH 314/349] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5b5482b3c7..438864f873 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From ab576d57a03fcad0c75774156367724f61fe36bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Mar 2025 18:12:00 +0900 Subject: [PATCH 315/349] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index b6ab7dc712..245d49abc2 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 486979487b..260b0cc0c3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 85076081d2bcf778aea1aa7dc845d43cdc8c396b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Mar 2025 12:02:12 +0100 Subject: [PATCH 316/349] Add failing test case --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 17153a3ff2..916e1e757a 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -43,6 +43,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var working = new TestWorkingBeatmap(decoder.Decode(stream)); Assert.AreEqual(6, working.Beatmap.BeatmapVersion); + Assert.That(working.Beatmap.BeatmapInfo.Ruleset.Name, Is.Not.EqualTo("null placeholder ruleset")); Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty()).BeatmapVersion); } } From 87a8281b29acb5728436066f7c9acd9770cecf41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Mar 2025 12:04:19 +0100 Subject: [PATCH 317/349] Fix editor crashing if beatmap does not have a mode explicitly specified in the `.osu` Closes https://github.com/ppy/osu/issues/32440. Probably another "regression" from https://github.com/ppy/osu/pull/32315. Trying very hard not to type out any hyperbolic rants here. --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 729daf5b0a..fae88a36a7 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -193,6 +193,7 @@ namespace osu.Game.Beatmaps.Formats internal static void ApplyLegacyDefaults(Beatmap beatmap) { beatmap.WidescreenStoryboard = false; + beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(0) ?? throw new ArgumentException("osu! ruleset is not available locally."); } protected override void ParseLine(Beatmap beatmap, Section section, string line) From efe1416aa3b2cf81b75559f8a839c6d5197cdfb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Mar 2025 14:17:14 +0100 Subject: [PATCH 318/349] Fix tests --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index fae88a36a7..765f2be345 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -193,7 +193,10 @@ namespace osu.Game.Beatmaps.Formats internal static void ApplyLegacyDefaults(Beatmap beatmap) { beatmap.WidescreenStoryboard = false; - beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(0) ?? throw new ArgumentException("osu! ruleset is not available locally."); + // in a perfect world this would throw if osu! ruleset couldn't be found, + // but unfortunately there are "legitimate" cases where it's not there (i.e. ruleset test projects), + // so attempt to trudge on with whatever it is that's in `BeatmapInfo` if the lookup fails. + beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(0) ?? beatmap.BeatmapInfo.Ruleset; } protected override void ParseLine(Beatmap beatmap, Section section, string line) From 4a906b0a1e9effaab42b8726bee9846f0fd9c304 Mon Sep 17 00:00:00 2001 From: "Giovanni D." Date: Tue, 18 Mar 2025 19:26:38 -0700 Subject: [PATCH 319/349] 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 320/349] 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 52dad654f776401e2aa757cdc8359b236e8ad11f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 12:45:27 +0900 Subject: [PATCH 321/349] Add some lenience around mod customisation expanding overlay It was quite easy to dismiss by accident. I've added some positional and time based lenience with numbers that feel good to me. Open to discussion on whether both are required and if the numbers feel good. Going forward, at some point, we'll likely want to standardise this across to other expand-on-hover elements (like player load overlays). Addresses https://github.com/ppy/osu/discussions/32368. --- .../Overlays/Mods/ModCustomisationPanel.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 03a1b3d0dd..e6d73fe092 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -223,15 +223,28 @@ namespace osu.Game.Overlays.Mods inputManager = GetContainingInputManager()!; } + private double timeUntilCollapse; + + private const double collapse_grace_time = 180; + private const float collapse_grace_position = 40; + protected override void Update() { base.Update(); - if (ExpandedState.Value == ModCustomisationPanelState.Expanded - && !ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position) - && inputManager.DraggedDrawable == null) + if (ExpandedState.Value == ModCustomisationPanelState.Expanded) { - ExpandedState.Value = ModCustomisationPanelState.Collapsed; + bool canCollapse = !DrawRectangle.Inflate(new Vector2(collapse_grace_position)).Contains(ToLocalSpace(inputManager.CurrentState.Mouse.Position)) + && inputManager.DraggedDrawable == null; + + if (canCollapse) + { + if (timeUntilCollapse <= 0) + ExpandedState.Value = ModCustomisationPanelState.Collapsed; + timeUntilCollapse -= Time.Elapsed; + } + else + timeUntilCollapse = collapse_grace_time; } } } From d6a06f2af6f5278fca49f35f5f166ac975f3d733 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 13:15:57 +0900 Subject: [PATCH 322/349] 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 323/349] 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 324/349] 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 325/349] 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 326/349] 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 327/349] 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 9272ada859e7aa2be993362c968936886ecaf79e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 16:13:56 +0900 Subject: [PATCH 328/349] 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 329/349] 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 330/349] 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 331/349] 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 332/349] 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 333/349] 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 334/349] 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 335/349] 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 336/349] 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 337/349] 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 338/349] 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 339/349] 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 340/349] 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 341/349] 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 342/349] 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 e74a22b8841ff4c723dd725346505bf053d86081 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Mar 2025 11:14:40 +0900 Subject: [PATCH 343/349] 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 344/349] 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 345/349] 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 346/349] 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 347/349] 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 0cc7cb02454b4e93abe95f7384e9916b4f97b279 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 22 Mar 2025 22:14:56 -0400 Subject: [PATCH 348/349] 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 349/349] 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()