From d19cdbdefb731664dda045512f0f7b07486cde75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 22:31:47 +0200 Subject: [PATCH 01/83] Add `InvitePlayer` method to multiplayer server --- osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs | 7 +++++++ osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 ++ osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 10 ++++++++++ 3 files changed, 19 insertions(+) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index b7a5faf7c9..64cd6df24d 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -99,5 +99,12 @@ namespace osu.Game.Online.Multiplayer /// /// The item to remove. Task RemovePlaylistItem(long playlistItemId); + + /// + /// Invites a player to the current room. + /// + /// + /// + Task InvitePlayer(int userId); } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 5716b7ad3b..957f55406a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -260,6 +260,8 @@ namespace osu.Game.Online.Multiplayer protected abstract Task LeaveRoomInternal(); + public abstract Task InvitePlayer(int userId); + /// /// Change the current settings. /// diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 8ff0ce4065..ebe89cf018 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -106,6 +106,16 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } + public override Task InvitePlayer(int userId) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId); + } + public override Task TransferHost(int userId) { if (!IsConnected.Value) From 574dc67a9ed12762d4a56b2bf20ab5cb128df7e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 22:53:28 +0200 Subject: [PATCH 02/83] Add `Invited` task to multiplayer client --- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 7 +++++++ osu.Game/Online/Multiplayer/MultiplayerClient.cs | 15 +++++++++++++++ .../Online/Multiplayer/OnlineMultiplayerClient.cs | 1 + .../Visual/Multiplayer/TestMultiplayerClient.cs | 5 +++++ 4 files changed, 28 insertions(+) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 995bac1af5..f59ded93f8 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -42,6 +42,13 @@ namespace osu.Game.Online.Multiplayer /// The user. Task UserKicked(MultiplayerRoomUser user); + /// + /// Signals that a user has been invited into a multiplayer room. + /// + /// Id of user that invited the player. + /// The room the user got invited to. + Task Invited(int invitedBy, MultiplayerRoom room); + /// /// Signal that the host of the room has changed. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 957f55406a..e438f9f96d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -442,6 +442,21 @@ namespace osu.Game.Online.Multiplayer return handleUserLeft(user, UserKicked); } + async Task IMultiplayerClient.Invited(int invitedBy, MultiplayerRoom room) + { + var user = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false); + + if (user == null) return; + + Scheduler.Add(() => + { + PostNotification?.Invoke(new SimpleNotification + { + Text = "You got invited into a multiplayer match by " + user.Username + "!", + }); + }); + } + private void addUserToAPIRoom(MultiplayerRoomUser user) { Debug.Assert(APIRoom != null); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index ebe89cf018..0e327bbc83 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -50,6 +50,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); connection.On(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked); + connection.On(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited); connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c27e30d5bb..d44eff47a3 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -263,6 +263,11 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override Task InvitePlayer(int userId) + { + return Task.CompletedTask; + } + public override Task TransferHost(int userId) { userId = clone(userId); From 7629b725a29834339bc029d1bd754c19e75a5199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 22:55:53 +0200 Subject: [PATCH 03/83] Add invite button to UserPanel context menu --- osu.Game/Users/UserPanel.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index e2dc511391..29a6c65555 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; @@ -18,6 +19,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.Multiplayer; namespace osu.Game.Users { @@ -61,6 +63,9 @@ namespace osu.Game.Users [Resolved] protected OsuColour Colours { get; private set; } = null!; + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { @@ -117,6 +122,15 @@ namespace osu.Game.Users })); } + if ( + User.IsOnline && + multiplayerClient.Room != null && + multiplayerClient.Room.Users.All(u => u.UserID != User.Id) + ) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(User.Id))); + } + return items.ToArray(); } } From 251e4d4de9d1bb5e44353ffec1f5bdacc546a3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 23:10:29 +0200 Subject: [PATCH 04/83] Add localisation for inviting a player --- osu.Game/Localisation/ContextMenuStrings.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Localisation/ContextMenuStrings.cs b/osu.Game/Localisation/ContextMenuStrings.cs index 8bc213016b..029fba67d8 100644 --- a/osu.Game/Localisation/ContextMenuStrings.cs +++ b/osu.Game/Localisation/ContextMenuStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap"); + /// + /// "Invite player" + /// + public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite player"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } From e81695bcacdde50284133c01db2fd33ecafabac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 23:10:51 +0200 Subject: [PATCH 05/83] Display avatar in invitation notification --- .../TestSceneNotificationOverlay.cs | 17 ++++ .../Online/Multiplayer/MultiplayerClient.cs | 7 +- osu.Game/Overlays/NotificationOverlay.cs | 2 +- .../Notifications/UserAvatarNotification.cs | 78 +++++++++++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Overlays/Notifications/UserAvatarNotification.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 4d3ae079e3..07bd722322 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -4,13 +4,17 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Database; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -31,6 +35,8 @@ namespace osu.Game.Tests.Visual.UserInterface public double TimeToCompleteProgress { get; set; } = 2000; + private readonly UserLookupCache userLookupCache = new TestUserLookupCache(); + [SetUp] public void SetUp() => Schedule(() => { @@ -60,6 +66,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep(@"simple #2", sendAmazingNotification); AddStep(@"progress #1", sendUploadProgress); AddStep(@"progress #2", sendDownloadProgress); + AddStep(@"User notification", sendUserNotification); checkProgressingCount(2); @@ -537,6 +544,16 @@ namespace osu.Game.Tests.Visual.UserInterface progressingNotifications.Add(n); } + private async void sendUserNotification() + { + var user = await userLookupCache.GetUserAsync(0).ConfigureAwait(true); + if (user == null) return; + + var n = new UserAvatarNotification(user, $"{user.Username} is telling you to NOT download Haitai!"); + + notificationOverlay.Post(n); + } + private void sendUploadProgress() { var n = new ProgressNotification diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index e438f9f96d..bb953bae58 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -450,10 +450,9 @@ namespace osu.Game.Online.Multiplayer Scheduler.Add(() => { - PostNotification?.Invoke(new SimpleNotification - { - Text = "You got invited into a multiplayer match by " + user.Username + "!", - }); + PostNotification?.Invoke( + new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!") + ); }); } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 6e0ea23dd1..67cf868fb0 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -113,7 +113,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }), + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification), typeof(UserAvatarNotification) }), new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }), } } diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs new file mode 100644 index 0000000000..191d63a76f --- /dev/null +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -0,0 +1,78 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users.Drawables; + +namespace osu.Game.Overlays.Notifications +{ + public partial class UserAvatarNotification : Notification + { + private LocalisableString text; + + public override LocalisableString Text + { + get => text; + set + { + text = value; + if (textDrawable != null) + textDrawable.Text = text; + } + } + + private TextFlowContainer? textDrawable; + + private APIUser user; + + public UserAvatarNotification(APIUser user, LocalisableString text) + { + this.user = user; + Text = text; + } + + private DrawableAvatar? avatar; + + protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IconContent.Masking = true; + + // Workaround for the corner radius on parent's mask breaking if we add masking to IconContent + IconContent.CornerRadius = 6; + + IconContent.AddRange(new Drawable[] + { + new Box() + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + }); + + avatar = new DrawableAvatar(user) + { + FillMode = FillMode.Fill, + }; + LoadComponentAsync(avatar, IconContent.Add); + + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Text = text + }); + } + } +} From 3879775219991cf44e9f8f44e320dc233637f286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 23:20:24 +0200 Subject: [PATCH 06/83] Add room name to invite notification --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index bb953bae58..6e46a5a3b9 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -451,7 +451,7 @@ namespace osu.Game.Online.Multiplayer Scheduler.Add(() => { PostNotification?.Invoke( - new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!") + new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match:\"{room.Settings.Name}\"!") ); }); } From 8e73dbc894d4297b46aaf1a881d40acda797445f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 3 Oct 2023 01:22:25 +0200 Subject: [PATCH 07/83] Load api room before displaying notification --- .../Online/Multiplayer/IMultiplayerClient.cs | 5 ++- .../Online/Multiplayer/MultiplayerClient.cs | 41 +++++++++++++++++-- .../Multiplayer/OnlineMultiplayerClient.cs | 2 +- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index f59ded93f8..ba9e8a237c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -46,8 +46,9 @@ namespace osu.Game.Online.Multiplayer /// Signals that a user has been invited into a multiplayer room. /// /// Id of user that invited the player. - /// The room the user got invited to. - Task Invited(int invitedBy, MultiplayerRoom room); + /// Id of the room the user got invited to. + /// Password to join the room. + Task Invited(int invitedBy, long roomID, string password); /// /// Signal that the host of the room has changed. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 6e46a5a3b9..61c2fa495a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Database; @@ -30,6 +31,8 @@ namespace osu.Game.Online.Multiplayer { public Action? PostNotification { protected get; set; } + public Action? InviteAccepted { protected get; set; } + /// /// Invoked when any change occurs to the multiplayer room. /// @@ -442,20 +445,50 @@ namespace osu.Game.Online.Multiplayer return handleUserLeft(user, UserKicked); } - async Task IMultiplayerClient.Invited(int invitedBy, MultiplayerRoom room) + async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) { - var user = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false); + var loadUserTask = userLookupCache.GetUserAsync(invitedBy); + var loadRoomTask = loadRoom(roomID); - if (user == null) return; + await Task.WhenAll(loadUserTask, loadRoomTask).ConfigureAwait(false); + + APIUser? apiUser = loadUserTask.GetResultSafely(); + Room? apiRoom = loadRoomTask.GetResultSafely(); + + if (apiUser == null || apiRoom == null) return; Scheduler.Add(() => { PostNotification?.Invoke( - new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match:\"{room.Settings.Name}\"!") + new UserAvatarNotification(apiUser, $"{apiUser.Username} invited you to a multiplayer match:\"{apiRoom.Name}\"!") + { + Activated = () => + { + InviteAccepted?.Invoke(apiRoom, password); + return true; + } + } ); }); } + private Task loadRoom(long id) + { + return Task.Run(() => + { + var t = new TaskCompletionSource(); + var request = new GetRoomRequest(id); + + request.Success += room => t.TrySetResult(room); + + request.Failure += e => t.TrySetResult(null); + + API.Queue(request); + + return t.Task; + }); + } + private void addUserToAPIRoom(MultiplayerRoomUser user) { Debug.Assert(APIRoom != null); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 0e327bbc83..5b5741ef1c 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -50,7 +50,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); connection.On(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked); - connection.On(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited); + connection.On(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited); connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); From a171fa7649c7c4e59544a3c7a167694fba51bfe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 3 Oct 2023 01:31:30 +0200 Subject: [PATCH 08/83] Join multiplayer match when clicking the invite notification --- osu.Game/OsuGame.cs | 22 +++++++++++++++++++ .../OnlinePlay/Multiplayer/Multiplayer.cs | 6 +++++ .../Screens/OnlinePlay/OnlinePlayScreen.cs | 13 ++++++----- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c60bff9e4c..5372f9227f 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -46,6 +46,7 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.Chat; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Music; @@ -58,6 +59,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; +using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; @@ -643,6 +645,25 @@ namespace osu.Game }); } + /// + /// Join a multiplayer match immediately. + /// + /// The room to join. + /// The password to join the room, if any is given. + public void PresentMultiplayerMatch(Room room, string password) + { + PerformFromScreen(screen => + { + Multiplayer multiplayer = new Multiplayer(); + multiplayer.OnLoadComplete += _ => + { + multiplayer.Join(room, password); + }; + + screen.Push(multiplayer); + }); + } + /// /// Present a score's replay immediately. /// The user should have already requested this interactively. @@ -853,6 +874,7 @@ namespace osu.Game ScoreManager.PresentImport = items => PresentScore(items.First().Value); MultiplayerClient.PostNotification = n => Notifications.Post(n); + MultiplayerClient.InviteAccepted = PresentMultiplayerMatch; // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 514b80b999..f74b5dd96e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.ObjectExtensions; 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; @@ -90,6 +91,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); + public void Join(Room room, string? password) + { + LoungeSubScreen.Join(room, password); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 37b50b4863..85eec7ac07 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -32,8 +32,9 @@ namespace osu.Game.Screens.OnlinePlay // while leases may be taken out by a subscreen. public override bool DisallowExternalBeatmapRulesetChanges => true; + protected LoungeSubScreen LoungeSubScreen; + private MultiplayerWaveContainer waves; - private LoungeSubScreen loungeSubScreen; private ScreenStack screenStack; [Cached(Type = typeof(IRoomManager))] @@ -89,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay screenStack.ScreenPushed += screenPushed; screenStack.ScreenExited += screenExited; - screenStack.Push(loungeSubScreen = CreateLounge()); + screenStack.Push(LoungeSubScreen = CreateLounge()); apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); @@ -120,10 +121,10 @@ namespace osu.Game.Screens.OnlinePlay Mods.SetDefault(); - if (loungeSubScreen.IsCurrentScreen()) - loungeSubScreen.OnEntering(e); + if (LoungeSubScreen.IsCurrentScreen()) + LoungeSubScreen.OnEntering(e); else - loungeSubScreen.MakeCurrent(); + LoungeSubScreen.MakeCurrent(); } public override void OnResuming(ScreenTransitionEvent e) @@ -158,7 +159,7 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnExiting(ScreenExitEvent e) { - while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not LoungeSubScreen) + while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not Lounge.LoungeSubScreen) { var subScreen = (Screen)screenStack.CurrentScreen; if (subScreen.IsLoaded && subScreen.OnExiting(e)) From 267d1ee7d442ef09bbdd5b7eb285c264d424ffb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 3 Oct 2023 22:08:14 +0200 Subject: [PATCH 09/83] Handle cases when player cannot be invited. --- .../Multiplayer/IMultiplayerRoomServer.cs | 3 +- .../Multiplayer/OnlineMultiplayerClient.cs | 29 +++++++++++++++++-- .../Multiplayer/UserBlockedException.cs | 26 +++++++++++++++++ .../Multiplayer/UserBlocksPMsException.cs | 26 +++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/UserBlockedException.cs create mode 100644 osu.Game/Online/Multiplayer/UserBlocksPMsException.cs diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 64cd6df24d..e98570dc29 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -104,7 +104,8 @@ namespace osu.Game.Online.Multiplayer /// Invites a player to the current room. /// /// - /// + /// The user has blocked or has been blocked by the invited user. + /// The invited user does not accept private messages. Task InvitePlayer(int userId); } } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 5b5741ef1c..08e82f2ad3 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Notifications; namespace osu.Game.Online.Multiplayer { @@ -107,14 +108,36 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } - public override Task InvitePlayer(int userId) + public override async Task InvitePlayer(int userId) { if (!IsConnected.Value) - return Task.CompletedTask; + return; Debug.Assert(connection != null); - return connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId); + try + { + await connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId).ConfigureAwait(false); + } + catch (HubException exception) + { + switch (exception.GetHubExceptionMessage()) + { + case UserBlockedException.MESSAGE: + PostNotification?.Invoke(new SimpleErrorNotification + { + Text = "User cannot be invited by someone they have blocked or are blocked by." + }); + break; + + case UserBlocksPMsException.MESSAGE: + PostNotification?.Invoke(new SimpleErrorNotification + { + Text = "User cannot be invited because they cannot receive private messages from people not on their friends list." + }); + break; + } + } } public override Task TransferHost(int userId) diff --git a/osu.Game/Online/Multiplayer/UserBlockedException.cs b/osu.Game/Online/Multiplayer/UserBlockedException.cs new file mode 100644 index 0000000000..363f878183 --- /dev/null +++ b/osu.Game/Online/Multiplayer/UserBlockedException.cs @@ -0,0 +1,26 @@ +// 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.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; + +namespace osu.Game.Online.Multiplayer +{ + [Serializable] + public class UserBlockedException : HubException + { + public const string MESSAGE = "User cannot be invited by someone they have blocked or are blocked by."; + + public UserBlockedException() + : + base(MESSAGE) + { + } + + protected UserBlockedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs new file mode 100644 index 0000000000..220a84cfe8 --- /dev/null +++ b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs @@ -0,0 +1,26 @@ +// 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.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; + +namespace osu.Game.Online.Multiplayer +{ + [Serializable] + public class UserBlocksPMsException : HubException + { + public const string MESSAGE = "User cannot be invited because they have disabled private messages."; + + public UserBlocksPMsException() + : + base(MESSAGE) + { + } + + protected UserBlocksPMsException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} From 32f69cd0ba58e9433214862751e830d33e70e748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 00:20:07 +0200 Subject: [PATCH 10/83] Make `UserAvatarNotification.user` readonly --- osu.Game/Overlays/Notifications/UserAvatarNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs index 191d63a76f..8f32c9d395 100644 --- a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Notifications private TextFlowContainer? textDrawable; - private APIUser user; + private readonly APIUser user; public UserAvatarNotification(APIUser user, LocalisableString text) { From 5678d904615f6f300613904363c1ac8d72b26a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 00:20:38 +0200 Subject: [PATCH 11/83] Reduce silliness of notification test case --- .../Visual/UserInterface/TestSceneNotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 07bd722322..332053d71e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -549,7 +549,7 @@ namespace osu.Game.Tests.Visual.UserInterface var user = await userLookupCache.GetUserAsync(0).ConfigureAwait(true); if (user == null) return; - var n = new UserAvatarNotification(user, $"{user.Username} is telling you to NOT download Haitai!"); + var n = new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!"); notificationOverlay.Post(n); } From 5469d134cb0919b70fc6168d24d948d3133b67ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 00:28:01 +0200 Subject: [PATCH 12/83] Add missing parameter description to docs. --- osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index e98570dc29..b7a608581c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -103,7 +103,7 @@ namespace osu.Game.Online.Multiplayer /// /// Invites a player to the current room. /// - /// + /// The user to invite. /// The user has blocked or has been blocked by the invited user. /// The invited user does not accept private messages. Task InvitePlayer(int userId); From fe5177fa4fe29ea35b3fac74f50b330899dd7066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 00:50:48 +0200 Subject: [PATCH 13/83] Remove unused import --- osu.Game/Overlays/Notifications/UserAvatarNotification.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs index 8f32c9d395..af881a5008 100644 --- a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; From bfeafd6f70bcd0832c144b3bf8d7cbd75c148285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:30:48 +0200 Subject: [PATCH 14/83] Fix formatting --- osu.Game/Online/Multiplayer/UserBlockedException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/UserBlockedException.cs b/osu.Game/Online/Multiplayer/UserBlockedException.cs index 363f878183..f2b69411c7 100644 --- a/osu.Game/Online/Multiplayer/UserBlockedException.cs +++ b/osu.Game/Online/Multiplayer/UserBlockedException.cs @@ -13,8 +13,7 @@ namespace osu.Game.Online.Multiplayer public const string MESSAGE = "User cannot be invited by someone they have blocked or are blocked by."; public UserBlockedException() - : - base(MESSAGE) + : base(MESSAGE) { } From 74ed3bc4ff8f8206188ec115c319222b0b31a99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:31:50 +0200 Subject: [PATCH 15/83] Rename `loadRoom` to `lookupRoom` --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 61c2fa495a..8b4c38a152 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -448,7 +448,7 @@ namespace osu.Game.Online.Multiplayer async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) { var loadUserTask = userLookupCache.GetUserAsync(invitedBy); - var loadRoomTask = loadRoom(roomID); + var loadRoomTask = lookupRoom(roomID); await Task.WhenAll(loadUserTask, loadRoomTask).ConfigureAwait(false); @@ -472,7 +472,7 @@ namespace osu.Game.Online.Multiplayer }); } - private Task loadRoom(long id) + private Task lookupRoom(long id) { return Task.Run(() => { From 0726ccb9887a9be8c653cf61de6fbf0a4c31bbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:32:18 +0200 Subject: [PATCH 16/83] Fix formatting --- osu.Game/Online/Multiplayer/UserBlocksPMsException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs index 220a84cfe8..0fd4e1f0c1 100644 --- a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs +++ b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs @@ -13,8 +13,7 @@ namespace osu.Game.Online.Multiplayer public const string MESSAGE = "User cannot be invited because they have disabled private messages."; public UserBlocksPMsException() - : - base(MESSAGE) + : base(MESSAGE) { } From 6bd51b32b47919a69a096c09ff8eac76c8bbe477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:35:45 +0200 Subject: [PATCH 17/83] Make resolved `multiplayerClient` field nullable --- osu.Game/Users/UserPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 29a6c65555..98b306e264 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -64,7 +64,7 @@ namespace osu.Game.Users protected OsuColour Colours { get; private set; } = null!; [Resolved] - private MultiplayerClient multiplayerClient { get; set; } = null!; + private MultiplayerClient? multiplayerClient { get; set; } [BackgroundDependencyLoader] private void load() @@ -124,7 +124,7 @@ namespace osu.Game.Users if ( User.IsOnline && - multiplayerClient.Room != null && + multiplayerClient?.Room != null && multiplayerClient.Room.Users.All(u => u.UserID != User.Id) ) { From 6d97f89399ec328b178f8f0561d0479650f2c86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:37:22 +0200 Subject: [PATCH 18/83] Rename `OnlinePlayScreen.LoungeSubScreen` to `Lounge` --- .../Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index f74b5dd96e..f899b24d30 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public void Join(Room room, string? password) { - LoungeSubScreen.Join(room, password); + Lounge.Join(room, password); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 85eec7ac07..57e388f016 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay // while leases may be taken out by a subscreen. public override bool DisallowExternalBeatmapRulesetChanges => true; - protected LoungeSubScreen LoungeSubScreen; + protected LoungeSubScreen Lounge; private MultiplayerWaveContainer waves; private ScreenStack screenStack; @@ -90,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay screenStack.ScreenPushed += screenPushed; screenStack.ScreenExited += screenExited; - screenStack.Push(LoungeSubScreen = CreateLounge()); + screenStack.Push(Lounge = CreateLounge()); apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); @@ -121,10 +121,10 @@ namespace osu.Game.Screens.OnlinePlay Mods.SetDefault(); - if (LoungeSubScreen.IsCurrentScreen()) - LoungeSubScreen.OnEntering(e); + if (Lounge.IsCurrentScreen()) + Lounge.OnEntering(e); else - LoungeSubScreen.MakeCurrent(); + Lounge.MakeCurrent(); } public override void OnResuming(ScreenTransitionEvent e) @@ -159,7 +159,7 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnExiting(ScreenExitEvent e) { - while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not Lounge.LoungeSubScreen) + while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not LoungeSubScreen) { var subScreen = (Screen)screenStack.CurrentScreen; if (subScreen.IsLoaded && subScreen.OnExiting(e)) From 256c95f0456721fdd23a27060857f6a0df141a60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 15:42:16 +0900 Subject: [PATCH 19/83] Add (failing) test coverage of exit/retry during progress to results --- .../Navigation/TestSceneScreenNavigation.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 3acc2f0384..fa1ebf5c56 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -215,6 +215,24 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("retry count is 1", () => player.RestartCount == 1); } + [Test] + public void TestRetryImmediatelyAfterCompletion() + { + var getOriginalPlayer = playToCompletion(); + + AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action()); + AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); + } + + [Test] + public void TestExitImmediatelyAfterCompletion() + { + var player = playToCompletion(); + + AddStep("attempt to exit", () => player().ChildrenOfType().First().Action()); + AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); + } + [Test] public void TestRetryFromResults() { @@ -778,6 +796,13 @@ namespace osu.Game.Tests.Visual.Navigation } private Func playToResults() + { + var player = playToCompletion(); + AddUntilStep("wait for results", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true); + return player; + } + + private Func playToCompletion() { Player player = null; @@ -803,7 +828,8 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); - AddUntilStep("wait for pass", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true); + AddUntilStep("wait for complete", () => player.GameplayState.HasPassed); + return () => player; } From 7c0d4967305a7290e26cb91224b843ae6439b748 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 14:42:45 +0900 Subject: [PATCH 20/83] Show results immediately if user hits "back" key after finishing gameplay I've gone ahead and matched the osu!stable behaviour for this, as it seems like it's what people are used to and they will settle for no less. Closes https://github.com/ppy/osu/issues/18089. --- osu.Game/Screens/Play/Player.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 91131b0aba..66b4b306f3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -601,8 +601,13 @@ namespace osu.Game.Screens.Play } } - // if an exit has been requested, cancel any pending completion (the user has shown intention to exit). - resultsDisplayDelegate?.Cancel(); + // Matching osu!stable behaviour, if the results screen is pending and the user requests an exit, + // show the results instead. + if (resultsDisplayDelegate != null) + { + progressToResults(false); + return; + } // import current score if possible. prepareAndImportScoreAsync(); From 242a41371c10503b9221b4b16db8ec98bbfc1bdc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 15:42:50 +0900 Subject: [PATCH 21/83] Fix `HotkeyOverlay` fade out occurring when exit is blocked --- osu.Game/Screens/Play/Player.cs | 39 ++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 66b4b306f3..ad796744fc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -279,8 +279,10 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - fadeOut(true); - PerformExit(false); + if (PerformExit(false)) + // The hotkey overlay dims the screen. + // If the operation succeeds, we want to make sure we stay dimmed to keep continuity. + fadeOut(true); }, }, }); @@ -298,8 +300,10 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - fadeOut(true); - Restart(true); + if (Restart(true)) + // The hotkey overlay dims the screen. + // If the operation succeeds, we want to make sure we stay dimmed to keep continuity. + fadeOut(true); }, }, }); @@ -565,7 +569,8 @@ namespace osu.Game.Screens.Play /// Whether the pause or fail dialog should be shown before performing an exit. /// If and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead. /// - protected void PerformExit(bool showDialogFirst) + /// Whether this call resulted in a final exit. + protected bool PerformExit(bool showDialogFirst) { // there is a chance that an exit request occurs after the transition to results has already started. // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process). @@ -576,7 +581,7 @@ namespace osu.Game.Screens.Play // in the potential case that this instance has already been exited, this is required to avoid a crash. if (this.GetChildScreen() != null) this.MakeCurrent(); - return; + return true; } bool pauseOrFailDialogVisible = @@ -588,7 +593,7 @@ namespace osu.Game.Screens.Play if (ValidForResume && GameplayState.HasFailed) { failAnimationContainer.FinishTransforms(true); - return; + return false; } // even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing. @@ -597,16 +602,16 @@ namespace osu.Game.Screens.Play // in the case a dialog needs to be shown, attempt to pause and show it. // this may fail (see internal checks in Pause()) but the fail cases are temporary, so don't fall through to Exit(). Pause(); - return; + return false; } } // Matching osu!stable behaviour, if the results screen is pending and the user requests an exit, // show the results instead. - if (resultsDisplayDelegate != null) + if (resultsDisplayDelegate != null && !isRestarting) { progressToResults(false); - return; + return false; } // import current score if possible. @@ -617,6 +622,7 @@ namespace osu.Game.Screens.Play // - the pause / fail dialog was requested but is already displayed (user showing intention to exit). // - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance. this.Exit(); + return true; } private void performUserRequestedSkip() @@ -665,10 +671,10 @@ namespace osu.Game.Screens.Play /// This can be called from a child screen in order to trigger the restart process. /// /// Whether a quick restart was requested (skipping intro etc.). - public void Restart(bool quickRestart = false) + public bool Restart(bool quickRestart = false) { if (!Configuration.AllowRestart) - return; + return false; isRestarting = true; @@ -678,7 +684,7 @@ namespace osu.Game.Screens.Play RestartRequested?.Invoke(quickRestart); - PerformExit(false); + return PerformExit(false); } /// @@ -1205,8 +1211,11 @@ namespace osu.Game.Screens.Play float fadeOutDuration = instant ? 0 : 250; this.FadeOut(fadeOutDuration); - ApplyToBackground(b => b.IgnoreUserSettings.Value = true); - storyboardReplacesBackground.Value = false; + if (this.IsCurrentScreen()) + { + ApplyToBackground(b => b.IgnoreUserSettings.Value = true); + storyboardReplacesBackground.Value = false; + } } #endregion From 73b81467298301dc47d35d3b23c033f28c4b2664 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 14:49:07 +0900 Subject: [PATCH 22/83] Update tests in line with new behaviour --- osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 283866bef2..09f729e468 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("disable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, false)); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("exit via pause", () => Player.ExitViaPause()); - AddAssert("player exited", () => Stack.CurrentScreen == null); + AddUntilStep("reached results screen", () => Stack.CurrentScreen is ResultsScreen); } [Test] From 3023e4419607de57f8605a1c27f3a6274a3b45f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 15:52:48 +0900 Subject: [PATCH 23/83] Remove unused using statements --- .../Visual/UserInterface/TestSceneNotificationOverlay.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 332053d71e..114c070acc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -4,9 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -14,7 +12,6 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Database; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; From 55a9de034d9d79d8b9d9818e55ea914f8ab29e59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 16:00:23 +0900 Subject: [PATCH 24/83] Change `NotificationOverlay` type based logic to not require specifying every type of notification --- osu.Game/Overlays/NotificationOverlay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 67cf868fb0..dd4924d07a 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -113,7 +113,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification), typeof(UserAvatarNotification) }), + // The main section adds as a catch-all for notifications which don't group into other sections. + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(Notification) }), new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }), } } @@ -205,7 +206,7 @@ namespace osu.Game.Overlays var ourType = notification.GetType(); int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; - var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); + var section = sections.Children.Last(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); section.Add(notification, depth); From e6103fea95ed8bffde81ca16bfd4da2631c91ad4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 16:00:36 +0900 Subject: [PATCH 25/83] Fix `async` usage in `TestSceneNotificationOverlay` --- .../Visual/UserInterface/TestSceneNotificationOverlay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 114c070acc..c584c7dba0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -541,9 +542,9 @@ namespace osu.Game.Tests.Visual.UserInterface progressingNotifications.Add(n); } - private async void sendUserNotification() + private void sendUserNotification() { - var user = await userLookupCache.GetUserAsync(0).ConfigureAwait(true); + var user = userLookupCache.GetUserAsync(0).GetResultSafely(); if (user == null) return; var n = new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!"); From aee8ba789c1f4f022eaa3abf90173afce1fcc591 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 16:02:19 +0900 Subject: [PATCH 26/83] Tidy up `UserAvatarNotification` implementation --- .../Overlays/Notifications/Notification.cs | 4 +- .../Notifications/UserAvatarNotification.cs | 39 ++++++++----------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 8cdc373417..d619d1d3c3 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -53,6 +53,8 @@ namespace osu.Game.Overlays.Notifications public virtual string PopInSampleName => "UI/notification-default"; public virtual string PopOutSampleName => "UI/overlay-pop-out"; + protected const float CORNER_RADIUS = 6; + protected NotificationLight Light; protected Container IconContent; @@ -128,7 +130,7 @@ namespace osu.Game.Overlays.Notifications AutoSizeAxes = Axes.Y, }.WithChild(MainContent = new Container { - CornerRadius = 6, + CornerRadius = CORNER_RADIUS, Masking = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs index af881a5008..04766f7743 100644 --- a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -39,39 +39,34 @@ namespace osu.Game.Overlays.Notifications Text = text; } - private DrawableAvatar? avatar; - protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - IconContent.Masking = true; - - // Workaround for the corner radius on parent's mask breaking if we add masking to IconContent - IconContent.CornerRadius = 6; - - IconContent.AddRange(new Drawable[] - { - new Box() - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, - }, - }); - - avatar = new DrawableAvatar(user) - { - FillMode = FillMode.Fill, - }; - LoadComponentAsync(avatar, IconContent.Add); - Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Text = text }); + + IconContent.Masking = true; + IconContent.CornerRadius = CORNER_RADIUS; + + IconContent.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + }); + + LoadComponentAsync(new DrawableAvatar(user) + { + FillMode = FillMode.Fill, + }, IconContent.Add); } } } From 20f32e2025a3d992bdd1b4b75eeb9360ea9932fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 16:05:48 +0900 Subject: [PATCH 27/83] Add light colouring for user notifications and adjust lighting slightly --- osu.Game/Overlays/Notifications/Notification.cs | 5 ++--- osu.Game/Overlays/Notifications/UserAvatarNotification.cs | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index d619d1d3c3..8108861dca 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -473,10 +473,9 @@ namespace osu.Game.Overlays.Notifications base.Colour = value; pulsateLayer.EdgeEffect = new EdgeEffectParameters { - Colour = ((Color4)value).Opacity(0.5f), //todo: avoid cast + Colour = ((Color4)value).Opacity(0.18f), Type = EdgeEffectType.Glow, - Radius = 12, - Roundness = 12, + Radius = 14, }; } } diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs index 04766f7743..5a9241a2a1 100644 --- a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -42,8 +42,10 @@ namespace osu.Game.Overlays.Notifications protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { + Light.Colour = colours.Orange2; + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { AutoSizeAxes = Axes.Y, From a512ef56379637181bd0eab2393295d5608d9726 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 17:41:31 +0900 Subject: [PATCH 28/83] Add exceptions to online state handling --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 5 ++++- osu.Game/Users/UserPanel.cs | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 5047992c8b..02f0a6e80d 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Dashboard { users.GetUserAsync(userId).ContinueWith(task => { - var user = task.GetResultSafely(); + APIUser user = task.GetResultSafely(); if (user == null) return; @@ -130,6 +130,9 @@ namespace osu.Game.Overlays.Dashboard if (!playingUsers.Contains(user.Id)) return; + // TODO: remove this once online state is being updated more correctly. + user.IsOnline = true; + userFlow.Add(createUserPanel(user)); }); }); diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 98b306e264..273faf9bd1 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -123,7 +123,8 @@ namespace osu.Game.Users } if ( - User.IsOnline && + // TODO: uncomment this once lazer / osu-web is updating online states + // User.IsOnline && multiplayerClient?.Room != null && multiplayerClient.Room.Users.All(u => u.UserID != User.Id) ) From 94d7a65e4062578ac13b96da10e9dc3c70b67899 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 17:42:02 +0900 Subject: [PATCH 29/83] Schedule `Join` operations rather than using `OnLoadComplete` for added safety --- osu.Game/OsuGame.cs | 11 +++--- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 37 ++++++++++--------- .../OnlinePlay/Multiplayer/Multiplayer.cs | 5 +-- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 4 +- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5372f9227f..ecf27cd116 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -654,14 +654,13 @@ namespace osu.Game { PerformFromScreen(screen => { - Multiplayer multiplayer = new Multiplayer(); - multiplayer.OnLoadComplete += _ => - { - multiplayer.Join(room, password); - }; + if (!(screen is Multiplayer multiplayer)) + screen.Push(multiplayer = new Multiplayer()); - screen.Push(multiplayer); + multiplayer.Join(room, password); }); + // TODO: We should really be able to use `validScreens: new[] { typeof(Multiplayer) }` here + // but `PerformFromScreen` doesn't understand nested stacks. } /// diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index fc4a5357c6..04ea59621d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -297,26 +297,29 @@ 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) { - if (joiningRoomOperation != null) - return; - - joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - - RoomManager?.JoinRoom(room, password, _ => + Schedule(() => { - Open(room); - joiningRoomOperation?.Dispose(); - joiningRoomOperation = null; - onSuccess?.Invoke(room); - }, error => - { - joiningRoomOperation?.Dispose(); - joiningRoomOperation = null; - onFailure?.Invoke(error); + if (joiningRoomOperation != null) + return; + + joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); + + RoomManager?.JoinRoom(room, password, _ => + { + Open(room); + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; + onSuccess?.Invoke(room); + }, error => + { + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; + onFailure?.Invoke(error); + }); }); - }); + } /// /// Copies a room and opens it as a fresh (not-yet-created) one. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index f899b24d30..edf5ce276a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -91,10 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); - public void Join(Room room, string? password) - { - Lounge.Join(room, password); - } + public void Join(Room room, string? password) => Schedule(() => Lounge.Join(room, password)); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 57e388f016..88bbff6b92 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.OnlinePlay [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + public IScreen CurrentSubScreen => screenStack.CurrentScreen; + public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; // this is required due to PlayerLoader eventually being pushed to the main stack @@ -225,8 +227,6 @@ namespace osu.Game.Screens.OnlinePlay ((IBindable)Activity).BindTo(newOsuScreen.Activity); } - public IScreen CurrentSubScreen => screenStack.CurrentScreen; - protected abstract string ScreenTitle { get; } protected virtual RoomManager CreateRoomManager() => new RoomManager(); From cde4fad61027010d411e444b25ac0d111db16dce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 17:55:14 +0900 Subject: [PATCH 30/83] Simplify `async` lookup logic in `Invited` handling --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 8b4c38a152..6ba7953c8d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -11,7 +11,6 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Database; @@ -447,13 +446,8 @@ namespace osu.Game.Online.Multiplayer async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) { - var loadUserTask = userLookupCache.GetUserAsync(invitedBy); - var loadRoomTask = lookupRoom(roomID); - - await Task.WhenAll(loadUserTask, loadRoomTask).ConfigureAwait(false); - - APIUser? apiUser = loadUserTask.GetResultSafely(); - Room? apiRoom = loadRoomTask.GetResultSafely(); + APIUser? apiUser = await userLookupCache.GetUserAsync(invitedBy); + Room? apiRoom = await lookupRoom(roomID); if (apiUser == null || apiRoom == null) return; From a1a9bb75b79097e3abace2fa2e533ef031c73848 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 17:55:58 +0900 Subject: [PATCH 31/83] Remove unnecessary schedule logic --- .../Online/Multiplayer/MultiplayerClient.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 6ba7953c8d..3f4a26bf6c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -451,19 +451,16 @@ namespace osu.Game.Online.Multiplayer if (apiUser == null || apiRoom == null) return; - Scheduler.Add(() => - { - PostNotification?.Invoke( - new UserAvatarNotification(apiUser, $"{apiUser.Username} invited you to a multiplayer match:\"{apiRoom.Name}\"!") + PostNotification?.Invoke( + new UserAvatarNotification(apiUser, $"{apiUser.Username} invited you to a multiplayer match:\"{apiRoom.Name}\"!") + { + Activated = () => { - Activated = () => - { - InviteAccepted?.Invoke(apiRoom, password); - return true; - } + InviteAccepted?.Invoke(apiRoom, password); + return true; } - ); - }); + } + ); } private Task lookupRoom(long id) From d2aa601912c3cd75fd8d28491e06584b493b0294 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 17:59:08 +0900 Subject: [PATCH 32/83] Allow localisation of the invite notification --- osu.Game/Localisation/NotificationsStrings.cs | 5 +++++ osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 53687f2b28..9cb286bc91 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -93,6 +93,11 @@ Please try changing your audio device to a working setting."); /// public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username); + /// + /// "{0} invited you to the multiplayer match "{1}"! Click to join." + /// + public static LocalisableString InvitedYouToTheMultiplayer(string username, string roomName) => new TranslatableString(getKey(@"invited_you_to_the_multiplayer"), @"{0} invited you to the multiplayer match ""{1}""! Click to join.", username, roomName); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 3f4a26bf6c..e8653b86be 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -23,6 +23,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Utils; +using osu.Game.Localisation; namespace osu.Game.Online.Multiplayer { @@ -452,7 +453,7 @@ namespace osu.Game.Online.Multiplayer if (apiUser == null || apiRoom == null) return; PostNotification?.Invoke( - new UserAvatarNotification(apiUser, $"{apiUser.Username} invited you to a multiplayer match:\"{apiRoom.Name}\"!") + new UserAvatarNotification(apiUser, NotificationsStrings.InvitedYouToTheMultiplayer(apiUser.Username, apiRoom.Name.Value)) { Activated = () => { From 361d70f68affb6e422edc9cc521be89d66a0a66c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 17:59:47 +0900 Subject: [PATCH 33/83] Rename callback to match standards --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- osu.Game/OsuGame.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 e8653b86be..978eee505c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -31,7 +31,7 @@ namespace osu.Game.Online.Multiplayer { public Action? PostNotification { protected get; set; } - public Action? InviteAccepted { protected get; set; } + public Action? PresentMatch { protected get; set; } /// /// Invoked when any change occurs to the multiplayer room. @@ -457,7 +457,7 @@ namespace osu.Game.Online.Multiplayer { Activated = () => { - InviteAccepted?.Invoke(apiRoom, password); + PresentMatch?.Invoke(apiRoom, password); return true; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ecf27cd116..885077a8e8 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -873,7 +873,7 @@ namespace osu.Game ScoreManager.PresentImport = items => PresentScore(items.First().Value); MultiplayerClient.PostNotification = n => Notifications.Post(n); - MultiplayerClient.InviteAccepted = PresentMultiplayerMatch; + MultiplayerClient.PresentMatch = PresentMultiplayerMatch; // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. From 5f62c225bf7e7b03950aa931962a2fc168216ffe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 18:07:03 +0900 Subject: [PATCH 34/83] Ad localisation (and adjust messaging) of invite failures --- osu.Game/Localisation/OnlinePlayStrings.cs | 10 ++++++++++ .../Online/Multiplayer/OnlineMultiplayerClient.cs | 11 +++-------- osu.Game/Online/Multiplayer/UserBlockedException.cs | 2 +- osu.Game/Online/Multiplayer/UserBlocksPMsException.cs | 2 +- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game/Localisation/OnlinePlayStrings.cs b/osu.Game/Localisation/OnlinePlayStrings.cs index 1853cb753a..1918519d36 100644 --- a/osu.Game/Localisation/OnlinePlayStrings.cs +++ b/osu.Game/Localisation/OnlinePlayStrings.cs @@ -14,6 +14,16 @@ namespace osu.Game.Localisation /// public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag."); + /// + /// "Can't invite this user as you have blocked them or they have blocked you." + /// + public static LocalisableString InviteFailedUserBlocked => new TranslatableString(getKey(@"cant_invite_this_user_as"), @"Can't invite this user as you have blocked them or they have blocked you."); + + /// + /// "Can't invite this user as they have opted out of non-friend communications." + /// + public static LocalisableString InviteFailedUserOptOut => new TranslatableString(getKey(@"cant_invite_this_user_as1"), @"Can't invite this user as they have opted out of non-friend communications."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 08e82f2ad3..20ec030eac 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -13,6 +13,7 @@ using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays.Notifications; +using osu.Game.Localisation; namespace osu.Game.Online.Multiplayer { @@ -124,17 +125,11 @@ namespace osu.Game.Online.Multiplayer switch (exception.GetHubExceptionMessage()) { case UserBlockedException.MESSAGE: - PostNotification?.Invoke(new SimpleErrorNotification - { - Text = "User cannot be invited by someone they have blocked or are blocked by." - }); + PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserBlocked }); break; case UserBlocksPMsException.MESSAGE: - PostNotification?.Invoke(new SimpleErrorNotification - { - Text = "User cannot be invited because they cannot receive private messages from people not on their friends list." - }); + PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserOptOut }); break; } } diff --git a/osu.Game/Online/Multiplayer/UserBlockedException.cs b/osu.Game/Online/Multiplayer/UserBlockedException.cs index f2b69411c7..e964b13c75 100644 --- a/osu.Game/Online/Multiplayer/UserBlockedException.cs +++ b/osu.Game/Online/Multiplayer/UserBlockedException.cs @@ -10,7 +10,7 @@ namespace osu.Game.Online.Multiplayer [Serializable] public class UserBlockedException : HubException { - public const string MESSAGE = "User cannot be invited by someone they have blocked or are blocked by."; + public const string MESSAGE = @"Cannot perform action due to user being blocked."; public UserBlockedException() : base(MESSAGE) diff --git a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs index 0fd4e1f0c1..14ed6fc212 100644 --- a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs +++ b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs @@ -10,7 +10,7 @@ namespace osu.Game.Online.Multiplayer [Serializable] public class UserBlocksPMsException : HubException { - public const string MESSAGE = "User cannot be invited because they have disabled private messages."; + public const string MESSAGE = "Cannot perform action because user has disabled non-friend communications."; public UserBlocksPMsException() : base(MESSAGE) From a591af2b9754af0cd09ad5b1c1e28b2f07235c19 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 18:11:04 +0900 Subject: [PATCH 35/83] Fix too many tasks in `lookupRoom` method --- .../Online/Multiplayer/MultiplayerClient.cs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 978eee505c..dd131ee2e6 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -448,7 +448,7 @@ namespace osu.Game.Online.Multiplayer async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) { APIUser? apiUser = await userLookupCache.GetUserAsync(invitedBy); - Room? apiRoom = await lookupRoom(roomID); + Room? apiRoom = await getRoomAsync(roomID); if (apiUser == null || apiRoom == null) return; @@ -462,23 +462,19 @@ namespace osu.Game.Online.Multiplayer } } ); - } - private Task lookupRoom(long id) - { - return Task.Run(() => + Task getRoomAsync(long id) { - var t = new TaskCompletionSource(); + TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + var request = new GetRoomRequest(id); - - request.Success += room => t.TrySetResult(room); - - request.Failure += e => t.TrySetResult(null); + request.Success += room => taskCompletionSource.TrySetResult(room); + request.Failure += _ => taskCompletionSource.TrySetResult(null); API.Queue(request); - return t.Task; - }); + return taskCompletionSource.Task; + } } private void addUserToAPIRoom(MultiplayerRoomUser user) From e48c4fa430ca938b9c3648f906ac36525d770a32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 18:15:15 +0900 Subject: [PATCH 36/83] Add missing `ConfigureAwait` calls --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index dd131ee2e6..515a0dda08 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -447,8 +447,8 @@ namespace osu.Game.Online.Multiplayer async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) { - APIUser? apiUser = await userLookupCache.GetUserAsync(invitedBy); - Room? apiRoom = await getRoomAsync(roomID); + APIUser? apiUser = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false); + Room? apiRoom = await getRoomAsync(roomID).ConfigureAwait(false); if (apiUser == null || apiRoom == null) return; From fc1287b4c695d821465984f0a2835e17e6ad0f02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 18:25:20 +0900 Subject: [PATCH 37/83] Reword xmldoc on invite method to better describe what is happening --- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index ba9e8a237c..327fb0d76a 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -43,7 +43,7 @@ namespace osu.Game.Online.Multiplayer Task UserKicked(MultiplayerRoomUser user); /// - /// Signals that a user has been invited into a multiplayer room. + /// Signals that the local user has been invited into a multiplayer room. /// /// Id of user that invited the player. /// Id of the room the user got invited to. From 1ce268be3fe04c62b0bdae4ed100aa07011d500e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 18:58:42 +0900 Subject: [PATCH 38/83] Update some packages to match `osu.Server.Spectator` --- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- osu.Game/osu.Game.csproj | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index febe353b81..5de21a68d0 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -7,7 +7,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 69675e9c1b..f0a4b38c03 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,13 +21,13 @@ - + - - - - - + + + + + @@ -38,8 +38,8 @@ - - + + From 32eda99c3a985fb88ba59cbffe0627eaab724b7c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 19:06:19 +0900 Subject: [PATCH 39/83] Add missing xmldoc returns --- osu.Game/Screens/Play/Player.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ad796744fc..54693a95cc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -671,6 +671,7 @@ namespace osu.Game.Screens.Play /// This can be called from a child screen in order to trigger the restart process. /// /// Whether a quick restart was requested (skipping intro etc.). + /// Whether this call resulted in a restart. public bool Restart(bool quickRestart = false) { if (!Configuration.AllowRestart) From d174a6ce613c39efd24ec8167288a453d58dd90f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 19:09:43 +0900 Subject: [PATCH 40/83] Remove dead code which was only there for the exit-specific scenario --- osu.Game/Screens/Play/Player.cs | 38 ++++++++++++--------------- osu.Game/Screens/Play/PlayerLoader.cs | 2 ++ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 54693a95cc..50ebe8cd86 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -572,18 +572,6 @@ namespace osu.Game.Screens.Play /// Whether this call resulted in a final exit. protected bool PerformExit(bool showDialogFirst) { - // there is a chance that an exit request occurs after the transition to results has already started. - // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process). - if (!this.IsCurrentScreen()) - { - ValidForResume = false; - - // in the potential case that this instance has already been exited, this is required to avoid a crash. - if (this.GetChildScreen() != null) - this.MakeCurrent(); - return true; - } - bool pauseOrFailDialogVisible = PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible; @@ -617,11 +605,16 @@ namespace osu.Game.Screens.Play // import current score if possible. prepareAndImportScoreAsync(); - // The actual exit is performed if - // - the pause / fail dialog was not requested - // - the pause / fail dialog was requested but is already displayed (user showing intention to exit). - // - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance. - this.Exit(); + // Screen may not be current if a restart has been performed. + if (this.IsCurrentScreen()) + { + // The actual exit is performed if + // - the pause / fail dialog was not requested + // - the pause / fail dialog was requested but is already displayed (user showing intention to exit). + // - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance. + this.Exit(); + } + return true; } @@ -741,9 +734,6 @@ namespace osu.Game.Screens.Play // is no chance that a user could return to the (already completed) Player instance from a child screen. ValidForResume = false; - if (!Configuration.ShowResults) - return; - bool storyboardStillRunning = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; // If the current beatmap has a storyboard, this method will be called again on storyboard completion. @@ -766,10 +756,16 @@ namespace osu.Game.Screens.Play /// Whether a minimum delay () should be added before the screen is displayed. private void progressToResults(bool withDelay) { - resultsDisplayDelegate?.Cancel(); + if (!Configuration.ShowResults) + return; + + // Setting this early in the process means that even if something were to go wrong in the order of events following, there + // is no chance that a user could return to the (already completed) Player instance from a child screen. + ValidForResume = false; double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; + resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = new ScheduledDelegate(() => { if (prepareScoreForDisplayTask == null) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 4054e456b9..681189d184 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -414,6 +414,8 @@ namespace osu.Game.Screens.Play quickRestart = quickRestartRequested; hideOverlays = true; ValidForResume = true; + + this.MakeCurrent(); } private void contentIn() From fa47309eeffbeba428af323b1ce317f4b761b63f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 19:26:32 +0900 Subject: [PATCH 41/83] Fix exit key during storyboard outro not progressing to results --- .../Visual/Gameplay/TestSceneStoryboardWithOutro.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 09f729e468..98825b27d4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -72,12 +72,12 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestStoryboardExitDuringOutroStillExits() + public void TestStoryboardExitDuringOutroProgressesToResults() { CreateTest(); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("exit via pause", () => Player.ExitViaPause()); - AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null); + AddUntilStep("reached results screen", () => Stack.CurrentScreen is ResultsScreen); } [TestCase(false)] diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 50ebe8cd86..97bfa35d49 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -596,7 +596,7 @@ namespace osu.Game.Screens.Play // Matching osu!stable behaviour, if the results screen is pending and the user requests an exit, // show the results instead. - if (resultsDisplayDelegate != null && !isRestarting) + if (GameplayState.HasPassed && !isRestarting) { progressToResults(false); return false; From 3849b1164436d227f2418a645d062c96711934e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 19:48:45 +0900 Subject: [PATCH 42/83] Fix "Hard Rock" mod affecting CS/AR in osu!mania Closes https://github.com/ppy/osu/issues/25090. Not sure if there are other exceptions we should account for here. --- osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs | 9 +++++++++ osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs | 9 +++++++++ osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs | 3 +++ osu.Game/Rulesets/Mods/ModHardRock.cs | 9 ++++----- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs index 93eadcc13e..62fded0980 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.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.Rulesets.Mods; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; @@ -16,5 +17,13 @@ namespace osu.Game.Rulesets.Catch.Mods var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor; catchProcessor.HardRockOffsets = true; } + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + + difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. + difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index 19d4a1bf83..d24597eeed 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; @@ -22,5 +23,13 @@ namespace osu.Game.Rulesets.Osu.Mods OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject); } + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + + difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. + difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); + } } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs index ba41175461..37a630c8ad 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.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.Beatmaps; using osu.Game.Rulesets.Mods; @@ -23,7 +24,9 @@ namespace osu.Game.Rulesets.Taiko.Mods public override void ApplyToDifficulty(BeatmapDifficulty difficulty) { base.ApplyToDifficulty(difficulty); + difficulty.SliderMultiplier *= slider_multiplier; + difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); } } } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 2886e59c54..4b2d1d050e 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -18,17 +18,16 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Everything just got a bit harder..."; public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; + protected const float ADJUST_RATIO = 1.4f; + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { - const float ratio = 1.4f; - difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. - difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ratio, 10.0f); - difficulty.DrainRate = Math.Min(difficulty.DrainRate * ratio, 10.0f); - difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ratio, 10.0f); + difficulty.DrainRate = Math.Min(difficulty.DrainRate * ADJUST_RATIO, 10.0f); + difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f); } } } From 5f0b1d69a508925fd326a374e119a2ec7dba2532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 11:08:10 +0200 Subject: [PATCH 43/83] Prepare `KeyBindingRow` for accepting external changes --- .../Settings/TestSceneKeyBindingPanel.cs | 4 +- .../Visual/Settings/TestSceneKeyBindingRow.cs | 60 +++++++++++++++++++ .../Settings/Sections/Input/KeyBindingRow.cs | 58 +++++++++++------- .../Sections/Input/KeyBindingsSubsection.cs | 13 ++-- .../Input/VariantBindingsSubsection.cs | 4 +- 5 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneKeyBindingRow.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 449ca0f258..5a001ca127 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -207,7 +207,7 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); AddAssert("binding cleared", - () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); + () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.Value.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); } [Test] @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); AddAssert("binding cleared", - () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); + () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.Value.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); } [Test] diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingRow.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingRow.cs new file mode 100644 index 0000000000..ff996a9ca1 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingRow.cs @@ -0,0 +1,60 @@ +// 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; +using osu.Framework.Input.Bindings; +using osu.Framework.Testing; +using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings.Sections.Input; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneKeyBindingRow : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Test] + public void TestChangesAfterConstruction() + { + KeyBindingRow row = null!; + + AddStep("create row", () => Child = new Container + { + Width = 500, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = row = new KeyBindingRow(GlobalAction.Back) + { + Defaults = new[] + { + new KeyCombination(InputKey.Escape), + new KeyCombination(InputKey.ExtraMouseButton1) + } + } + }); + + AddStep("change key bindings", () => + { + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.Escape))); + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.ExtraMouseButton1))); + }); + AddUntilStep("revert to default button not shown", () => row.ChildrenOfType>().Single().Alpha, () => Is.Zero); + + AddStep("change key bindings", () => + { + row.KeyBindings.Clear(); + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.X))); + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.Z))); + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.I))); + }); + AddUntilStep("revert to default button not shown", () => row.ChildrenOfType>().Single().Alpha, () => Is.Not.Zero); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 2e44d8b02d..0ba3dc9b9a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -44,6 +44,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// public bool AllowMainMouseButtons { get; init; } + /// + /// The bindings to display in this row. + /// + public BindableList KeyBindings { get; } = new BindableList(); + /// /// The default key bindings for this row. /// @@ -65,12 +70,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input public bool FilteringActive { get; set; } - public IEnumerable FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text); + public IEnumerable FilterTerms => KeyBindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text); #endregion private readonly object action; - private readonly IEnumerable bindings; private Bindable isDefault { get; } = new BindableBool(true); @@ -101,11 +105,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// Creates a new . /// /// The action that this row contains bindings for. - /// The keybindings to display in this row. - public KeyBindingRow(object action, List bindings) + public KeyBindingRow(object action) { this.action = action; - this.bindings = bindings; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -191,10 +193,23 @@ namespace osu.Game.Overlays.Settings.Sections.Input } }; - foreach (var b in bindings) - buttons.Add(new KeyButton(b)); + KeyBindings.BindCollectionChanged((_, _) => + { + Scheduler.AddOnce(updateButtons); + updateIsDefaultValue(); + }, true); + } - updateIsDefaultValue(); + private void updateButtons() + { + if (buttons.Count > KeyBindings.Count) + buttons.RemoveRange(buttons.Skip(KeyBindings.Count).ToArray(), true); + + while (buttons.Count < KeyBindings.Count) + buttons.Add(new KeyButton()); + + foreach (var (button, binding) in buttons.Zip(KeyBindings)) + button.KeyBinding.Value = binding; } public void RestoreDefaults() @@ -472,11 +487,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input } private void updateStoreFromButton(KeyButton button) => - realm.WriteAsync(r => r.Find(button.KeyBinding.ID)!.KeyCombinationString = button.KeyBinding.KeyCombinationString); + realm.WriteAsync(r => r.Find(button.KeyBinding.Value.ID)!.KeyCombinationString = button.KeyBinding.Value.KeyCombinationString); private void updateIsDefaultValue() { - isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); + isDefault.Value = KeyBindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); } private partial class CancelButton : RoundedButton @@ -499,7 +514,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input public partial class KeyButton : Container { - public readonly RealmKeyBinding KeyBinding; + public Bindable KeyBinding { get; } = new Bindable(); private readonly Box box; public readonly OsuSpriteText Text; @@ -525,13 +540,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input } } - public KeyButton(RealmKeyBinding keyBinding) + public KeyButton() { - if (keyBinding.IsManaged) - throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(keyBinding)); - - KeyBinding = keyBinding; - Margin = new MarginPadding(padding); Masking = true; @@ -567,6 +577,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input { base.LoadComplete(); + KeyBinding.BindValueChanged(_ => + { + if (KeyBinding.Value.IsManaged) + throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(KeyBinding)); + + updateKeyCombinationText(); + }); keyCombinationProvider.KeymapChanged += updateKeyCombinationText; updateKeyCombinationText(); } @@ -575,6 +592,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void load() { updateHoverState(); + FinishTransforms(true); } protected override bool OnHover(HoverEvent e) @@ -613,10 +631,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input public void UpdateKeyCombination(KeyCombination newCombination) { - if (KeyBinding.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) + if (KeyBinding.Value.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) return; - KeyBinding.KeyCombination = newCombination; + KeyBinding.Value.KeyCombination = newCombination; updateKeyCombinationText(); } @@ -624,7 +642,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { Scheduler.AddOnce(updateText); - void updateText() => Text.Text = keyCombinationProvider.GetReadableString(KeyBinding.KeyCombination); + void updateText() => Text.Text = keyCombinationProvider.GetReadableString(KeyBinding.Value.KeyCombination); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 1ad9442631..f7e36e0dd2 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -42,11 +42,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input int intKey = (int)defaultGroup.Key; // one row per valid action. - Add(CreateKeyBindingRow( - defaultGroup.Key, - bindings.Where(b => b.ActionInt.Equals(intKey)).ToList(), - defaultGroup) - .With(row => row.BindingUpdated = onBindingUpdated)); + var row = CreateKeyBindingRow(defaultGroup.Key, defaultGroup) + .With(row => row.BindingUpdated = onBindingUpdated); + row.KeyBindings.AddRange(bindings.Where(b => b.ActionInt.Equals(intKey))); + Add(row); } Add(new ResetButton @@ -57,8 +56,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected abstract IEnumerable GetKeyBindings(Realm realm); - protected virtual KeyBindingRow CreateKeyBindingRow(object action, IEnumerable keyBindings, IEnumerable defaults) - => new KeyBindingRow(action, keyBindings.ToList()) + protected virtual KeyBindingRow CreateKeyBindingRow(object action, IEnumerable defaults) + => new KeyBindingRow(action) { AllowMainMouseButtons = false, Defaults = defaults.Select(d => d.KeyCombination), diff --git a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs index 46da8a1453..6db8aa7259 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs @@ -39,8 +39,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input .Where(b => b.RulesetName == rulesetName && b.Variant == variant); } - protected override KeyBindingRow CreateKeyBindingRow(object action, IEnumerable keyBindings, IEnumerable defaults) - => new KeyBindingRow(action, keyBindings.ToList()) + protected override KeyBindingRow CreateKeyBindingRow(object action, IEnumerable defaults) + => new KeyBindingRow(action) { AllowMainMouseButtons = true, Defaults = defaults.Select(d => d.KeyCombination), From aa8dbd742e847fbb86ec19b84593b06c06745dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 11:25:19 +0200 Subject: [PATCH 44/83] Hoist database update to subsection level The end goal here is to be able to better coordinate deconfliction of bindings at the subsection level rather than try to jam that logic into individual rows somehow. The flipside is that this is going to require a flow to update the individual rows after the subsection's intervention, but that's what the previous commit was for. --- .../Settings/Sections/Input/KeyBindingRow.cs | 21 +++++++------------ .../Sections/Input/KeyBindingsSubsection.cs | 9 ++++++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 0ba3dc9b9a..aab6ed6d74 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -18,7 +18,6 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -37,7 +36,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// /// Invoked when the binding of this row is updated with a change being written. /// - public Action? BindingUpdated { get; set; } + public KeyBindingUpdated? BindingUpdated { get; set; } + + public delegate void KeyBindingUpdated(KeyBindingRow sender, KeyBindingUpdatedEventArgs args); + + public record KeyBindingUpdatedEventArgs(Guid KeyBindingID, string KeyCombinationString); /// /// Whether left and right mouse button clicks should be included in the edited bindings. @@ -81,9 +84,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input [Resolved] private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; - [Resolved] - private RealmAccess realm { get; set; } = null!; - private Container content = null!; private OsuSpriteText text = null!; @@ -220,8 +220,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { var button = buttons[i++]; button.UpdateKeyCombination(d); - - updateStoreFromButton(button); + finalise(); } isDefault.Value = true; @@ -437,17 +436,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input { if (bindTarget != null) { - updateStoreFromButton(bindTarget); - updateIsDefaultValue(); bindTarget.IsBinding = false; + var args = new KeyBindingUpdatedEventArgs(bindTarget.KeyBinding.Value.ID, bindTarget.KeyBinding.Value.KeyCombinationString); Schedule(() => { // schedule to ensure we don't instantly get focus back on next OnMouseClick (see AcceptFocus impl.) bindTarget = null; if (hasChanged) - BindingUpdated?.Invoke(this); + BindingUpdated?.Invoke(this, args); }); } @@ -486,9 +484,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (bindTarget != null) bindTarget.IsBinding = true; } - private void updateStoreFromButton(KeyButton button) => - realm.WriteAsync(r => r.Find(button.KeyBinding.Value.ID)!.KeyCombinationString = button.KeyBinding.Value.KeyCombinationString); - private void updateIsDefaultValue() { isDefault.Value = KeyBindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index f7e36e0dd2..57f6e58058 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -27,13 +27,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected IEnumerable Defaults { get; init; } = Array.Empty(); + [Resolved] + private RealmAccess realm { get; set; } = null!; + protected KeyBindingsSubsection() { FlowContent.Spacing = new Vector2(0, 3); } [BackgroundDependencyLoader] - private void load(RealmAccess realm) + private void load() { var bindings = realm.Run(r => GetKeyBindings(r).Detach()); @@ -63,8 +66,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input Defaults = defaults.Select(d => d.KeyCombination), }; - private void onBindingUpdated(KeyBindingRow sender) + private void onBindingUpdated(KeyBindingRow sender, KeyBindingRow.KeyBindingUpdatedEventArgs args) { + realm.WriteAsync(r => r.Find(args.KeyBindingID)!.KeyCombinationString = args.KeyCombinationString); + if (AutoAdvanceTarget) { var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault(); From 7b6563116ac9bfb57eacd19889d612d326f90489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 13:09:33 +0200 Subject: [PATCH 45/83] Implement visual appearance of key binding conflict popover --- .../TestSceneKeyBindingConflictPopover.cs | 59 +++++ .../Input/KeyBindingConflictPopover.cs | 249 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs new file mode 100644 index 0000000000..972e1cbe79 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs @@ -0,0 +1,59 @@ +// 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.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings.Sections.Input; +using osu.Game.Rulesets.Osu; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneKeyBindingConflictPopover : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Test] + public void TestAppearance() + { + ButtonWithConflictPopover button = null!; + + AddStep("create content", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = button = new ButtonWithConflictPopover + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Open popover", + Width = 300 + } + }; + }); + AddStep("show popover", () => button.TriggerClick()); + } + + private partial class ButtonWithConflictPopover : RoundedButton, IHasPopover + { + [BackgroundDependencyLoader] + private void load() + { + Action = this.ShowPopover; + } + + public Popover GetPopover() => new KeyBindingConflictPopover( + OsuAction.LeftButton, + OsuAction.RightButton, + new KeyCombination(InputKey.Z)); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs new file mode 100644 index 0000000000..55b4ee3390 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -0,0 +1,249 @@ +// 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.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class KeyBindingConflictPopover : OsuPopover + { + private readonly object existingAction; + private readonly object newAction; + private readonly KeyCombination conflictingCombination; + + private ConflictingKeyBindingPreview newPreview = null!; + private ConflictingKeyBindingPreview existingPreview = null!; + private HoverableRoundedButton keepExistingButton = null!; + private HoverableRoundedButton applyNewButton = null!; + + public KeyBindingConflictPopover(object existingAction, object newAction, KeyCombination conflictingCombination) + { + this.existingAction = existingAction; + this.newAction = newAction; + this.conflictingCombination = conflictingCombination; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 250, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = "The binding you've selected conflicts with another existing binding.", + Margin = new MarginPadding { Bottom = 10 } + }, + existingPreview = new ConflictingKeyBindingPreview(existingAction, conflictingCombination), + newPreview = new ConflictingKeyBindingPreview(newAction, conflictingCombination), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Children = new[] + { + keepExistingButton = new HoverableRoundedButton + { + Text = "Keep existing", + RelativeSizeAxes = Axes.X, + Width = 0.48f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Action = Hide + }, + applyNewButton = new HoverableRoundedButton + { + Text = "Apply new", + RelativeSizeAxes = Axes.X, + Width = 0.48f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Action = Hide + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + keepExistingButton.IsHoveredBindable.BindValueChanged(_ => updatePreviews()); + applyNewButton.IsHoveredBindable.BindValueChanged(_ => updatePreviews()); + updatePreviews(); + } + + private void updatePreviews() + { + if (!keepExistingButton.IsHovered && !applyNewButton.IsHovered) + { + existingPreview.IsChosen.Value = newPreview.IsChosen.Value = null; + return; + } + + existingPreview.IsChosen.Value = keepExistingButton.IsHovered; + newPreview.IsChosen.Value = applyNewButton.IsHovered; + } + + private partial class ConflictingKeyBindingPreview : CompositeDrawable + { + private readonly object action; + private readonly KeyCombination keyCombination; + + private OsuSpriteText newBindingText = null!; + + public Bindable IsChosen { get; } = new Bindable(); + + [Resolved] + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ConflictingKeyBindingPreview(object action, KeyCombination keyCombination) + { + this.action = action; + this.keyCombination = keyCombination; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + CornerRadius = 5, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5 + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize, minSize: 80), + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Text = action.GetLocalisableDescription(), + Margin = new MarginPadding(7.5f), + }, + new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + CornerRadius = 5, + Masking = true, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6 + }, + newBindingText = new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: 10), + Margin = new MarginPadding(5), + Text = keyCombinationProvider.GetReadableString(keyCombination), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + } + }, + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + IsChosen.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + switch (IsChosen.Value) + { + case true: + newBindingText.Text = keyCombinationProvider.GetReadableString(keyCombination); + newBindingText.Colour = colours.Green1; + break; + + case false: + newBindingText.Text = "(none)"; + newBindingText.Colour = colours.Red1; + break; + + case null: + newBindingText.Text = keyCombinationProvider.GetReadableString(keyCombination); + newBindingText.Colour = Colour4.White; + break; + } + } + } + + private partial class HoverableRoundedButton : RoundedButton + { + public BindableBool IsHoveredBindable { get; set; } = new BindableBool(); + + protected override bool OnHover(HoverEvent e) + { + IsHoveredBindable.Value = IsHovered; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + IsHoveredBindable.Value = IsHovered; + base.OnHoverLost(e); + } + } + } +} From f5a6781e5a439c5a364e38fd76a9c15b6dc2394d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 15:36:27 +0200 Subject: [PATCH 46/83] Integrate key binding conflict popover into keybindings panel --- .../TestSceneKeyBindingConflictPopover.cs | 15 +++- .../Sections/Input/KeyBindingConflictInfo.cs | 15 ++++ .../Input/KeyBindingConflictPopover.cs | 75 ++++++++++++++----- .../Settings/Sections/Input/KeyBindingRow.cs | 42 ++++++++--- .../Sections/Input/KeyBindingsSubsection.cs | 44 ++++++++++- osu.Game/Overlays/SettingsPanel.cs | 55 +++++++------- 6 files changed, 184 insertions(+), 62 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictInfo.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs index 972e1cbe79..7251350616 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; @@ -12,6 +13,7 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Rulesets.Osu; +using osuTK.Input; namespace osu.Game.Tests.Visual.Settings { @@ -50,10 +52,15 @@ namespace osu.Game.Tests.Visual.Settings Action = this.ShowPopover; } - public Popover GetPopover() => new KeyBindingConflictPopover( - OsuAction.LeftButton, - OsuAction.RightButton, - new KeyCombination(InputKey.Z)); + public Popover GetPopover() => new KeyBindingConflictPopover + { + ConflictInfo = + { + Value = new KeyBindingConflictInfo( + new ConflictingKeyBinding(Guid.NewGuid(), OsuAction.LeftButton, KeyCombination.FromKey(Key.X), new KeyCombination(InputKey.None)), + new ConflictingKeyBinding(Guid.NewGuid(), OsuAction.RightButton, KeyCombination.FromKey(Key.Z), KeyCombination.FromKey(Key.X))) + } + }; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictInfo.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictInfo.cs new file mode 100644 index 0000000000..ea127456af --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictInfo.cs @@ -0,0 +1,15 @@ +// 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.Input.Bindings; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + /// + /// Contains information about the key binding conflict to be resolved. + /// + public record KeyBindingConflictInfo(ConflictingKeyBinding Existing, ConflictingKeyBinding New); + + public record ConflictingKeyBinding(Guid ID, object Action, KeyCombination CombinationWhenChosen, KeyCombination CombinationWhenNotChosen); +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 55b4ee3390..4206380dfe 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.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.Bindables; using osu.Framework.Extensions; @@ -10,34 +11,34 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osuTK; namespace osu.Game.Overlays.Settings.Sections.Input { public partial class KeyBindingConflictPopover : OsuPopover { - private readonly object existingAction; - private readonly object newAction; - private readonly KeyCombination conflictingCombination; + public Bindable ConflictInfo { get; } = new Bindable(); + + public Action? BindingConflictResolved { get; init; } private ConflictingKeyBindingPreview newPreview = null!; private ConflictingKeyBindingPreview existingPreview = null!; private HoverableRoundedButton keepExistingButton = null!; private HoverableRoundedButton applyNewButton = null!; - public KeyBindingConflictPopover(object existingAction, object newAction, KeyCombination conflictingCombination) - { - this.existingAction = existingAction; - this.newAction = newAction; - this.conflictingCombination = conflictingCombination; - } + [Resolved] + private RealmAccess realm { get; set; } = null!; [BackgroundDependencyLoader] - private void load() + private void load() => recreateDisplay(); + + private void recreateDisplay() { Child = new FillFlowContainer { @@ -54,8 +55,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input Text = "The binding you've selected conflicts with another existing binding.", Margin = new MarginPadding { Bottom = 10 } }, - existingPreview = new ConflictingKeyBindingPreview(existingAction, conflictingCombination), - newPreview = new ConflictingKeyBindingPreview(newAction, conflictingCombination), + existingPreview = new ConflictingKeyBindingPreview(ConflictInfo.Value.Existing.Action, ConflictInfo.Value.Existing.CombinationWhenChosen, ConflictInfo.Value.Existing.CombinationWhenNotChosen), + newPreview = new ConflictingKeyBindingPreview(ConflictInfo.Value.New.Action, ConflictInfo.Value.New.CombinationWhenChosen, ConflictInfo.Value.New.CombinationWhenNotChosen), new Container { RelativeSizeAxes = Axes.X, @@ -79,7 +80,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Width = 0.48f, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Action = Hide + Action = applyNew } } } @@ -87,6 +88,32 @@ namespace osu.Game.Overlays.Settings.Sections.Input }; } + private void applyNew() + { + // only "apply new" needs to cause actual realm changes, since the flow in `KeyBindingsSubsection` does not actually make db changes + // if it detects a binding conflict. + // the temporary visual changes will be reverted by calling `Hide()` / `BindingConflictResolved`. + realm.Write(r => + { + var existingBinding = r.Find(ConflictInfo.Value.Existing.ID); + existingBinding!.KeyCombinationString = ConflictInfo.Value.Existing.CombinationWhenNotChosen.ToString(); + + var newBinding = r.Find(ConflictInfo.Value.New.ID); + newBinding!.KeyCombinationString = ConflictInfo.Value.Existing.CombinationWhenChosen.ToString(); + }); + + Hide(); + } + + protected override void PopOut() + { + base.PopOut(); + + // workaround for `VisibilityContainer.PopOut()` being called in `LoadAsyncComplete()` + if (IsLoaded) + BindingConflictResolved?.Invoke(); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -111,7 +138,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private partial class ConflictingKeyBindingPreview : CompositeDrawable { private readonly object action; - private readonly KeyCombination keyCombination; + private readonly KeyCombination combinationWhenChosen; + private readonly KeyCombination combinationWhenNotChosen; private OsuSpriteText newBindingText = null!; @@ -123,10 +151,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input [Resolved] private OsuColour colours { get; set; } = null!; - public ConflictingKeyBindingPreview(object action, KeyCombination keyCombination) + public ConflictingKeyBindingPreview(object action, KeyCombination combinationWhenChosen, KeyCombination combinationWhenNotChosen) { this.action = action; - this.keyCombination = keyCombination; + this.combinationWhenChosen = combinationWhenChosen; + this.combinationWhenNotChosen = combinationWhenNotChosen; } [BackgroundDependencyLoader] @@ -187,7 +216,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input { Font = OsuFont.Numeric.With(size: 10), Margin = new MarginPadding(5), - Text = keyCombinationProvider.GetReadableString(keyCombination), Anchor = Anchor.Centre, Origin = Anchor.Centre } @@ -209,23 +237,30 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateState() { + string newBinding; + switch (IsChosen.Value) { case true: - newBindingText.Text = keyCombinationProvider.GetReadableString(keyCombination); + newBinding = keyCombinationProvider.GetReadableString(combinationWhenChosen); newBindingText.Colour = colours.Green1; break; case false: - newBindingText.Text = "(none)"; + newBinding = keyCombinationProvider.GetReadableString(combinationWhenNotChosen); newBindingText.Colour = colours.Red1; break; case null: - newBindingText.Text = keyCombinationProvider.GetReadableString(keyCombination); + newBinding = keyCombinationProvider.GetReadableString(combinationWhenChosen); newBindingText.Colour = Colour4.White; break; } + + if (string.IsNullOrEmpty(newBinding)) + newBinding = "(none)"; + + newBindingText.Text = newBinding; } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index aab6ed6d74..c8d4f339da 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -12,8 +12,10 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; 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.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -31,7 +33,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Settings.Sections.Input { - public partial class KeyBindingRow : Container, IFilterable + public partial class KeyBindingRow : Container, IFilterable, IHasPopover { /// /// Invoked when the binding of this row is updated with a change being written. @@ -40,7 +42,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input public delegate void KeyBindingUpdated(KeyBindingRow sender, KeyBindingUpdatedEventArgs args); - public record KeyBindingUpdatedEventArgs(Guid KeyBindingID, string KeyCombinationString); + public record KeyBindingUpdatedEventArgs(object Action, Guid KeyBindingID, KeyCombination KeyCombination); + + public Action? BindingConflictResolved { get; set; } /// /// Whether left and right mouse button clicks should be included in the edited bindings. @@ -77,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input #endregion - private readonly object action; + public readonly object Action; private Bindable isDefault { get; } = new BindableBool(true); @@ -107,7 +111,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// The action that this row contains bindings for. public KeyBindingRow(object action) { - this.action = action; + this.Action = action; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -163,7 +167,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, text = new OsuSpriteText { - Text = action.GetLocalisableDescription(), + Text = Action.GetLocalisableDescription(), Margin = new MarginPadding(1.5f * padding), }, buttons = new FillFlowContainer @@ -220,7 +224,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input { var button = buttons[i++]; button.UpdateKeyCombination(d); - finalise(); + + var args = new KeyBindingUpdatedEventArgs(Action, button.KeyBinding.Value.ID, button.KeyBinding.Value.KeyCombination); + BindingUpdated?.Invoke(this, args); } isDefault.Value = true; @@ -280,7 +286,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Debug.Assert(bindTarget != null); if (bindTarget.IsHovered) - finalise(false); + finalise(); // prevent updating bind target before clear button's action else if (!cancelAndClearButtons.Any(b => b.IsHovered)) updateBindTarget(); @@ -429,7 +435,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input return; bindTarget.UpdateKeyCombination(InputKey.None); - finalise(false); + finalise(); } private void finalise(bool hasChanged = true) @@ -439,7 +445,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input updateIsDefaultValue(); bindTarget.IsBinding = false; - var args = new KeyBindingUpdatedEventArgs(bindTarget.KeyBinding.Value.ID, bindTarget.KeyBinding.Value.KeyCombinationString); + var args = new KeyBindingUpdatedEventArgs(Action, bindTarget.KeyBinding.Value.ID, bindTarget.KeyBinding.Value.KeyCombination); Schedule(() => { // schedule to ensure we don't instantly get focus back on next OnMouseClick (see AcceptFocus impl.) @@ -489,6 +495,24 @@ namespace osu.Game.Overlays.Settings.Sections.Input isDefault.Value = KeyBindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); } + #region Handling conflicts + + private readonly Bindable keyBindingConflictInfo = new Bindable(); + + public Popover GetPopover() => new KeyBindingConflictPopover + { + ConflictInfo = { BindTarget = keyBindingConflictInfo }, + BindingConflictResolved = BindingConflictResolved + }; + + public void ShowBindingConflictPopover(KeyBindingConflictInfo conflictInfo) + { + keyBindingConflictInfo.Value = conflictInfo; + this.ShowPopover(); + } + + #endregion + private partial class CancelButton : RoundedButton { public CancelButton() diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 57f6e58058..6360877116 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -38,15 +38,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input [BackgroundDependencyLoader] private void load() { - var bindings = realm.Run(r => GetKeyBindings(r).Detach()); + var bindings = getAllBindings(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { int intKey = (int)defaultGroup.Key; - // one row per valid action. var row = CreateKeyBindingRow(defaultGroup.Key, defaultGroup) - .With(row => row.BindingUpdated = onBindingUpdated); + .With(row => + { + row.BindingUpdated = onBindingUpdated; + row.BindingConflictResolved = reloadAllBindings; + }); row.KeyBindings.AddRange(bindings.Where(b => b.ActionInt.Equals(intKey))); Add(row); } @@ -59,6 +62,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected abstract IEnumerable GetKeyBindings(Realm realm); + private List getAllBindings() => realm.Run(r => GetKeyBindings(r).Detach()); + protected virtual KeyBindingRow CreateKeyBindingRow(object action, IEnumerable defaults) => new KeyBindingRow(action) { @@ -66,9 +71,40 @@ namespace osu.Game.Overlays.Settings.Sections.Input Defaults = defaults.Select(d => d.KeyCombination), }; + private void reloadAllBindings() + { + var bindings = getAllBindings(); + + foreach (var row in Children.OfType()) + { + row.KeyBindings.Clear(); + row.KeyBindings.AddRange(bindings.Where(b => b.ActionInt.Equals((int)row.Action))); + } + } + private void onBindingUpdated(KeyBindingRow sender, KeyBindingRow.KeyBindingUpdatedEventArgs args) { - realm.WriteAsync(r => r.Find(args.KeyBindingID)!.KeyCombinationString = args.KeyCombinationString); + var bindings = getAllBindings(); + var existingBinding = args.KeyCombination.Equals(new KeyCombination(InputKey.None)) + ? null + : bindings.FirstOrDefault(kb => kb.ID != args.KeyBindingID && kb.KeyCombination.Equals(args.KeyCombination)); + + if (existingBinding != null) + { + // `RealmKeyBinding`'s `Action` is just an int, always. + // we need more than that for proper display, so leverage `Defaults` (which have the correct enum-typed object in `Action` inside). + object existingAssignedAction = Defaults.First(binding => (int)binding.Action == existingBinding.ActionInt).Action; + var bindingBeforeUpdate = bindings.Single(binding => binding.ID == args.KeyBindingID); + + sender.ShowBindingConflictPopover( + new KeyBindingConflictInfo( + new ConflictingKeyBinding(existingBinding.ID, existingAssignedAction, existingBinding.KeyCombination, new KeyCombination(InputKey.None)), + new ConflictingKeyBinding(bindingBeforeUpdate.ID, args.Action, args.KeyCombination, bindingBeforeUpdate.KeyCombination))); + + return; + } + + realm.WriteAsync(r => r.Find(args.KeyBindingID)!.KeyCombinationString = args.KeyCombination.ToString()); if (AutoAdvanceTarget) { diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 58c56a5514..2517a58491 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; @@ -106,39 +107,43 @@ namespace osu.Game.Overlays } }; - Add(SectionsContainer = new SettingsSectionsContainer + Add(new PopoverContainer { - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0), - Type = EdgeEffectType.Shadow, - Hollow = true, - Radius = 10 - }, - MaskingSmoothness = 0, RelativeSizeAxes = Axes.Both, - ExpandableHeader = CreateHeader(), - SelectedSection = { BindTarget = CurrentSection }, - FixedHeader = new Container + Child = SectionsContainer = new SettingsSectionsContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding + Masking = true, + EdgeEffect = new EdgeEffectParameters { - Vertical = 20, - Horizontal = CONTENT_MARGINS + Colour = Color4.Black.Opacity(0), + Type = EdgeEffectType.Shadow, + Hollow = true, + Radius = 10 }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Child = searchTextBox = new SeekLimitedSearchTextBox + MaskingSmoothness = 0, + RelativeSizeAxes = Axes.Both, + ExpandableHeader = CreateHeader(), + SelectedSection = { BindTarget = CurrentSection }, + FixedHeader = new Container { RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Vertical = 20, + Horizontal = CONTENT_MARGINS + }, Anchor = Anchor.TopCentre, - } - }, - Footer = CreateFooter().With(f => f.Alpha = 0) + Origin = Anchor.TopCentre, + Child = searchTextBox = new SeekLimitedSearchTextBox + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + } + }, + Footer = CreateFooter().With(f => f.Alpha = 0) + } }); if (showSidebar) From b09252a8f85e22132922b76c67bcc26ab9dbc5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Oct 2023 12:19:07 +0200 Subject: [PATCH 47/83] Fix restore-to-default button for entire section triggering conflicts --- .../Sections/Input/KeyBindingsSubsection.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 6360877116..31c57452a9 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Localisation; @@ -56,7 +55,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input Add(new ResetButton { - Action = () => Children.OfType().ForEach(k => k.RestoreDefaults()) + Action = () => + { + realm.Write(r => + { + // can't use `RestoreDefaults()` for each key binding row here as it might trigger binding conflicts along the way. + foreach (var row in Children.OfType()) + { + foreach (var (currentBinding, defaultBinding) in row.KeyBindings.Zip(row.Defaults)) + r.Find(currentBinding.ID)!.KeyCombinationString = defaultBinding.ToString(); + } + }); + reloadAllBindings(); + } }); } From 5d637520e702de574a10ec79728e051015bf34f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 08:38:51 +0200 Subject: [PATCH 48/83] Add test coverage for key binding conflict resolution --- .../Settings/TestSceneKeyBindingPanel.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 5a001ca127..fcbb16bef6 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -10,8 +10,11 @@ using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; +using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections.Input; +using osu.Game.Rulesets.Taiko; using osuTK.Input; namespace osu.Game.Tests.Visual.Settings @@ -288,6 +291,106 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("all reset section bindings buttons shown", () => panel.ChildrenOfType().All(button => button.Alpha == 1)); } + [Test] + public void TestBindingConflictResolvedByRollback() + { + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); + scrollToAndStartBinding("Left (rim)"); + AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left)); + + KeyBindingConflictPopover popover = null; + AddUntilStep("wait for popover", () => popover = panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + AddStep("click first button", () => popover.ChildrenOfType().First().TriggerClick()); + checkBinding("Left (centre)", "M1"); + checkBinding("Left (rim)", "M2"); + } + + [Test] + public void TestBindingConflictResolvedByOverwrite() + { + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); + scrollToAndStartBinding("Left (rim)"); + AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left)); + + KeyBindingConflictPopover popover = null; + AddUntilStep("wait for popover", () => popover = panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + AddStep("click second button", () => popover.ChildrenOfType().ElementAt(1).TriggerClick()); + checkBinding("Left (centre)", string.Empty); + checkBinding("Left (rim)", "M1"); + } + + [Test] + public void TestBindingConflictCausedByResetToDefaultOfSingleRow() + { + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); + scrollToAndStartBinding("Left (centre)"); + AddStep("clear binding", () => + { + var row = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == "Left (centre)")); + row.ChildrenOfType().Single().TriggerClick(); + }); + scrollToAndStartBinding("Left (rim)"); + AddStep("bind M1", () => InputManager.Click(MouseButton.Left)); + + AddStep("reset Left (centre) to default", () => + { + var row = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == "Left (centre)")); + row.ChildrenOfType>().Single().TriggerClick(); + }); + + KeyBindingConflictPopover popover = null; + AddUntilStep("wait for popover", () => popover = panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + AddStep("click second button", () => popover.ChildrenOfType().ElementAt(1).TriggerClick()); + checkBinding("Left (centre)", "M1"); + checkBinding("Left (rim)", string.Empty); + } + + [Test] + public void TestResettingEntireSectionDoesNotCauseBindingConflicts() + { + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); + scrollToAndStartBinding("Left (centre)"); + AddStep("clear binding", () => + { + var row = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == "Left (centre)")); + row.ChildrenOfType().Single().TriggerClick(); + }); + scrollToAndStartBinding("Left (rim)"); + AddStep("bind M1", () => InputManager.Click(MouseButton.Left)); + + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddWaitStep("wait a bit", 3); + AddUntilStep("conflict popover not shown", () => panel.ChildrenOfType().SingleOrDefault(), () => Is.Null); + } + private void checkBinding(string name, string keyName) { AddAssert($"Check {name} is bound to {keyName}", () => From 9468371556fbf59175229f3e47110665a5ce5e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Oct 2023 13:05:50 +0200 Subject: [PATCH 49/83] Fix min width not really working as intended --- .../Settings/Sections/Input/KeyBindingConflictPopover.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 4206380dfe..9f5766f29d 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -185,7 +185,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input ColumnDimensions = new[] { new Dimension(), - new Dimension(GridSizeMode.AutoSize, minSize: 80), + new Dimension(GridSizeMode.AutoSize), }, Content = new[] { @@ -198,20 +198,20 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, new Container { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Both, CornerRadius = 5, Masking = true, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, X = -5, - Children = new Drawable[] + Children = new[] { new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background6 }, + Empty().With(d => d.Width = 80), // poor man's min-width newBindingText = new OsuSpriteText { Font = OsuFont.Numeric.With(size: 10), From 991e425b9ab8fa82985c95ccc5986d81dee6b166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Oct 2023 16:30:22 +0200 Subject: [PATCH 50/83] Refactor conflict handling flow to be less back-and-forth --- .../Settings/Sections/Input/KeyBindingRow.cs | 54 ++++++++++++++----- .../Sections/Input/KeyBindingsSubsection.cs | 33 ++++-------- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index c8d4f339da..ef54cecf0a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -20,6 +20,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -27,6 +28,7 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -42,9 +44,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input public delegate void KeyBindingUpdated(KeyBindingRow sender, KeyBindingUpdatedEventArgs args); - public record KeyBindingUpdatedEventArgs(object Action, Guid KeyBindingID, KeyCombination KeyCombination); + public record KeyBindingUpdatedEventArgs(bool BindingConflictResolved, bool CanAdvanceToNextBinding); - public Action? BindingConflictResolved { get; set; } + public Func> GetAllSectionBindings { get; set; } = null!; /// /// Whether left and right mouse button clicks should be included in the edited bindings. @@ -85,6 +87,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input private Bindable isDefault { get; } = new BindableBool(true); + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + [Resolved] private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; @@ -111,7 +119,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// The action that this row contains bindings for. public KeyBindingRow(object action) { - this.Action = action; + Action = action; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -225,8 +233,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input var button = buttons[i++]; button.UpdateKeyCombination(d); - var args = new KeyBindingUpdatedEventArgs(Action, button.KeyBinding.Value.ID, button.KeyBinding.Value.KeyCombination); - BindingUpdated?.Invoke(this, args); + tryPersistKeyBinding(button.KeyBinding.Value, advanceToNextBinding: false); } isDefault.Value = true; @@ -286,7 +293,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Debug.Assert(bindTarget != null); if (bindTarget.IsHovered) - finalise(); + finalise(false); // prevent updating bind target before clear button's action else if (!cancelAndClearButtons.Any(b => b.IsHovered)) updateBindTarget(); @@ -435,23 +442,22 @@ namespace osu.Game.Overlays.Settings.Sections.Input return; bindTarget.UpdateKeyCombination(InputKey.None); - finalise(); + finalise(false); } - private void finalise(bool hasChanged = true) + private void finalise(bool advanceToNextBinding = true) { if (bindTarget != null) { updateIsDefaultValue(); bindTarget.IsBinding = false; - var args = new KeyBindingUpdatedEventArgs(Action, bindTarget.KeyBinding.Value.ID, bindTarget.KeyBinding.Value.KeyCombination); + var bindingToPersist = bindTarget.KeyBinding.Value; Schedule(() => { // schedule to ensure we don't instantly get focus back on next OnMouseClick (see AcceptFocus impl.) bindTarget = null; - if (hasChanged) - BindingUpdated?.Invoke(this, args); + tryPersistKeyBinding(bindingToPersist, advanceToNextBinding); }); } @@ -462,6 +468,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y; } + private void tryPersistKeyBinding(RealmKeyBinding keyBinding, bool advanceToNextBinding) + { + var bindings = GetAllSectionBindings(); + var existingBinding = keyBinding.KeyCombination.Equals(new KeyCombination(InputKey.None)) + ? null + : bindings.FirstOrDefault(other => other.ID != keyBinding.ID && other.KeyCombination.Equals(keyBinding.KeyCombination)); + + if (existingBinding == null) + { + realm.WriteAsync(r => r.Find(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString()); + BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(BindingConflictResolved: false, advanceToNextBinding)); + return; + } + + var keyBindingBeforeUpdate = bindings.Single(other => other.ID == keyBinding.ID); + + showBindingConflictPopover( + new KeyBindingConflictInfo( + new ConflictingKeyBinding(existingBinding.ID, existingBinding.GetAction(rulesets), existingBinding.KeyCombination, new KeyCombination(InputKey.None)), + new ConflictingKeyBinding(keyBindingBeforeUpdate.ID, Action, keyBinding.KeyCombination, keyBindingBeforeUpdate.KeyCombination))); + } + protected override void OnFocus(FocusEvent e) { content.AutoSizeDuration = 500; @@ -502,10 +530,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input public Popover GetPopover() => new KeyBindingConflictPopover { ConflictInfo = { BindTarget = keyBindingConflictInfo }, - BindingConflictResolved = BindingConflictResolved + BindingConflictResolved = () => BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(BindingConflictResolved: true, CanAdvanceToNextBinding: false)) }; - public void ShowBindingConflictPopover(KeyBindingConflictInfo conflictInfo) + private void showBindingConflictPopover(KeyBindingConflictInfo conflictInfo) { keyBindingConflictInfo.Value = conflictInfo; this.ShowPopover(); diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 31c57452a9..dd0a88bfb1 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input .With(row => { row.BindingUpdated = onBindingUpdated; - row.BindingConflictResolved = reloadAllBindings; + row.GetAllSectionBindings = getAllBindings; }); row.KeyBindings.AddRange(bindings.Where(b => b.ActionInt.Equals(intKey))); Add(row); @@ -73,7 +73,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected abstract IEnumerable GetKeyBindings(Realm realm); - private List getAllBindings() => realm.Run(r => GetKeyBindings(r).Detach()); + private List getAllBindings() => realm.Run(r => + { + r.Refresh(); + return GetKeyBindings(r).Detach(); + }); protected virtual KeyBindingRow CreateKeyBindingRow(object action, IEnumerable defaults) => new KeyBindingRow(action) @@ -95,29 +99,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void onBindingUpdated(KeyBindingRow sender, KeyBindingRow.KeyBindingUpdatedEventArgs args) { - var bindings = getAllBindings(); - var existingBinding = args.KeyCombination.Equals(new KeyCombination(InputKey.None)) - ? null - : bindings.FirstOrDefault(kb => kb.ID != args.KeyBindingID && kb.KeyCombination.Equals(args.KeyCombination)); + if (args.BindingConflictResolved) + reloadAllBindings(); - if (existingBinding != null) - { - // `RealmKeyBinding`'s `Action` is just an int, always. - // we need more than that for proper display, so leverage `Defaults` (which have the correct enum-typed object in `Action` inside). - object existingAssignedAction = Defaults.First(binding => (int)binding.Action == existingBinding.ActionInt).Action; - var bindingBeforeUpdate = bindings.Single(binding => binding.ID == args.KeyBindingID); - - sender.ShowBindingConflictPopover( - new KeyBindingConflictInfo( - new ConflictingKeyBinding(existingBinding.ID, existingAssignedAction, existingBinding.KeyCombination, new KeyCombination(InputKey.None)), - new ConflictingKeyBinding(bindingBeforeUpdate.ID, args.Action, args.KeyCombination, bindingBeforeUpdate.KeyCombination))); - - return; - } - - realm.WriteAsync(r => r.Find(args.KeyBindingID)!.KeyCombinationString = args.KeyCombination.ToString()); - - if (AutoAdvanceTarget) + if (AutoAdvanceTarget && args.CanAdvanceToNextBinding) { var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault(); if (next != null) From 08bdea0036397bd2618aa9e8177c13483989c947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Oct 2023 19:04:47 +0200 Subject: [PATCH 51/83] Reformat code --- .../Sections/Input/KeyBindingConflictPopover.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 9f5766f29d..9634f30451 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -55,8 +55,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input Text = "The binding you've selected conflicts with another existing binding.", Margin = new MarginPadding { Bottom = 10 } }, - existingPreview = new ConflictingKeyBindingPreview(ConflictInfo.Value.Existing.Action, ConflictInfo.Value.Existing.CombinationWhenChosen, ConflictInfo.Value.Existing.CombinationWhenNotChosen), - newPreview = new ConflictingKeyBindingPreview(ConflictInfo.Value.New.Action, ConflictInfo.Value.New.CombinationWhenChosen, ConflictInfo.Value.New.CombinationWhenNotChosen), + existingPreview = new ConflictingKeyBindingPreview( + ConflictInfo.Value.Existing.Action, + ConflictInfo.Value.Existing.CombinationWhenChosen, + ConflictInfo.Value.Existing.CombinationWhenNotChosen), + newPreview = new ConflictingKeyBindingPreview( + ConflictInfo.Value.New.Action, + ConflictInfo.Value.New.CombinationWhenChosen, + ConflictInfo.Value.New.CombinationWhenNotChosen), new Container { RelativeSizeAxes = Axes.X, From f1c1ffbdfd0c830ca3610863f89385f748400429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Oct 2023 19:10:33 +0200 Subject: [PATCH 52/83] Add localisation support --- osu.Game/Localisation/InputSettingsStrings.cs | 20 +++++++++++++++++ .../Input/KeyBindingConflictPopover.cs | 22 ++++++++++--------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs index 2c9b175dfb..834609053b 100644 --- a/osu.Game/Localisation/InputSettingsStrings.cs +++ b/osu.Game/Localisation/InputSettingsStrings.cs @@ -64,6 +64,26 @@ namespace osu.Game.Localisation /// public static LocalisableString KeyBindingPanelDescription => new TranslatableString(getKey(@"key_binding_panel_description"), @"Customise your keys!"); + /// + /// "The binding you've selected conflicts with another existing binding." + /// + public static LocalisableString KeyBindingConflictDetected => new TranslatableString(getKey(@"key_binding_conflict_detected"), @"The binding you've selected conflicts with another existing binding."); + + /// + /// "Keep existing" + /// + public static LocalisableString KeepExisting => new TranslatableString(getKey(@"keep_existing"), @"Keep existing"); + + /// + /// "Apply new" + /// + public static LocalisableString ApplyNew => new TranslatableString(getKey(@"apply_new"), @"Apply new"); + + /// + /// "(none)" + /// + public static LocalisableString EmptyBinding => new TranslatableString(getKey(@"none"), @"(none)"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 9634f30451..8f836bd9c6 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -18,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Input { @@ -52,7 +54,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = "The binding you've selected conflicts with another existing binding.", + Text = InputSettingsStrings.KeyBindingConflictDetected, Margin = new MarginPadding { Bottom = 10 } }, existingPreview = new ConflictingKeyBindingPreview( @@ -72,7 +74,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { keepExistingButton = new HoverableRoundedButton { - Text = "Keep existing", + Text = InputSettingsStrings.KeepExisting, RelativeSizeAxes = Axes.X, Width = 0.48f, Anchor = Anchor.CentreLeft, @@ -81,7 +83,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, applyNewButton = new HoverableRoundedButton { - Text = "Apply new", + Text = InputSettingsStrings.ApplyNew, RelativeSizeAxes = Axes.X, Width = 0.48f, Anchor = Anchor.CentreRight, @@ -243,30 +245,30 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateState() { - string newBinding; + LocalisableString keyCombinationText; switch (IsChosen.Value) { case true: - newBinding = keyCombinationProvider.GetReadableString(combinationWhenChosen); + keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenChosen); newBindingText.Colour = colours.Green1; break; case false: - newBinding = keyCombinationProvider.GetReadableString(combinationWhenNotChosen); + keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenNotChosen); newBindingText.Colour = colours.Red1; break; case null: - newBinding = keyCombinationProvider.GetReadableString(combinationWhenChosen); + keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenChosen); newBindingText.Colour = Colour4.White; break; } - if (string.IsNullOrEmpty(newBinding)) - newBinding = "(none)"; + if (LocalisableString.IsNullOrEmpty(keyCombinationText)) + keyCombinationText = InputSettingsStrings.EmptyBinding; - newBindingText.Text = newBinding; + newBindingText.Text = keyCombinationText; } } From f083309e64e1901ee7167920adf498ed5b2c4896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Oct 2023 19:27:19 +0200 Subject: [PATCH 53/83] Move type back next to exposing class --- .../TestSceneKeyBindingConflictPopover.cs | 6 +++--- .../Sections/Input/KeyBindingConflictInfo.cs | 15 --------------- .../Sections/Input/KeyBindingConflictPopover.cs | 2 +- .../Settings/Sections/Input/KeyBindingRow.cs | 7 +++++++ 4 files changed, 11 insertions(+), 19 deletions(-) delete mode 100644 osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictInfo.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs index 7251350616..da5a9dd513 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs @@ -56,9 +56,9 @@ namespace osu.Game.Tests.Visual.Settings { ConflictInfo = { - Value = new KeyBindingConflictInfo( - new ConflictingKeyBinding(Guid.NewGuid(), OsuAction.LeftButton, KeyCombination.FromKey(Key.X), new KeyCombination(InputKey.None)), - new ConflictingKeyBinding(Guid.NewGuid(), OsuAction.RightButton, KeyCombination.FromKey(Key.Z), KeyCombination.FromKey(Key.X))) + Value = new KeyBindingRow.KeyBindingConflictInfo( + new KeyBindingRow.ConflictingKeyBinding(Guid.NewGuid(), OsuAction.LeftButton, KeyCombination.FromKey(Key.X), new KeyCombination(InputKey.None)), + new KeyBindingRow.ConflictingKeyBinding(Guid.NewGuid(), OsuAction.RightButton, KeyCombination.FromKey(Key.Z), KeyCombination.FromKey(Key.X))) } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictInfo.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictInfo.cs deleted file mode 100644 index ea127456af..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictInfo.cs +++ /dev/null @@ -1,15 +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.Input.Bindings; - -namespace osu.Game.Overlays.Settings.Sections.Input -{ - /// - /// Contains information about the key binding conflict to be resolved. - /// - public record KeyBindingConflictInfo(ConflictingKeyBinding Existing, ConflictingKeyBinding New); - - public record ConflictingKeyBinding(Guid ID, object Action, KeyCombination CombinationWhenChosen, KeyCombination CombinationWhenNotChosen); -} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 8f836bd9c6..a7e871d508 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public partial class KeyBindingConflictPopover : OsuPopover { - public Bindable ConflictInfo { get; } = new Bindable(); + public Bindable ConflictInfo { get; } = new Bindable(); public Action? BindingConflictResolved { get; init; } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index ef54cecf0a..8c645996c8 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -539,6 +539,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input this.ShowPopover(); } + /// + /// Contains information about the key binding conflict to be resolved. + /// + public record KeyBindingConflictInfo(ConflictingKeyBinding Existing, ConflictingKeyBinding New); + + public record ConflictingKeyBinding(Guid ID, object Action, KeyCombination CombinationWhenChosen, KeyCombination CombinationWhenNotChosen); + #endregion private partial class CancelButton : RoundedButton From 6c8490bc7ef47b521d3c90c59573c82ecc2f18bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Oct 2023 20:39:04 +0200 Subject: [PATCH 54/83] Revert changes to `LoungeSubScreen.Join()` - `virtual` modifier was used in mocking. - The spacing change revert is just mostly to keep it out of the final diff. --- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 04ea59621d..fc4a5357c6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -297,29 +297,26 @@ namespace osu.Game.Screens.OnlinePlay.Lounge popoverContainer.HidePopover(); } - public void Join(Room room, string password, Action onSuccess = null, Action onFailure = null) + public virtual void Join(Room room, string password, Action onSuccess = null, Action onFailure = null) => Schedule(() => { - Schedule(() => + if (joiningRoomOperation != null) + return; + + joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); + + RoomManager?.JoinRoom(room, password, _ => { - if (joiningRoomOperation != null) - return; - - joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - - RoomManager?.JoinRoom(room, password, _ => - { - Open(room); - joiningRoomOperation?.Dispose(); - joiningRoomOperation = null; - onSuccess?.Invoke(room); - }, error => - { - joiningRoomOperation?.Dispose(); - joiningRoomOperation = null; - onFailure?.Invoke(error); - }); + Open(room); + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; + onSuccess?.Invoke(room); + }, error => + { + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; + onFailure?.Invoke(error); }); - } + }); /// /// Copies a room and opens it as a fresh (not-yet-created) one. From 17df2fdf0104d8433be92f48384e783a32fcd03c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 15:42:49 +0900 Subject: [PATCH 55/83] Restrict `followpoint` size See https://github.com/ppy/osu/issues/24940#issuecomment-1760980461. --- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 88a4e17120..f06cad7061 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy switch (osuComponent.Component) { case OsuSkinComponents.FollowPoint: - return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false); + return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); case OsuSkinComponents.SliderScorePoint: return this.GetAnimation("sliderscorepoint", false, false); From 0b64852181f219f05529d90d6c0c08807c81fa80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 15:43:18 +0900 Subject: [PATCH 56/83] Limit `sliderscorepoint` to hitcicle dimensions You'd never use anything above this unless crazy. --- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index f06cad7061..3002cbe0ae 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); case OsuSkinComponents.SliderScorePoint: - return this.GetAnimation("sliderscorepoint", false, false); + return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS); case OsuSkinComponents.SliderFollowCircle: var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE); From 61b8d035fe938ef274694357ac15f0e0774b81d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 09:08:13 +0200 Subject: [PATCH 57/83] Use dangerous colour for "apply new" button --- .../Settings/Sections/Input/KeyBindingConflictPopover.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index a7e871d508..297d8fab63 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -37,6 +37,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input [Resolved] private RealmAccess realm { get; set; } = null!; + [Resolved] + private OsuColour colours { get; set; } = null!; + [BackgroundDependencyLoader] private void load() => recreateDisplay(); @@ -84,6 +87,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input applyNewButton = new HoverableRoundedButton { Text = InputSettingsStrings.ApplyNew, + BackgroundColour = colours.Pink3, RelativeSizeAxes = Axes.X, Width = 0.48f, Anchor = Anchor.CentreRight, From 79cec7707d46b4381c59e5d51d5d258f779d45a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 09:56:46 +0200 Subject: [PATCH 58/83] Privatise setter --- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 88bbff6b92..f652e88f5a 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay // while leases may be taken out by a subscreen. public override bool DisallowExternalBeatmapRulesetChanges => true; - protected LoungeSubScreen Lounge; + protected LoungeSubScreen Lounge { get; private set; } private MultiplayerWaveContainer waves; private ScreenStack screenStack; From e04a57d67f68db5b45a9ab5767bc0ed57f0b1a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 10:31:00 +0200 Subject: [PATCH 59/83] Use less dodgy method of specifying allowable notification types --- osu.Game/Overlays/NotificationOverlay.cs | 5 +++-- osu.Game/Overlays/Notifications/NotificationSection.cs | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index dd4924d07a..81233b4343 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -114,7 +114,7 @@ namespace osu.Game.Overlays Children = new[] { // The main section adds as a catch-all for notifications which don't group into other sections. - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(Notification) }), + new NotificationSection(AccountsStrings.NotificationsTitle), new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }), } } @@ -206,7 +206,8 @@ namespace osu.Game.Overlays var ourType = notification.GetType(); int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; - var section = sections.Children.Last(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); + var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes?.Any(accept => accept.IsAssignableFrom(ourType)) == true) + ?? sections.First(); section.Add(notification, depth); diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 10c2900d63..895f13ee9c 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -37,13 +37,17 @@ namespace osu.Game.Overlays.Notifications notifications.Insert((int)position, notification); } - public IEnumerable AcceptedNotificationTypes { get; } + /// + /// Enumerable of notification types accepted in this section. + /// If , the section accepts any and all notifications. + /// + public IEnumerable? AcceptedNotificationTypes { get; } private readonly LocalisableString titleText; - public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes) + public NotificationSection(LocalisableString title, IEnumerable? acceptedNotificationTypes = null) { - AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); + AcceptedNotificationTypes = acceptedNotificationTypes?.ToArray(); titleText = title; } From d7a06059f201420da2cd0b223f0a0a8aa33a2384 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 17:49:48 +0900 Subject: [PATCH 60/83] Rename string properties to give more hinting to localisers --- osu.Game/Localisation/InputSettingsStrings.cs | 6 +++--- .../Settings/Sections/Input/KeyBindingConflictPopover.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs index 834609053b..a13a9dc498 100644 --- a/osu.Game/Localisation/InputSettingsStrings.cs +++ b/osu.Game/Localisation/InputSettingsStrings.cs @@ -72,17 +72,17 @@ namespace osu.Game.Localisation /// /// "Keep existing" /// - public static LocalisableString KeepExisting => new TranslatableString(getKey(@"keep_existing"), @"Keep existing"); + public static LocalisableString KeepExistingBinding => new TranslatableString(getKey(@"keep_existing"), @"Keep existing"); /// /// "Apply new" /// - public static LocalisableString ApplyNew => new TranslatableString(getKey(@"apply_new"), @"Apply new"); + public static LocalisableString ApplyNewBinding => new TranslatableString(getKey(@"apply_new"), @"Apply new"); /// /// "(none)" /// - public static LocalisableString EmptyBinding => new TranslatableString(getKey(@"none"), @"(none)"); + public static LocalisableString ActionHasNoKeyBinding => new TranslatableString(getKey(@"none"), @"(none)"); private static string getKey(string key) => $"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 297d8fab63..a0f6192587 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { keepExistingButton = new HoverableRoundedButton { - Text = InputSettingsStrings.KeepExisting, + Text = InputSettingsStrings.KeepExistingBinding, RelativeSizeAxes = Axes.X, Width = 0.48f, Anchor = Anchor.CentreLeft, @@ -86,7 +86,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, applyNewButton = new HoverableRoundedButton { - Text = InputSettingsStrings.ApplyNew, + Text = InputSettingsStrings.ApplyNewBinding, BackgroundColour = colours.Pink3, RelativeSizeAxes = Axes.X, Width = 0.48f, @@ -270,7 +270,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } if (LocalisableString.IsNullOrEmpty(keyCombinationText)) - keyCombinationText = InputSettingsStrings.EmptyBinding; + keyCombinationText = InputSettingsStrings.ActionHasNoKeyBinding; newBindingText.Text = keyCombinationText; } From 8e609b6f1dbfad804eca91394d5bccfcf20aaa75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 18:18:06 +0900 Subject: [PATCH 61/83] Switch records to classes for sanity I don't have anything against records except for the capitalisation when including the paramter names in a constructor. ie. `new Record(This: 1);` --- .../Settings/Sections/Input/KeyBindingRow.cs | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 8c645996c8..c7791f4412 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -44,8 +44,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input public delegate void KeyBindingUpdated(KeyBindingRow sender, KeyBindingUpdatedEventArgs args); - public record KeyBindingUpdatedEventArgs(bool BindingConflictResolved, bool CanAdvanceToNextBinding); - public Func> GetAllSectionBindings { get; set; } = null!; /// @@ -470,15 +468,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void tryPersistKeyBinding(RealmKeyBinding keyBinding, bool advanceToNextBinding) { - var bindings = GetAllSectionBindings(); - var existingBinding = keyBinding.KeyCombination.Equals(new KeyCombination(InputKey.None)) + List bindings = GetAllSectionBindings(); + RealmKeyBinding? existingBinding = keyBinding.KeyCombination.Equals(new KeyCombination(InputKey.None)) ? null : bindings.FirstOrDefault(other => other.ID != keyBinding.ID && other.KeyCombination.Equals(keyBinding.KeyCombination)); if (existingBinding == null) { realm.WriteAsync(r => r.Find(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString()); - BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(BindingConflictResolved: false, advanceToNextBinding)); + BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: false, advanceToNextBinding)); return; } @@ -530,7 +528,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input public Popover GetPopover() => new KeyBindingConflictPopover { ConflictInfo = { BindTarget = keyBindingConflictInfo }, - BindingConflictResolved = () => BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(BindingConflictResolved: true, CanAdvanceToNextBinding: false)) + BindingConflictResolved = () => BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: true, canAdvanceToNextBinding: false)) }; private void showBindingConflictPopover(KeyBindingConflictInfo conflictInfo) @@ -542,9 +540,48 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// /// Contains information about the key binding conflict to be resolved. /// - public record KeyBindingConflictInfo(ConflictingKeyBinding Existing, ConflictingKeyBinding New); + public class KeyBindingConflictInfo + { + public ConflictingKeyBinding Existing { get; } + public ConflictingKeyBinding New { get; } - public record ConflictingKeyBinding(Guid ID, object Action, KeyCombination CombinationWhenChosen, KeyCombination CombinationWhenNotChosen); + /// + /// Contains information about the key binding conflict to be resolved. + /// + public KeyBindingConflictInfo(ConflictingKeyBinding existingBinding, ConflictingKeyBinding newBinding) + { + Existing = existingBinding; + New = newBinding; + } + } + + public class ConflictingKeyBinding + { + public Guid ID { get; } + public object Action { get; } + public KeyCombination CombinationWhenChosen { get; } + public KeyCombination CombinationWhenNotChosen { get; } + + public ConflictingKeyBinding(Guid id, object action, KeyCombination combinationWhenChosen, KeyCombination combinationWhenNotChosen) + { + ID = id; + Action = action; + CombinationWhenChosen = combinationWhenChosen; + CombinationWhenNotChosen = combinationWhenNotChosen; + } + } + + public class KeyBindingUpdatedEventArgs + { + public bool BindingConflictResolved { get; } + public bool CanAdvanceToNextBinding { get; } + + public KeyBindingUpdatedEventArgs(bool bindingConflictResolved, bool canAdvanceToNextBinding) + { + BindingConflictResolved = bindingConflictResolved; + CanAdvanceToNextBinding = canAdvanceToNextBinding; + } + } #endregion From 9b0c4acdefdcd47c0e98f55aa18d5a00bf272cb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 18:29:06 +0900 Subject: [PATCH 62/83] Split `KeyBindingRow` out into partial pieces --- .../Input/KeyBindingRow.ConflictResolution.cs | 75 +++++ .../Sections/Input/KeyBindingRow.KeyButton.cs | 168 +++++++++++ .../Settings/Sections/Input/KeyBindingRow.cs | 278 +++--------------- 3 files changed, 276 insertions(+), 245 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs new file mode 100644 index 0000000000..8afa43b1ff --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs @@ -0,0 +1,75 @@ +// 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.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class KeyBindingRow : IHasPopover + { + private readonly Bindable keyBindingConflictInfo = new Bindable(); + + public Popover GetPopover() => new KeyBindingConflictPopover + { + ConflictInfo = { BindTarget = keyBindingConflictInfo }, + BindingConflictResolved = () => BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: true, canAdvanceToNextBinding: false)) + }; + + private void showBindingConflictPopover(KeyBindingConflictInfo conflictInfo) + { + keyBindingConflictInfo.Value = conflictInfo; + this.ShowPopover(); + } + + /// + /// Contains information about the key binding conflict to be resolved. + /// + public class KeyBindingConflictInfo + { + public ConflictingKeyBinding Existing { get; } + public ConflictingKeyBinding New { get; } + + /// + /// Contains information about the key binding conflict to be resolved. + /// + public KeyBindingConflictInfo(ConflictingKeyBinding existingBinding, ConflictingKeyBinding newBinding) + { + Existing = existingBinding; + New = newBinding; + } + } + + public class ConflictingKeyBinding + { + public Guid ID { get; } + public object Action { get; } + public KeyCombination CombinationWhenChosen { get; } + public KeyCombination CombinationWhenNotChosen { get; } + + public ConflictingKeyBinding(Guid id, object action, KeyCombination combinationWhenChosen, KeyCombination combinationWhenNotChosen) + { + ID = id; + Action = action; + CombinationWhenChosen = combinationWhenChosen; + CombinationWhenNotChosen = combinationWhenNotChosen; + } + } + + public class KeyBindingUpdatedEventArgs + { + public bool BindingConflictResolved { get; } + public bool CanAdvanceToNextBinding { get; } + + public KeyBindingUpdatedEventArgs(bool bindingConflictResolved, bool canAdvanceToNextBinding) + { + BindingConflictResolved = bindingConflictResolved; + CanAdvanceToNextBinding = canAdvanceToNextBinding; + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs new file mode 100644 index 0000000000..cf26300fb6 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs @@ -0,0 +1,168 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class KeyBindingRow + { + public partial class KeyButton : Container + { + public Bindable KeyBinding { get; } = new Bindable(); + + private readonly Box box; + public readonly OsuSpriteText Text; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; + + private bool isBinding; + + public bool IsBinding + { + get => isBinding; + set + { + if (value == isBinding) return; + + isBinding = value; + + updateHoverState(); + } + } + + public KeyButton() + { + Margin = new MarginPadding(padding); + + Masking = true; + CornerRadius = padding; + + Height = height; + AutoSizeAxes = Axes.X; + + Children = new Drawable[] + { + new Container + { + AlwaysPresent = true, + Width = 80, + Height = height, + }, + box = new Box + { + RelativeSizeAxes = Axes.Both, + }, + Text = new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: 10), + Margin = new MarginPadding(5), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new HoverSounds() + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + KeyBinding.BindValueChanged(_ => + { + if (KeyBinding.Value.IsManaged) + throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(KeyBinding)); + + updateKeyCombinationText(); + }); + keyCombinationProvider.KeymapChanged += updateKeyCombinationText; + updateKeyCombinationText(); + } + + [BackgroundDependencyLoader] + private void load() + { + updateHoverState(); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateHoverState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHoverState(); + base.OnHoverLost(e); + } + + private void updateHoverState() + { + if (isBinding) + { + box.FadeColour(colourProvider.Light2, transition_time, Easing.OutQuint); + Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint); + } + else + { + box.FadeColour(IsHovered ? colourProvider.Light4 : colourProvider.Background6, transition_time, Easing.OutQuint); + Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint); + } + } + + /// + /// Update from a key combination, only allowing a single non-modifier key to be specified. + /// + /// A generated from the full input state. + /// The key which triggered this update, and should be used as the binding. + public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) => + UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey))); + + public void UpdateKeyCombination(KeyCombination newCombination) + { + if (KeyBinding.Value.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) + return; + + KeyBinding.Value.KeyCombination = newCombination; + updateKeyCombinationText(); + } + + private void updateKeyCombinationText() + { + Scheduler.AddOnce(updateText); + + void updateText() => Text.Text = keyCombinationProvider.GetReadableString(KeyBinding.Value.KeyCombination); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (keyCombinationProvider.IsNotNull()) + keyCombinationProvider.KeymapChanged -= updateKeyCombinationText; + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index c7791f4412..7a5a269eb3 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -9,33 +9,27 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; 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.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Database; -using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Overlays.Settings.Sections.Input { - public partial class KeyBindingRow : Container, IFilterable, IHasPopover + public partial class KeyBindingRow : Container, IFilterable { /// /// Invoked when the binding of this row is updated with a change being written. @@ -210,18 +204,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, true); } - private void updateButtons() - { - if (buttons.Count > KeyBindings.Count) - buttons.RemoveRange(buttons.Skip(KeyBindings.Count).ToArray(), true); - - while (buttons.Count < KeyBindings.Count) - buttons.Add(new KeyButton()); - - foreach (var (button, binding) in buttons.Zip(KeyBindings)) - button.KeyBinding.Value = binding; - } - public void RestoreDefaults() { int i = 0; @@ -251,8 +233,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input base.OnHoverLost(e); } - private bool isModifier(Key k) => k < Key.F1; - protected override bool OnClick(ClickEvent e) => true; protected override bool OnMouseDown(MouseDownEvent e) @@ -325,6 +305,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!isModifier(e.Key)) finalise(); return true; + + bool isModifier(Key k) => k < Key.F1; } protected override void OnKeyUp(KeyUpEvent e) @@ -434,6 +416,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input finalise(); } + private void updateButtons() + { + if (buttons.Count > KeyBindings.Count) + buttons.RemoveRange(buttons.Skip(KeyBindings.Count).ToArray(), true); + + while (buttons.Count < KeyBindings.Count) + buttons.Add(new KeyButton()); + + foreach (var (button, binding) in buttons.Zip(KeyBindings)) + button.KeyBinding.Value = binding; + } + private void clear() { if (bindTarget == null) @@ -466,6 +460,24 @@ namespace osu.Game.Overlays.Settings.Sections.Input cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y; } + protected override void OnFocus(FocusEvent e) + { + content.AutoSizeDuration = 500; + content.AutoSizeEasing = Easing.OutQuint; + + cancelAndClearButtons.FadeIn(300, Easing.OutQuint); + cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y; + + updateBindTarget(); + base.OnFocus(e); + } + + protected override void OnFocusLost(FocusLostEvent e) + { + finalise(false); + base.OnFocusLost(e); + } + private void tryPersistKeyBinding(RealmKeyBinding keyBinding, bool advanceToNextBinding) { List bindings = GetAllSectionBindings(); @@ -488,24 +500,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input new ConflictingKeyBinding(keyBindingBeforeUpdate.ID, Action, keyBinding.KeyCombination, keyBindingBeforeUpdate.KeyCombination))); } - protected override void OnFocus(FocusEvent e) - { - content.AutoSizeDuration = 500; - content.AutoSizeEasing = Easing.OutQuint; - - cancelAndClearButtons.FadeIn(300, Easing.OutQuint); - cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y; - - updateBindTarget(); - base.OnFocus(e); - } - - protected override void OnFocusLost(FocusLostEvent e) - { - finalise(false); - base.OnFocusLost(e); - } - /// /// Updates the bind target to the currently hovered key button or the first if clicked anywhere else. /// @@ -521,70 +515,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input isDefault.Value = KeyBindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); } - #region Handling conflicts - - private readonly Bindable keyBindingConflictInfo = new Bindable(); - - public Popover GetPopover() => new KeyBindingConflictPopover - { - ConflictInfo = { BindTarget = keyBindingConflictInfo }, - BindingConflictResolved = () => BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: true, canAdvanceToNextBinding: false)) - }; - - private void showBindingConflictPopover(KeyBindingConflictInfo conflictInfo) - { - keyBindingConflictInfo.Value = conflictInfo; - this.ShowPopover(); - } - - /// - /// Contains information about the key binding conflict to be resolved. - /// - public class KeyBindingConflictInfo - { - public ConflictingKeyBinding Existing { get; } - public ConflictingKeyBinding New { get; } - - /// - /// Contains information about the key binding conflict to be resolved. - /// - public KeyBindingConflictInfo(ConflictingKeyBinding existingBinding, ConflictingKeyBinding newBinding) - { - Existing = existingBinding; - New = newBinding; - } - } - - public class ConflictingKeyBinding - { - public Guid ID { get; } - public object Action { get; } - public KeyCombination CombinationWhenChosen { get; } - public KeyCombination CombinationWhenNotChosen { get; } - - public ConflictingKeyBinding(Guid id, object action, KeyCombination combinationWhenChosen, KeyCombination combinationWhenNotChosen) - { - ID = id; - Action = action; - CombinationWhenChosen = combinationWhenChosen; - CombinationWhenNotChosen = combinationWhenNotChosen; - } - } - - public class KeyBindingUpdatedEventArgs - { - public bool BindingConflictResolved { get; } - public bool CanAdvanceToNextBinding { get; } - - public KeyBindingUpdatedEventArgs(bool bindingConflictResolved, bool canAdvanceToNextBinding) - { - BindingConflictResolved = bindingConflictResolved; - CanAdvanceToNextBinding = canAdvanceToNextBinding; - } - } - - #endregion - private partial class CancelButton : RoundedButton { public CancelButton() @@ -602,147 +532,5 @@ namespace osu.Game.Overlays.Settings.Sections.Input Size = new Vector2(80, 20); } } - - public partial class KeyButton : Container - { - public Bindable KeyBinding { get; } = new Bindable(); - - private readonly Box box; - public readonly OsuSpriteText Text; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - [Resolved] - private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; - - private bool isBinding; - - public bool IsBinding - { - get => isBinding; - set - { - if (value == isBinding) return; - - isBinding = value; - - updateHoverState(); - } - } - - public KeyButton() - { - Margin = new MarginPadding(padding); - - Masking = true; - CornerRadius = padding; - - Height = height; - AutoSizeAxes = Axes.X; - - Children = new Drawable[] - { - new Container - { - AlwaysPresent = true, - Width = 80, - Height = height, - }, - box = new Box - { - RelativeSizeAxes = Axes.Both, - }, - Text = new OsuSpriteText - { - Font = OsuFont.Numeric.With(size: 10), - Margin = new MarginPadding(5), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new HoverSounds() - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - KeyBinding.BindValueChanged(_ => - { - if (KeyBinding.Value.IsManaged) - throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(KeyBinding)); - - updateKeyCombinationText(); - }); - keyCombinationProvider.KeymapChanged += updateKeyCombinationText; - updateKeyCombinationText(); - } - - [BackgroundDependencyLoader] - private void load() - { - updateHoverState(); - FinishTransforms(true); - } - - protected override bool OnHover(HoverEvent e) - { - updateHoverState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateHoverState(); - base.OnHoverLost(e); - } - - private void updateHoverState() - { - if (isBinding) - { - box.FadeColour(colourProvider.Light2, transition_time, Easing.OutQuint); - Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint); - } - else - { - box.FadeColour(IsHovered ? colourProvider.Light4 : colourProvider.Background6, transition_time, Easing.OutQuint); - Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint); - } - } - - /// - /// Update from a key combination, only allowing a single non-modifier key to be specified. - /// - /// A generated from the full input state. - /// The key which triggered this update, and should be used as the binding. - public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) => - UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey))); - - public void UpdateKeyCombination(KeyCombination newCombination) - { - if (KeyBinding.Value.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) - return; - - KeyBinding.Value.KeyCombination = newCombination; - updateKeyCombinationText(); - } - - private void updateKeyCombinationText() - { - Scheduler.AddOnce(updateText); - - void updateText() => Text.Text = keyCombinationProvider.GetReadableString(KeyBinding.Value.KeyCombination); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (keyCombinationProvider.IsNotNull()) - keyCombinationProvider.KeymapChanged -= updateKeyCombinationText; - } - } } } From 17b6ebbfbff78622bb3a5933910d2e73ddadeec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Oct 2023 18:38:49 +0900 Subject: [PATCH 63/83] Avoid using a bindable in `KeyBindingConflictPopover` where data is never mutated --- .../TestSceneKeyBindingConflictPopover.cs | 15 ++++---- .../Input/KeyBindingConflictPopover.cs | 35 ++++++++++--------- .../Input/KeyBindingRow.ConflictResolution.cs | 17 +++++---- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs index da5a9dd513..03f74fa35c 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs @@ -52,15 +52,12 @@ namespace osu.Game.Tests.Visual.Settings Action = this.ShowPopover; } - public Popover GetPopover() => new KeyBindingConflictPopover - { - ConflictInfo = - { - Value = new KeyBindingRow.KeyBindingConflictInfo( - new KeyBindingRow.ConflictingKeyBinding(Guid.NewGuid(), OsuAction.LeftButton, KeyCombination.FromKey(Key.X), new KeyCombination(InputKey.None)), - new KeyBindingRow.ConflictingKeyBinding(Guid.NewGuid(), OsuAction.RightButton, KeyCombination.FromKey(Key.Z), KeyCombination.FromKey(Key.X))) - } - }; + public Popover GetPopover() => new KeyBindingConflictPopover( + new KeyBindingRow.KeyBindingConflictInfo( + new KeyBindingRow.ConflictingKeyBinding(Guid.NewGuid(), OsuAction.LeftButton, KeyCombination.FromKey(Key.X), new KeyCombination(InputKey.None)), + new KeyBindingRow.ConflictingKeyBinding(Guid.NewGuid(), OsuAction.RightButton, KeyCombination.FromKey(Key.Z), KeyCombination.FromKey(Key.X)) + ) + ); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index a0f6192587..610325e412 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -18,15 +18,13 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; -using osuTK; using osu.Game.Localisation; +using osuTK; namespace osu.Game.Overlays.Settings.Sections.Input { public partial class KeyBindingConflictPopover : OsuPopover { - public Bindable ConflictInfo { get; } = new Bindable(); - public Action? BindingConflictResolved { get; init; } private ConflictingKeyBindingPreview newPreview = null!; @@ -40,10 +38,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input [Resolved] private OsuColour colours { get; set; } = null!; - [BackgroundDependencyLoader] - private void load() => recreateDisplay(); + private readonly KeyBindingRow.KeyBindingConflictInfo conflictInfo; - private void recreateDisplay() + public KeyBindingConflictPopover(KeyBindingRow.KeyBindingConflictInfo conflictInfo) + { + this.conflictInfo = conflictInfo; + } + + [BackgroundDependencyLoader] + private void load() { Child = new FillFlowContainer { @@ -61,13 +64,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input Margin = new MarginPadding { Bottom = 10 } }, existingPreview = new ConflictingKeyBindingPreview( - ConflictInfo.Value.Existing.Action, - ConflictInfo.Value.Existing.CombinationWhenChosen, - ConflictInfo.Value.Existing.CombinationWhenNotChosen), + conflictInfo.Existing.Action, + conflictInfo.Existing.CombinationWhenChosen, + conflictInfo.Existing.CombinationWhenNotChosen), newPreview = new ConflictingKeyBindingPreview( - ConflictInfo.Value.New.Action, - ConflictInfo.Value.New.CombinationWhenChosen, - ConflictInfo.Value.New.CombinationWhenNotChosen), + conflictInfo.New.Action, + conflictInfo.New.CombinationWhenChosen, + conflictInfo.New.CombinationWhenNotChosen), new Container { RelativeSizeAxes = Axes.X, @@ -107,11 +110,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input // the temporary visual changes will be reverted by calling `Hide()` / `BindingConflictResolved`. realm.Write(r => { - var existingBinding = r.Find(ConflictInfo.Value.Existing.ID); - existingBinding!.KeyCombinationString = ConflictInfo.Value.Existing.CombinationWhenNotChosen.ToString(); + var existingBinding = r.Find(conflictInfo.Existing.ID); + existingBinding!.KeyCombinationString = conflictInfo.Existing.CombinationWhenNotChosen.ToString(); - var newBinding = r.Find(ConflictInfo.Value.New.ID); - newBinding!.KeyCombinationString = ConflictInfo.Value.Existing.CombinationWhenChosen.ToString(); + var newBinding = r.Find(conflictInfo.New.ID); + newBinding!.KeyCombinationString = conflictInfo.Existing.CombinationWhenChosen.ToString(); }); Hide(); diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs index 8afa43b1ff..b5d482b6b4 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; +using System.Diagnostics; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; @@ -12,17 +12,20 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public partial class KeyBindingRow : IHasPopover { - private readonly Bindable keyBindingConflictInfo = new Bindable(); + private KeyBindingConflictInfo? pendingKeyBindingConflict; - public Popover GetPopover() => new KeyBindingConflictPopover + public Popover GetPopover() { - ConflictInfo = { BindTarget = keyBindingConflictInfo }, - BindingConflictResolved = () => BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: true, canAdvanceToNextBinding: false)) - }; + Debug.Assert(pendingKeyBindingConflict != null); + return new KeyBindingConflictPopover(pendingKeyBindingConflict) + { + BindingConflictResolved = () => BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: true, canAdvanceToNextBinding: false)) + }; + } private void showBindingConflictPopover(KeyBindingConflictInfo conflictInfo) { - keyBindingConflictInfo.Value = conflictInfo; + pendingKeyBindingConflict = conflictInfo; this.ShowPopover(); } From ea766b792d886907dbc9862a45556530bc93629a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 11:58:55 +0200 Subject: [PATCH 64/83] Update localisation keys to match localisable member names --- osu.Game/Localisation/InputSettingsStrings.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs index a13a9dc498..fcfe48bedb 100644 --- a/osu.Game/Localisation/InputSettingsStrings.cs +++ b/osu.Game/Localisation/InputSettingsStrings.cs @@ -72,17 +72,17 @@ namespace osu.Game.Localisation /// /// "Keep existing" /// - public static LocalisableString KeepExistingBinding => new TranslatableString(getKey(@"keep_existing"), @"Keep existing"); + public static LocalisableString KeepExistingBinding => new TranslatableString(getKey(@"keep_existing_binding"), @"Keep existing"); /// /// "Apply new" /// - public static LocalisableString ApplyNewBinding => new TranslatableString(getKey(@"apply_new"), @"Apply new"); + public static LocalisableString ApplyNewBinding => new TranslatableString(getKey(@"apply_new_binding"), @"Apply new"); /// /// "(none)" /// - public static LocalisableString ActionHasNoKeyBinding => new TranslatableString(getKey(@"none"), @"(none)"); + public static LocalisableString ActionHasNoKeyBinding => new TranslatableString(getKey(@"action_has_no_key_binding"), @"(none)"); private static string getKey(string key) => $"{prefix}:{key}"; } From a1c68b66f212c9cfb178bc0b57281a6912f7b430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 12:06:25 +0200 Subject: [PATCH 65/83] Rename `KeyBindingRow` parts to appease CodeFileSanity --- ....ConflictResolution.cs => KeyBindingRow_ConflictResolution.cs} | 0 .../{KeyBindingRow.KeyButton.cs => KeyBindingRow_KeyButton.cs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game/Overlays/Settings/Sections/Input/{KeyBindingRow.ConflictResolution.cs => KeyBindingRow_ConflictResolution.cs} (100%) rename osu.Game/Overlays/Settings/Sections/Input/{KeyBindingRow.KeyButton.cs => KeyBindingRow_KeyButton.cs} (100%) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs similarity index 100% rename from osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs rename to osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs similarity index 100% rename from osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs rename to osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs From a88779e5880f9a02b442ace50faf23c9a6bd4491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 13:10:04 +0200 Subject: [PATCH 66/83] Use dimmed placeholder text instead of empty box on cleared bindings --- .../Sections/Input/KeyBindingRow_KeyButton.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs index cf26300fb6..53d0f50605 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs @@ -12,11 +12,13 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osuTK.Graphics; namespace osu.Game.Overlays.Settings.Sections.Input @@ -153,7 +155,20 @@ namespace osu.Game.Overlays.Settings.Sections.Input { Scheduler.AddOnce(updateText); - void updateText() => Text.Text = keyCombinationProvider.GetReadableString(KeyBinding.Value.KeyCombination); + void updateText() + { + LocalisableString keyCombinationString = keyCombinationProvider.GetReadableString(KeyBinding.Value.KeyCombination); + float alpha = 1; + + if (LocalisableString.IsNullOrEmpty(keyCombinationString)) + { + keyCombinationString = InputSettingsStrings.ActionHasNoKeyBinding; + alpha = 0.4f; + } + + Text.Text = keyCombinationString; + Text.Alpha = alpha; + } } protected override void Dispose(bool isDisposing) From af89d69fc4d405a6d02d6fb442c9d57e8f5045d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 13:16:24 +0200 Subject: [PATCH 67/83] Fix dangerous buttons using different shades of pink --- osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs | 2 +- osu.Game/Graphics/OsuColour.cs | 2 ++ osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs | 2 +- osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs | 2 +- osu.Game/Overlays/Settings/DangerousSettingsButton.cs | 2 +- .../Settings/Sections/Input/KeyBindingConflictPopover.cs | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 9dd028fa79..aa3718150c 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tournament.Screens.Editors new TourneyButton { RelativeSizeAxes = Axes.X, - BackgroundColour = colours.Pink3, + BackgroundColour = colours.DangerousButtonColour, Text = "Clear all", Action = () => { diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index d0e07a9e66..75d313d98c 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -397,5 +397,7 @@ namespace osu.Game.Graphics public Color4 SpotlightColour => Green2; public Color4 FeaturedArtistColour => Blue2; + + public Color4 DangerousButtonColour => Pink3; } } diff --git a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs index b237dd9b71..39ef7924b9 100644 --- a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs +++ b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs @@ -11,7 +11,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = colours.PinkDark; + BackgroundColour = colours.DangerousButtonColour; } } } diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs index 95af8ec0f3..31a56c9748 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs @@ -65,7 +65,7 @@ namespace osu.Game.Overlays.FirstRunSetup { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - BackgroundColour = colours.Pink3, + BackgroundColour = colours.DangerousButtonColour, Text = FirstRunSetupOverlayStrings.ClassicDefaults, RelativeSizeAxes = Axes.X, Action = applyClassic diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs index 42b042ae75..9a9d40cada 100644 --- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs +++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Settings [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = colours.Pink3; + BackgroundColour = colours.DangerousButtonColour; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 610325e412..15061857e1 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -90,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input applyNewButton = new HoverableRoundedButton { Text = InputSettingsStrings.ApplyNewBinding, - BackgroundColour = colours.Pink3, + BackgroundColour = colours.DangerousButtonColour, RelativeSizeAxes = Axes.X, Width = 0.48f, Anchor = Anchor.CentreRight, From d656306692f34dda17b163712b931828d276253a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 14:05:43 +0200 Subject: [PATCH 68/83] Remove approach rate adjustment from `TaikoModHardRock` Approach rate does nothing in taiko. --- osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs index 37a630c8ad..ba41175461 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.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 osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -24,9 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override void ApplyToDifficulty(BeatmapDifficulty difficulty) { base.ApplyToDifficulty(difficulty); - difficulty.SliderMultiplier *= slider_multiplier; - difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); } } } From d97b618d024ce01d17ec1f5121c2811ad7bb3c43 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 13 Oct 2023 21:06:50 +0900 Subject: [PATCH 69/83] Use new `generic-error` sample in appropriate places --- .../Settings/Sections/Input/KeyBindingConflictPopover.cs | 2 ++ osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 610325e412..357ffac458 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -40,6 +40,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly KeyBindingRow.KeyBindingConflictInfo conflictInfo; + protected override string PopInSampleName => @"UI/generic-error"; + public KeyBindingConflictPopover(KeyBindingRow.KeyBindingConflictInfo conflictInfo) { this.conflictInfo = conflictInfo; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 5cf2f91ff4..66bbf92e58 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -239,7 +239,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge } }; - sampleJoinFail = audio.Samples.Get(@"UI/password-fail"); + sampleJoinFail = audio.Samples.Get(@"UI/generic-error"); joinButton.Action = performJoin; } From 9b474b656d40f3b452d5c981ee78461ac2f83cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Oct 2023 15:03:41 +0200 Subject: [PATCH 70/83] Fix key binding panel test failures from introducing placeholder text Whoops. --- .../Visual/Settings/TestSceneKeyBindingPanel.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index fcbb16bef6..1c4e89e1a2 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -11,6 +11,7 @@ using osu.Framework.Threading; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections.Input; @@ -157,7 +158,9 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().First().Text.Text.ToString())); + AddAssert("first binding cleared", + () => multiBindingRow.ChildrenOfType().First().Text.Text, + () => Is.EqualTo(InputSettingsStrings.ActionHasNoKeyBinding)); AddStep("click second binding", () => { @@ -169,7 +172,9 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().ElementAt(1).Text.Text.ToString())); + AddAssert("second binding cleared", + () => multiBindingRow.ChildrenOfType().ElementAt(1).Text.Text, + () => Is.EqualTo(InputSettingsStrings.ActionHasNoKeyBinding)); void clickClearButton() { @@ -327,7 +332,7 @@ namespace osu.Game.Tests.Visual.Settings KeyBindingConflictPopover popover = null; AddUntilStep("wait for popover", () => popover = panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); AddStep("click second button", () => popover.ChildrenOfType().ElementAt(1).TriggerClick()); - checkBinding("Left (centre)", string.Empty); + checkBinding("Left (centre)", InputSettingsStrings.ActionHasNoKeyBinding.ToString()); checkBinding("Left (rim)", "M1"); } @@ -360,7 +365,7 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("wait for popover", () => popover = panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); AddStep("click second button", () => popover.ChildrenOfType().ElementAt(1).TriggerClick()); checkBinding("Left (centre)", "M1"); - checkBinding("Left (rim)", string.Empty); + checkBinding("Left (rim)", InputSettingsStrings.ActionHasNoKeyBinding.ToString()); } [Test] From d4627a240261add2217d10c136935b4142433de9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Oct 2023 03:48:59 +0900 Subject: [PATCH 71/83] 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 f0a4b38c03..c289ae89ee 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 48832c64acedcc144bba60fd8487c9703766c0a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 13:09:22 +0900 Subject: [PATCH 72/83] Fix health bar animating when it shouldn't be It wasn't correctly checking the current underlying health, which could be zero in usages of `AccumulatingHealthProcessor`, for instance. Closes #25046. --- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 2f96233939..fdbce15b40 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.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.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -78,22 +79,25 @@ namespace osu.Game.Screens.Play.HUD if (PlayInitialIncreaseAnimation) startInitialAnimation(); else - Current.Value = 1; + Current.Value = health.Value; } private void startInitialAnimation() { + if (Current.Value >= health.Value) + return; + // TODO: this should run in gameplay time, including showing a larger increase when skipping. // TODO: it should also start increasing relative to the first hitobject. const double increase_delay = 150; initialIncrease = Scheduler.AddDelayed(() => { - double newValue = Current.Value + 0.05f; + double newValue = Math.Min(Current.Value + 0.05f, health.Value); this.TransformBindableTo(Current, newValue, increase_delay); Scheduler.AddOnce(Flash); - if (newValue >= 1) + if (newValue >= health.Value) finishInitialAnimation(); }, increase_delay, true); } From 36112302d671bae3df0477346caec9960ddde936 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 15:51:18 +0900 Subject: [PATCH 73/83] Remove drag handles from manage collections dialog for now The realm implementation doesn't support this. --- osu.Game/Collections/DrawableCollectionListItem.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 4131148f3f..596bb5d673 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -36,7 +36,13 @@ namespace osu.Game.Collections public DrawableCollectionListItem(Live item, bool isCreated) : base(item) { - ShowDragHandle.Value = item.IsManaged; + // For now we don't support rearranging and always use alphabetical sort. + // Change this to: + // + // ShowDragHandle.Value = item.IsManaged; + // + // if we want to support user sorting (but changes will need to be made to realm to persist). + ShowDragHandle.Value = false; } protected override Drawable CreateContent() => new ItemContent(Model); From 7139592e431ee6da7a6fa48c47f43fd6cf316d15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 15:53:40 +0900 Subject: [PATCH 74/83] Fix collections not being sorted correctly in context menus --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 6 +++++- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 6 +++++- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 7 ++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 9e07279be2..8f405399a7 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -500,7 +500,11 @@ namespace osu.Game.Screens.OnlinePlay { if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending) { - var collectionItems = realm.Realm.All().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast().ToList(); + var collectionItems = realm.Realm.All() + .OrderBy(c => c.Name) + .AsEnumerable() + .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast().ToList(); + if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index f08d14720b..3dfd801f02 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -233,7 +233,11 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID))); - var collectionItems = realm.Realm.All().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); + var collectionItems = realm.Realm.All() + .OrderBy(c => c.Name) + .AsEnumerable() + .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); + if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 4234184ad1..dd711b2513 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -225,7 +225,12 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.OnlineID > 0 && viewDetails != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID))); - var collectionItems = realm.Realm.All().AsEnumerable().Select(createCollectionMenuItem).ToList(); + var collectionItems = realm.Realm.All() + .OrderBy(c => c.Name) + .AsEnumerable() + .Select(createCollectionMenuItem) + .ToList(); + if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); From 84be714d6bd100e69e464dafda6c471aa8e292e8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 30 Sep 2023 02:19:11 +0900 Subject: [PATCH 75/83] Fix large instantaneous delta on first frame Happens when the first update frame comes in before any mouse input. --- .../Default/SpinnerRotationTracker.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 719cf57d98..41d6e689b1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -22,11 +22,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private readonly DrawableSpinner drawableSpinner; - private Vector2 mousePosition; + private Vector2? mousePosition; + private float? lastAngle; - private float lastAngle; private float currentRotation; - private bool rotationTransferred; [Resolved(canBeNull: true)] @@ -63,17 +62,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default protected override void Update() { base.Update(); - float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - float delta = thisAngle - lastAngle; + if (mousePosition is Vector2 pos) + { + float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2)); + float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value; - if (Tracking) - AddRotation(delta); + if (Tracking) + AddRotation(delta); - lastAngle = thisAngle; + lastAngle = thisAngle; + } IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; - Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } @@ -116,8 +117,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { Tracking = false; IsSpinning.Value = false; - mousePosition = default; - lastAngle = currentRotation = Rotation = 0; + mousePosition = null; + lastAngle = null; + currentRotation = 0; + Rotation = 0; rotationTransferred = false; } From 159b24acf767b07e1dc803d7875788bd8ebf8e72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 18:25:03 +0900 Subject: [PATCH 76/83] Rename `RateAdjustedRotation` to `TotalRotation` --- .../TestSceneSpinnerApplication.cs | 4 ++-- .../TestSceneSpinnerRotation.cs | 12 ++++++------ .../Judgements/OsuSpinnerJudgementResult.cs | 2 +- .../Objects/Drawables/DrawableSpinner.cs | 6 +++--- .../Skinning/Argon/ArgonSpinnerDisc.cs | 2 +- .../Skinning/Default/DefaultSpinnerDisc.cs | 2 +- .../Skinning/Default/SpinnerRotationTracker.cs | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 1ae17432be..dae81f4cff 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("rotate some", () => dho.RotationTracker.AddRotation(180)); - AddAssert("rotation is set", () => dho.Result.RateAdjustedRotation == 180); + AddAssert("rotation is set", () => dho.Result.TotalRotation == 180); AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner { @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests Duration = 1000, }))); - AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0); + AddAssert("rotation is reset", () => dho.Result.TotalRotation == 0); } private Spinner prepareObject(Spinner circle) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 116c974f32..8711aa9c09 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -63,11 +63,11 @@ namespace osu.Game.Rulesets.Osu.Tests trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100)); - AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100)); addSeekStep(0); AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(0).Within(100)); } [Test] @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f); }); - AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); + AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.TotalRotation); addSeekStep(spinner_start_time + 2500); AddAssert("disc rotation rewound", @@ -92,13 +92,13 @@ namespace osu.Game.Rulesets.Osu.Tests () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. - () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); + () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); + () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); } [Test] @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; + return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index 941cb667cf..c5e15d63ea 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Judgements /// If Double Time is active instead (with a speed multiplier of 1.5x), /// in the same scenario the property will return 720 * 1.5 = 1080. /// - public float RateAdjustedRotation; + public float TotalRotation; /// /// Time instant at which the spin was started (the first user input which caused an increase in spin). diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 24446db92a..9fa180cf93 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // these become implicitly hit. return 1; - return Math.Clamp(Result.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); + return Math.Clamp(Result.TotalRotation / 360 / HitObject.SpinsRequired, 0, 1); } } @@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // don't update after end time to avoid the rate display dropping during fade out. // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period. if (Time.Current <= HitObject.EndTime) - spmCalculator.SetRotation(Result.RateAdjustedRotation); + spmCalculator.SetRotation(Result.TotalRotation); updateBonusScore(); } @@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (ticks.Count == 0) return; - int spins = (int)(Result.RateAdjustedRotation / 360); + int spins = (int)(Result.TotalRotation / 360); if (spins < completedFullSpins) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs index bdc93eb63f..079758c21e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { get { - int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.TotalRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs index 75f3247448..b498975a83 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { get { - int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.TotalRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 41d6e689b1..77d410887c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate)); + drawableSpinner.Result.TotalRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate)); } private void resetState(DrawableHitObject obj) From cfa4adb24d2ed5282ef0e4c8616e3ab9f70c0b43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 18:25:40 +0900 Subject: [PATCH 77/83] Add `SpinFramesGenerator` class to simplify creating spinner tests --- .../SpinFramesGenerator.cs | 111 ++++++++++++++++++ .../TestSceneLegacyHitPolicy.cs | 15 +-- .../TestSceneSpinnerJudgement.cs | 26 +--- .../TestSceneStartTimeOrderedHitPolicy.cs | 15 +-- 4 files changed, 130 insertions(+), 37 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs diff --git a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs new file mode 100644 index 0000000000..43adfb7f1f --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs @@ -0,0 +1,111 @@ +// 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.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class SpinFramesGenerator + { + /// + /// A small amount to spin beyond a given angle to mitigate floating-point precision errors. + /// + public const float SPIN_ERROR = MathF.PI / 8; + + /// + /// The offset from the centre of the spinner at which to spin. + /// + private const float centre_spin_offset = 50; + + private readonly double startTime; + private readonly float startAngle; + private readonly List<(float deltaAngle, double duration)> sequences = new List<(float deltaAngle, double duration)>(); + + /// + /// Creates a new that can be used to generate spinner spin frames. + /// + /// The time at which to start spinning. + /// The angle, in radians, at which to start spinning from. Defaults to the positive-y-axis. + public SpinFramesGenerator(double startTime, float startAngle = -MathF.PI / 2f) + { + this.startTime = startTime; + this.startAngle = startAngle; + } + + /// + /// Performs a single spin. + /// + /// The amount, relative to a full circle, to spin. + /// The time to spend to perform the spin. + /// This . + public SpinFramesGenerator Spin(float delta, double duration) + { + sequences.Add((delta * 2 * MathF.PI, duration)); + return this; + } + + /// + /// Constructs the replay frames. + /// + /// The replay frames. + public List Build() + { + List frames = new List(); + + double lastTime = startTime; + float lastAngle = startAngle; + int lastDirection = 0; + + for (int i = 0; i < sequences.Count; i++) + { + var seq = sequences[i]; + + int seqDirection = Math.Sign(seq.deltaAngle); + float seqError = SPIN_ERROR * seqDirection; + + if (seqDirection == lastDirection) + { + // Spinning in the same direction, but the error was already added in the last rotation. + seqError = 0; + } + else if (lastDirection != 0) + { + // Spinning in a different direction, we need to account for the error of the start angle, so double it. + seqError *= 2; + } + + double seqStartTime = lastTime; + double seqEndTime = lastTime + seq.duration; + float seqStartAngle = lastAngle; + float seqEndAngle = seqStartAngle + seq.deltaAngle + seqError; + + // Intermediate spin frames. + for (; lastTime < seqEndTime; lastTime += 10) + frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt((lastTime - seqStartTime) / (seqEndTime - seqStartTime), seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + + // Final frame at the end of the current spin. + frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + + lastTime = seqEndTime; + lastAngle = seqEndAngle; + lastDirection = seqDirection; + } + + // Key release frame. + if (frames.Count > 0) + frames.Add(new OsuReplayFrame(frames[^1].Time, ((OsuReplayFrame)frames[^1]).Position)); + + return frames; + } + + private static Vector2 calcOffsetAt(double p, float startAngle, float endAngle) + { + float angle = startAngle + (endAngle - startAngle) * (float)p; + return new Vector2(256, 192) + centre_spin_offset * new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs index a2ef72fe57..e0a618b187 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs @@ -356,15 +356,16 @@ namespace osu.Game.Rulesets.Osu.Tests }, }; - performTest(hitObjects, new List + List frames = new List { new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - }); + }; + + frames.AddRange(new SpinFramesGenerator(time_spinner + 10) + .Spin(1, 500) + .Build()); + + performTest(hitObjects, frames); addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Meh); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs index c969cb11b4..6a50f08508 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.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 NUnit.Framework; @@ -11,14 +10,12 @@ using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -59,26 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult)); } - private static List generateReplay(int spins) - { - var replayFrames = new List(); - - const int frames_per_spin = 30; - - for (int i = 0; i < spins * frames_per_spin; ++i) - { - float totalProgress = i / (float)(spins * frames_per_spin); - float spinProgress = (i % frames_per_spin) / (float)frames_per_spin; - double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress; - float posX = MathF.Cos(2 * MathF.PI * spinProgress); - float posY = MathF.Sin(2 * MathF.PI * spinProgress); - Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50; - - replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton)); - } - - return replayFrames; - } + private static List generateReplay(int spins) => new SpinFramesGenerator(time_spinner_start) + .Spin(spins, time_spinner_end - time_spinner_start) + .Build(); private void performTest(List frames) { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index f0af3f0c39..19413a50a8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -284,15 +284,16 @@ namespace osu.Game.Rulesets.Osu.Tests }, }; - performTest(hitObjects, new List + List frames = new List { new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - }); + }; + + frames.AddRange(new SpinFramesGenerator(time_spinner + 10) + .Spin(1, 500) + .Build()); + + performTest(hitObjects, frames); addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great); From 28ee99f132bf48a3368bcac3f51fc2721b079225 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 18:31:01 +0900 Subject: [PATCH 78/83] Add prospective test coverage of spinner input handling --- .../TestSceneSpinnerInput.cs | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs new file mode 100644 index 0000000000..d7151f9370 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -0,0 +1,290 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneSpinnerInput : RateAdjustedBeatmapTestScene + { + private const int centre_x = 256; + private const int centre_y = 192; + private const double time_spinner_start = 1500; + private const double time_spinner_end = 8000; + + private readonly List judgementResults = new List(); + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + private ManualClock? manualClock; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) + { + return manualClock == null + ? base.CreateWorkingBeatmap(beatmap, storyboard) + : new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio); + } + + [SetUp] + public void Setup() => Schedule(() => + { + manualClock = null; + }); + + /// + /// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms. + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestVibrateWithoutSpinningOffCentre() + { + List frames = new List(); + + const int vibrate_time = 50; + const float y_pos = centre_y - 50; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, y_pos), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, y_pos), OsuAction.LeftButton)); + + direction *= -1; + } + + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms. + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestVibrateWithoutSpinningOnCentre() + { + List frames = new List(); + + const int vibrate_time = 50; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton)); + + direction *= -1; + } + + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// Spins in a single direction. + /// + [TestCase(0.5f, 0)] + [TestCase(-0.5f, 0)] + [TestCase(1, 1)] + [TestCase(-1, 1)] + [TestCase(1.5f, 1)] + [TestCase(-1.5f, 1)] + [TestCase(2f, 2)] + [TestCase(-2f, 2)] + public void TestSpinSingleDirection(float amount, int expectedTicks) + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(amount, 500) + .Build()); + + assertTicksHit(expectedTicks); + assertSpinnerHit(false); + } + + /// + /// Spin half-way clockwise then perform one full spin counter-clockwise. + /// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW). + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestSpinHalfBothDirections() + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(0.5f, 500) // Rotate to +0.5. + .Spin(-1f, 500) // Rotate to -0.5 + .Build()); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// Spin in one direction then spin in the other. + /// + [TestCase(0.5f, -1.5f, 1)] + [TestCase(-0.5f, 1.5f, 1)] + [TestCase(0.5f, -2.5f, 2)] + [TestCase(-0.5f, 2.5f, 2)] + [Ignore("An upcoming implementation will fix this case")] + public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks) + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(direction1, 500) + .Spin(direction2, 500) + .Build()); + + assertTicksHit(expectedTicks); + assertSpinnerHit(false); + } + + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestRewind() + { + AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 }); + + List frames = new SpinFramesGenerator(time_spinner_start) + .Spin(1f, 500) // 2000ms -> 1 full CW spin + .Spin(-0.5f, 500) // 2500ms -> 0.5 CCW spins + .Spin(0.25f, 500) // 3000ms -> 0.25 CW spins + .Spin(1.25f, 500) // 3500ms -> 1 full CW spin + .Spin(0.5f, 500) // 4000ms -> 0.5 CW spins + .Build(); + + loadPlayer(frames); + + GameplayClockContainer clock = null!; + DrawableRuleset drawableRuleset = null!; + AddStep("get gameplay objects", () => + { + clock = currentPlayer.ChildrenOfType().Single(); + drawableRuleset = currentPlayer.ChildrenOfType().Single(); + }); + + addSeekStep(frames.Last().Time); + + DrawableSpinner drawableSpinner = null!; + AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType().Single()) != null); + + assertTotalRotation(4000, 900); + assertTotalRotation(3750, 810); + assertTotalRotation(3500, 720); + assertTotalRotation(3250, 530); + assertTotalRotation(3000, 540); + assertTotalRotation(2750, 540); + assertTotalRotation(2500, 540); + assertTotalRotation(2250, 360); + assertTotalRotation(2000, 180); + assertTotalRotation(1500, 0); + + void assertTotalRotation(double time, float expected) + { + addSeekStep(time); + AddAssert($"total rotation @ {time} is {expected}", () => drawableSpinner.Result.TotalRotation, + () => Is.EqualTo(expected).Within(MathHelper.RadiansToDegrees(SpinFramesGenerator.SPIN_ERROR * 2))); + } + + void addSeekStep(double time) + { + AddStep($"seek to {time}", () => clock.Seek(time)); + AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time)); + } + } + + private void assertTicksHit(int count) + { + AddAssert($"{count} ticks hit", () => judgementResults.Where(r => r.HitObject is SpinnerTick).Count(r => r.IsHit), () => Is.EqualTo(count)); + } + + private void assertSpinnerHit(bool shouldBeHit) + { + AddAssert($"spinner is {(shouldBeHit ? "hit" : "missed")}", () => judgementResults.Single(r => r.HitObject is Spinner).IsHit, () => Is.EqualTo(shouldBeHit)); + } + + private void loadPlayer(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = time_spinner_start, + EndTime = time_spinner_end, + Position = new Vector2(centre_x, centre_y) + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty(), + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults.Clear(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + } + + private void performTest(List frames) + { + loadPlayer(frames); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} From 04af46b8c72a03030aa61b69bc4a1ef72b8d9843 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 18:34:56 +0900 Subject: [PATCH 79/83] Change `SpinFramesGenerator` to take degrees as input --- .../SpinFramesGenerator.cs | 4 +- .../TestSceneLegacyHitPolicy.cs | 2 +- .../TestSceneSpinnerInput.cs | 38 +++++++++---------- .../TestSceneSpinnerJudgement.cs | 2 +- .../TestSceneStartTimeOrderedHitPolicy.cs | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs index 43adfb7f1f..dbdfa1f258 100644 --- a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs +++ b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs @@ -39,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests /// /// Performs a single spin. /// - /// The amount, relative to a full circle, to spin. + /// The amount of degrees to spin. /// The time to spend to perform the spin. /// This . public SpinFramesGenerator Spin(float delta, double duration) { - sequences.Add((delta * 2 * MathF.PI, duration)); + sequences.Add((delta / 360 * 2 * MathF.PI, duration)); return this; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs index e0a618b187..fa6aa580a3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs @@ -362,7 +362,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; frames.AddRange(new SpinFramesGenerator(time_spinner + 10) - .Spin(1, 500) + .Spin(360, 500) .Build()); performTest(hitObjects, frames); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs index d7151f9370..c4bf0d4e2e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Osu.Tests /// /// Spins in a single direction. /// - [TestCase(0.5f, 0)] - [TestCase(-0.5f, 0)] - [TestCase(1, 1)] - [TestCase(-1, 1)] - [TestCase(1.5f, 1)] - [TestCase(-1.5f, 1)] - [TestCase(2f, 2)] - [TestCase(-2f, 2)] + [TestCase(180, 0)] + [TestCase(-180, 0)] + [TestCase(360, 1)] + [TestCase(-360, 1)] + [TestCase(540, 1)] + [TestCase(-540, 1)] + [TestCase(720, 2)] + [TestCase(-720, 2)] public void TestSpinSingleDirection(float amount, int expectedTicks) { performTest(new SpinFramesGenerator(time_spinner_start) @@ -134,8 +134,8 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSpinHalfBothDirections() { performTest(new SpinFramesGenerator(time_spinner_start) - .Spin(0.5f, 500) // Rotate to +0.5. - .Spin(-1f, 500) // Rotate to -0.5 + .Spin(180, 500) // Rotate to +0.5. + .Spin(-360, 500) // Rotate to -0.5 .Build()); assertTicksHit(0); @@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Osu.Tests /// /// Spin in one direction then spin in the other. /// - [TestCase(0.5f, -1.5f, 1)] - [TestCase(-0.5f, 1.5f, 1)] - [TestCase(0.5f, -2.5f, 2)] - [TestCase(-0.5f, 2.5f, 2)] + [TestCase(180, -540, 1)] + [TestCase(-180, 540, 1)] + [TestCase(180, -900, 2)] + [TestCase(-180, 900, 2)] [Ignore("An upcoming implementation will fix this case")] public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks) { @@ -168,11 +168,11 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 }); List frames = new SpinFramesGenerator(time_spinner_start) - .Spin(1f, 500) // 2000ms -> 1 full CW spin - .Spin(-0.5f, 500) // 2500ms -> 0.5 CCW spins - .Spin(0.25f, 500) // 3000ms -> 0.25 CW spins - .Spin(1.25f, 500) // 3500ms -> 1 full CW spin - .Spin(0.5f, 500) // 4000ms -> 0.5 CW spins + .Spin(360, 500) // 2000ms -> 1 full CW spin + .Spin(-180, 500) // 2500ms -> 0.5 CCW spins + .Spin(90, 500) // 3000ms -> 0.25 CW spins + .Spin(450, 500) // 3500ms -> 1 full CW spin + .Spin(180, 500) // 4000ms -> 0.5 CW spins .Build(); loadPlayer(frames); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs index 6a50f08508..8d8c2e9639 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests } private static List generateReplay(int spins) => new SpinFramesGenerator(time_spinner_start) - .Spin(spins, time_spinner_end - time_spinner_start) + .Spin(spins * 360, time_spinner_end - time_spinner_start) .Build(); private void performTest(List frames) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 19413a50a8..3475680c71 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; frames.AddRange(new SpinFramesGenerator(time_spinner + 10) - .Spin(1, 500) + .Spin(360, 500) .Build()); performTest(hitObjects, frames); From 10bab614412a17752af5ae50bfba5c65ecf6538a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 19:23:35 +0900 Subject: [PATCH 80/83] Tidy up `lastAngle` usage and add assertion of maximum delta --- .../Default/SpinnerRotationTracker.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 77d410887c..174ba1c402 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -68,6 +69,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2)); float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value; + // Normalise the delta to -180 .. 180 + if (delta > 180) delta -= 360; + if (delta < -180) delta += 360; + if (Tracking) AddRotation(delta); @@ -84,8 +89,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// /// Will be a no-op if not a valid time to spin. /// - /// The delta angle. - public void AddRotation(float angle) + /// The delta angle. + public void AddRotation(float delta) { if (!isSpinnableTime) return; @@ -96,21 +101,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default rotationTransferred = true; } - if (angle > 180) - { - lastAngle += 360; - angle -= 360; - } - else if (-angle > 180) - { - lastAngle -= 360; - angle += 360; - } + currentRotation += delta; + + double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate; + + Debug.Assert(Math.Abs(delta) <= 180); - currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - drawableSpinner.Result.TotalRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate)); + drawableSpinner.Result.TotalRotation += (float)(Math.Abs(delta) * rate); } private void resetState(DrawableHitObject obj) From 0bb95cfa88fb798f07ad7a35aca0ef61f755790d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Oct 2023 19:34:55 +0900 Subject: [PATCH 81/83] Fix incorrect initial rotation transfer value Should have been removed as part of https://github.com/ppy/osu/pull/24360. --- .../Skinning/Default/SpinnerRotationTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 174ba1c402..69c2bf3dd0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (!rotationTransferred) { - currentRotation = Rotation * 2; + currentRotation = Rotation; rotationTransferred = true; } From f2436a5ecb96b7e4aa9a38d76caacc5e4687d879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 12:36:58 +0200 Subject: [PATCH 82/83] Remove no longer used scoring difficulty attributes --- .../Rulesets/Difficulty/DifficultyAttributes.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 6043731e6e..9690924b1c 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Difficulty { @@ -45,22 +44,6 @@ namespace osu.Game.Rulesets.Difficulty [JsonProperty("max_combo", Order = -2)] public int MaxCombo { get; set; } - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// - public int LegacyAccuracyScore { get; set; } - - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// - public int LegacyComboScore { get; set; } - - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// - public double LegacyBonusScoreRatio { get; set; } - /// /// Creates new . /// From 3065c9f23dfa7f9018320392e03a29ea25dee28b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 16 Oct 2023 22:49:41 +0900 Subject: [PATCH 83/83] Fix potential frame misordering in generator --- osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs index dbdfa1f258..e6dc72033a 100644 --- a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs +++ b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Tests frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt((lastTime - seqStartTime) / (seqEndTime - seqStartTime), seqStartAngle, seqEndAngle), OsuAction.LeftButton)); // Final frame at the end of the current spin. - frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(seqEndTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton)); lastTime = seqEndTime; lastAngle = seqEndAngle;