From 63e0768207752f674a44ada84acfc61be63f508c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Mar 2023 14:44:52 +0900 Subject: [PATCH 001/620] Show count of beatmaps in collections in manage dialog --- .../Collections/DrawableCollectionListItem.cs | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 23156b1ad5..efeb066869 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,6 +14,7 @@ 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.UserInterface; using osu.Game.Overlays; using osuTK; @@ -25,7 +27,7 @@ namespace osu.Game.Collections /// public partial class DrawableCollectionListItem : OsuRearrangeableListItem> { - private const float item_height = 35; + private const float item_height = 45; private const float button_width = item_height * 0.75f; /// @@ -81,12 +83,10 @@ namespace osu.Game.Collections Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 }, Children = new Drawable[] { - textBox = new ItemTextBox + textBox = new ItemTextBox(collection) { - RelativeSizeAxes = Axes.Both, - Size = Vector2.One, - CornerRadius = item_height / 2, - PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection" + RelativeSizeAxes = Axes.X, + Height = item_height }, } }, @@ -117,11 +117,64 @@ namespace osu.Game.Collections { protected override float LeftRightPadding => item_height / 2; + private const float count_text_size = 12; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private readonly Live collection; + + private OsuSpriteText countText = null!; + + private IDisposable? itemCountSubscription; + + public ItemTextBox(Live collection) + { + this.collection = collection; + + CornerRadius = item_height / 2; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { BackgroundUnfocused = colours.GreySeaFoamDarker.Darken(0.5f); BackgroundFocused = colours.GreySeaFoam; + + if (collection.IsManaged) + { + TextContainer.Height *= (Height - count_text_size) / Height; + TextContainer.Margin = new MarginPadding { Bottom = count_text_size }; + + TextContainer.Add(countText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + Depth = float.MinValue, + Font = OsuFont.Default.With(size: count_text_size, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Top = 2, Left = 2 }, + Colour = colours.Yellow + }); + + itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, items => + { + countText.Text = items.Count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{items.Count:#,0} beatmap" + : $"{items.Count:#,0} beatmaps"; + }); + } + else + { + PlaceholderText = "Create a new collection"; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + itemCountSubscription?.Dispose(); } } From 954be126922a63458dff577917ebf46e3ac72b75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Mar 2023 14:46:13 +0900 Subject: [PATCH 002/620] Debounce updates to ensure event isn't fired too often after much collection management --- .../Collections/DrawableCollectionListItem.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index efeb066869..87cc14ecb9 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -156,14 +156,17 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, items => - { - countText.Text = items.Count == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{items.Count:#,0} beatmap" - : $"{items.Count:#,0} beatmaps"; - }); + itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => + Scheduler.AddOnce(() => + { + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} beatmap" + : $"{count:#,0} beatmaps"; + })); } else { From 256789193f7d99d6e1dd5ca00f23a82830d195a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Apr 2023 15:28:01 +0900 Subject: [PATCH 003/620] Remove redundant type specification --- osu.Game/Collections/DrawableCollectionListItem.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 87cc14ecb9..31b127ef2a 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -156,7 +155,7 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => + itemCountSubscription = realm.SubscribeToPropertyChanged(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => Scheduler.AddOnce(() => { int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); From 42b76294db48e604b48ade266444190a29bf1424 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Apr 2024 09:48:57 +0800 Subject: [PATCH 004/620] Update all packages --- ...u.Game.Rulesets.EmptyFreeform.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 4 ++-- ....Game.Rulesets.EmptyScrolling.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 4 ++-- .../osu.Game.Benchmarks.csproj | 2 +- osu.Game/Database/EmptyRealmSet.cs | 2 ++ osu.Game/osu.Game.csproj | 20 +++++++++---------- 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 7d43eb2b05..c2c91596fa 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -9,8 +9,8 @@ false - - + + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 7dc8a1336b..2f56869fc3 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,8 +9,8 @@ false - - + + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 9c4c8217f0..350f8ca6a9 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -9,8 +9,8 @@ false - - + + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 7dc8a1336b..2f56869fc3 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,8 +9,8 @@ false - - + + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index af84ee47f1..66027040d3 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -8,7 +8,7 @@ - + diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs index 02dfa50fe5..e548d28f68 100644 --- a/osu.Game/Database/EmptyRealmSet.cs +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -35,6 +35,8 @@ namespace osu.Game.Database } public IRealmCollection Freeze() => throw new NotImplementedException(); + public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback, KeyPathsCollection? keyPathCollection = null) => throw new NotImplementedException(); + public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback) => throw new NotImplementedException(); public bool IsValid => throw new NotImplementedException(); public Realm Realm => throw new NotImplementedException(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 21b5bc60a5..7b211cd7ea 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,26 +18,26 @@ - + - + - - - - - + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + From 1bd17d41a99bbc0dcdf9ed46fc9bce78bad8945d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Apr 2024 09:54:17 +0800 Subject: [PATCH 005/620] Remove obsoleted serialisation path from signalr exceptions --- osu.Game/Online/Multiplayer/InvalidPasswordException.cs | 6 ------ osu.Game/Online/Multiplayer/InvalidStateChangeException.cs | 6 ------ osu.Game/Online/Multiplayer/InvalidStateException.cs | 6 ------ osu.Game/Online/Multiplayer/NotHostException.cs | 6 ------ osu.Game/Online/Multiplayer/NotJoinedRoomException.cs | 6 ------ osu.Game/Online/Multiplayer/UserBlockedException.cs | 6 ------ osu.Game/Online/Multiplayer/UserBlocksPMsException.cs | 6 ------ 7 files changed, 42 deletions(-) diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index d3da8f491b..8f2543ee1e 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -2,7 +2,6 @@ // 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 @@ -13,10 +12,5 @@ namespace osu.Game.Online.Multiplayer public InvalidPasswordException() { } - - protected InvalidPasswordException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs index 4c793dba68..2bae31196a 100644 --- a/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs @@ -2,7 +2,6 @@ // 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 @@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer : base($"Cannot change from {oldState} to {newState}") { } - - protected InvalidStateChangeException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/InvalidStateException.cs b/osu.Game/Online/Multiplayer/InvalidStateException.cs index 27b111a781..c9705e9e53 100644 --- a/osu.Game/Online/Multiplayer/InvalidStateException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateException.cs @@ -2,7 +2,6 @@ // 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 @@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer : base(message) { } - - protected InvalidStateException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/NotHostException.cs b/osu.Game/Online/Multiplayer/NotHostException.cs index cd43b13e52..f4fd217c87 100644 --- a/osu.Game/Online/Multiplayer/NotHostException.cs +++ b/osu.Game/Online/Multiplayer/NotHostException.cs @@ -2,7 +2,6 @@ // 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 @@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer : base("User is attempting to perform a host level operation while not the host") { } - - protected NotHostException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs index 0a96406c16..72773e28db 100644 --- a/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs @@ -2,7 +2,6 @@ // 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 @@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer : base("This user has not yet joined a multiplayer room.") { } - - protected NotJoinedRoomException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/UserBlockedException.cs b/osu.Game/Online/Multiplayer/UserBlockedException.cs index e964b13c75..58e86d9f32 100644 --- a/osu.Game/Online/Multiplayer/UserBlockedException.cs +++ b/osu.Game/Online/Multiplayer/UserBlockedException.cs @@ -2,7 +2,6 @@ // 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 @@ -16,10 +15,5 @@ namespace osu.Game.Online.Multiplayer : 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 index 14ed6fc212..0ea583ae2c 100644 --- a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs +++ b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs @@ -2,7 +2,6 @@ // 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 @@ -16,10 +15,5 @@ namespace osu.Game.Online.Multiplayer : base(MESSAGE) { } - - protected UserBlocksPMsException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } From 5f0af6085120b316beafcdc6c03972e14812d149 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Apr 2024 09:54:37 +0800 Subject: [PATCH 006/620] Update mismatching translation xmldocs --- .../FirstRunOverlayImportFromStableScreenStrings.cs | 10 ++++------ osu.Game/Localisation/NotificationsStrings.cs | 8 ++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs index 04fecab3df..6293a4f840 100644 --- a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs +++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs @@ -15,10 +15,9 @@ namespace osu.Game.Localisation public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import"); /// - /// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way." + /// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way." /// - public static LocalisableString Description => new TranslatableString(getKey(@"description"), - @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way."); + public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way."); /// /// "previous osu! install" @@ -38,8 +37,7 @@ namespace osu.Game.Localisation /// /// "Your import will continue in the background. Check on its progress in the notifications sidebar!" /// - public static LocalisableString ImportInProgress => - new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!"); + public static LocalisableString ImportInProgress => new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!"); /// /// "calculating..." @@ -47,7 +45,7 @@ namespace osu.Game.Localisation public static LocalisableString Calculating => new TranslatableString(getKey(@"calculating"), @"calculating..."); /// - /// "{0} items" + /// "{0} item(s)" /// public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0); diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 3188ca5533..5857b33f52 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -84,12 +84,12 @@ Please try changing your audio device to a working setting."); public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!"); /// - /// "You received a private message from '{0}'. Click to read it!" + /// "You received a private message from '{0}'. Click to read it!" /// public static LocalisableString PrivateMessageReceived(string username) => new TranslatableString(getKey(@"private_message_received"), @"You received a private message from '{0}'. Click to read it!", username); /// - /// "Your name was mentioned in chat by '{0}'. Click to find out why!" + /// "Your name was mentioned in chat by '{0}'. Click to find out why!" /// 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); @@ -114,8 +114,8 @@ Please try changing your audio device to a working setting."); public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it."); /// - /// "You are now running osu! {version}. - /// Click to see what's new!" + /// "You are now running osu! {0}. + /// Click to see what's new!" /// public static LocalisableString GameVersionAfterUpdate(string version) => new TranslatableString(getKey(@"game_version_after_update"), @"You are now running osu! {0}. Click to see what's new!", version); From 9363194f156101728527555730f4da71de8602dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Apr 2024 09:31:27 +0200 Subject: [PATCH 007/620] Remove old signature --- osu.Game/Database/EmptyRealmSet.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs index e548d28f68..7b5296b5a1 100644 --- a/osu.Game/Database/EmptyRealmSet.cs +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -37,7 +37,6 @@ namespace osu.Game.Database public IRealmCollection Freeze() => throw new NotImplementedException(); public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback, KeyPathsCollection? keyPathCollection = null) => throw new NotImplementedException(); - public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback) => throw new NotImplementedException(); public bool IsValid => throw new NotImplementedException(); public Realm Realm => throw new NotImplementedException(); public ObjectSchema ObjectSchema => throw new NotImplementedException(); From 5f3241978cba695b1f3ee197841d73122fec6642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Apr 2024 09:31:50 +0200 Subject: [PATCH 008/620] Remove redundant constructor --- osu.Game/Online/Multiplayer/InvalidPasswordException.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index 8f2543ee1e..b76a1cc05d 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -9,8 +9,5 @@ namespace osu.Game.Online.Multiplayer [Serializable] public class InvalidPasswordException : HubException { - public InvalidPasswordException() - { - } } } From 16e69b08a161506d191ddf89e782928da79146d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Sep 2024 19:52:51 +0900 Subject: [PATCH 009/620] Avoid unnecessarily handling two skin changed events when making mutable skin --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 6f7781ee9c..eca8b7f1d2 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -401,6 +401,10 @@ namespace osu.Game.Overlays.SkinEditor private void skinChanged() { + if (skins.EnsureMutableSkin()) + // Another skin changed event will arrive which will complete the process. + return; + headerText.Clear(); headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16)); From 1f2f4a533f8159b986f90538388845820a2c50b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Sep 2024 19:53:06 +0900 Subject: [PATCH 010/620] Fix initial skin state being stored wrong to undo history --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index eca8b7f1d2..ec9931c673 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -422,17 +422,24 @@ namespace osu.Game.Overlays.SkinEditor }); changeHandler?.Dispose(); + changeHandler = null; - skins.EnsureMutableSkin(); + // Schedule is required to ensure that all layout in `LoadComplete` methods has been completed + // before storing an undo state. + // + // See https://github.com/ppy/osu/blob/8e6a4559e3ae8c9892866cf9cf8d4e8d1b72afd0/osu.Game/Skinning/SkinReloadableDrawable.cs#L76. + Schedule(() => + { + var targetContainer = getTarget(selectedTarget.Value); - var targetContainer = getTarget(selectedTarget.Value); + if (targetContainer != null) + changeHandler = new SkinEditorChangeHandler(targetContainer); - if (targetContainer != null) - changeHandler = new SkinEditorChangeHandler(targetContainer); - hasBegunMutating = true; + hasBegunMutating = true; - // Reload sidebar components. - selectedTarget.TriggerChange(); + // Reload sidebar components. + selectedTarget.TriggerChange(); + }); } /// From f84f6b78d9fdd4a1fda1a36c97cb4915981a3a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Oct 2024 13:48:29 +0200 Subject: [PATCH 011/620] Add failing test coverage of skin editor still not undoing correctly to initial state --- .../TestSceneSkinEditorNavigation.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 5267a57a05..8323aaeaf4 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -5,6 +5,7 @@ using System; using System.Linq; +using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; using osu.Game.Tests.Beatmaps.IO; @@ -101,6 +103,77 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get().CurrentSkin.Value.SkinInfo.Value.Protected); } + [Test] + public void TestMutateProtectedSkinFromMainMenu_UndoToInitialStateIsCorrect() + { + AddStep("set default skin", () => Game.Dependencies.Get().CurrentSkinInfo.SetDefault()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + openSkinEditor(); + AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get().CurrentSkin.Value.SkinInfo.Value.Protected); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + + string state = string.Empty; + + AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.IsLoaded)); + AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo())); + AddStep("add any component", () => Game.ChildrenOfType().First().TriggerClick()); + AddStep("undo", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Z); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("only one accuracy meter left", + () => Game.ChildrenOfType().Single().ChildrenOfType().Count(), + () => Is.EqualTo(1)); + AddAssert("accuracy meter state unchanged", + () => JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo()), + () => Is.EqualTo(state)); + } + + [Test] + public void TestMutateProtectedSkinFromPlayer_UndoToInitialStateIsCorrect() + { + AddStep("set default skin", () => Game.Dependencies.Get().CurrentSkinInfo.SetDefault()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + advanceToSongSelect(); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() }); + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + openSkinEditor(); + + string state = string.Empty; + + AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.IsLoaded)); + AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo())); + AddStep("add any component", () => Game.ChildrenOfType().First().TriggerClick()); + AddStep("undo", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Z); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("only one accuracy meter left", + () => Game.ChildrenOfType().Single().ChildrenOfType().Count(), + () => Is.EqualTo(1)); + AddAssert("accuracy meter state unchanged", + () => JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo()), + () => Is.EqualTo(state)); + } + [Test] public void TestComponentsDeselectedOnSkinEditorHide() { From 66ca7448436e7d66072343a1c4af950da3e0d385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Oct 2024 14:23:16 +0200 Subject: [PATCH 012/620] Fix `SkinEditorChangeHandler` not actually storing initial state --- osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs index 673ba873c4..b805e50df6 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.SkinEditor return; components = new BindableList { BindTarget = firstTarget.Components }; - components.BindCollectionChanged((_, _) => SaveState()); + components.BindCollectionChanged((_, _) => SaveState(), true); } protected override void WriteCurrentStateToStream(MemoryStream stream) From 936677f56abd22328fc9450d3b529b87a672f440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Oct 2024 14:47:29 +0200 Subject: [PATCH 013/620] Fix `SkinEditor` potentially initialising change handler while components are not loaded yet --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index ec9931c673..130684e289 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -353,9 +353,10 @@ namespace osu.Game.Overlays.SkinEditor return; } - changeHandler = new SkinEditorChangeHandler(skinComponentsContainer); - changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); - changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + if (skinComponentsContainer.IsLoaded) + bindChangeHandler(skinComponentsContainer); + else + skinComponentsContainer.OnLoadComplete += d => Schedule(() => bindChangeHandler((SkinnableContainer)d)); content.Child = new SkinBlueprintContainer(skinComponentsContainer); @@ -397,6 +398,13 @@ namespace osu.Game.Overlays.SkinEditor SelectedComponents.Clear(); placeComponent(component); } + + void bindChangeHandler(SkinnableContainer skinnableContainer) + { + changeHandler = new SkinEditorChangeHandler(skinnableContainer); + changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); + changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + } } private void skinChanged() From 99518f4a564ed2e14895c5744a25f3af4138db64 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 4 Nov 2024 04:28:16 -0500 Subject: [PATCH 014/620] Specify type of text input in most `TextBox` usages --- osu.Game/Graphics/UserInterface/OsuNumberBox.cs | 7 +++---- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 10 +++------- osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs | 6 ++++++ osu.Game/Overlays/Login/LoginForm.cs | 2 ++ osu.Game/Overlays/Settings/SettingsNumberBox.cs | 6 +++++- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 6 +++++- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index db4b7b2ab3..86753f6aa9 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -1,17 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Input; + namespace osu.Game.Graphics.UserInterface { public partial class OsuNumberBox : OsuTextBox { - protected override bool AllowIme => false; - public OsuNumberBox() { + InputProperties = new TextInputProperties(TextInputType.Number, false); SelectAllOnFocus = true; } - - protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); } } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 0be7b4dc48..143962542d 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -18,7 +18,7 @@ using osu.Game.Localisation; namespace osu.Game.Graphics.UserInterface { - public partial class OsuPasswordTextBox : OsuTextBox, ISuppressKeyEventLogging + public partial class OsuPasswordTextBox : OsuTextBox { protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer { @@ -28,12 +28,6 @@ namespace osu.Game.Graphics.UserInterface protected override bool AllowUniqueCharacterSamples => false; - protected override bool AllowClipboardExport => false; - - protected override bool AllowWordNavigation => false; - - protected override bool AllowIme => false; - private readonly CapsWarning warning; [Resolved] @@ -41,6 +35,8 @@ namespace osu.Game.Graphics.UserInterface public OsuPasswordTextBox() { + InputProperties = new TextInputProperties(TextInputType.Password, false); + Add(warning = new CapsWarning { Size = new Vector2(20), diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs index c3256e0038..61d3b3fc31 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Globalization; +using osu.Framework.Input; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -19,6 +20,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 { public bool AllowDecimals { get; init; } + public InnerNumberBox() + { + InputProperties = new TextInputProperties(TextInputType.Number, false); + } + protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character)); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 13e528ff8f..0ff30da2a1 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Graphics; @@ -63,6 +64,7 @@ namespace osu.Game.Overlays.Login }, username = new OsuTextBox { + InputProperties = new TextInputProperties(TextInputType.Username, false), PlaceholderText = UsersStrings.LoginUsername.ToLower(), RelativeSizeAxes = Axes.X, Text = api.ProvidedUsername, diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index fbcdb4a968..2548f3c87b 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; namespace osu.Game.Overlays.Settings { @@ -66,7 +67,10 @@ namespace osu.Game.Overlays.Settings private partial class OutlinedNumberBox : OutlinedTextBox { - protected override bool AllowIme => false; + public OutlinedNumberBox() + { + InputProperties = new TextInputProperties(TextInputType.Number, false); + } protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 20c0a74d84..3acaefe91e 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; @@ -119,7 +120,10 @@ namespace osu.Game.Screens.Edit.Setup private partial class RomanisedTextBox : InnerTextBox { - protected override bool AllowIme => false; + public RomanisedTextBox() + { + InputProperties = new TextInputProperties(TextInputType.Text, false); + } protected override bool CanAddCharacter(char character) => MetadataUtils.IsRomanised(character); From a99a992ceba30b3ff0208de4873eacd41719b65e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Dec 2024 13:48:05 +0900 Subject: [PATCH 015/620] Adjust test to load song select during setup --- .../Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 2a5f16d091..a266b1d95e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -60,14 +60,15 @@ namespace osu.Game.Tests.Visual.Multiplayer private void setUp() { - AddStep("reset", () => + AddStep("create song select", () => { Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); SelectedMods.SetDefault(); + + LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!)); }); - AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!))); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } From 2bae93d7add0d6d24758040b46d3542200a40480 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 9 Dec 2024 01:59:16 -0500 Subject: [PATCH 016/620] Add special handling for file import button on iOS --- .../Sections/Maintenance/GeneralSettings.cs | 20 ++++++-- .../Maintenance/SystemFileImportComponent.cs | 51 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index f75fc2c8bc..ed3e72adbe 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Localisation; using osu.Game.Screens; @@ -15,22 +17,32 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { protected override LocalisableString Header => CommonStrings.General; + private SystemFileImportComponent systemFileImport = null!; + [BackgroundDependencyLoader] - private void load(IPerformFromScreenRunner? performer) + private void load(OsuGame game, GameHost host, IPerformFromScreenRunner? performer) { - Children = new[] + Add(systemFileImport = new SystemFileImportComponent(game, host)); + + AddRange(new Drawable[] { new SettingsButton { Text = DebugSettingsStrings.ImportFiles, - Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) + Action = () => + { + if (systemFileImport.PresentIfAvailable()) + return; + + performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); + }, }, new SettingsButton { Text = DebugSettingsStrings.RunLatencyCertifier, Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen())) } - }; + }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs new file mode 100644 index 0000000000..9827872702 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Graphics; +using osu.Framework.Platform; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public partial class SystemFileImportComponent : Component + { + private readonly OsuGame game; + private readonly GameHost host; + + private ISystemFileSelector? selector; + + public SystemFileImportComponent(OsuGame game, GameHost host) + { + this.game = game; + this.host = host; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray()); + + if (selector != null) + selector.Selected += f => Schedule(() => startImport(f.FullName)); + } + + public bool PresentIfAvailable() + { + if (selector == null) + return false; + + selector.Present(); + return true; + } + + private void startImport(string path) + { + Task.Factory.StartNew(async () => + { + await game.Import(path).ConfigureAwait(false); + }, TaskCreationOptions.LongRunning); + } + } +} From 9abb92a8d659982b76d0ece4ac45c7bb98132020 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 15:46:28 +0900 Subject: [PATCH 017/620] Add BeatmapSetId to playlist items --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 3 +++ osu.Game/Online/Rooms/PlaylistItem.cs | 6 ++++++ .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 1 + 3 files changed, 10 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8be703e620..027d5b4a17 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -56,6 +56,9 @@ namespace osu.Game.Online.Rooms [Key(10)] public double StarRating { get; set; } + [Key(11)] + public int? BeatmapSetID { get; set; } + [SerializationConstructor] public MultiplayerPlaylistItem() { diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 47d4e163bf..3d829d1e4e 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -67,6 +67,9 @@ namespace osu.Game.Online.Rooms set => Beatmap = new APIBeatmap { OnlineID = value }; } + [JsonProperty("beatmapset_id")] + public int? BeatmapSetId { get; set; } + /// /// A beatmap representing this playlist item. /// In many cases, this will *not* contain any usable information apart from OnlineID. @@ -101,6 +104,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); + BeatmapSetId = item.BeatmapSetID; } public void MarkInvalid() => valid.Value = false; @@ -133,12 +137,14 @@ namespace osu.Game.Online.Rooms AllowedMods = AllowedMods, RequiredMods = RequiredMods, valid = { Value = Valid.Value }, + BeatmapSetId = BeatmapSetId }; } public bool Equals(PlaylistItem? other) => ID == other?.ID && Beatmap.OnlineID == other.Beatmap.OnlineID + && BeatmapSetId == other.BeatmapSetId && RulesetID == other.RulesetID && Expired == other.Expired && PlaylistOrder == other.PlaylistOrder diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 4e03c19095..9f9e6349a6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -83,6 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { ID = itemToEdit?.ID ?? 0, BeatmapID = item.Beatmap.OnlineID, + BeatmapSetID = item.BeatmapSetId, BeatmapChecksum = item.Beatmap.MD5Hash, RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), From 0fb75233ffe501b51ca5cf605f3390c87695dcb9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 23:02:26 +0900 Subject: [PATCH 018/620] Add "freeplay" button to multiplayer song select --- .../OnlinePlay/FooterButtonFreePlay.cs | 94 +++++++++++++++++++ .../OnlinePlay/OnlinePlaySongSelect.cs | 55 ++++++++--- .../Playlists/PlaylistsSongSelect.cs | 3 +- osu.Game/Screens/Select/SongSelect.cs | 7 +- 4 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs new file mode 100644 index 0000000000..367857e780 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs @@ -0,0 +1,94 @@ +// 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.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Select; + +namespace osu.Game.Screens.OnlinePlay +{ + public class FooterButtonFreePlay : FooterButton, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private OsuSpriteText text = null!; + private Circle circle = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + ButtonContentContainer.AddRange(new[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + UseFullGlyphHeight = false, + } + } + } + }); + + SelectedColour = colours.Yellow; + DeselectedColour = SelectedColour.Opacity(0.5f); + Text = @"freeplay"; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateDisplay(), true); + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + Action = () => current.Value = !current.Value; + } + + private void updateDisplay() + { + if (current.Value) + { + text.Text = "on"; + text.FadeColour(colours.Gray2, 200, Easing.OutQuint); + circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); + } + else + { + text.Text = "off"; + text.FadeColour(colours.GrayF, 200, Easing.OutQuint); + circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f6b6dfd3ab..1f1d259d0a 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -41,10 +41,12 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); + protected readonly Bindable FreePlay = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; - private readonly FreeModSelectOverlay freeModSelectOverlay; + private readonly FreeModSelectOverlay freeModSelect; + private FooterButton freeModsFooterButton = null!; private IDisposable? freeModSelectOverlayRegistration; @@ -61,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - freeModSelectOverlay = new FreeModSelectOverlay + freeModSelect = new FreeModSelectOverlay { SelectedMods = { BindTarget = FreeMods }, IsValidMod = IsValidFreeMod, @@ -72,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay private void load() { LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; - LoadComponent(freeModSelectOverlay); + LoadComponent(freeModSelect); } protected override void LoadComplete() @@ -108,12 +110,36 @@ namespace osu.Game.Screens.OnlinePlay Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } + + if (initialItem.BeatmapSetId != null) + FreePlay.Value = true; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); + FreePlay.BindValueChanged(onFreePlayChanged, true); - freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelectOverlay); + freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); + } + + private void onFreePlayChanged(ValueChangedEvent enabled) + { + if (enabled.NewValue) + { + freeModsFooterButton.Enabled.Value = false; + ModsFooterButton.Enabled.Value = false; + + ModSelect.Hide(); + freeModSelect.Hide(); + + Mods.Value = []; + FreeMods.Value = []; + } + else + { + freeModsFooterButton.Enabled.Value = true; + ModsFooterButton.Enabled.Value = true; + } } private void onModsChanged(ValueChangedEvent> mods) @@ -121,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); // Reset the validity delegate to update the overlay's display. - freeModSelectOverlay.IsValidMod = IsValidFreeMod; + freeModSelect.IsValidMod = IsValidFreeMod; } private void onRulesetChanged(ValueChangedEvent ruleset) @@ -135,7 +161,8 @@ namespace osu.Game.Screens.OnlinePlay { RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), - AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), + BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null }; return SelectItem(item); @@ -150,9 +177,9 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnBackButton() { - if (freeModSelectOverlay.State.Value == Visibility.Visible) + if (freeModSelect.State.Value == Visibility.Visible) { - freeModSelectOverlay.Hide(); + freeModSelect.Hide(); return true; } @@ -161,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnExiting(ScreenExitEvent e) { - freeModSelectOverlay.Hide(); + freeModSelect.Hide(); return base.OnExiting(e); } @@ -173,9 +200,15 @@ namespace osu.Game.Screens.OnlinePlay protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() { var baseButtons = base.CreateSongSelectFooterButtons().ToList(); - var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods }; - baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay)); + freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; + var freePlayButton = new FooterButtonFreePlay { Current = FreePlay }; + + baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] + { + (freeModsFooterButton, freeModSelect), + (freePlayButton, null) + }); return baseButtons; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 23824b6a73..f9e014a727 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,9 +37,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, + BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), - AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), }; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9f7a2c02ff..9ebd9c9846 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -82,6 +82,11 @@ namespace osu.Game.Screens.Select /// protected Container FooterPanels { get; private set; } = null!; + /// + /// The that opens the mod select dialog. + /// + protected FooterButton ModsFooterButton { get; private set; } = null!; + /// /// Whether entering editor mode should be allowed. /// @@ -407,7 +412,7 @@ namespace osu.Game.Screens.Select /// A set of and an optional which the button opens when pressed. protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] { - (new FooterButtonMods { Current = Mods }, ModSelect), + (ModsFooterButton = new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom { NextRandom = () => Carousel.SelectNextRandom(), From a6e00d6eac9ee5e14436aec06f456cb61c7753ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Dec 2024 10:49:19 +0900 Subject: [PATCH 019/620] Implement ability to mark beatmap as played Reported at https://osu.ppy.sh/community/forums/topics/2015478?n=1. Would you believe it that this button that has been there for literal years never did anything? Implemented at a per-beatmap level. Also additionally added to context menu (at @peppy's suggestion), and also copy reworded from "Delete from unplayed" to "Mark as played" because double negation hurt my tiny brain. --- osu.Game/Beatmaps/BeatmapManager.cs | 10 ++++++++++ .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 8 +++++++- osu.Game/Screens/Select/SongSelect.cs | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 148bd90f28..aa67d3c548 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -533,6 +533,16 @@ namespace osu.Game.Beatmaps } } + public void MarkPlayed(BeatmapInfo beatmapSetInfo) => Realm.Run(r => + { + using var transaction = r.BeginWrite(); + + var beatmap = r.Find(beatmapSetInfo.ID)!; + beatmap.LastPlayed = DateTimeOffset.Now; + + transaction.Commit(); + }); + #region Implementation of ICanAcceptFiles public Task Import(params string[] paths) => beatmapImporter.Import(paths); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 75c13c1be6..4451cfcf32 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -88,6 +88,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private OsuGame? game { get; set; } + [Resolved] + private BeatmapManager? manager { get; set; } + private IBindable starDifficultyBindable = null!; private CancellationTokenSource? starDifficultyCancellationSource; @@ -98,7 +101,7 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader] - private void load(BeatmapManager? manager, SongSelect? songSelect) + private void load(SongSelect? songSelect) { Header.Height = height; @@ -300,6 +303,9 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapInfo.GetOnlineURL(api, ruleset.Value) is string url) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyUrlToClipboard(url))); + if (manager != null) + items.Add(new OsuMenuItem("Mark as played", MenuItemType.Standard, () => manager.MarkPlayed(beatmapInfo))); + if (hideRequested != null) items.Add(new OsuMenuItem(WebCommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo))); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9f7a2c02ff..651a7fe4a1 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -375,7 +375,7 @@ namespace osu.Game.Screens.Select BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => DeleteBeatmap(Beatmap.Value.BeatmapSetInfo)); - BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); + BeatmapOptions.AddButton(@"Mark", @"as played", FontAwesome.Regular.TimesCircle, colours.Purple, () => beatmaps.MarkPlayed(Beatmap.Value.BeatmapInfo)); BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => ClearScores(Beatmap.Value.BeatmapInfo)); } From d97ea781364323383fa59512e45cac494387fb4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 15:22:30 +0900 Subject: [PATCH 020/620] Change beat snap divisior adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll Matches stable. - [ ] Depends on https://github.com/ppy/osu/pull/31146, else this will adjust the global volume. --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 170d247023..c343b4e1e6 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -144,8 +144,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), // Framework automatically converts wheel up/down to left/right when shift is held. // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38. - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), From 5a2cae89ff8a9035ca17af7e76a8b1ac7325a060 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 23:02:35 +0900 Subject: [PATCH 021/620] Fix free mod button overriding enabled state --- osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index dd6536cf26..952b15a873 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -36,8 +36,9 @@ namespace osu.Game.Screens.OnlinePlay } } - private OsuSpriteText count = null!; + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } + private OsuSpriteText count = null!; private Circle circle = null!; private readonly FreeModSelectOverlay freeModSelectOverlay; @@ -45,6 +46,9 @@ namespace osu.Game.Screens.OnlinePlay public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay) { this.freeModSelectOverlay = freeModSelectOverlay; + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + base.Action = toggleAllFreeMods; } [Resolved] @@ -98,9 +102,6 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateModDisplay(), true); - - // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - Action = toggleAllFreeMods; } /// From 159f6025b8a80a4d666506c47833190c0fcdcb71 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 Dec 2024 23:19:14 +0900 Subject: [PATCH 022/620] Fix incorrect behaviour --- osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs index 367857e780..bcc7bb787d 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.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.Color4Extensions; @@ -24,12 +25,20 @@ namespace osu.Game.Screens.OnlinePlay set => current.Current = value; } + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } + private OsuSpriteText text = null!; private Circle circle = null!; [Resolved] private OsuColour colours { get; set; } = null!; + public FooterButtonFreePlay() + { + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + base.Action = () => current.Value = !current.Value; + } + [BackgroundDependencyLoader] private void load() { @@ -70,9 +79,6 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateDisplay(), true); - - // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - Action = () => current.Value = !current.Value; } private void updateDisplay() From 80ae7942dfd4e6a8c4ece991243dfcc7e5cf167a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:52:50 +0900 Subject: [PATCH 023/620] Add christmas-specific logo heartbeat --- osu.Game/Screens/Menu/OsuLogo.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f2e2e25fa6..f3c37c6960 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -271,8 +271,16 @@ namespace osu.Game.Screens.Menu private void load(TextureStore textures, AudioManager audio) { sampleClick = audio.Samples.Get(@"Menu/osu-logo-select"); - sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); - sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); + + if (SeasonalUI.ENABLED) + { + sampleDownbeat = sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell"); + } + else + { + sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); + sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); + } logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); @@ -303,7 +311,10 @@ namespace osu.Game.Screens.Menu else { var channel = sampleBeat.GetChannel(); - channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1); + if (SeasonalUI.ENABLED) + channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); + else + channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1); channel.Play(); } }); From 180a381b6fb0973b04d414c6b7f4755a8958d724 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:57:12 +0900 Subject: [PATCH 024/620] Adjust menu side flashes to be brighter and coloured when seasonal active --- osu.Game/Screens/Menu/MenuSideFlashes.cs | 25 +++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 533c39826c..cc2d22a7fa 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -3,22 +3,23 @@ #nullable disable -using osuTK.Graphics; +using System; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Skinning; using osu.Game.Online.API; -using System; -using osu.Framework.Audio.Track; -using osu.Framework.Bindables; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Skinning; +using osuTK.Graphics; namespace osu.Game.Screens.Menu { @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Y, - Width = box_width * 2, + Width = box_width * (SeasonalUI.ENABLED ? 4 : 2), Height = 1.5f, // align off-screen to make sure our edges don't become visible during parallax. X = -box_width, @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, - Width = box_width * 2, + Width = box_width * (SeasonalUI.ENABLED ? 4 : 2), Height = 1.5f, X = box_width, Alpha = 0, @@ -104,7 +105,11 @@ namespace osu.Game.Screens.Menu private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) { - d.FadeTo(Math.Max(0, ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier)), box_fade_in_time) + if (SeasonalUI.ENABLED) + updateColour(); + + d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1), + box_fade_in_time) .Then() .FadeOut(beatLength, Easing.In); } @@ -113,7 +118,9 @@ namespace osu.Game.Screens.Menu { Color4 baseColour = colours.Blue; - if (user.Value?.IsSupporter ?? false) + if (SeasonalUI.ENABLED) + baseColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2; + else if (user.Value?.IsSupporter ?? false) baseColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? baseColour; // linear colour looks better in this case, so let's use it for now. From a4bf29e98f4aac7306164eb90edab065d83198eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:57:42 +0900 Subject: [PATCH 025/620] Adjust menu logo visualiser to use seasonal colours --- osu.Game/Screens/Menu/MenuLogoVisualisation.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index f4e992be9a..4537b79b62 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -3,12 +3,12 @@ #nullable disable -using osuTK.Graphics; -using osu.Game.Skinning; -using osu.Game.Online.API; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Skinning; +using osuTK.Graphics; namespace osu.Game.Screens.Menu { @@ -29,7 +29,9 @@ namespace osu.Game.Screens.Menu private void updateColour() { - if (user.Value?.IsSupporter ?? false) + if (SeasonalUI.ENABLED) + Colour = SeasonalUI.AMBIENT_COLOUR_1; + else if (user.Value?.IsSupporter ?? false) Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White; else Colour = Color4.White; From 618a9849e314a99aff70baec7f2b1ef295b4e1e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:59:31 +0900 Subject: [PATCH 026/620] Increase intro time allowance to account for seasonal tracks with actual long intros --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 0dc54b321f..9885c061a9 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -207,7 +207,7 @@ namespace osu.Game.Screens.Menu Text = NotificationsStrings.AudioPlaybackIssue }); } - }, 5000); + }, 8000); } public override void OnResuming(ScreenTransitionEvent e) From 024029822ab0e74880de27ce073fe88d735659b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:59:48 +0900 Subject: [PATCH 027/620] Add christmas intro --- .../Visual/Menus/TestSceneIntroChristmas.cs | 15 + osu.Game/Screens/Loader.cs | 3 + osu.Game/Screens/Menu/IntroChristmas.cs | 328 ++++++++++++++++++ osu.Game/Screens/SeasonalUI.cs | 21 ++ 4 files changed, 367 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs create mode 100644 osu.Game/Screens/Menu/IntroChristmas.cs create mode 100644 osu.Game/Screens/SeasonalUI.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs new file mode 100644 index 0000000000..13377f49df --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.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 NUnit.Framework; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public partial class TestSceneIntroChristmas : IntroTestScene + { + protected override bool IntroReliesOnTrack => true; + protected override IntroScreen CreateScreen() => new IntroChristmas(); + } +} diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index d71ee05b27..811e4600eb 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -37,6 +37,9 @@ namespace osu.Game.Screens private IntroScreen getIntroSequence() { + if (SeasonalUI.ENABLED) + return new IntroChristmas(createMainMenu); + if (introSequence == IntroSequence.Random) introSequence = (IntroSequence)RNG.Next(0, (int)IntroSequence.Random); diff --git a/osu.Game/Screens/Menu/IntroChristmas.cs b/osu.Game/Screens/Menu/IntroChristmas.cs new file mode 100644 index 0000000000..0a1cf32b85 --- /dev/null +++ b/osu.Game/Screens/Menu/IntroChristmas.cs @@ -0,0 +1,328 @@ +// 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.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Framework.Screens; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public partial class IntroChristmas : IntroScreen + { + protected override string BeatmapHash => "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"; + + protected override string BeatmapFile => "christmas2024.osz"; + + private const double beat_length = 60000 / 172.0; + private const double offset = 5924; + + protected override string SeeyaSampleName => "Intro/Welcome/seeya"; + + private TrianglesIntroSequence intro = null!; + + public IntroChristmas(Func? createNextScreen = null) + : base(createNextScreen) + { + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + if (!resuming) + { + PrepareMenuLoad(); + + var decouplingClock = new DecouplingFramedClock(UsingThemedIntro ? Track : null); + + LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground()) + { + RelativeSizeAxes = Axes.Both, + Clock = new InterpolatingFramedClock(decouplingClock), + LoadMenu = LoadMenu + }, _ => + { + AddInternal(intro); + + // There is a chance that the intro timed out before being displayed, and this scheduled callback could + // happen during the outro rather than intro. + // In such a scenario, we don't want to play the intro sample, nor attempt to start the intro track + // (that may have already been since disposed by MusicController). + if (DidLoadMenu) + return; + + // If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles. + // The triangles intro voice and theme are combined which makes it impossible to use. + StartTrack(); + + // no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure. + decouplingClock.Start(); + }); + } + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + base.OnSuspending(e); + + // important as there is a clock attached to a track which will likely be disposed before returning to this screen. + intro.Expire(); + } + + private partial class TrianglesIntroSequence : CompositeDrawable + { + private readonly OsuLogo logo; + private readonly Action showBackgroundAction; + private OsuSpriteText welcomeText = null!; + + private Container logoContainerSecondary = null!; + private LazerLogo lazerLogo = null!; + + private Drawable triangles = null!; + + public Action LoadMenu = null!; + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + public TrianglesIntroSequence(OsuLogo logo, Action showBackgroundAction) + { + this.logo = logo; + this.showBackgroundAction = showBackgroundAction; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new[] + { + welcomeText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Bottom = 10 }, + Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42), + Alpha = 1, + Spacing = new Vector2(5), + }, + logoContainerSecondary = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = lazerLogo = new LazerLogo + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }, + triangles = new CircularContainer + { + Alpha = 0, + Masking = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(960), + Child = new GlitchingTriangles + { + RelativeSizeAxes = Axes.Both, + }, + } + }; + } + + private static double getTimeForBeat(int beat) => offset + beat_length * beat; + + protected override void LoadComplete() + { + base.LoadComplete(); + + lazerLogo.Hide(); + + using (BeginAbsoluteSequence(0)) + { + using (BeginDelayedSequence(getTimeForBeat(-16))) + welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!"); + + using (BeginDelayedSequence(getTimeForBeat(-15))) + welcomeText.FadeIn().OnComplete(t => t.Text = ""); + + using (BeginDelayedSequence(getTimeForBeat(-14))) + welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!"); + + using (BeginDelayedSequence(getTimeForBeat(-13))) + welcomeText.FadeIn().OnComplete(t => t.Text = ""); + + using (BeginDelayedSequence(getTimeForBeat(-12))) + welcomeText.FadeIn().OnComplete(t => t.Text = "merry christmas!"); + + using (BeginDelayedSequence(getTimeForBeat(-11))) + welcomeText.FadeIn().OnComplete(t => t.Text = ""); + + using (BeginDelayedSequence(getTimeForBeat(-10))) + welcomeText.FadeIn().OnComplete(t => t.Text = "merry osumas!"); + + using (BeginDelayedSequence(getTimeForBeat(-9))) + { + welcomeText.FadeIn().OnComplete(t => t.Text = ""); + } + + lazerLogo.Scale = new Vector2(0.2f); + triangles.Scale = new Vector2(0.2f); + + for (int i = 0; i < 8; i++) + { + using (BeginDelayedSequence(getTimeForBeat(-8 + i))) + { + triangles.FadeIn(); + + lazerLogo.ScaleTo(new Vector2(0.2f + (i + 1) / 8f * 0.3f), beat_length * 1, Easing.OutQuint); + triangles.ScaleTo(new Vector2(0.2f + (i + 1) / 8f * 0.3f), beat_length * 1, Easing.OutQuint); + lazerLogo.FadeTo((i + 1) * 0.06f); + lazerLogo.TransformTo(nameof(LazerLogo.Progress), (i + 1) / 10f); + } + } + + GameWideFlash flash = new GameWideFlash(); + + using (BeginDelayedSequence(getTimeForBeat(-2))) + { + lazerLogo.FadeIn().OnComplete(_ => game.Add(flash)); + } + + flash.FadeInCompleted = () => + { + logoContainerSecondary.Remove(lazerLogo, true); + triangles.FadeOut(); + logo.FadeIn(); + showBackgroundAction(); + LoadMenu(); + }; + } + } + + private partial class GameWideFlash : Box + { + public Action? FadeInCompleted; + + public GameWideFlash() + { + Colour = Color4.White; + RelativeSizeAxes = Axes.Both; + Blending = BlendingParameters.Additive; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Alpha = 0; + + this.FadeTo(0.5f, beat_length * 2, Easing.In) + .OnComplete(_ => FadeInCompleted?.Invoke()); + + this.Delay(beat_length * 2) + .Then() + .FadeOutFromOne(3000, Easing.OutQuint); + } + } + + private partial class LazerLogo : CompositeDrawable + { + private LogoAnimation highlight = null!; + private LogoAnimation background = null!; + + public float Progress + { + get => background.AnimationProgress; + set + { + background.AnimationProgress = value; + highlight.AnimationProgress = value; + } + } + + public LazerLogo() + { + Size = new Vector2(960); + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + InternalChildren = new Drawable[] + { + highlight = new LogoAnimation + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(@"Intro/Triangles/logo-highlight"), + Colour = Color4.White, + }, + background = new LogoAnimation + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(@"Intro/Triangles/logo-background"), + Colour = OsuColour.Gray(0.6f), + }, + }; + } + } + + private partial class GlitchingTriangles : BeatSyncedContainer + { + private int beatsHandled; + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + Divisor = beatsHandled < 4 ? 1 : 4; + + for (int i = 0; i < (beatsHandled + 1); i++) + { + float angle = (float)(RNG.NextDouble() * 2 * Math.PI); + float randomRadius = (float)(Math.Sqrt(RNG.NextDouble())); + + float x = 0.5f + 0.5f * randomRadius * (float)Math.Cos(angle); + float y = 0.5f + 0.5f * randomRadius * (float)Math.Sin(angle); + + Color4 christmasColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2; + + Drawable triangle = new Triangle + { + Size = new Vector2(RNG.NextSingle() + 1.2f) * 80, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Both, + Position = new Vector2(x, y), + Colour = christmasColour + }; + + if (beatsHandled >= 10) + triangle.Blending = BlendingParameters.Additive; + + AddInternal(triangle); + triangle + .ScaleTo(0.9f) + .ScaleTo(1, beat_length / 2, Easing.Out); + triangle.FadeInFromZero(100, Easing.OutQuint); + } + + beatsHandled += 1; + } + } + } + } +} diff --git a/osu.Game/Screens/SeasonalUI.cs b/osu.Game/Screens/SeasonalUI.cs new file mode 100644 index 0000000000..ebe4d74301 --- /dev/null +++ b/osu.Game/Screens/SeasonalUI.cs @@ -0,0 +1,21 @@ +// 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.Extensions.Color4Extensions; +using osuTK.Graphics; + +namespace osu.Game.Screens +{ + public static class SeasonalUI + { + public static readonly bool ENABLED = true; + + public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex("D32F2F"); + + public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex("388E3C"); + + public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex("FFC"); + + public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex("FFE4B5"); + } +} From 0954e0b0321d6872e16b73055a7b171f1cbbc9f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 18:00:00 +0900 Subject: [PATCH 028/620] Add seasonal lighting Replaces kiai fountains for now. --- .../TestSceneMainMenuSeasonalLighting.cs | 46 +++++ osu.Game/Screens/Menu/MainMenu.cs | 4 +- .../Screens/Menu/MainMenuSeasonalLighting.cs | 188 ++++++++++++++++++ 3 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs create mode 100644 osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs new file mode 100644 index 0000000000..bfdc07fba6 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -0,0 +1,46 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneMainMenuSeasonalLighting : OsuTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("prepare beatmap", () => + { + var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"); + + Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo!.Value.Beatmaps.First()); + }); + + AddStep("create lighting", () => Child = new MainMenuSeasonalLighting()); + + AddStep("restart beatmap", () => + { + Beatmap.Value.Track.Start(); + Beatmap.Value.Track.Seek(4000); + }); + } + + [Test] + public void TestBasic() + { + } + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 0630b9612e..42aa2342da 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -124,6 +124,7 @@ namespace osu.Game.Screens.Menu AddRangeInternal(new[] { + SeasonalUI.ENABLED ? new MainMenuSeasonalLighting() : Empty(), buttonsContainer = new ParallaxContainer { ParallaxAmount = 0.01f, @@ -166,7 +167,8 @@ namespace osu.Game.Screens.Menu Origin = Anchor.TopRight, Margin = new MarginPadding { Right = 15, Top = 5 } }, - new KiaiMenuFountains(), + // For now, this is too much alongside the seasonal lighting. + SeasonalUI.ENABLED ? Empty() : new KiaiMenuFountains(), bottomElementsFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs new file mode 100644 index 0000000000..7ba4e998d2 --- /dev/null +++ b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs @@ -0,0 +1,188 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public partial class MainMenuSeasonalLighting : CompositeDrawable + { + private IBindable working = null!; + + private InterpolatingFramedClock beatmapClock = null!; + + private List hitObjects = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public MainMenuSeasonalLighting() + { + RelativeChildSize = new Vector2(512, 384); + + RelativeSizeAxes = Axes.X; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(IBindable working) + { + this.working = working.GetBoundCopy(); + this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true); + } + + private void updateBeatmap() + { + lastObjectIndex = null; + beatmapClock = new InterpolatingFramedClock(new FramedClock(working.Value.Track)); + hitObjects = working.Value.GetPlayableBeatmap(rulesets.GetRuleset(0)).HitObjects.SelectMany(h => h.NestedHitObjects.Prepend(h)) + .OrderBy(h => h.StartTime) + .ToList(); + } + + private int? lastObjectIndex; + + protected override void Update() + { + base.Update(); + + Height = DrawWidth / 16 * 10; + + beatmapClock.ProcessFrame(); + + // intentionally slightly early since we are doing fades on the lighting. + double time = beatmapClock.CurrentTime + 50; + + // handle seeks or OOB by skipping to current. + if (lastObjectIndex == null || lastObjectIndex >= hitObjects.Count || (lastObjectIndex >= 0 && hitObjects[lastObjectIndex.Value].StartTime > time) + || Math.Abs(beatmapClock.ElapsedFrameTime) > 500) + lastObjectIndex = hitObjects.Count(h => h.StartTime < time) - 1; + + while (lastObjectIndex < hitObjects.Count - 1) + { + var h = hitObjects[lastObjectIndex.Value + 1]; + + if (h.StartTime > time) + break; + + // Don't add lighting if the game is running too slow. + if (Clock.ElapsedFrameTime < 20) + addLight(h); + + lastObjectIndex++; + } + } + + private void addLight(HitObject h) + { + var light = new Light + { + RelativePositionAxes = Axes.Both, + Position = ((IHasPosition)h).Position + }; + + AddInternal(light); + + if (h.GetType().Name.Contains("Tick")) + { + light.Colour = SeasonalUI.AMBIENT_COLOUR_1; + light.Scale = new Vector2(0.5f); + light + .FadeInFromZero(250) + .Then() + .FadeOutFromOne(1000, Easing.Out); + + light.MoveToOffset(new Vector2(RNG.Next(-20, 20), RNG.Next(-20, 20)), 1400, Easing.Out); + } + else + { + // default green + Color4 col = SeasonalUI.PRIMARY_COLOUR_2; + + // whistle red + if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE)) + col = SeasonalUI.PRIMARY_COLOUR_1; + // clap is third colour + else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)) + col = SeasonalUI.AMBIENT_COLOUR_1; + + light.Colour = col; + + // finish larger lighting + if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH)) + light.Scale = new Vector2(3); + + light + .FadeInFromZero(150) + .Then() + .FadeOutFromOne(1000, Easing.In); + + light.Expire(); + } + } + + public partial class Light : CompositeDrawable + { + private readonly Circle circle; + + public new Color4 Colour + { + set + { + circle.Colour = value.Darken(0.8f); + circle.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = value, + Radius = 80, + }; + } + } + + public Light() + { + InternalChildren = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(12), + Colour = SeasonalUI.AMBIENT_COLOUR_1, + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = SeasonalUI.AMBIENT_COLOUR_2, + Radius = 80, + } + } + }; + + Origin = Anchor.Centre; + Alpha = 0.5f; + } + } + } +} From 22f3831c0d46d11f7770c62c2dab4c2ee1132e36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 18:44:44 +0900 Subject: [PATCH 029/620] Add logo hat --- .../Visual/UserInterface/TestSceneOsuLogo.cs | 11 +++- osu.Game/Screens/Menu/OsuLogo.cs | 50 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs index 62a493815b..c112d26870 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs @@ -4,22 +4,31 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Menu; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneOsuLogo : OsuTestScene { + private OsuLogo? logo; + [Test] public void TestBasic() { AddStep("Add logo", () => { - Child = new OsuLogo + Child = logo = new OsuLogo { Anchor = Anchor.Centre, Origin = Anchor.Centre, }; }); + + AddSliderStep("scale", 0.1, 2, 1, scale => + { + if (logo != null) + Child.Scale = new Vector2((float)scale); + }); } } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f3c37c6960..2c62a10a8f 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -211,6 +212,15 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, }, + SeasonalUI.ENABLED + ? hat = new Sprite + { + BypassAutoSizeAxes = Axes.Both, + Alpha = 0, + Origin = Anchor.BottomCentre, + Scale = new Vector2(-1, 1), + } + : Empty(), } }, impactContainer = new CircularContainer @@ -284,6 +294,8 @@ namespace osu.Game.Screens.Menu logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); + if (hat != null) + hat.Texture = textures.Get(@"Menu/hat"); } private int lastBeatIndex; @@ -369,6 +381,9 @@ namespace osu.Game.Screens.Menu const float scale_adjust_cutoff = 0.4f; + if (SeasonalUI.ENABLED) + updateHat(); + if (musicController.CurrentTrack.IsRunning) { float maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0; @@ -382,6 +397,38 @@ namespace osu.Game.Screens.Menu } } + private bool hasHat; + + private void updateHat() + { + if (hat == null) + return; + + bool shouldHat = DrawWidth * Scale.X < 400; + + if (shouldHat != hasHat) + { + hasHat = shouldHat; + + if (hasHat) + { + hat.Delay(400) + .Then() + .MoveTo(new Vector2(120, 160)) + .RotateTo(0) + .RotateTo(-20, 500, Easing.OutQuint) + .FadeIn(250, Easing.OutQuint); + } + else + { + hat.Delay(100) + .Then() + .MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint) + .FadeOut(500, Easing.OutQuint); + } + } + } + public override bool HandlePositionalInput => base.HandlePositionalInput && Alpha > 0.2f; protected override bool OnMouseDown(MouseDownEvent e) @@ -459,6 +506,9 @@ namespace osu.Game.Screens.Menu private Container currentProxyTarget; private Drawable proxy; + [CanBeNull] + private readonly Sprite hat; + public void StopSamplePlayback() => sampleClickChannel?.Stop(); public Drawable ProxyToContainer(Container c) From 4924a35c3133345ebc314d1fea03c8c69d8665c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 19:14:48 +0900 Subject: [PATCH 030/620] Fix light expiry --- osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs index 7ba4e998d2..fb16e8e0bb 100644 --- a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs +++ b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -137,9 +136,9 @@ namespace osu.Game.Screens.Menu .FadeInFromZero(150) .Then() .FadeOutFromOne(1000, Easing.In); - - light.Expire(); } + + light.Expire(); } public partial class Light : CompositeDrawable From 8c7af79f9667e1cd4db2e1ec3f480f98542b5945 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:21:45 +0900 Subject: [PATCH 031/620] Tidy up for pull request attempt --- .../TestSceneMainMenuSeasonalLighting.cs | 6 +-- osu.Game/Screens/Menu/IntroChristmas.cs | 5 ++- .../Screens/Menu/MainMenuSeasonalLighting.cs | 38 +++++++++++++------ osu.Game/Screens/Menu/OsuLogo.cs | 2 +- osu.Game/Screens/SeasonalUI.cs | 8 ++-- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs index bfdc07fba6..81862da9df 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus @@ -16,15 +15,12 @@ namespace osu.Game.Tests.Visual.Menus [Resolved] private BeatmapManager beatmaps { get; set; } = null!; - [Resolved] - private RealmAccess realm { get; set; } = null!; - [SetUpSteps] public void SetUpSteps() { AddStep("prepare beatmap", () => { - var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"); + var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH); Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo!.Value.Beatmaps.First()); }); diff --git a/osu.Game/Screens/Menu/IntroChristmas.cs b/osu.Game/Screens/Menu/IntroChristmas.cs index 0a1cf32b85..273baa3c52 100644 --- a/osu.Game/Screens/Menu/IntroChristmas.cs +++ b/osu.Game/Screens/Menu/IntroChristmas.cs @@ -22,7 +22,10 @@ namespace osu.Game.Screens.Menu { public partial class IntroChristmas : IntroScreen { - protected override string BeatmapHash => "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"; + // nekodex - circle the halls + public const string CHRISTMAS_BEATMAP_SET_HASH = "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"; + + protected override string BeatmapHash => CHRISTMAS_BEATMAP_SET_HASH; protected override string BeatmapFile => "christmas2024.osz"; diff --git a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs index fb16e8e0bb..f46a1387ab 100644 --- a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs +++ b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs @@ -31,11 +31,13 @@ namespace osu.Game.Screens.Menu private List hitObjects = null!; - [Resolved] - private RulesetStore rulesets { get; set; } = null!; + private RulesetInfo? osuRuleset; + + private int? lastObjectIndex; public MainMenuSeasonalLighting() { + // match beatmap playfield RelativeChildSize = new Vector2(512, 384); RelativeSizeAxes = Axes.X; @@ -45,23 +47,37 @@ namespace osu.Game.Screens.Menu } [BackgroundDependencyLoader] - private void load(IBindable working) + private void load(IBindable working, RulesetStore rulesets) { this.working = working.GetBoundCopy(); this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true); + + // operate in osu! ruleset to keep things simple for now. + osuRuleset = rulesets.GetRuleset(0); } private void updateBeatmap() { lastObjectIndex = null; + + if (osuRuleset == null) + { + beatmapClock = new InterpolatingFramedClock(Clock); + hitObjects = new List(); + return; + } + + // Intentionally maintain separately so the lighting is not in audio clock space (it shouldn't rewind etc.) beatmapClock = new InterpolatingFramedClock(new FramedClock(working.Value.Track)); - hitObjects = working.Value.GetPlayableBeatmap(rulesets.GetRuleset(0)).HitObjects.SelectMany(h => h.NestedHitObjects.Prepend(h)) + + hitObjects = working.Value + .GetPlayableBeatmap(osuRuleset) + .HitObjects + .SelectMany(h => h.NestedHitObjects.Prepend(h)) .OrderBy(h => h.StartTime) .ToList(); } - private int? lastObjectIndex; - protected override void Update() { base.Update(); @@ -116,19 +132,19 @@ namespace osu.Game.Screens.Menu } else { - // default green + // default are green Color4 col = SeasonalUI.PRIMARY_COLOUR_2; - // whistle red + // whistles are red if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE)) col = SeasonalUI.PRIMARY_COLOUR_1; - // clap is third colour + // clap is third ambient (yellow) colour else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)) col = SeasonalUI.AMBIENT_COLOUR_1; light.Colour = col; - // finish larger lighting + // finish results in larger lighting if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH)) light.Scale = new Vector2(3); @@ -141,7 +157,7 @@ namespace osu.Game.Screens.Menu light.Expire(); } - public partial class Light : CompositeDrawable + private partial class Light : CompositeDrawable { private readonly Circle circle; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 2c62a10a8f..272f53e087 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Menu new Container { AutoSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { logoContainer = new CircularContainer { diff --git a/osu.Game/Screens/SeasonalUI.cs b/osu.Game/Screens/SeasonalUI.cs index ebe4d74301..fc2303f285 100644 --- a/osu.Game/Screens/SeasonalUI.cs +++ b/osu.Game/Screens/SeasonalUI.cs @@ -10,12 +10,12 @@ namespace osu.Game.Screens { public static readonly bool ENABLED = true; - public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex("D32F2F"); + public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex(@"D32F2F"); - public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex("388E3C"); + public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex(@"388E3C"); - public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex("FFC"); + public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex(@"FFFFCC"); - public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex("FFE4B5"); + public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex(@"FFE4B5"); } } From e5dbf9ce453e359a2e07b375ba9cbdcbe159b764 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:46:34 +0900 Subject: [PATCH 032/620] Subclass osu logo instead of adding much code to it --- .../TestSceneMainMenuSeasonalLighting.cs | 1 + .../Visual/UserInterface/TestSceneOsuLogo.cs | 29 ++++++- osu.Game/OsuGame.cs | 6 +- osu.Game/Screens/Loader.cs | 3 +- osu.Game/Screens/Menu/IntroChristmas.cs | 3 +- osu.Game/Screens/Menu/MainMenu.cs | 5 +- .../Screens/Menu/MenuLogoVisualisation.cs | 5 +- osu.Game/Screens/Menu/MenuSideFlashes.cs | 11 +-- osu.Game/Screens/Menu/OsuLogo.cs | 83 ++++--------------- .../MainMenuSeasonalLighting.cs | 14 ++-- osu.Game/Seasonal/OsuLogoChristmas.cs | 74 +++++++++++++++++ .../SeasonalUIConfig.cs} | 7 +- 12 files changed, 148 insertions(+), 93 deletions(-) rename osu.Game/{Screens/Menu => Seasonal}/MainMenuSeasonalLighting.cs (93%) create mode 100644 osu.Game/Seasonal/OsuLogoChristmas.cs rename osu.Game/{Screens/SeasonalUI.cs => Seasonal/SeasonalUIConfig.cs} (78%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs index 81862da9df..bf499f1beb 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.Menu; +using osu.Game.Seasonal; namespace osu.Game.Tests.Visual.Menus { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs index c112d26870..27d2ff97fa 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Menu; +using osu.Game.Seasonal; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -12,6 +13,19 @@ namespace osu.Game.Tests.Visual.UserInterface { private OsuLogo? logo; + private float scale = 1; + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("scale", 0.1, 2, 1, scale => + { + if (logo != null) + Child.Scale = new Vector2(this.scale = (float)scale); + }); + } + [Test] public void TestBasic() { @@ -21,13 +35,22 @@ namespace osu.Game.Tests.Visual.UserInterface { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Scale = new Vector2(scale), }; }); + } - AddSliderStep("scale", 0.1, 2, 1, scale => + [Test] + public void TestChristmas() + { + AddStep("Add logo", () => { - if (logo != null) - Child.Scale = new Vector2((float)scale); + Child = logo = new OsuLogoChristmas + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(scale), + }; }); } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e808e570c7..0dd1746aa4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -69,6 +69,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Seasonal; using osu.Game.Skinning; using osu.Game.Updater; using osu.Game.Users; @@ -362,7 +363,10 @@ namespace osu.Game { SentryLogger.AttachUser(API.LocalUser); - dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 }); + if (SeasonalUIConfig.ENABLED) + dependencies.CacheAs(osuLogo = new OsuLogoChristmas { Alpha = 0 }); + else + dependencies.CacheAs(osuLogo = new OsuLogo { Alpha = 0 }); // bind config int to database RulesetInfo configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 811e4600eb..dfa5d2c369 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -15,6 +15,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Seasonal; using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Screens @@ -37,7 +38,7 @@ namespace osu.Game.Screens private IntroScreen getIntroSequence() { - if (SeasonalUI.ENABLED) + if (SeasonalUIConfig.ENABLED) return new IntroChristmas(createMainMenu); if (introSequence == IntroSequence.Random) diff --git a/osu.Game/Screens/Menu/IntroChristmas.cs b/osu.Game/Screens/Menu/IntroChristmas.cs index 273baa3c52..aa16f33c3d 100644 --- a/osu.Game/Screens/Menu/IntroChristmas.cs +++ b/osu.Game/Screens/Menu/IntroChristmas.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Seasonal; using osuTK; using osuTK.Graphics; @@ -302,7 +303,7 @@ namespace osu.Game.Screens.Menu float x = 0.5f + 0.5f * randomRadius * (float)Math.Cos(angle); float y = 0.5f + 0.5f * randomRadius * (float)Math.Sin(angle); - Color4 christmasColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2; + Color4 christmasColour = RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2; Drawable triangle = new Triangle { diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 42aa2342da..a4b269ad0d 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -35,6 +35,7 @@ using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Select; +using osu.Game.Seasonal; using osuTK; using osuTK.Graphics; @@ -124,7 +125,7 @@ namespace osu.Game.Screens.Menu AddRangeInternal(new[] { - SeasonalUI.ENABLED ? new MainMenuSeasonalLighting() : Empty(), + SeasonalUIConfig.ENABLED ? new MainMenuSeasonalLighting() : Empty(), buttonsContainer = new ParallaxContainer { ParallaxAmount = 0.01f, @@ -168,7 +169,7 @@ namespace osu.Game.Screens.Menu Margin = new MarginPadding { Right = 15, Top = 5 } }, // For now, this is too much alongside the seasonal lighting. - SeasonalUI.ENABLED ? Empty() : new KiaiMenuFountains(), + SeasonalUIConfig.ENABLED ? Empty() : new KiaiMenuFountains(), bottomElementsFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index 4537b79b62..32b5c706a3 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Seasonal; using osu.Game.Skinning; using osuTK.Graphics; @@ -29,8 +30,8 @@ namespace osu.Game.Screens.Menu private void updateColour() { - if (SeasonalUI.ENABLED) - Colour = SeasonalUI.AMBIENT_COLOUR_1; + if (SeasonalUIConfig.ENABLED) + Colour = SeasonalUIConfig.AMBIENT_COLOUR_1; else if (user.Value?.IsSupporter ?? false) Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White; else diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index cc2d22a7fa..808da5dd47 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Seasonal; using osu.Game.Skinning; using osuTK.Graphics; @@ -68,7 +69,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Y, - Width = box_width * (SeasonalUI.ENABLED ? 4 : 2), + Width = box_width * (SeasonalUIConfig.ENABLED ? 4 : 2), Height = 1.5f, // align off-screen to make sure our edges don't become visible during parallax. X = -box_width, @@ -80,7 +81,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, - Width = box_width * (SeasonalUI.ENABLED ? 4 : 2), + Width = box_width * (SeasonalUIConfig.ENABLED ? 4 : 2), Height = 1.5f, X = box_width, Alpha = 0, @@ -105,7 +106,7 @@ namespace osu.Game.Screens.Menu private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) { - if (SeasonalUI.ENABLED) + if (SeasonalUIConfig.ENABLED) updateColour(); d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1), @@ -118,8 +119,8 @@ namespace osu.Game.Screens.Menu { Color4 baseColour = colours.Blue; - if (SeasonalUI.ENABLED) - baseColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2; + if (SeasonalUIConfig.ENABLED) + baseColour = RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2; else if (user.Value?.IsSupporter ?? false) baseColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? baseColour; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 272f53e087..dc2dfefddb 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -54,8 +53,10 @@ namespace osu.Game.Screens.Menu private Sample sampleClick; private SampleChannel sampleClickChannel; - private Sample sampleBeat; - private Sample sampleDownbeat; + protected virtual double BeatSampleVariance => 0.1; + + protected Sample SampleBeat; + protected Sample SampleDownbeat; private readonly Container colourAndTriangles; private readonly TrianglesV2 triangles; @@ -160,10 +161,10 @@ namespace osu.Game.Screens.Menu Alpha = visualizer_default_alpha, Size = SCALE_ADJUST }, - new Container + LogoElements = new Container { AutoSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { logoContainer = new CircularContainer { @@ -212,15 +213,6 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - SeasonalUI.ENABLED - ? hat = new Sprite - { - BypassAutoSizeAxes = Axes.Both, - Alpha = 0, - Origin = Anchor.BottomCentre, - Scale = new Vector2(-1, 1), - } - : Empty(), } }, impactContainer = new CircularContainer @@ -253,6 +245,8 @@ namespace osu.Game.Screens.Menu }; } + public Container LogoElements { get; private set; } + /// /// Schedule a new external animation. Handled queueing and finishing previous animations in a sane way. /// @@ -282,20 +276,11 @@ namespace osu.Game.Screens.Menu { sampleClick = audio.Samples.Get(@"Menu/osu-logo-select"); - if (SeasonalUI.ENABLED) - { - sampleDownbeat = sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell"); - } - else - { - sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); - sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); - } + SampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); + SampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); - if (hat != null) - hat.Texture = textures.Get(@"Menu/hat"); } private int lastBeatIndex; @@ -318,15 +303,13 @@ namespace osu.Game.Screens.Menu { if (beatIndex % timingPoint.TimeSignature.Numerator == 0) { - sampleDownbeat?.Play(); + SampleDownbeat?.Play(); } else { - var channel = sampleBeat.GetChannel(); - if (SeasonalUI.ENABLED) - channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); - else - channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1); + var channel = SampleBeat.GetChannel(); + + channel.Frequency.Value = 1 - BeatSampleVariance / 2 + RNG.NextDouble(BeatSampleVariance); channel.Play(); } }); @@ -381,9 +364,6 @@ namespace osu.Game.Screens.Menu const float scale_adjust_cutoff = 0.4f; - if (SeasonalUI.ENABLED) - updateHat(); - if (musicController.CurrentTrack.IsRunning) { float maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0; @@ -397,38 +377,6 @@ namespace osu.Game.Screens.Menu } } - private bool hasHat; - - private void updateHat() - { - if (hat == null) - return; - - bool shouldHat = DrawWidth * Scale.X < 400; - - if (shouldHat != hasHat) - { - hasHat = shouldHat; - - if (hasHat) - { - hat.Delay(400) - .Then() - .MoveTo(new Vector2(120, 160)) - .RotateTo(0) - .RotateTo(-20, 500, Easing.OutQuint) - .FadeIn(250, Easing.OutQuint); - } - else - { - hat.Delay(100) - .Then() - .MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint) - .FadeOut(500, Easing.OutQuint); - } - } - } - public override bool HandlePositionalInput => base.HandlePositionalInput && Alpha > 0.2f; protected override bool OnMouseDown(MouseDownEvent e) @@ -506,9 +454,6 @@ namespace osu.Game.Screens.Menu private Container currentProxyTarget; private Drawable proxy; - [CanBeNull] - private readonly Sprite hat; - public void StopSamplePlayback() => sampleClickChannel?.Stop(); public Drawable ProxyToContainer(Container c) diff --git a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs similarity index 93% rename from osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs rename to osu.Game/Seasonal/MainMenuSeasonalLighting.cs index f46a1387ab..a382785499 100644 --- a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs +++ b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs @@ -21,7 +21,7 @@ using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Menu +namespace osu.Game.Seasonal { public partial class MainMenuSeasonalLighting : CompositeDrawable { @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Menu if (h.GetType().Name.Contains("Tick")) { - light.Colour = SeasonalUI.AMBIENT_COLOUR_1; + light.Colour = SeasonalUIConfig.AMBIENT_COLOUR_1; light.Scale = new Vector2(0.5f); light .FadeInFromZero(250) @@ -133,14 +133,14 @@ namespace osu.Game.Screens.Menu else { // default are green - Color4 col = SeasonalUI.PRIMARY_COLOUR_2; + Color4 col = SeasonalUIConfig.PRIMARY_COLOUR_2; // whistles are red if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE)) - col = SeasonalUI.PRIMARY_COLOUR_1; + col = SeasonalUIConfig.PRIMARY_COLOUR_1; // clap is third ambient (yellow) colour else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)) - col = SeasonalUI.AMBIENT_COLOUR_1; + col = SeasonalUIConfig.AMBIENT_COLOUR_1; light.Colour = col; @@ -184,12 +184,12 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(12), - Colour = SeasonalUI.AMBIENT_COLOUR_1, + Colour = SeasonalUIConfig.AMBIENT_COLOUR_1, Blending = BlendingParameters.Additive, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = SeasonalUI.AMBIENT_COLOUR_2, + Colour = SeasonalUIConfig.AMBIENT_COLOUR_2, Radius = 80, } } diff --git a/osu.Game/Seasonal/OsuLogoChristmas.cs b/osu.Game/Seasonal/OsuLogoChristmas.cs new file mode 100644 index 0000000000..ec9cac94ea --- /dev/null +++ b/osu.Game/Seasonal/OsuLogoChristmas.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Seasonal +{ + public partial class OsuLogoChristmas : OsuLogo + { + protected override double BeatSampleVariance => 0.02; + + private Sprite? hat; + + private bool hasHat; + + [BackgroundDependencyLoader] + private void load(TextureStore textures, AudioManager audio) + { + LogoElements.Add(hat = new Sprite + { + BypassAutoSizeAxes = Axes.Both, + Alpha = 0, + Origin = Anchor.BottomCentre, + Scale = new Vector2(-1, 1), + Texture = textures.Get(@"Menu/hat"), + }); + + // override base samples with our preferred ones. + SampleDownbeat = SampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell"); + } + + protected override void Update() + { + base.Update(); + updateHat(); + } + + private void updateHat() + { + if (hat == null) + return; + + bool shouldHat = DrawWidth * Scale.X < 400; + + if (shouldHat != hasHat) + { + hasHat = shouldHat; + + if (hasHat) + { + hat.Delay(400) + .Then() + .MoveTo(new Vector2(120, 160)) + .RotateTo(0) + .RotateTo(-20, 500, Easing.OutQuint) + .FadeIn(250, Easing.OutQuint); + } + else + { + hat.Delay(100) + .Then() + .MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint) + .FadeOut(500, Easing.OutQuint); + } + } + } + } +} diff --git a/osu.Game/Screens/SeasonalUI.cs b/osu.Game/Seasonal/SeasonalUIConfig.cs similarity index 78% rename from osu.Game/Screens/SeasonalUI.cs rename to osu.Game/Seasonal/SeasonalUIConfig.cs index fc2303f285..060913a8bf 100644 --- a/osu.Game/Screens/SeasonalUI.cs +++ b/osu.Game/Seasonal/SeasonalUIConfig.cs @@ -4,9 +4,12 @@ using osu.Framework.Extensions.Color4Extensions; using osuTK.Graphics; -namespace osu.Game.Screens +namespace osu.Game.Seasonal { - public static class SeasonalUI + /// + /// General configuration setting for seasonal event adjustments to the game. + /// + public static class SeasonalUIConfig { public static readonly bool ENABLED = true; From 2a720ef200897f0430a630d2d565ab52c8875278 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:51:33 +0900 Subject: [PATCH 033/620] Move christmas intro screen to seasonal namespace --- osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs | 1 + .../Visual/Menus/TestSceneMainMenuSeasonalLighting.cs | 1 - osu.Game/{Screens/Menu => Seasonal}/IntroChristmas.cs | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename osu.Game/{Screens/Menu => Seasonal}/IntroChristmas.cs (99%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs index 13377f49df..0398b4fbb6 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Game.Screens.Menu; +using osu.Game.Seasonal; namespace osu.Game.Tests.Visual.Menus { diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs index bf499f1beb..11356f7eeb 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Screens.Menu; using osu.Game.Seasonal; namespace osu.Game.Tests.Visual.Menus diff --git a/osu.Game/Screens/Menu/IntroChristmas.cs b/osu.Game/Seasonal/IntroChristmas.cs similarity index 99% rename from osu.Game/Screens/Menu/IntroChristmas.cs rename to osu.Game/Seasonal/IntroChristmas.cs index aa16f33c3d..ac3286f277 100644 --- a/osu.Game/Screens/Menu/IntroChristmas.cs +++ b/osu.Game/Seasonal/IntroChristmas.cs @@ -15,11 +15,11 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Seasonal; +using osu.Game.Screens.Menu; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Menu +namespace osu.Game.Seasonal { public partial class IntroChristmas : IntroScreen { From ad4a8a1e0a345c75b0f43186f00d985e653ad7bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:58:45 +0900 Subject: [PATCH 034/620] Subclass menu flashes instead of adding local code to it --- osu.Game/Screens/Menu/MainMenu.cs | 2 +- osu.Game/Screens/Menu/MenuSideFlashes.cs | 31 +++++++++++++------- osu.Game/Seasonal/SeasonalMenuSideFlashes.cs | 18 ++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 osu.Game/Seasonal/SeasonalMenuSideFlashes.cs diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index a4b269ad0d..58d97bfe16 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Menu } }, logoTarget = new Container { RelativeSizeAxes = Axes.Both, }, - sideFlashes = new MenuSideFlashes(), + sideFlashes = SeasonalUIConfig.ENABLED ? new SeasonalMenuSideFlashes() : new MenuSideFlashes(), songTicker = new SongTicker { Anchor = Anchor.TopRight, diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 808da5dd47..426896825e 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -11,14 +11,12 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Seasonal; using osu.Game.Skinning; using osuTK.Graphics; @@ -26,6 +24,10 @@ namespace osu.Game.Screens.Menu { public partial class MenuSideFlashes : BeatSyncedContainer { + protected virtual bool RefreshColoursEveryFlash => false; + + protected virtual float Intensity => 2; + private readonly IBindable beatmap = new Bindable(); private Box leftBox; @@ -69,7 +71,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Y, - Width = box_width * (SeasonalUIConfig.ENABLED ? 4 : 2), + Width = box_width * Intensity, Height = 1.5f, // align off-screen to make sure our edges don't become visible during parallax. X = -box_width, @@ -81,7 +83,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, - Width = box_width * (SeasonalUIConfig.ENABLED ? 4 : 2), + Width = box_width * Intensity, Height = 1.5f, X = box_width, Alpha = 0, @@ -89,8 +91,11 @@ namespace osu.Game.Screens.Menu } }; - user.ValueChanged += _ => updateColour(); - skin.BindValueChanged(_ => updateColour(), true); + if (!RefreshColoursEveryFlash) + { + user.ValueChanged += _ => updateColour(); + skin.BindValueChanged(_ => updateColour(), true); + } } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) @@ -106,7 +111,7 @@ namespace osu.Game.Screens.Menu private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) { - if (SeasonalUIConfig.ENABLED) + if (RefreshColoursEveryFlash) updateColour(); d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1), @@ -115,15 +120,19 @@ namespace osu.Game.Screens.Menu .FadeOut(beatLength, Easing.In); } - private void updateColour() + protected virtual Color4 GetBaseColour() { Color4 baseColour = colours.Blue; - if (SeasonalUIConfig.ENABLED) - baseColour = RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2; - else if (user.Value?.IsSupporter ?? false) + if (user.Value?.IsSupporter ?? false) baseColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? baseColour; + return baseColour; + } + + private void updateColour() + { + var baseColour = GetBaseColour(); // linear colour looks better in this case, so let's use it for now. Color4 gradientDark = baseColour.Opacity(0).ToLinear(); Color4 gradientLight = baseColour.Opacity(0.6f).ToLinear(); diff --git a/osu.Game/Seasonal/SeasonalMenuSideFlashes.cs b/osu.Game/Seasonal/SeasonalMenuSideFlashes.cs new file mode 100644 index 0000000000..46a0a973bb --- /dev/null +++ b/osu.Game/Seasonal/SeasonalMenuSideFlashes.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Utils; +using osu.Game.Screens.Menu; +using osuTK.Graphics; + +namespace osu.Game.Seasonal +{ + public partial class SeasonalMenuSideFlashes : MenuSideFlashes + { + protected override bool RefreshColoursEveryFlash => true; + + protected override float Intensity => 4; + + protected override Color4 GetBaseColour() => RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2; + } +} From 8e9377914d96a4d65a96335da0cd169e3721128d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 15:04:37 +0900 Subject: [PATCH 035/620] Subclass menu logo visualisation --- .../Screens/Menu/MenuLogoVisualisation.cs | 19 +++++++------------ osu.Game/Screens/Menu/OsuLogo.cs | 16 +++++++++------- osu.Game/Seasonal/OsuLogoChristmas.cs | 2 ++ .../Seasonal/SeasonalMenuLogoVisualisation.cs | 12 ++++++++++++ 4 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Seasonal/SeasonalMenuLogoVisualisation.cs diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index 32b5c706a3..f152c0c93c 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -1,22 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Seasonal; using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Screens.Menu { - internal partial class MenuLogoVisualisation : LogoVisualisation + public partial class MenuLogoVisualisation : LogoVisualisation { - private IBindable user; - private Bindable skin; + private IBindable user = null!; + private Bindable skin = null!; [BackgroundDependencyLoader] private void load(IAPIProvider api, SkinManager skinManager) @@ -24,15 +21,13 @@ namespace osu.Game.Screens.Menu user = api.LocalUser.GetBoundCopy(); skin = skinManager.CurrentSkin.GetBoundCopy(); - user.ValueChanged += _ => updateColour(); - skin.BindValueChanged(_ => updateColour(), true); + user.ValueChanged += _ => UpdateColour(); + skin.BindValueChanged(_ => UpdateColour(), true); } - private void updateColour() + protected virtual void UpdateColour() { - if (SeasonalUIConfig.ENABLED) - Colour = SeasonalUIConfig.AMBIENT_COLOUR_1; - else if (user.Value?.IsSupporter ?? false) + if (user.Value?.IsSupporter ?? false) Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White; else Colour = Color4.White; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index dc2dfefddb..31f47c1349 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -53,6 +53,8 @@ namespace osu.Game.Screens.Menu private Sample sampleClick; private SampleChannel sampleClickChannel; + protected virtual MenuLogoVisualisation CreateMenuLogoVisualisation() => new MenuLogoVisualisation(); + protected virtual double BeatSampleVariance => 0.1; protected Sample SampleBeat; @@ -153,14 +155,14 @@ namespace osu.Game.Screens.Menu AutoSizeAxes = Axes.Both, Children = new Drawable[] { - visualizer = new MenuLogoVisualisation + visualizer = CreateMenuLogoVisualisation().With(v => { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Alpha = visualizer_default_alpha, - Size = SCALE_ADJUST - }, + v.RelativeSizeAxes = Axes.Both; + v.Origin = Anchor.Centre; + v.Anchor = Anchor.Centre; + v.Alpha = visualizer_default_alpha; + v.Size = SCALE_ADJUST; + }), LogoElements = new Container { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Seasonal/OsuLogoChristmas.cs b/osu.Game/Seasonal/OsuLogoChristmas.cs index ec9cac94ea..8975a69c32 100644 --- a/osu.Game/Seasonal/OsuLogoChristmas.cs +++ b/osu.Game/Seasonal/OsuLogoChristmas.cs @@ -19,6 +19,8 @@ namespace osu.Game.Seasonal private bool hasHat; + protected override MenuLogoVisualisation CreateMenuLogoVisualisation() => new SeasonalMenuLogoVisualisation(); + [BackgroundDependencyLoader] private void load(TextureStore textures, AudioManager audio) { diff --git a/osu.Game/Seasonal/SeasonalMenuLogoVisualisation.cs b/osu.Game/Seasonal/SeasonalMenuLogoVisualisation.cs new file mode 100644 index 0000000000..f00da3fe7e --- /dev/null +++ b/osu.Game/Seasonal/SeasonalMenuLogoVisualisation.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Screens.Menu; + +namespace osu.Game.Seasonal +{ + internal partial class SeasonalMenuLogoVisualisation : MenuLogoVisualisation + { + protected override void UpdateColour() => Colour = SeasonalUIConfig.AMBIENT_COLOUR_1; + } +} From 3fc99904113036e4edd0fbd750e17605e900d953 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 15:28:33 +0900 Subject: [PATCH 036/620] Fix some failing tests --- .../Editor/TestSceneSliderVelocityAdjust.cs | 3 ++- .../Visual/Menus/TestSceneMainMenuSeasonalLighting.cs | 3 ++- osu.Game/Screens/Menu/IntroScreen.cs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index 175cbeca6e..6690d043f8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -7,6 +7,7 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private Slider? slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); - private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault()!; + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(b => b.Item.GetEndTime() != b.Item.StartTime)!; private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs index 11356f7eeb..46fddf823e 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -22,7 +22,8 @@ namespace osu.Game.Tests.Visual.Menus { var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH); - Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo!.Value.Beatmaps.First()); + if (setInfo != null) + Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo.Value.Beatmaps.First()); }); AddStep("create lighting", () => Child = new MainMenuSeasonalLighting()); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 9885c061a9..a5c2497618 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -200,7 +201,7 @@ namespace osu.Game.Screens.Menu PrepareMenuLoad(); LoadMenu(); - if (!Debugger.IsAttached) + if (!Debugger.IsAttached && !DebugUtils.IsNUnitRunning) { notifications.Post(new SimpleErrorNotification { From 7ebc9dd843b0b801bbfb3a1e72c1be669fff197a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 15:32:00 +0900 Subject: [PATCH 037/620] Disable seasonal for now --- osu.Game/Seasonal/SeasonalUIConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Seasonal/SeasonalUIConfig.cs b/osu.Game/Seasonal/SeasonalUIConfig.cs index 060913a8bf..b894a42108 100644 --- a/osu.Game/Seasonal/SeasonalUIConfig.cs +++ b/osu.Game/Seasonal/SeasonalUIConfig.cs @@ -11,7 +11,7 @@ namespace osu.Game.Seasonal /// public static class SeasonalUIConfig { - public static readonly bool ENABLED = true; + public static readonly bool ENABLED = false; public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex(@"D32F2F"); From f5b019807730a4b1d45158939f55299d54ac5cc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 16:02:43 +0900 Subject: [PATCH 038/620] Fix test faiulres when seasonal set to `true` due to non-circles intro --- osu.Game/Screens/Loader.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index dfa5d2c369..9e7ff80f7c 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shaders; @@ -38,7 +39,9 @@ namespace osu.Game.Screens private IntroScreen getIntroSequence() { - if (SeasonalUIConfig.ENABLED) + // Headless tests run too fast to load non-circles intros correctly. + // They will hit the "audio can't play" notification and cause random test failures. + if (SeasonalUIConfig.ENABLED && !DebugUtils.IsNUnitRunning) return new IntroChristmas(createMainMenu); if (introSequence == IntroSequence.Random) From 139fb2cdd3a60faee550be9a9cb816c4943c9141 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 19:44:43 +0900 Subject: [PATCH 039/620] Revert and fix some tests still --- .../Editor/TestSceneSliderVelocityAdjust.cs | 3 +-- osu.Game/Screens/Menu/IntroScreen.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index 6690d043f8..175cbeca6e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -7,7 +7,6 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; @@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private Slider? slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); - private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(b => b.Item.GetEndTime() != b.Item.StartTime)!; + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault()!; private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index a5c2497618..9885c061a9 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -12,7 +12,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -201,7 +200,7 @@ namespace osu.Game.Screens.Menu PrepareMenuLoad(); LoadMenu(); - if (!Debugger.IsAttached && !DebugUtils.IsNUnitRunning) + if (!Debugger.IsAttached) { notifications.Post(new SimpleErrorNotification { From 1fcd953e4a55c8d6576e64c737fa05b19a40a829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 21 Dec 2024 20:17:27 +0900 Subject: [PATCH 040/620] Fetch ruleset before initialising beatmap the first time --- osu.Game/Seasonal/MainMenuSeasonalLighting.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Seasonal/MainMenuSeasonalLighting.cs b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs index a382785499..718dd38fe7 100644 --- a/osu.Game/Seasonal/MainMenuSeasonalLighting.cs +++ b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs @@ -49,11 +49,11 @@ namespace osu.Game.Seasonal [BackgroundDependencyLoader] private void load(IBindable working, RulesetStore rulesets) { - this.working = working.GetBoundCopy(); - this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true); - // operate in osu! ruleset to keep things simple for now. osuRuleset = rulesets.GetRuleset(0); + + this.working = working.GetBoundCopy(); + this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true); } private void updateBeatmap() From d897a31f0c5b63534f60d165857bd67123a854e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 21 Dec 2024 20:30:00 +0900 Subject: [PATCH 041/620] Add extra safeties against null ref when rulesets are missing --- osu.Game/Seasonal/MainMenuSeasonalLighting.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Seasonal/MainMenuSeasonalLighting.cs b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs index 718dd38fe7..30ad7acefe 100644 --- a/osu.Game/Seasonal/MainMenuSeasonalLighting.cs +++ b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs @@ -27,7 +27,7 @@ namespace osu.Game.Seasonal { private IBindable working = null!; - private InterpolatingFramedClock beatmapClock = null!; + private InterpolatingFramedClock? beatmapClock; private List hitObjects = null!; @@ -82,6 +82,9 @@ namespace osu.Game.Seasonal { base.Update(); + if (osuRuleset == null || beatmapClock == null) + return; + Height = DrawWidth / 16 * 10; beatmapClock.ProcessFrame(); From 5f617e6697aa6e2e4f8be7e411612725a364cc0a Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sat, 21 Dec 2024 20:31:12 +0800 Subject: [PATCH 042/620] Implement rename skin popover and button --- osu.Game/Localisation/SkinSettingsStrings.cs | 5 + .../Overlays/Settings/Sections/SkinSection.cs | 96 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs index 4b6b0ce1d6..1a812ad04d 100644 --- a/osu.Game/Localisation/SkinSettingsStrings.cs +++ b/osu.Game/Localisation/SkinSettingsStrings.cs @@ -54,6 +54,11 @@ namespace osu.Game.Localisation /// public static LocalisableString BeatmapHitsounds => new TranslatableString(getKey(@"beatmap_hitsounds"), @"Beatmap hitsounds"); + /// + /// "Rename selected skin" + /// + public static LocalisableString RenameSkinButton = new TranslatableString(getKey(@"rename_skin_button"), @"Rename selected skin"); + /// /// "Export selected skin" /// diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 9b04f208a7..c015affcd2 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -9,17 +9,23 @@ using System.Linq; using JetBrains.Annotations; 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.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Overlays.SkinEditor; using osu.Game.Screens.Select; using osu.Game.Skinning; +using osuTK; using Realms; namespace osu.Game.Overlays.Settings.Sections @@ -69,6 +75,7 @@ namespace osu.Game.Overlays.Settings.Sections Text = SkinSettingsStrings.SkinLayoutEditor, Action = () => skinEditor?.ToggleVisibility(), }, + new RenameSkinButton(), new ExportSkinButton(), new DeleteSkinButton(), }; @@ -136,6 +143,95 @@ namespace osu.Game.Overlays.Settings.Sections } } + public partial class RenameSkinButton : SettingsButton, IHasPopover + { + [Resolved] + private SkinManager skins { get; set; } + + private Bindable currentSkin; + + [BackgroundDependencyLoader] + private void load() + { + Text = SkinSettingsStrings.RenameSkinButton; + Action = this.ShowPopover; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + currentSkin = skins.CurrentSkin.GetBoundCopy(); + currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true); + } + + public Popover GetPopover() + { + return new RenameSkinPopover(); + } + + public partial class RenameSkinPopover : OsuPopover + { + [Resolved] + private SkinManager skins { get; set; } + + public Action Rename { get; init; } + + private readonly FocusedTextBox textBox; + private readonly RoundedButton renameButton; + + public RenameSkinPopover() + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.TopCentre; + + Child = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + Width = 250, + Spacing = new Vector2(10f), + Children = new Drawable[] + { + textBox = new FocusedTextBox + { + PlaceholderText = @"Skin name", + FontSize = OsuFont.DEFAULT_FONT_SIZE, + RelativeSizeAxes = Axes.X, + }, + renameButton = new RoundedButton + { + Height = 40, + RelativeSizeAxes = Axes.X, + MatchingFilter = true, + Text = SkinSettingsStrings.RenameSkinButton, + } + } + }; + + renameButton.Action += rename; + textBox.OnCommit += delegate (TextBox _, bool _) { rename(); }; + } + + protected override void PopIn() + { + textBox.Text = skins.CurrentSkinInfo.Value.Value.Name; + textBox.TakeFocus(); + base.PopIn(); + } + + private void rename() + { + skins.CurrentSkinInfo.Value.PerformWrite(skin => + { + skin.Name = textBox.Text; + PopOut(); + }); + } + } + } + + public partial class ExportSkinButton : SettingsButton { [Resolved] From 9a0d9641ab9d608713f2a3588a2c571c8b7b2aa2 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sat, 21 Dec 2024 21:26:56 +0800 Subject: [PATCH 043/620] Select all on focus when popover just open --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index c015affcd2..5ff8c88756 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -178,13 +178,14 @@ namespace osu.Game.Overlays.Settings.Sections public Action Rename { get; init; } private readonly FocusedTextBox textBox; - private readonly RoundedButton renameButton; public RenameSkinPopover() { AutoSizeAxes = Axes.Both; Origin = Anchor.TopCentre; + RoundedButton renameButton; + Child = new FillFlowContainer { Direction = FillDirection.Vertical, @@ -198,6 +199,7 @@ namespace osu.Game.Overlays.Settings.Sections PlaceholderText = @"Skin name", FontSize = OsuFont.DEFAULT_FONT_SIZE, RelativeSizeAxes = Axes.X, + SelectAllOnFocus = true, }, renameButton = new RoundedButton { @@ -231,7 +233,6 @@ namespace osu.Game.Overlays.Settings.Sections } } - public partial class ExportSkinButton : SettingsButton { [Resolved] From ae7f1a9ef104d8f18d1f1d24c8fe822e5b95bda0 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sat, 21 Dec 2024 22:27:21 +0800 Subject: [PATCH 044/620] Fix code quality --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5ff8c88756..1792c61d48 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -212,7 +212,13 @@ namespace osu.Game.Overlays.Settings.Sections }; renameButton.Action += rename; - textBox.OnCommit += delegate (TextBox _, bool _) { rename(); }; + + void onTextboxCommit(TextBox sender, bool newText) + { + rename(); + } + + textBox.OnCommit += onTextboxCommit; } protected override void PopIn() From 1c48fdb2350b2389f3d79fdaad9fb32194c9fa48 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 21 Dec 2024 14:03:20 -0500 Subject: [PATCH 045/620] Add `Hidden` cursor state flag on all platforms --- osu.Desktop/OsuGameDesktop.cs | 1 - osu.Game/OsuGame.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 46bd894c07..2d3f4e0ed6 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -134,7 +134,6 @@ namespace osu.Desktop if (iconStream != null) host.Window.SetIconFromStream(iconStream); - host.Window.CursorState |= CursorState.Hidden; host.Window.Title = Name; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 244b72edaa..96899e0ddb 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -319,6 +319,7 @@ namespace osu.Game if (host.Window != null) { + host.Window.CursorState |= CursorState.Hidden; host.Window.DragDrop += path => { // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. From 431d57a8a11671d9fd787ea26a60c7ff414c9eac Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 21 Dec 2024 17:22:07 -0500 Subject: [PATCH 046/620] Make "featured artist" beatmap listing filter persist in config --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index df0a823648..deac1a5128 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -57,6 +57,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f, 0.01f); SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal); + SetDefault(OsuSetting.BeatmapListingFeaturedArtistFilter, true); SetDefault(OsuSetting.ProfileCoverExpanded, true); @@ -450,5 +451,6 @@ namespace osu.Game.Configuration EditorAdjustExistingObjectsOnTimingChanges, AlwaysRequireHoldingForPause, MultiplayerShowInProgressFilter, + BeatmapListingFeaturedArtistFilter, } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index 34b7d45a77..c297e4305d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -125,6 +125,9 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + [Resolved] private SessionStatics sessionStatics { get; set; } = null!; @@ -135,7 +138,12 @@ namespace osu.Game.Overlays.BeatmapListing { base.LoadComplete(); + config.BindWith(OsuSetting.BeatmapListingFeaturedArtistFilter, Active); disclaimerShown = sessionStatics.GetBindable(Static.FeaturedArtistDisclaimerShownOnce); + + // no need to show the disclaimer if the user already had it toggled off in config. + if (!Active.Value) + disclaimerShown.Value = true; } protected override Color4 ColourNormal => colours.Orange1; From c24f690019fd1871a941abcbb5d70ca386387137 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 07:47:57 -0500 Subject: [PATCH 047/620] Allow disabling filter items in beatmap listing overlay --- ...BeatmapSearchMultipleSelectionFilterRow.cs | 33 +++++++++++++++---- .../Overlays/BeatmapListing/FilterTabItem.cs | 2 ++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 958297b559..50e3c0e931 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -73,7 +73,12 @@ namespace osu.Game.Overlays.BeatmapListing private void currentChanged(object? sender, NotifyCollectionChangedEventArgs e) { foreach (var c in Children) - c.Active.Value = Current.Contains(c.Value); + { + if (!c.Active.Disabled) + c.Active.Value = Current.Contains(c.Value); + else if (c.Active.Value != Current.Contains(c.Value)) + throw new InvalidOperationException($"Expected filter {c.Value} to be set to {Current.Contains(c.Value)}, but was {c.Active.Value}"); + } } /// @@ -100,8 +105,9 @@ namespace osu.Game.Overlays.BeatmapListing protected partial class MultipleSelectionFilterTabItem : FilterTabItem { - private Drawable activeContent = null!; + private Container activeContent = null!; private Circle background = null!; + private SpriteIcon icon = null!; public MultipleSelectionFilterTabItem(T value) : base(value) @@ -123,7 +129,6 @@ namespace osu.Game.Overlays.BeatmapListing Alpha = 0, Padding = new MarginPadding { - Left = -16, Right = -4, Vertical = -2 }, @@ -134,8 +139,9 @@ namespace osu.Game.Overlays.BeatmapListing Colour = Color4.White, RelativeSizeAxes = Axes.Both, }, - new SpriteIcon + icon = new SpriteIcon { + Alpha = 0f, Icon = FontAwesome.Solid.TimesCircle, Size = new Vector2(10), Colour = ColourProvider.Background4, @@ -160,13 +166,26 @@ namespace osu.Game.Overlays.BeatmapListing { Color4 colour = Active.Value ? ColourActive : ColourNormal; - if (IsHovered) + if (!Enabled.Value) + colour = colour.Darken(1f); + else if (IsHovered) colour = Active.Value ? colour.Darken(0.2f) : colour.Lighten(0.2f); if (Active.Value) { - // This just allows enough spacing for adjacent tab items to show the "x". - Padding = new MarginPadding { Left = 12 }; + if (Enabled.Value) + { + // This just allows enough spacing for adjacent tab items to show the "x". + Padding = new MarginPadding { Left = 12 }; + activeContent.Padding = activeContent.Padding with { Left = -16 }; + icon.Show(); + } + else + { + Padding = new MarginPadding(); + activeContent.Padding = activeContent.Padding with { Left = -6 }; + icon.Hide(); + } activeContent.FadeIn(200, Easing.OutQuint); background.FadeColour(colour, 200, Easing.OutQuint); diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 8f4ecaa0f5..e357718103 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -57,7 +57,9 @@ namespace osu.Game.Overlays.BeatmapListing { base.LoadComplete(); + Enabled.BindValueChanged(_ => UpdateState()); UpdateState(); + FinishTransforms(true); } From 589e187a80b022b4ce20e265fb4e5af775b2369f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 07:50:08 -0500 Subject: [PATCH 048/620] Disable ability to toggle "featured artists" beatmap listing filter in iOS --- osu.Game/OsuGame.cs | 6 ++++++ .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 244b72edaa..36f7bcbb1e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -220,6 +220,12 @@ namespace osu.Game private readonly List visibleBlockingOverlays = new List(); + /// + /// Whether the game should be limited from providing access to download non-featured-artist beatmaps. + /// This only affects the "featured artists" filter in the beatmap listing overlay. + /// + public bool LimitedToFeaturedArtists => RuntimeInfo.OS == RuntimeInfo.Platform.iOS && true; + public OsuGame(string[] args = null) { this.args = args; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index c297e4305d..d7201d4df8 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -125,6 +125,9 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OsuGame game { get; set; } = null!; + [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -144,6 +147,12 @@ namespace osu.Game.Overlays.BeatmapListing // no need to show the disclaimer if the user already had it toggled off in config. if (!Active.Value) disclaimerShown.Value = true; + + if (game.LimitedToFeaturedArtists) + { + Enabled.Value = false; + Active.Disabled = true; + } } protected override Color4 ColourNormal => colours.Orange1; @@ -151,6 +160,9 @@ namespace osu.Game.Overlays.BeatmapListing protected override bool OnClick(ClickEvent e) { + if (!Enabled.Value) + return true; + if (!disclaimerShown.Value && dialogOverlay != null) { dialogOverlay.Push(new FeaturedArtistConfirmDialog(() => From e716919a07599068556b3f07aab191c9c266bf8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Dec 2024 22:57:17 +0900 Subject: [PATCH 049/620] Remove redundant `&& true` Co-authored-by: Susko3 --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 36f7bcbb1e..17ad67b733 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -224,7 +224,7 @@ namespace osu.Game /// Whether the game should be limited from providing access to download non-featured-artist beatmaps. /// This only affects the "featured artists" filter in the beatmap listing overlay. /// - public bool LimitedToFeaturedArtists => RuntimeInfo.OS == RuntimeInfo.Platform.iOS && true; + public bool LimitedToFeaturedArtists => RuntimeInfo.OS == RuntimeInfo.Platform.iOS; public OsuGame(string[] args = null) { From 0aed625bb8027bea06a98833904b2687c8619650 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Dec 2024 23:58:35 +0900 Subject: [PATCH 050/620] Rename variable and adjust commentary --- osu.Game/OsuGame.cs | 5 ++--- .../Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 17ad67b733..3864c518d2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -221,10 +221,9 @@ namespace osu.Game private readonly List visibleBlockingOverlays = new List(); /// - /// Whether the game should be limited from providing access to download non-featured-artist beatmaps. - /// This only affects the "featured artists" filter in the beatmap listing overlay. + /// Whether the game should be limited to only display licensed content. /// - public bool LimitedToFeaturedArtists => RuntimeInfo.OS == RuntimeInfo.Platform.iOS; + public bool HideUnlicensedContent => RuntimeInfo.OS == RuntimeInfo.Platform.iOS; public OsuGame(string[] args = null) { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index d7201d4df8..b525d8282e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.BeatmapListing if (!Active.Value) disclaimerShown.Value = true; - if (game.LimitedToFeaturedArtists) + if (game.HideUnlicensedContent) { Enabled.Value = false; Active.Disabled = true; From fcfab9e53c5fdb98e38d84903120611d48fa439e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 10:14:52 -0500 Subject: [PATCH 051/620] Fix spacing --- .../BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 50e3c0e931..27b630d623 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -183,7 +183,7 @@ namespace osu.Game.Overlays.BeatmapListing else { Padding = new MarginPadding(); - activeContent.Padding = activeContent.Padding with { Left = -6 }; + activeContent.Padding = activeContent.Padding with { Left = -4 }; icon.Hide(); } From b3056d6114b9a3d439ae437537568fc9124c4a58 Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sun, 22 Dec 2024 16:58:00 -0500 Subject: [PATCH 052/620] Change score background to pink if user is friended --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 5651f01645..9aa0e0fbe2 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -101,6 +101,7 @@ namespace osu.Game.Online.Leaderboards private void load(IAPIProvider api, OsuColour colour) { var user = Score.User; + bool isUserFriend = api.Friends.Any(friend => friend.TargetID == user.OnlineID); statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList(); @@ -129,7 +130,7 @@ namespace osu.Game.Online.Leaderboards background = new Box { RelativeSizeAxes = Axes.Both, - Colour = user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black, + Colour = isUserFriend ? colour.Pink : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), Alpha = background_alpha, }, }, From 1a7feeb4edab01db1ab6c9fa5c501b69456a78da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 14:39:07 +0900 Subject: [PATCH 053/620] Use `virtual` property rather than inline iOS conditional --- osu.Game/OsuGame.cs | 4 ++-- osu.iOS/OsuGameIOS.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3864c518d2..c5c6ef8cc7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -221,9 +221,9 @@ namespace osu.Game private readonly List visibleBlockingOverlays = new List(); /// - /// Whether the game should be limited to only display licensed content. + /// Whether the game should be limited to only display officially licensed content. /// - public bool HideUnlicensedContent => RuntimeInfo.OS == RuntimeInfo.Platform.iOS; + public virtual bool HideUnlicensedContent => false; public OsuGame(string[] args = null) { diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index c0bd77366e..a9ca1778a0 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -17,6 +17,8 @@ namespace osu.iOS { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); + public override bool HideUnlicensedContent => true; + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From f12fffd116eb3488586405de0177ed63e1fffa30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 14:43:36 +0900 Subject: [PATCH 054/620] Fix more than obvious test failure Please run tests please run tests please run tests. --- .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index b525d8282e..e4c663ee13 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -125,9 +125,6 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuColour colours { get; set; } = null!; - [Resolved] - private OsuGame game { get; set; } = null!; - [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -137,6 +134,9 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] + private OsuGame? game { get; set; } + protected override void LoadComplete() { base.LoadComplete(); @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.BeatmapListing if (!Active.Value) disclaimerShown.Value = true; - if (game.HideUnlicensedContent) + if (game?.HideUnlicensedContent == true) { Enabled.Value = false; Active.Disabled = true; From 638d959c5cc3fdcdb6d070eb976191e2b6f734ec Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 23 Dec 2024 20:12:25 +0900 Subject: [PATCH 055/620] Initial support for free style selection --- osu.Game/Online/Rooms/PlaylistItem.cs | 5 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 118 ++++++++++++++++-- .../MultiplayerMatchStyleSelect.cs | 84 +++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 75 +++++++---- .../Select/Carousel/CarouselBeatmap.cs | 3 + osu.Game/Screens/Select/FilterControl.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 1 + osu.Game/Screens/Select/SongSelect.cs | 10 +- 8 files changed, 252 insertions(+), 46 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 3d829d1e4e..937bc40e9b 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -124,13 +124,14 @@ namespace osu.Game.Online.Rooms #endregion - public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default) + public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, + Optional ruleset = default) { return new PlaylistItem(beatmap.GetOr(Beatmap)) { ID = id.GetOr(ID), OwnerID = OwnerID, - RulesetID = RulesetID, + RulesetID = ruleset.GetOr(RulesetID), Expired = Expired, PlaylistOrder = playlistOrder.GetOr(PlaylistOrder), PlayedAt = PlayedAt, diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..c9e0cbc1e9 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -36,6 +37,18 @@ namespace osu.Game.Screens.OnlinePlay.Match { public readonly Bindable SelectedItem = new Bindable(); + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. + /// + public readonly Bindable DifficultyOverride = new Bindable(); + + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local ruleset selection. + /// + public readonly Bindable RulesetOverride = new Bindable(); + public override bool? ApplyModTrackAdjustments => true; protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault()) @@ -51,6 +64,17 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected Drawable? UserModsSection; + /// + /// A container that provides controls for selection of the user's difficulty override. + /// This will be shown/hidden automatically when applicable. + /// + protected Drawable? UserDifficultySection; + + /// + /// A container that will display the user's difficulty override. + /// + protected Container? UserStyleDisplayContainer; + private Sample? sampleStart; /// @@ -250,6 +274,8 @@ namespace osu.Game.Screens.OnlinePlay.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); + DifficultyOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); + RulesetOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); @@ -383,7 +409,7 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay() { - if (SelectedItem.Value == null) + if (GetGameplayItem() is not PlaylistItem item) return; // User may be at song select or otherwise when the host starts gameplay. @@ -401,7 +427,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). var targetScreen = (Screen?)ParentScreen ?? this; - targetScreen.Push(CreateGameplayScreen(SelectedItem.Value)); + targetScreen.Push(CreateGameplayScreen(item)); } /// @@ -413,11 +439,18 @@ namespace osu.Game.Screens.OnlinePlay.Match private void selectedItemChanged() { - updateWorkingBeatmap(); - if (SelectedItem.Value is not PlaylistItem selected) return; + if (selected.BeatmapSetId == null || selected.BeatmapSetId != DifficultyOverride.Value?.BeatmapSet.AsNonNull().OnlineID) + { + DifficultyOverride.Value = null; + RulesetOverride.Value = null; + } + + updateStyleOverride(); + updateWorkingBeatmap(); + var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance)); @@ -439,37 +472,96 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSection?.Show(); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } + + if (selected.BeatmapSetId == null) + UserDifficultySection?.Hide(); + else + UserDifficultySection?.Show(); } private void updateWorkingBeatmap() { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) return; - var beatmap = SelectedItem.Value?.Beatmap; - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID); + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } protected virtual void UpdateMods() { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) return; - var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } - private void updateRuleset() + private void updateStyleOverride() { if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - Ruleset.Value = Rulesets.GetRuleset(SelectedItem.Value.RulesetID); + if (UserStyleDisplayContainer == null) + return; + + PlaylistItem gameplayItem = GetGameplayItem()!; + + if (UserStyleDisplayContainer.SingleOrDefault()?.Item.Equals(gameplayItem) == true) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = openStyleSelection + }; + } + + protected PlaylistItem? GetGameplayItem() + { + PlaylistItem? selectedItemWithOverride = SelectedItem.Value; + + if (selectedItemWithOverride?.BeatmapSetId == null) + return selectedItemWithOverride; + + // Sanity check. + if (DifficultyOverride.Value?.BeatmapSet?.OnlineID != selectedItemWithOverride.BeatmapSetId) + return selectedItemWithOverride; + + if (DifficultyOverride.Value != null) + selectedItemWithOverride = selectedItemWithOverride.With(beatmap: DifficultyOverride.Value); + + if (RulesetOverride.Value != null) + selectedItemWithOverride = selectedItemWithOverride.With(ruleset: RulesetOverride.Value.OnlineID); + + return selectedItemWithOverride; + } + + private void openStyleSelection(PlaylistItem item) + { + if (!this.IsCurrentScreen()) + return; + + this.Push(new MultiplayerMatchStyleSelect(Room, item, (beatmap, ruleset) => + { + if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) + return; + + DifficultyOverride.Value = beatmap; + RulesetOverride.Value = ruleset; + })); + } + + private void updateRuleset() + { + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + return; + + Ruleset.Value = Rulesets.GetRuleset(item.RulesetID); } private void beginHandlingTrack() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs new file mode 100644 index 0000000000..dc1393bf96 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -0,0 +1,84 @@ +// 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 Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen + { + public string ShortTitle => "style selection"; + + public override string Title => ShortTitle.Humanize(); + + public override bool AllowEditing => false; + + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); + + private readonly Room room; + private readonly PlaylistItem item; + private readonly Action onSelect; + + public MultiplayerMatchStyleSelect(Room room, PlaylistItem item, Action onSelect) + { + this.room = room; + this.item = item; + this.onSelect = onSelect; + + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + } + + [BackgroundDependencyLoader] + private void load() + { + LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; + } + + protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); + + protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + { + // Required to create the drawable components. + base.CreateSongSelectFooterButtons(); + return Enumerable.Empty<(FooterButton, OverlayContainer?)>(); + } + + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + + protected override bool OnStart() + { + onSelect(Beatmap.Value.BeatmapInfo, Ruleset.Value); + this.Exit(); + return true; + } + + private partial class DifficultySelectFilterControl : FilterControl + { + private readonly PlaylistItem item; + + public DifficultySelectFilterControl(PlaylistItem item) + { + this.item = item; + } + + public override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + criteria.BeatmapSetId = item.BeatmapSetId; + return criteria; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edc45dbf7c..d807fe8177 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -145,43 +145,66 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem } }, - new[] + new Drawable[] { - UserModsSection = new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 10 }, - Alpha = 0, - Children = new Drawable[] + Children = new[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + UserModsSection = new FillFlowContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, Children = new Drawable[] { - new UserModSelectButton + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } }, } }, + UserDifficultySection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, } - }, + } }, }, RowDimensions = new[] @@ -240,14 +263,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void UpdateMods() { - if (SelectedItem.Value == null || client.LocalUser == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || client.LocalUser == null || !this.IsCurrentScreen()) return; // update local mods based on room's reported status for the local user (omitting the base call implementation). // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). - var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index c007fa29ed..95186e98d8 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -90,6 +90,9 @@ namespace osu.Game.Screens.Select.Carousel if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria); + if (match && criteria.BeatmapSetId != null) + match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID; + return match; } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index b221296ba8..488f63accb 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select [CanBeNull] private FilterCriteria currentCriteria; - public FilterCriteria CreateCriteria() + public virtual FilterCriteria CreateCriteria() { string query = searchTextBox.Text; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 76c0f769f0..63dbdfbed3 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -56,6 +56,7 @@ namespace osu.Game.Screens.Select public RulesetInfo? Ruleset; public IReadOnlyList? Mods; public bool AllowConvertedBeatmaps; + public int? BeatmapSetId; private string searchText = string.Empty; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9ebd9c9846..c8d50436d9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -216,11 +216,11 @@ namespace osu.Game.Screens.Select }, } }, - FilterControl = new FilterControl + FilterControl = CreateFilterControl().With(d => { - RelativeSizeAxes = Axes.X, - Height = FilterControl.HEIGHT, - }, + d.RelativeSizeAxes = Axes.X; + d.Height = FilterControl.HEIGHT; + }), new GridContainer // used for max width implementation { RelativeSizeAxes = Axes.Both, @@ -389,6 +389,8 @@ namespace osu.Game.Screens.Select SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); } + protected virtual FilterControl CreateFilterControl() => new FilterControl(); + protected override void LoadComplete() { base.LoadComplete(); From 097828ded208d872bf886579741fe72197781f01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 22:07:42 +0900 Subject: [PATCH 056/620] Fix incorrect mouse wheel mappings --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index c343b4e1e6..35d2465084 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,10 +142,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), - // Framework automatically converts wheel up/down to left/right when shift is held. - // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38. - new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), - new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), From 9ff4a58fa3724904c13f1117c14ab03824963dda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 22:14:03 +0900 Subject: [PATCH 057/620] Add migration to update users which have previous default bindings for beat snap --- osu.Game/Database/RealmAccess.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index a520040ad1..e9fd82c4ff 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -96,7 +96,7 @@ namespace osu.Game.Database /// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user. /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. /// - private const int schema_version = 44; + private const int schema_version = 45; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1205,6 +1205,22 @@ namespace osu.Game.Database break; } + + case 45: + { + // Cycling beat snap divisors no longer requires holding shift (just control). + var keyBindings = migration.NewRealm.All(); + + var nextBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCycleNextBeatSnapDivisor); + if (nextBeatSnapBinding != null && nextBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.MouseWheelLeft })) + migration.NewRealm.Remove(nextBeatSnapBinding); + + var previousBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCyclePreviousBeatSnapDivisor); + if (previousBeatSnapBinding != null && previousBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.MouseWheelRight })) + migration.NewRealm.Remove(previousBeatSnapBinding); + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); From 050bf9ec6033b26a4a0cb6878738dc66346ba0b7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Dec 2024 10:52:18 -0500 Subject: [PATCH 058/620] Keep 'x' symbol visible even while disabled --- ...BeatmapSearchMultipleSelectionFilterRow.cs | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 27b630d623..b4940d3aa1 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -107,7 +107,6 @@ namespace osu.Game.Overlays.BeatmapListing { private Container activeContent = null!; private Circle background = null!; - private SpriteIcon icon = null!; public MultipleSelectionFilterTabItem(T value) : base(value) @@ -129,6 +128,7 @@ namespace osu.Game.Overlays.BeatmapListing Alpha = 0, Padding = new MarginPadding { + Left = -16, Right = -4, Vertical = -2 }, @@ -139,9 +139,8 @@ namespace osu.Game.Overlays.BeatmapListing Colour = Color4.White, RelativeSizeAxes = Axes.Both, }, - icon = new SpriteIcon + new SpriteIcon { - Alpha = 0f, Icon = FontAwesome.Solid.TimesCircle, Size = new Vector2(10), Colour = ColourProvider.Background4, @@ -173,19 +172,8 @@ namespace osu.Game.Overlays.BeatmapListing if (Active.Value) { - if (Enabled.Value) - { - // This just allows enough spacing for adjacent tab items to show the "x". - Padding = new MarginPadding { Left = 12 }; - activeContent.Padding = activeContent.Padding with { Left = -16 }; - icon.Show(); - } - else - { - Padding = new MarginPadding(); - activeContent.Padding = activeContent.Padding with { Left = -4 }; - icon.Hide(); - } + // This just allows enough spacing for adjacent tab items to show the "x". + Padding = new MarginPadding { Left = 12 }; activeContent.FadeIn(200, Easing.OutQuint); background.FadeColour(colour, 200, Easing.OutQuint); From 7e3477f4bbfaa9cb1c01dea68b320e7267c5bbda Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Dec 2024 10:54:52 -0500 Subject: [PATCH 059/620] Remove unnecessary guarding --- .../BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index b4940d3aa1..73af62c322 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -76,8 +76,6 @@ namespace osu.Game.Overlays.BeatmapListing { if (!c.Active.Disabled) c.Active.Value = Current.Contains(c.Value); - else if (c.Active.Value != Current.Contains(c.Value)) - throw new InvalidOperationException($"Expected filter {c.Value} to be set to {Current.Contains(c.Value)}, but was {c.Active.Value}"); } } From 6b635d588f16af12bde4340640aee476197795fd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Dec 2024 10:59:06 -0500 Subject: [PATCH 060/620] Add tooltip --- osu.Game/Localisation/BeatmapOverlayStrings.cs | 5 +++++ .../Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/BeatmapOverlayStrings.cs b/osu.Game/Localisation/BeatmapOverlayStrings.cs index fc818f7596..43ffa17d93 100644 --- a/osu.Game/Localisation/BeatmapOverlayStrings.cs +++ b/osu.Game/Localisation/BeatmapOverlayStrings.cs @@ -28,6 +28,11 @@ This includes content that may not be correctly licensed for osu! usage. Browse /// public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand"); + /// + /// "Toggling this filter is disabled in this platform." + /// + public static LocalisableString FeaturedArtistsDisabledTooltip => new TranslatableString(getKey(@"featured_artists_disabled_tooltip"), @"Toggling this filter is disabled in this platform."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index e4c663ee13..b9720f06e8 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; @@ -113,7 +114,7 @@ namespace osu.Game.Overlays.BeatmapListing } } - private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem + private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem, IHasTooltip { private Bindable disclaimerShown = null!; @@ -137,6 +138,8 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuGame? game { get; set; } + public LocalisableString TooltipText => !Enabled.Value ? BeatmapOverlayStrings.FeaturedArtistsDisabledTooltip : string.Empty; + protected override void LoadComplete() { base.LoadComplete(); From 47afab8a32fb312601f8d5b18fb6a9cae6de6e97 Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:47:50 -0500 Subject: [PATCH 061/620] Use yellow instead of pink --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 9aa0e0fbe2..32b25a866d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -130,7 +130,7 @@ namespace osu.Game.Online.Leaderboards background = new Box { RelativeSizeAxes = Axes.Both, - Colour = isUserFriend ? colour.Pink : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), + Colour = isUserFriend ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), Alpha = background_alpha, }, }, From 7e8aaa68ff11082ff60a3c8b85d54e21444553a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 11:46:39 +0900 Subject: [PATCH 062/620] Add keywords for intro-related settings --- .../Settings/Sections/UserInterface/MainMenuSettings.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 5e42c3035c..c50d56b458 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -36,11 +36,13 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface }, new SettingsCheckbox { + Keywords = new[] { "intro", "welcome" }, LabelText = UserInterfaceStrings.InterfaceVoices, Current = config.GetBindable(OsuSetting.MenuVoice) }, new SettingsCheckbox { + Keywords = new[] { "intro", "welcome" }, LabelText = UserInterfaceStrings.OsuMusicTheme, Current = config.GetBindable(OsuSetting.MenuMusic) }, From 282c67d14bf5d4071beb64602d0c5d3420ea864a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 11:59:45 +0900 Subject: [PATCH 063/620] 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 fe3bdbffa3..51bed31afb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 8762e3fedb5139a70b1914dbb5e797e865a1cd85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 12:12:49 +0900 Subject: [PATCH 064/620] Always show tooltip, and reword to be always applicable --- osu.Game/Localisation/BeatmapOverlayStrings.cs | 4 ++-- .../Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/BeatmapOverlayStrings.cs b/osu.Game/Localisation/BeatmapOverlayStrings.cs index 43ffa17d93..f8122c1ef9 100644 --- a/osu.Game/Localisation/BeatmapOverlayStrings.cs +++ b/osu.Game/Localisation/BeatmapOverlayStrings.cs @@ -29,9 +29,9 @@ This includes content that may not be correctly licensed for osu! usage. Browse public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand"); /// - /// "Toggling this filter is disabled in this platform." + /// "Featured Artists are music artists who have collaborated with osu! to make a selection of their tracks available for use in beatmaps. For some osu! releases, we showcase only featured artist beatmaps to better support the surrounding ecosystem." /// - public static LocalisableString FeaturedArtistsDisabledTooltip => new TranslatableString(getKey(@"featured_artists_disabled_tooltip"), @"Toggling this filter is disabled in this platform."); + public static LocalisableString FeaturedArtistsTooltip => new TranslatableString(getKey(@"featured_artists_disabled_tooltip"), @"Featured Artists are music artists who have collaborated with osu! to make a selection of their tracks available for use in beatmaps. For some osu! releases, we showcase only featured artist beatmaps to better support the surrounding ecosystem."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index b9720f06e8..b62836dfde 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -138,7 +138,7 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuGame? game { get; set; } - public LocalisableString TooltipText => !Enabled.Value ? BeatmapOverlayStrings.FeaturedArtistsDisabledTooltip : string.Empty; + public LocalisableString TooltipText => BeatmapOverlayStrings.FeaturedArtistsTooltip; protected override void LoadComplete() { From ae9c7e1b354c43fc606a75031514ea56ec648723 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 13:17:58 +0900 Subject: [PATCH 065/620] Adjust layout and remove localisable strings for temporary buttons --- osu.Game/Localisation/SkinSettingsStrings.cs | 15 -- .../Overlays/Settings/Sections/SkinSection.cs | 150 +++++++++--------- 2 files changed, 76 insertions(+), 89 deletions(-) diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs index 1a812ad04d..16dca7fd87 100644 --- a/osu.Game/Localisation/SkinSettingsStrings.cs +++ b/osu.Game/Localisation/SkinSettingsStrings.cs @@ -54,21 +54,6 @@ namespace osu.Game.Localisation /// public static LocalisableString BeatmapHitsounds => new TranslatableString(getKey(@"beatmap_hitsounds"), @"Beatmap hitsounds"); - /// - /// "Rename selected skin" - /// - public static LocalisableString RenameSkinButton = new TranslatableString(getKey(@"rename_skin_button"), @"Rename selected skin"); - - /// - /// "Export selected skin" - /// - public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin"); - - /// - /// "Delete selected skin" - /// - public static LocalisableString DeleteSkinButton => new TranslatableString(getKey(@"delete_skin_button"), @"Delete selected skin"); - private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 1792c61d48..7fffd3693c 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -75,9 +75,21 @@ namespace osu.Game.Overlays.Settings.Sections Text = SkinSettingsStrings.SkinLayoutEditor, Action = () => skinEditor?.ToggleVisibility(), }, - new RenameSkinButton(), - new ExportSkinButton(), - new DeleteSkinButton(), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, + Children = new Drawable[] + { + // This is all super-temporary until we move skin settings to their own panel / overlay. + new RenameSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 120 }, + new ExportSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 120 }, + new DeleteSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 110 }, + } + }, }; } @@ -153,7 +165,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = SkinSettingsStrings.RenameSkinButton; + Text = "Rename"; Action = this.ShowPopover; } @@ -169,74 +181,6 @@ namespace osu.Game.Overlays.Settings.Sections { return new RenameSkinPopover(); } - - public partial class RenameSkinPopover : OsuPopover - { - [Resolved] - private SkinManager skins { get; set; } - - public Action Rename { get; init; } - - private readonly FocusedTextBox textBox; - - public RenameSkinPopover() - { - AutoSizeAxes = Axes.Both; - Origin = Anchor.TopCentre; - - RoundedButton renameButton; - - Child = new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Y, - Width = 250, - Spacing = new Vector2(10f), - Children = new Drawable[] - { - textBox = new FocusedTextBox - { - PlaceholderText = @"Skin name", - FontSize = OsuFont.DEFAULT_FONT_SIZE, - RelativeSizeAxes = Axes.X, - SelectAllOnFocus = true, - }, - renameButton = new RoundedButton - { - Height = 40, - RelativeSizeAxes = Axes.X, - MatchingFilter = true, - Text = SkinSettingsStrings.RenameSkinButton, - } - } - }; - - renameButton.Action += rename; - - void onTextboxCommit(TextBox sender, bool newText) - { - rename(); - } - - textBox.OnCommit += onTextboxCommit; - } - - protected override void PopIn() - { - textBox.Text = skins.CurrentSkinInfo.Value.Value.Name; - textBox.TakeFocus(); - base.PopIn(); - } - - private void rename() - { - skins.CurrentSkinInfo.Value.PerformWrite(skin => - { - skin.Name = textBox.Text; - PopOut(); - }); - } - } } public partial class ExportSkinButton : SettingsButton @@ -249,7 +193,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = SkinSettingsStrings.ExportSkinButton; + Text = "Export"; Action = export; } @@ -287,7 +231,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = SkinSettingsStrings.DeleteSkinButton; + Text = "Delete"; Action = delete; } @@ -304,5 +248,63 @@ namespace osu.Game.Overlays.Settings.Sections dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value)); } } + + public partial class RenameSkinPopover : OsuPopover + { + [Resolved] + private SkinManager skins { get; set; } + + private readonly FocusedTextBox textBox; + + public RenameSkinPopover() + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.TopCentre; + + RoundedButton renameButton; + + Child = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + Width = 250, + Spacing = new Vector2(10f), + Children = new Drawable[] + { + textBox = new FocusedTextBox + { + PlaceholderText = @"Skin name", + FontSize = OsuFont.DEFAULT_FONT_SIZE, + RelativeSizeAxes = Axes.X, + SelectAllOnFocus = true, + }, + renameButton = new RoundedButton + { + Height = 40, + RelativeSizeAxes = Axes.X, + MatchingFilter = true, + Text = "Save", + } + } + }; + + renameButton.Action += rename; + textBox.OnCommit += (_, _) => rename(); + } + + protected override void PopIn() + { + textBox.Text = skins.CurrentSkinInfo.Value.Value.Name; + textBox.TakeFocus(); + + base.PopIn(); + } + + private void rename() => skins.CurrentSkinInfo.Value.PerformWrite(skin => + { + skin.Name = textBox.Text; + PopOut(); + }); + } } } From 378bef34efab9980bbb6de9d62726a3349ae3a6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 13:42:18 +0900 Subject: [PATCH 066/620] Change order of skin layout editor button for better visual balance --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 7fffd3693c..a89d5e2f4a 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -70,11 +70,6 @@ namespace osu.Game.Overlays.Settings.Sections Current = skins.CurrentSkinInfo, Keywords = new[] { @"skins" }, }, - new SettingsButton - { - Text = SkinSettingsStrings.SkinLayoutEditor, - Action = () => skinEditor?.ToggleVisibility(), - }, new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -90,6 +85,11 @@ namespace osu.Game.Overlays.Settings.Sections new DeleteSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 110 }, } }, + new SettingsButton + { + Text = SkinSettingsStrings.SkinLayoutEditor, + Action = () => skinEditor?.ToggleVisibility(), + }, }; } From a5d354d753302c318ade8cb56fbe1d884e20942a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 15:17:10 +0900 Subject: [PATCH 067/620] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f13760bd21..84827ce76b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 3e618a3a74..349d6fa1d7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From b8d6bba03924ed96328d04e6c9ce7fe5041afa59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 16:05:44 +0900 Subject: [PATCH 068/620] Fix legacy hitcircle fallback logic being broken with recent fix I was a bit too eager to replace all calls with the new `provider` in https://github.com/ppy/osu/commit/dae380b7fa927c351e2e413c5b23834f717908d9, while it doesn't actually make sense. To handle the case that was trying to be fixed, using the `provider` to check whether the *prefix* version of the circle sprite is available is enough alone. Closes https://github.com/ppy/osu/issues/31200 --- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 0dc0f065d4..e74ffaac0c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -61,13 +61,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy var drawableOsuObject = (DrawableOsuHitObject?)drawableObject; - // As a precondition, ensure that any prefix lookups are run against the skin which is providing "hitcircle". + // As a precondition, prefer that any *prefix* lookups are run against the skin which is providing "hitcircle". // This is to correctly handle a case such as: // // - Beatmap provides `hitcircle` // - User skin provides `sliderstartcircle` // // In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override. + // + // Of note, this consideration should only be used to decide whether to continue looking up the prefixed name or not. + // The final lookups must still run on the full skin hierarchy as per usual in order to correctly handle fallback cases. var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin; // if a base texture for the specified prefix exists, continue using it for subsequent lookups. @@ -81,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. InternalChildren = new[] { - CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(circleName)?.WithMaximumSize(maxSize) }) + CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) + Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From d9be172647c81972247075c5eae14608ace9f99d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Dec 2024 08:17:25 +0100 Subject: [PATCH 069/620] Add explanatory comment for schema version bump --- osu.Game/Database/RealmAccess.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e9fd82c4ff..b412348595 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -95,6 +95,7 @@ namespace osu.Game.Database /// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction /// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user. /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. + /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. /// private const int schema_version = 45; From d8686f55f7178bbdbee3d85a60c3f3e5c36431c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 17:10:48 +0900 Subject: [PATCH 070/620] Slightly reduce background brightness at main menu when seasonal lighting is active --- osu.Game/Screens/Menu/MainMenu.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index a5acc6a1c2..99bc1825f5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -202,18 +202,20 @@ namespace osu.Game.Screens.Menu holdToExitGameOverlay?.CreateProxy() ?? Empty() }); + float baseDim = SeasonalUIConfig.ENABLED ? 0.84f : 1; + Buttons.StateChanged += state => { switch (state) { case ButtonSystemState.Initial: case ButtonSystemState.Exit: - ApplyToBackground(b => b.FadeColour(Color4.White, 500, Easing.OutSine)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(baseDim), 500, Easing.OutSine)); onlineMenuBanner.State.Value = Visibility.Hidden; break; default: - ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(baseDim * 0.8f), 500, Easing.OutSine)); onlineMenuBanner.State.Value = Visibility.Visible; break; } From ce1eda7e54516921bc25d1a3ed6ee0c7e307ade9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 17:11:21 +0900 Subject: [PATCH 071/620] Fix adjusting volume using scroll wheel not working during intro --- osu.Game/Screens/Menu/IntroScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 9885c061a9..c110c53df8 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -24,6 +24,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; @@ -174,6 +175,8 @@ namespace osu.Game.Screens.Menu return UsingThemedIntro = initialBeatmap != null; } + + AddInternal(new GlobalScrollAdjustsVolume()); } public override void OnEntering(ScreenTransitionEvent e) From 7777c447754a0bcfd64036681175712528c5d454 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 17:57:59 +0900 Subject: [PATCH 072/620] Only allow selecting beatmaps within 30s length --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 ++++---- .../Multiplayer/MultiplayerMatchStyleSelect.cs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c9e0cbc1e9..49144f9de5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -517,7 +517,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { AllowReordering = false, AllowEditing = true, - RequestEdit = openStyleSelection + RequestEdit = _ => openStyleSelection() }; } @@ -541,12 +541,12 @@ namespace osu.Game.Screens.OnlinePlay.Match return selectedItemWithOverride; } - private void openStyleSelection(PlaylistItem item) + private void openStyleSelection() { - if (!this.IsCurrentScreen()) + if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - this.Push(new MultiplayerMatchStyleSelect(Room, item, (beatmap, ruleset) => + this.Push(new MultiplayerMatchStyleSelect(Room, SelectedItem.Value, (beatmap, ruleset) => { if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index dc1393bf96..19d8b96f2b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.Select; @@ -67,16 +68,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private partial class DifficultySelectFilterControl : FilterControl { private readonly PlaylistItem item; + private double itemLength; public DifficultySelectFilterControl(PlaylistItem item) { this.item = item; } + [BackgroundDependencyLoader] + private void load(RealmAccess realm) + { + int beatmapId = item.Beatmap.OnlineID; + itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + } + public override FilterCriteria CreateCriteria() { var criteria = base.CreateCriteria(); + + // Must be from the same set as the playlist item. criteria.BeatmapSetId = item.BeatmapSetId; + + // Must be within 30s of the playlist item. + criteria.Length.Min = itemLength - 30000; + criteria.Length.Max = itemLength + 30000; + criteria.Length.IsLowerInclusive = true; + criteria.Length.IsUpperInclusive = true; + return criteria; } } From 40486c4f38bfd60099c30fc3d20fcd148123c605 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 18:04:36 +0900 Subject: [PATCH 073/620] Block beatmap presents in style select screen --- .../OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index 19d8b96f2b..867579171d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -18,7 +18,7 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen + public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap { public string ShortTitle => "style selection"; @@ -65,6 +65,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + // This screen cannot present beatmaps. + } + private partial class DifficultySelectFilterControl : FilterControl { private readonly PlaylistItem item; From 971ccb6a4e6a93b44e8bc17eb1ad577e334e6e6c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:05:50 +0900 Subject: [PATCH 074/620] Adjust namings --- ...rButtonFreePlay.cs => FooterButtonFreeStyle.cs} | 6 +++--- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 14 +++++++------- .../OnlinePlay/Playlists/PlaylistsSongSelect.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Screens/OnlinePlay/{FooterButtonFreePlay.cs => FooterButtonFreeStyle.cs} (95%) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs similarity index 95% rename from osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs rename to osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index bcc7bb787d..5edcddcb78 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { - public class FooterButtonFreePlay : FooterButton, IHasCurrentValue + public class FooterButtonFreeStyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private OsuColour colours { get; set; } = null!; - public FooterButtonFreePlay() + public FooterButtonFreeStyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. base.Action = () => current.Value = !current.Value; @@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); - Text = @"freeplay"; + Text = @"freestyle"; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1f1d259d0a..02f8c619a7 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable FreePlay = new Bindable(); + protected readonly Bindable FreeStyle = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; @@ -112,17 +112,17 @@ namespace osu.Game.Screens.OnlinePlay } if (initialItem.BeatmapSetId != null) - FreePlay.Value = true; + FreeStyle.Value = true; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); - FreePlay.BindValueChanged(onFreePlayChanged, true); + FreeStyle.BindValueChanged(onFreeStyleChanged, true); freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); } - private void onFreePlayChanged(ValueChangedEvent enabled) + private void onFreeStyleChanged(ValueChangedEvent enabled) { if (enabled.NewValue) { @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null + BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null }; return SelectItem(item); @@ -202,12 +202,12 @@ namespace osu.Game.Screens.OnlinePlay var baseButtons = base.CreateSongSelectFooterButtons().ToList(); freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freePlayButton = new FooterButtonFreePlay { Current = FreePlay }; + var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, freeModSelect), - (freePlayButton, null) + (freeStyleButton, null) }); return baseButtons; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index f9e014a727..a3b8a1575e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, - BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, + BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), From ac738f109ad4eb6ebf1790a26d031e3d8a738d85 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:28:09 +0900 Subject: [PATCH 075/620] Add style selection to playlists screen --- .../Playlists/PlaylistsRoomSubScreen.cs | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9573155f5a..98667c16fb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -171,39 +171,63 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new[] { - UserModsSection = new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Alpha = 0, Margin = new MarginPadding { Bottom = 10 }, - Children = new Drawable[] + Children = new[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + UserModsSection = new FillFlowContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Margin = new MarginPadding { Bottom = 10 }, Children = new Drawable[] { - new UserModSelectButton + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } + } } - } + }, + UserDifficultySection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, } }, }, From d8ff5bcacbb4460de8d51ff674b16f6a9aeba3b7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:39:56 +0900 Subject: [PATCH 076/620] Fix freemods button opening overlay unexpectedly --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 02f8c619a7..a91f43635b 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -206,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton, freeModSelect), + (freeModsFooterButton, null), (freeStyleButton, null) }); From c88e906cb69bbc17c826fc1c9c0860cb64adc069 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:40:06 +0900 Subject: [PATCH 077/620] Add some comments --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 4 ++++ osu.Game/Online/Rooms/PlaylistItem.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 027d5b4a17..4a15fd9690 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -56,6 +56,10 @@ namespace osu.Game.Online.Rooms [Key(10)] public double StarRating { get; set; } + /// + /// A non-null value indicates "freestyle" mode where players are able to individually select + /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// [Key(11)] public int? BeatmapSetID { get; set; } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 937bc40e9b..16c252befc 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -67,6 +67,10 @@ namespace osu.Game.Online.Rooms set => Beatmap = new APIBeatmap { OnlineID = value }; } + /// + /// A non-null value indicates "freestyle" mode where players are able to individually select + /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// [JsonProperty("beatmapset_id")] public int? BeatmapSetId { get; set; } From b4f35f330ce215cd9aa7049d3ceb9e5e75fb2b8f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 20:13:35 +0900 Subject: [PATCH 078/620] Use online ruleset_id to build local score models --- osu.Game/Online/Rooms/MultiplayerScore.cs | 11 +++++++---- .../DailyChallenge/DailyChallengeLeaderboard.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index faa66c571d..2adee26da3 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -77,11 +77,14 @@ namespace osu.Game.Online.Rooms [CanBeNull] public MultiplayerScoresAround ScoresAround { get; set; } - public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap) + [JsonProperty("ruleset_id")] + public int RulesetId { get; set; } + + public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap) { - var ruleset = rulesets.GetRuleset(playlistItem.RulesetID); + var ruleset = rulesets.GetRuleset(RulesetId); if (ruleset == null) - throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}"); + throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {RulesetId}"); var rulesetInstance = ruleset.CreateInstance(); @@ -91,7 +94,7 @@ namespace osu.Game.Online.Rooms TotalScore = TotalScore, MaxCombo = MaxCombo, BeatmapInfo = beatmap, - Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"), + Ruleset = ruleset, Passed = Passed, Statistics = Statistics, MaximumStatistics = MaximumStatistics, diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 9fe2b70a5a..4736ba28db 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -142,10 +142,10 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge request.Success += req => Schedule(() => { - var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray(); + var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo)).ToArray(); userBestScore.Value = req.UserScore; - var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo); + var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo); cancellationTokenSource?.Cancel(); cancellationTokenSource = null; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 81ae51bd1b..13ef5d6f64 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// An optional pivot around which the scores were retrieved. protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); + var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already. callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); From a2dc16f8dffab2521b83d154cdcecb8d6baa48c1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 20:22:16 +0900 Subject: [PATCH 079/620] Fix inspection --- osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index 5edcddcb78..cdfb73cee1 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { - public class FooterButtonFreeStyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreeStyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); From a407e3f3e04d5765d8678970c83e4fb13b04f513 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 16:46:02 +0900 Subject: [PATCH 080/620] Fix co-variant array conversion --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 98667c16fb..48d50d727b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RelativeSizeAxes = Axes.Both, Content = new[] { - new[] + new Drawable[] { new Container { From 95fe8d67e4fb899eec812e28a30528f145617caf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 16:51:50 +0900 Subject: [PATCH 081/620] Fix test --- .../Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8ea52f8099..e95209f993 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -30,6 +30,7 @@ using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -271,7 +272,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("last playlist item selected", () => { - var lastItem = this.ChildrenOfType().Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID); + var lastItem = this.ChildrenOfType() + .Single() + .ChildrenOfType() + .Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID); return lastItem.IsSelectedItem; }); } From 0093af8f5595bb28b8f39fc5faa2b96bf658ea5f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 22:24:21 +0900 Subject: [PATCH 082/620] Rewrite everything to better support spectator server messaging --- .../Online/Multiplayer/IMultiplayerClient.cs | 8 + .../Multiplayer/IMultiplayerRoomServer.cs | 7 + .../Online/Multiplayer/MultiplayerClient.cs | 21 ++ .../Online/Multiplayer/MultiplayerRoomUser.cs | 18 +- .../Multiplayer/OnlineMultiplayerClient.cs | 11 + .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 228 +++++++++--------- .../MultiplayerMatchStyleSelect.cs | 130 +++++----- .../Multiplayer/MultiplayerMatchSubScreen.cs | 37 ++- .../OnlinePlay/OnlinePlayStyleSelect.cs | 98 ++++++++ .../Playlists/PlaylistsRoomStyleSelect.cs | 30 +++ .../Playlists/PlaylistsRoomSubScreen.cs | 14 +- .../Multiplayer/TestMultiplayerClient.cs | 17 ++ 12 files changed, 417 insertions(+), 202 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 0452d8b79c..adb9b92614 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -95,6 +95,14 @@ namespace osu.Game.Online.Multiplayer /// The new beatmap availability state of the user. Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability); + /// + /// Signals that a user in this room changed their style. + /// + /// The ID of the user whose style changed. + /// The user's beatmap. + /// The user's ruleset. + Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId); + /// /// Signals that a user in this room changed their local mods. /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 55f00b447f..490973faa2 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -57,6 +57,13 @@ namespace osu.Game.Online.Multiplayer /// The proposed new beatmap availability state. Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + /// + /// Change the local user's style in the currently joined room. + /// + /// The beatmap. + /// The ruleset. + Task ChangeUserStyle(int? beatmapId, int? rulesetId); + /// /// Change the local user's mods in the currently joined room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 998a34931d..a588ec4441 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -359,6 +359,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task DisconnectInternal(); + public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId); + /// /// Change the local user's mods in the currently joined room. /// @@ -652,6 +654,25 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + public Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId) + { + Scheduler.Add(() => + { + var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + + // errors here are not critical - user style is mostly for display. + if (user == null) + return; + + user.BeatmapId = beatmapId; + user.RulesetId = rulesetId; + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + public Task UserModsChanged(int userId, IEnumerable mods) { Scheduler.Add(() => diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index f769b4c805..8142873fd5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -22,9 +22,6 @@ namespace osu.Game.Online.Multiplayer [Key(1)] public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; - [Key(4)] - public MatchUserState? MatchState { get; set; } - /// /// The availability state of the current beatmap. /// @@ -37,6 +34,21 @@ namespace osu.Game.Online.Multiplayer [Key(3)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); + [Key(4)] + public MatchUserState? MatchState { get; set; } + + /// + /// Any ruleset applicable only to the local user. + /// + [Key(5)] + public int? RulesetId; + + /// + /// Any beatmap applicable only to the local user. + /// + [Key(6)] + public int? BeatmapId; + [IgnoreMember] public APIUser? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 40436d730e..2660cd94e4 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -60,6 +60,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted); connection.On(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + connection.On(nameof(IMultiplayerClient.UserStyleChanged), ((IMultiplayerClient)this).UserStyleChanged); connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); connection.On(nameof(IMultiplayerClient.MatchRoomStateChanged), ((IMultiplayerClient)this).MatchRoomStateChanged); @@ -186,6 +187,16 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability); } + public override Task ChangeUserStyle(int? beatmapId, int? rulesetId) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserStyle), beatmapId, rulesetId); + } + public override Task ChangeUserMods(IEnumerable newMods) { if (!IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 49144f9de5..b51679ded6 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,14 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -28,6 +26,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Utils; using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Match @@ -37,18 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Match { public readonly Bindable SelectedItem = new Bindable(); - /// - /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), - /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. - /// - public readonly Bindable DifficultyOverride = new Bindable(); - - /// - /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), - /// a non-null value indicates a local ruleset selection. - /// - public readonly Bindable RulesetOverride = new Bindable(); - public override bool? ApplyModTrackAdjustments => true; protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault()) @@ -65,13 +52,13 @@ namespace osu.Game.Screens.OnlinePlay.Match protected Drawable? UserModsSection; /// - /// A container that provides controls for selection of the user's difficulty override. + /// A container that provides controls for selection of the user style. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserDifficultySection; + protected Drawable? UserStyleSection; /// - /// A container that will display the user's difficulty override. + /// A container that will display the user's style. /// protected Container? UserStyleDisplayContainer; @@ -82,6 +69,18 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. + /// + public readonly Bindable UserBeatmap = new Bindable(); + + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local ruleset selection. + /// + public readonly Bindable UserRuleset = new Bindable(); + [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -272,13 +271,25 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); - DifficultyOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); - RulesetOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); + SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + + UserMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); + + UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(() => + { + updateBeatmap(); + updateUserStyle(); + })); + + UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(() => + { + updateUserMods(); + updateRuleset(); + updateUserStyle(); + })); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay); @@ -347,7 +358,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnSuspending(ScreenTransitionEvent e) { // Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state. - updateWorkingBeatmap(); + updateBeatmap(); onLeaving(); base.OnSuspending(e); @@ -356,10 +367,11 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - updateWorkingBeatmap(); + updateBeatmap(); beginHandlingTrack(); - Scheduler.AddOnce(UpdateMods); + Scheduler.AddOnce(updateMods); Scheduler.AddOnce(updateRuleset); + Scheduler.AddOnce(updateUserStyle); } protected bool ExitConfirmed { get; private set; } @@ -409,9 +421,13 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay() { - if (GetGameplayItem() is not PlaylistItem item) + if (SelectedItem.Value is not PlaylistItem item) return; + item = item.With( + ruleset: GetGameplayRuleset().OnlineID, + beatmap: new Optional(GetGameplayBeatmap())); + // User may be at song select or otherwise when the host starts gameplay. // Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state. if (!this.IsCurrentScreen()) @@ -437,31 +453,26 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - private void selectedItemChanged() + protected void OnSelectedItemChanged() { - if (SelectedItem.Value is not PlaylistItem selected) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - if (selected.BeatmapSetId == null || selected.BeatmapSetId != DifficultyOverride.Value?.BeatmapSet.AsNonNull().OnlineID) + // Reset user style if no longer valid. + // Todo: In the future this can be made more lenient, such as allowing a non-null ruleset as the set changes. + if (item.BeatmapSetId == null || item.BeatmapSetId != UserBeatmap.Value?.BeatmapSet!.OnlineID) { - DifficultyOverride.Value = null; - RulesetOverride.Value = null; + UserBeatmap.Value = null; + UserRuleset.Value = null; } - updateStyleOverride(); - updateWorkingBeatmap(); - - var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance)); - - // Remove any user mods that are no longer allowed. - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); - - UpdateMods(); + updateUserMods(); + updateBeatmap(); + updateMods(); updateRuleset(); + updateUserStyle(); - if (!selected.AllowedMods.Any()) + if (!item.AllowedMods.Any()) { UserModsSection?.Hide(); UserModsSelectOverlay.Hide(); @@ -470,100 +481,89 @@ namespace osu.Game.Screens.OnlinePlay.Match else { UserModsSection?.Show(); + + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } - if (selected.BeatmapSetId == null) - UserDifficultySection?.Hide(); + if (item.BeatmapSetId == null) + UserStyleSection?.Hide(); else - UserDifficultySection?.Show(); + UserStyleSection?.Show(); } - private void updateWorkingBeatmap() + private void updateUserMods() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + // Remove any user mods that are no longer allowed. + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); + } + + private void updateBeatmap() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); - - UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + int beatmapId = GetGameplayBeatmap().OnlineID; + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; } - protected virtual void UpdateMods() + private void updateMods() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); - } - - private void updateStyleOverride() - { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) - return; - - if (UserStyleDisplayContainer == null) - return; - - PlaylistItem gameplayItem = GetGameplayItem()!; - - if (UserStyleDisplayContainer.SingleOrDefault()?.Item.Equals(gameplayItem) == true) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => openStyleSelection() - }; - } - - protected PlaylistItem? GetGameplayItem() - { - PlaylistItem? selectedItemWithOverride = SelectedItem.Value; - - if (selectedItemWithOverride?.BeatmapSetId == null) - return selectedItemWithOverride; - - // Sanity check. - if (DifficultyOverride.Value?.BeatmapSet?.OnlineID != selectedItemWithOverride.BeatmapSetId) - return selectedItemWithOverride; - - if (DifficultyOverride.Value != null) - selectedItemWithOverride = selectedItemWithOverride.With(beatmap: DifficultyOverride.Value); - - if (RulesetOverride.Value != null) - selectedItemWithOverride = selectedItemWithOverride.With(ruleset: RulesetOverride.Value.OnlineID); - - return selectedItemWithOverride; - } - - private void openStyleSelection() - { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) - return; - - this.Push(new MultiplayerMatchStyleSelect(Room, SelectedItem.Value, (beatmap, ruleset) => - { - if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) - return; - - DifficultyOverride.Value = beatmap; - RulesetOverride.Value = ruleset; - })); + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); } private void updateRuleset() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - Ruleset.Value = Rulesets.GetRuleset(item.RulesetID); + Ruleset.Value = GetGameplayRuleset(); } + private void updateUserStyle() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) + return; + + if (UserStyleDisplayContainer != null) + { + PlaylistItem gameplayItem = SelectedItem.Value.With( + ruleset: GetGameplayRuleset().OnlineID, + beatmap: new Optional(GetGameplayBeatmap())); + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; + } + } + + protected virtual APIMod[] GetGameplayMods() + => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + + protected virtual RulesetInfo GetGameplayRuleset() + => Rulesets.GetRuleset(UserRuleset.Value?.OnlineID ?? SelectedItem.Value!.RulesetID)!; + + protected virtual IBeatmapInfo GetGameplayBeatmap() + => UserBeatmap.Value ?? SelectedItem.Value!.Beatmap; + + protected abstract void OpenStyleSelection(); + private void beginHandlingTrack() { Beatmap.BindValueChanged(applyLoopingToTrack, true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index 867579171d..3fe4926052 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -2,106 +2,88 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; -using Humanizer; using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Bindables; +using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Beatmaps; -using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Screens.Select; -using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap + public partial class MultiplayerMatchStyleSelect : OnlinePlayStyleSelect { - public string ShortTitle => "style selection"; + [Resolved] + private MultiplayerClient client { get; set; } = null!; - public override string Title => ShortTitle.Humanize(); + [Resolved] + private OngoingOperationTracker operationTracker { get; set; } = null!; - public override bool AllowEditing => false; + private readonly IBindable operationInProgress = new Bindable(); - protected override UserActivity InitialActivity => new UserActivity.InLobby(room); + private LoadingLayer loadingLayer = null!; + private IDisposable? selectionOperation; - private readonly Room room; - private readonly PlaylistItem item; - private readonly Action onSelect; - - public MultiplayerMatchStyleSelect(Room room, PlaylistItem item, Action onSelect) + public MultiplayerMatchStyleSelect(Room room, PlaylistItem item) + : base(room, item) { - this.room = room; - this.item = item; - this.onSelect = onSelect; - - Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; } [BackgroundDependencyLoader] private void load() { - LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; + AddInternal(loadingLayer = new LoadingLayer(true)); } - protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); - - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override void LoadComplete() { - // Required to create the drawable components. - base.CreateSongSelectFooterButtons(); - return Enumerable.Empty<(FooterButton, OverlayContainer?)>(); + base.LoadComplete(); + + operationInProgress.BindTo(operationTracker.InProgress); + operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); } - protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + private void updateLoadingLayer() + { + if (operationInProgress.Value) + loadingLayer.Show(); + else + loadingLayer.Hide(); + } protected override bool OnStart() { - onSelect(Beatmap.Value.BeatmapInfo, Ruleset.Value); - this.Exit(); + if (operationInProgress.Value) + { + Logger.Log($"{nameof(OnStart)} aborted due to {nameof(operationInProgress)}"); + return false; + } + + selectionOperation = operationTracker.BeginOperation(); + + client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID) + .FireAndForget(onSuccess: () => + { + selectionOperation.Dispose(); + + Schedule(() => + { + // If an error or server side trigger occurred this screen may have already exited by external means. + if (this.IsCurrentScreen()) + this.Exit(); + }); + }, onError: _ => + { + selectionOperation.Dispose(); + + Schedule(() => + { + Carousel.AllowSelection = true; + }); + }); + return true; } - - public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) - { - // This screen cannot present beatmaps. - } - - private partial class DifficultySelectFilterControl : FilterControl - { - private readonly PlaylistItem item; - private double itemLength; - - public DifficultySelectFilterControl(PlaylistItem item) - { - this.item = item; - } - - [BackgroundDependencyLoader] - private void load(RealmAccess realm) - { - int beatmapId = item.Beatmap.OnlineID; - itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); - } - - public override FilterCriteria CreateCriteria() - { - var criteria = base.CreateCriteria(); - - // Must be from the same set as the playlist item. - criteria.BeatmapSetId = item.BeatmapSetId; - - // Must be within 30s of the playlist item. - criteria.Length.Min = itemLength - 30000; - criteria.Length.Max = itemLength + 30000; - criteria.Length.IsLowerInclusive = true; - criteria.Length.IsUpperInclusive = true; - - return criteria; - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index d807fe8177..edfb059c77 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -16,6 +16,8 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -188,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, } }, - UserDifficultySection = new FillFlowContainer + UserStyleSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -251,6 +253,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit)); } + protected override void OpenStyleSelection() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + this.Push(new MultiplayerMatchStyleSelect(Room, item)); + } + protected override Drawable CreateFooter() => new MultiplayerMatchFooter { SelectedItem = SelectedItem @@ -261,16 +271,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem }; - protected override void UpdateMods() + protected override APIMod[] GetGameplayMods() { - if (GetGameplayItem() is not PlaylistItem item || client.LocalUser == null || !this.IsCurrentScreen()) - return; + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray()!; + } - // update local mods based on room's reported status for the local user (omitting the base call implementation). - // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). - var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + protected override RulesetInfo GetGameplayRuleset() + { + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.RulesetId != null ? Rulesets.GetRuleset(client.LocalUser.RulesetId.Value)! : base.GetGameplayRuleset(); + } + + protected override IBeatmapInfo GetGameplayBeatmap() + { + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.BeatmapId != null ? new APIBeatmap { OnlineID = client.LocalUser.BeatmapId.Value } : base.GetGameplayBeatmap(); } [Resolved(canBeNull: true)] @@ -376,7 +392,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer addItemButton.Alpha = localUserCanAddItem ? 1 : 0; - Scheduler.AddOnce(UpdateMods); + // Forcefully update the selected item so that the user state is applied. + Scheduler.AddOnce(OnSelectedItemChanged); Activity.Value = new UserActivity.InLobby(Room); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs new file mode 100644 index 0000000000..89f2ffc883 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -0,0 +1,98 @@ +// 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 Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay +{ + public abstract partial class OnlinePlayStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap + { + public string ShortTitle => "style selection"; + + public override string Title => ShortTitle.Humanize(); + + public override bool AllowEditing => false; + + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); + + private readonly Room room; + private readonly PlaylistItem item; + + protected OnlinePlayStyleSelect(Room room, PlaylistItem item) + { + this.room = room; + this.item = item; + + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + } + + [BackgroundDependencyLoader] + private void load() + { + LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; + } + + protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); + + protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + { + // Required to create the drawable components. + base.CreateSongSelectFooterButtons(); + return Enumerable.Empty<(FooterButton, OverlayContainer?)>(); + } + + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + // This screen cannot present beatmaps. + } + + private partial class DifficultySelectFilterControl : FilterControl + { + private readonly PlaylistItem item; + private double itemLength; + + public DifficultySelectFilterControl(PlaylistItem item) + { + this.item = item; + } + + [BackgroundDependencyLoader] + private void load(RealmAccess realm) + { + int beatmapId = item.Beatmap.OnlineID; + itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + } + + public override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + + // Must be from the same set as the playlist item. + criteria.BeatmapSetId = item.BeatmapSetId; + + // Must be within 30s of the playlist item. + criteria.Length.Min = itemLength - 30000; + criteria.Length.Max = itemLength + 30000; + criteria.Length.IsLowerInclusive = true; + criteria.Length.IsUpperInclusive = true; + + return criteria; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs new file mode 100644 index 0000000000..f3d868b0de --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class PlaylistsRoomStyleSelect : OnlinePlayStyleSelect + { + public new readonly Bindable Beatmap = new Bindable(); + public new readonly Bindable Ruleset = new Bindable(); + + public PlaylistsRoomStyleSelect(Room room, PlaylistItem item) + : base(room, item) + { + } + + protected override bool OnStart() + { + Beatmap.Value = base.Beatmap.Value.BeatmapInfo; + Ruleset.Value = base.Ruleset.Value; + this.Exit(); + return true; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 48d50d727b..b941bbd290 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -213,7 +213,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - UserDifficultySection = new FillFlowContainer + UserStyleSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -299,6 +299,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, }; + protected override void OpenStyleSelection() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + this.Push(new PlaylistsRoomStyleSelect(Room, item) + { + Beatmap = { BindTarget = UserBeatmap }, + Ruleset = { BindTarget = UserRuleset } + }); + } + private void updatePollingRate() { selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4d812abf11..3abef523cd 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -335,6 +335,23 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override Task ChangeUserStyle(int? beatmapId, int? rulesetId) + { + ChangeUserStyle(api.LocalUser.Value.Id, beatmapId, rulesetId); + return Task.CompletedTask; + } + + public void ChangeUserStyle(int userId, int? beatmapId, int? rulesetId) + { + Debug.Assert(ServerRoom != null); + + var user = ServerRoom.Users.Single(u => u.UserID == userId); + user.BeatmapId = beatmapId; + user.RulesetId = rulesetId; + + ((IMultiplayerClient)this).UserStyleChanged(userId, beatmapId, rulesetId); + } + public void ChangeUserMods(int userId, IEnumerable newMods) => ChangeUserMods(userId, newMods.Select(m => new APIMod(m))); From c3aa9d6f8a495f4ef592767ddab579f8c232ce5b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:30:24 +0900 Subject: [PATCH 083/620] Display user style in participant panel --- .../TestSceneMultiplayerParticipantsList.cs | 27 +++++ .../Participants/ParticipantPanel.cs | 105 +++++++++++++++++- 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index d88741ec0c..238a716f91 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -308,6 +308,33 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set state: locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); } + [Test] + public void TestUserWithStyle() + { + AddStep("add users", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = 0, + Username = "User 0", + RulesetsStatistics = new Dictionary + { + { + Ruleset.Value.ShortName, + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } + } + }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + MultiplayerClient.ChangeUserStyle(0, 259, 2); + }); + + AddStep("set beatmap locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); + AddStep("change user style to beatmap: 258, ruleset: 1", () => MultiplayerClient.ChangeUserStyle(0, 258, 1)); + AddStep("change user style to beatmap: null, ruleset: null", () => MultiplayerClient.ChangeUserStyle(0, null, null)); + } + [Test] public void TestModOverlap() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 7e42b18240..64c4648125 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; @@ -14,6 +16,9 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Logging; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -47,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private SpriteIcon crown = null!; private OsuSpriteText userRankText = null!; + private StyleDisplayIcon userStyleDisplay = null!; private ModDisplay userModsDisplay = null!; private StateDisplay userStateDisplay = null!; @@ -149,16 +155,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } } }, - new Container + new FillFlowContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Right = 70 }, - Child = userModsDisplay = new ModDisplay + Children = new Drawable[] { - Scale = new Vector2(0.5f), - ExpansionMode = ExpansionMode.AlwaysContracted, + userStyleDisplay = new StyleDisplayIcon(), + userModsDisplay = new ModDisplay + { + Scale = new Vector2(0.5f), + ExpansionMode = ExpansionMode.AlwaysContracted, + } } }, userStateDisplay = new StateDisplay @@ -208,9 +218,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) + { userModsDisplay.FadeIn(fade_time); + userStyleDisplay.FadeIn(fade_time); + } else + { userModsDisplay.FadeOut(fade_time); + userStyleDisplay.FadeOut(fade_time); + } + + if (User.BeatmapId == null && User.RulesetId == null) + userStyleDisplay.Style = null; + else + userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; @@ -284,5 +305,81 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants IconHoverColour = colours.Red; } } + + private partial class StyleDisplayIcon : CompositeComponent + { + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public StyleDisplayIcon() + { + AutoSizeAxes = Axes.Both; + } + + private (int beatmap, int ruleset)? style; + + public (int beatmap, int ruleset)? Style + { + get => style; + set + { + if (style == value) + return; + + style = value; + Scheduler.Add(refresh); + } + } + + private CancellationTokenSource? cancellationSource; + + private void refresh() + { + cancellationSource?.Cancel(); + cancellationSource?.Dispose(); + cancellationSource = null; + + if (Style == null) + { + ClearInternal(); + return; + } + + cancellationSource = new CancellationTokenSource(); + CancellationToken token = cancellationSource.Token; + + int localBeatmap = Style.Value.beatmap; + int localRuleset = Style.Value.ruleset; + + Task.Run(async () => + { + try + { + var beatmap = await beatmapLookupCache.GetBeatmapAsync(localBeatmap, token).ConfigureAwait(false); + if (beatmap == null) + return; + + Schedule(() => + { + if (token.IsCancellationRequested) + return; + + InternalChild = new DifficultyIcon(beatmap, rulesets.GetRuleset(localRuleset)) + { + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + }; + }); + } + catch (Exception e) + { + Logger.Log($"Error while populating participant style icon {e}"); + } + }, token); + } + } } } From e7c272b8b9278e706baf9305c8ff92548c22ff32 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:39:01 +0900 Subject: [PATCH 084/620] Don't display on matching beatmap/ruleset --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 64c4648125..a2657019a3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -228,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if (User.BeatmapId == null && User.RulesetId == null) + if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID)) userStyleDisplay.Style = null; else userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); From 6579b055618f375e06437f05ff70f612316e72a6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:45:36 +0900 Subject: [PATCH 085/620] Remove unused usings --- osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 89f2ffc883..029ca68e36 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.Rooms; From 1f60adbaf144ab77dbc211f14c1a2ede46e6bf74 Mon Sep 17 00:00:00 2001 From: kongehund <63306696+kongehund@users.noreply.github.com> Date: Thu, 26 Dec 2024 00:35:21 +0100 Subject: [PATCH 086/620] Switch scroll direction for beat snap Matches stable better --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 35d2465084..2666b24be9 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,8 +142,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), - new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), - new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), From e752531aec5dea9401b55afc312c8f625673dba6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Dec 2024 15:05:59 +0900 Subject: [PATCH 087/620] Fix volume adjust key repeat not working as expected Regressed in https://github.com/ppy/osu/pull/31146. Closes part of https://github.com/ppy/osu/issues/31267. --- osu.Game/OsuGame.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 06e30e3fab..6812cd87cf 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1428,9 +1428,18 @@ namespace osu.Game public bool OnPressed(KeyBindingPressEvent e) { + switch (e.Action) + { + case GlobalAction.DecreaseVolume: + case GlobalAction.IncreaseVolume: + return volume.Adjust(e.Action); + } + + // All actions below this point don't allow key repeat. if (e.Repeat) return false; + // Wait until we're loaded at least to the intro before allowing various interactions. if (introScreen == null) return false; switch (e.Action) @@ -1442,10 +1451,6 @@ namespace osu.Game case GlobalAction.ToggleMute: case GlobalAction.NextVolumeMeter: case GlobalAction.PreviousVolumeMeter: - - if (e.Repeat) - return true; - return volume.Adjust(e.Action); case GlobalAction.ToggleFPSDisplay: From 2a374c06958d7a2ac0640e8dd506d91f236bbf17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Dec 2024 15:42:34 +0900 Subject: [PATCH 088/620] Add migration --- osu.Game/Database/RealmAccess.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index b412348595..e1b8de89fa 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -96,8 +96,9 @@ namespace osu.Game.Database /// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user. /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. + /// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯. /// - private const int schema_version = 45; + private const int schema_version = 46; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1222,6 +1223,22 @@ namespace osu.Game.Database break; } + + case 46: + { + // Stable direction didn't match. + var keyBindings = migration.NewRealm.All(); + + var nextBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCycleNextBeatSnapDivisor); + if (nextBeatSnapBinding != null && nextBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.MouseWheelDown })) + migration.NewRealm.Remove(nextBeatSnapBinding); + + var previousBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCyclePreviousBeatSnapDivisor); + if (previousBeatSnapBinding != null && previousBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.MouseWheelUp })) + migration.NewRealm.Remove(previousBeatSnapBinding); + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); From 94d56d3584c8c1021e11d00a71469d90bc4991b6 Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 26 Dec 2024 18:13:09 +0500 Subject: [PATCH 089/620] Change `OsuModRelax` hit leniency to be the same as in stable --- .../Mods/TestSceneOsuModRelax.cs | 100 ++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 4 +- 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs new file mode 100644 index 0000000000..1bb2f24c1c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs @@ -0,0 +1,100 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModRelax : OsuModTestScene + { + private readonly HitCircle hitObject; + private readonly HitWindows hitWindows = new OsuHitWindows(); + + public TestSceneOsuModRelax() + { + hitWindows.SetDifficulty(9); + + hitObject = new HitCircle + { + StartTime = 1000, + Position = new Vector2(100, 100), + HitWindows = hitWindows + }; + } + + protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail); + + [Test] + public void TestRelax() => CreateModTest(new ModTestData + { + Mod = new OsuModRelax(), + Autoplay = false, + CreateBeatmap = () => new Beatmap + { + HitObjects = new List { hitObject } + }, + ReplayFrames = new List + { + new OsuReplayFrame(0, new Vector2()), + new OsuReplayFrame(hitObject.StartTime, hitObject.Position), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 1 + }); + + [Test] + public void TestRelaxLeniency() => CreateModTest(new ModTestData + { + Mod = new OsuModRelax(), + Autoplay = false, + CreateBeatmap = () => new Beatmap + { + HitObjects = new List { hitObject } + }, + ReplayFrames = new List + { + new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long + new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)), + new OsuReplayFrame(hitObject.StartTime, new Vector2(0)), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 1 + }); + + protected partial class ModRelaxTestPlayer : ModTestPlayer + { + private readonly ModTestData currentTestData; + + public ModRelaxTestPlayer(ModTestData data, bool allowFail) + : base(data, allowFail) + { + currentTestData = data; + } + + protected override void PrepareReplay() + { + // We need to set IsLegacyScore to true otherwise the mod assumes that presses are already embedded into the replay + DrawableRuleset?.SetReplayScore(new Score + { + Replay = new Replay { Frames = currentTestData.ReplayFrames! }, + ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" }, IsLegacyScore = true, Mods = new Mod[] { new OsuModRelax() } }, + }); + + DrawableRuleset?.SetRecordTarget(Score); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 31511c01b8..71de3c269b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// How early before a hitobject's start time to trigger a hit. /// - private const float relax_leniency = 3; + public const float RELAX_LENIENCY = 12; private bool isDownState; private bool wasLeft; @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType()) { // we are not yet close enough to the object. - if (time < h.HitObject.StartTime - relax_leniency) + if (time < h.HitObject.StartTime - RELAX_LENIENCY) break; // already hit or beyond the hittable end time. From ed397c8feef6a49d5df7eb3ae977791dbc351551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 09:02:59 +0100 Subject: [PATCH 090/620] Add failing assertions --- osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 23efb40d3f..765ffb4549 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -177,6 +177,7 @@ namespace osu.Game.Tests.Visual.Editing // bit of a hack to ensure this test can be ran multiple times without running into UNIQUE constraint failures AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = Guid.NewGuid().ToString()); + AddStep("start playing track", () => InputManager.Key(Key.Space)); AddStep("click test gameplay button", () => { var button = Editor.ChildrenOfType().Single(); @@ -185,11 +186,13 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Click(MouseButton.Left); }); AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog); + AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning); AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction()); EditorPlayer editorPlayer = null; AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + AddUntilStep("track playing", () => Beatmap.Value.Track.IsRunning); AddAssert("beatmap has 1 object", () => editorPlayer.Beatmap.Value.Beatmap.HitObjects.Count == 1); AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor); From 5abad0741265097cfaa53eceb375a0540d7a4aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 09:08:16 +0100 Subject: [PATCH 091/620] Pause playback when entering gameplay test from editor Closes https://github.com/ppy/osu/issues/31290. Tend to agree that this is a good idea for gameplay test at least. Not sure about other similar interactions like exiting - I don't think it matters what's done in those cases, because for exiting timing is in no way key, so I just applied this locally to gameplay test. --- osu.Game/Screens/Edit/Editor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d031eb84c6..f6875a7aa4 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -523,6 +523,8 @@ namespace osu.Game.Screens.Edit public void TestGameplay() { + clock.Stop(); + if (HasUnsavedChanges) { dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => From 0c02369bdc173bc900aa3d7f069cdf3b75c03029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 11:01:44 +0100 Subject: [PATCH 092/620] Add failing test case --- .../Beatmaps/IO/LegacyBeatmapExporterTest.cs | 24 ++++++++++++++++++ .../Archives/fractional-coordinates.olz | Bin 0 -> 556 bytes 2 files changed, 24 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/fractional-coordinates.olz diff --git a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs index 8a95d26782..cf498c7856 100644 --- a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; using MemoryStream = System.IO.MemoryStream; @@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001)); } + [Test] + public void TestFractionalObjectCoordinatesRounded() + { + IWorkingBeatmap beatmap = null!; + MemoryStream outStream = null!; + + // Ensure importer encoding is correct + AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz")); + AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001)); + + // Ensure exporter legacy conversion is correct + AddStep("export", () => + { + outStream = new MemoryStream(); + + new LegacyBeatmapExporter(LocalStorage) + .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null); + }); + + AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); + AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001)); + } + [Test] public void TestExportStability() { diff --git a/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz b/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz new file mode 100644 index 0000000000000000000000000000000000000000..5c5af368c8b95fe76c9f45f0dfbc5f1e73a126a5 GIT binary patch literal 556 zcmWIWW@Zs#-~d9~@Y#_JP;iZrfkBEvfg!CZF}Wl&KQA#yH#tAQC?zv5u_U!vFTc1n zG=!IdUA-zVT^ES0fVi}Rn}Lz#1v3K!mGuF23h}>PvNx-V+F!!I-FH zEGNUKc=wUltddJxI@Uis=>AF6hpR8S=*F9W_n+N=^82&5>Dur`4C_O7B^`Tx=;4eB z)7d91n0aHOXY_(y+Z$3uQ{;*>1Fl|ljbPFiGy1G?GvkGTUvx>xoMVeSd_uK-7y9j; zw|SM=gyxfb+@Gs>GowmE~dXeXY2Xr<2s9#*=r^0|K^@- zHNB**ovbUc%0gG<_!rO2jVI{q%V0+1|qIjV5RN0_Oi>7Q6DvV1s&==x=om ze(Bb|a(pZ2t6k~;d*PMhQoX3GzO=pd%)n9y0g96VZ$>5&W<-1;%Yotx2DUVUSmeiEfHx}}$OJ|p%mC8s JfOa!5002C=(%Aq2 literal 0 HcmV?d00001 From 0d16ed028b89c4ed92aa2efd7968557a700dbfcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 10:56:52 +0100 Subject: [PATCH 093/620] Add setters to hitobject coordinate interfaces --- .../Objects/EmptyFreeformHitObject.cs | 13 +++++++++-- .../Objects/PippidonHitObject.cs | 13 +++++++++-- .../Objects/CatchHitObject.cs | 22 ++++++++++++++++--- .../Objects/ManiaHitObject.cs | 6 ++++- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 13 +++++++++-- .../Objects/Legacy/ConvertHitObject.cs | 12 ++++++++-- .../Rulesets/Objects/Types/IHasPosition.cs | 2 +- .../Rulesets/Objects/Types/IHasXPosition.cs | 2 +- .../Rulesets/Objects/Types/IHasYPosition.cs | 2 +- 9 files changed, 70 insertions(+), 15 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs index 9cd18d2d9f..0699f5d039 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs @@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects public Vector2 Position { get; set; } - public float X => Position.X; - public float Y => Position.Y; + public float X + { + get => Position.X; + set => Position = new Vector2(value, Y); + } + + public float Y + { + get => Position.Y; + set => Position = new Vector2(X, value); + } } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs index 0c22554e82..f938d26b26 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs @@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects public Vector2 Position { get; set; } - public float X => Position.X; - public float Y => Position.Y; + public float X + { + get => Position.X; + set => Position = new Vector2(value, Y); + } + + public float Y + { + get => Position.Y; + set => Position = new Vector2(X, value); + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 329055b3dd..2018fd5ea9 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -210,11 +210,27 @@ namespace osu.Game.Rulesets.Catch.Objects /// public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y; - float IHasXPosition.X => OriginalX; + float IHasXPosition.X + { + get => OriginalX; + set => OriginalX = value; + } - float IHasYPosition.Y => LegacyConvertedY; + float IHasYPosition.Y + { + get => LegacyConvertedY; + set => LegacyConvertedY = value; + } - Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY); + Vector2 IHasPosition.Position + { + get => new Vector2(OriginalX, LegacyConvertedY); + set + { + ((IHasXPosition)this).X = value.X; + ((IHasYPosition)this).Y = value.Y; + } + } #endregion } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index 25ad6b997d..c8c8867bc6 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects #region LegacyBeatmapEncoder - float IHasXPosition.X => Column; + float IHasXPosition.X + { + get => Column; + set => Column = (int)value; + } #endregion } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 1b0993b698..8c1bd6302e 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects set => position.Value = value; } - public float X => Position.X; - public float Y => Position.Y; + public float X + { + get => Position.X; + set => Position = new Vector2(value, Position.Y); + } + + public float Y + { + get => Position.Y; + set => Position = new Vector2(Position.X, value); + } public Vector2 StackedPosition => Position + StackOffset; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index ced9b24ebf..091b0a1e6f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -21,9 +21,17 @@ namespace osu.Game.Rulesets.Objects.Legacy public int ComboOffset { get; set; } - public float X => Position.X; + public float X + { + get => Position.X; + set => Position = new Vector2(value, Position.Y); + } - public float Y => Position.Y; + public float Y + { + get => Position.Y; + set => Position = new Vector2(Position.X, value); + } public Vector2 Position { get; set; } diff --git a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs index 8948fe59a9..e9b3cc46eb 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs @@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The starting position of the HitObject. /// - Vector2 Position { get; } + Vector2 Position { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs index 7e55b21050..18f1f996e3 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The starting X-position of this HitObject. /// - float X { get; } + float X { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs index d2561b10a7..dcaeaf594a 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The starting Y-position of this HitObject. /// - float Y { get; } + float Y { get; set; } } } From e9762422b3a8db3b73b0c153f4df7083632c44be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 11:10:29 +0100 Subject: [PATCH 094/620] Round object coordinates to nearest integers rather than truncating Addresses https://github.com/ppy/osu/issues/31256. --- osu.Game/Database/LegacyBeatmapExporter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index eb48425588..24e752da31 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -42,7 +42,10 @@ namespace osu.Game.Database return null; using var contentStreamReader = new LineBufferedReader(contentStream); - var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); + + // FIRST_LAZER_VERSION is specified here to avoid flooring object coordinates on decode via `(int)` casts. + // we will be making integers out of them lower down, but in a slightly different manner (rounding rather than truncating) + var beatmapContent = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION).Decode(contentStreamReader); var workingBeatmap = new FlatWorkingBeatmap(beatmapContent); var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset); @@ -93,6 +96,12 @@ namespace osu.Game.Database hitObject.StartTime = Math.Floor(hitObject.StartTime); + if (hitObject is IHasXPosition hasXPosition) + hasXPosition.X = MathF.Round(hasXPosition.X); + + if (hitObject is IHasYPosition hasYPosition) + hasYPosition.Y = MathF.Round(hasYPosition.Y); + if (hitObject is not IHasPath hasPath) continue; // stable's hit object parsing expects the entire slider to use only one type of curve, From ecf64dfc5796eb3526f84fcf763512fa6c57f1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 12:38:15 +0100 Subject: [PATCH 095/620] Add failing test case --- .../Beatmaps/SliderEventGenerationTest.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index c7cf3fe956..ee2733ad91 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -112,5 +112,20 @@ namespace osu.Game.Tests.Beatmaps } }); } + + [Test] + public void TestRepeatsGeneratedEvenForZeroLengthSlider() + { + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, 0, 2).ToArray(); + + Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); + Assert.That(events[0].Time, Is.EqualTo(start_time)); + + Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Repeat)); + Assert.That(events[1].Time, Is.EqualTo(span_duration)); + + Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tail)); + Assert.That(events[3].Time, Is.EqualTo(span_duration * 2)); + } } } From e7225399a282c4f7194dd5ef9453ee3f52dd25ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 12:25:51 +0100 Subject: [PATCH 096/620] Fix slider event generator incorrectly not generating repeats when tick distance is zero RFC. This closes https://github.com/ppy/osu/issues/31186. To explain why: The issue occurs on https://osu.ppy.sh/beatmapsets/594828#osu/1258033, specifically on the slider at time 128604. The failure site is https://github.com/ppy/osu/blob/fa0d2f4af22fb9319e2a8773bf635368d86360be/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs#L65-L66 wherein `LastRepeat` is `null`, even though the slider's `RepeatCount` is 1 and thus `SpanCount` is 2. In this case, `SliderEventGenerator` is given a non-zero `tickDistance` but a zero `length`. The former is clamped to the latter: https://github.com/ppy/osu/blob/fa0d2f4af22fb9319e2a8773bf635368d86360be/osu.Game/Rulesets/Objects/SliderEventGenerator.cs#L34 Because of this, a whole block of code pertaining to tick generation gets turned off, because of zero tick spacing - however, that block also includes within it *repeat* generation, for seemingly very little reason whatsoever: https://github.com/ppy/osu/blob/fa0d2f4af22fb9319e2a8773bf635368d86360be/osu.Game/Rulesets/Objects/SliderEventGenerator.cs#L47-L77 While a zero tick distance would indeed cause `generateTicks()` to loop forever, it should have absolutely no effect on repeats. While this *is* ultimately an aspire-tier bug caused by people pushing things to limits, I do believe that in this case a fix is warranted because of how hard the current behaviour violates invariants. I do not like the possibility of having a slider with multiple spans and no repeats. --- .../Rulesets/Objects/SliderEventGenerator.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 9b8375f208..f5146d1675 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -44,13 +44,13 @@ namespace osu.Game.Rulesets.Objects PathProgress = 0, }; - if (tickDistance != 0) + for (int span = 0; span < spanCount; span++) { - for (int span = 0; span < spanCount; span++) - { - double spanStartTime = startTime + span * spanDuration; - bool reversed = span % 2 == 1; + double spanStartTime = startTime + span * spanDuration; + bool reversed = span % 2 == 1; + if (tickDistance != 0) + { var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); if (reversed) @@ -61,18 +61,18 @@ namespace osu.Game.Rulesets.Objects foreach (var e in ticks) yield return e; + } - if (span < spanCount - 1) + if (span < spanCount - 1) + { + yield return new SliderEventDescriptor { - yield return new SliderEventDescriptor - { - Type = SliderEventType.Repeat, - SpanIndex = span, - SpanStartTime = startTime + span * spanDuration, - Time = spanStartTime + spanDuration, - PathProgress = (span + 1) % 2, - }; - } + Type = SliderEventType.Repeat, + SpanIndex = span, + SpanStartTime = startTime + span * spanDuration, + Time = spanStartTime + spanDuration, + PathProgress = (span + 1) % 2, + }; } } From a9a5bb2c6a172bd8dcd4d2f84bc425e903a47231 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Dec 2024 21:36:07 +0900 Subject: [PATCH 097/620] Remove duplicated block --- osu.Game/OsuGame.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6812cd87cf..c20536a1ec 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1444,10 +1444,6 @@ namespace osu.Game switch (e.Action) { - case GlobalAction.DecreaseVolume: - case GlobalAction.IncreaseVolume: - return volume.Adjust(e.Action); - case GlobalAction.ToggleMute: case GlobalAction.NextVolumeMeter: case GlobalAction.PreviousVolumeMeter: From 6a6db5a22bb355130ccb189e3540320573e7f29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 15:07:24 +0100 Subject: [PATCH 098/620] Populate metadata from ID3 tags when changing beatmap audio track in editor - Closes https://github.com/ppy/osu/issues/21189 - Supersedes / closes https://github.com/ppy/osu-framework/pull/5627 - Supersedes / closes https://github.com/ppy/osu/pull/22235 The reason why I opted for a complete rewrite rather than a revival of that aforementioned pull series is that it always felt quite gross to me to be pulling framework's audio subsystem into the task of reading ID3 tags, and I also partially don't believe that BASS is *good* at reading ID3 tags. Meanwhile, we already have another library pulled in that is *explicitly* intended for reading multimedia metadata, and using it does not require framework changes. (And it was pulled in explicitly for use in the editor verify tab as well.) The hard and dumb part of this diff is hacking the gibson such that the metadata section on setup screen actually *updates itself* after the resources section is done doing its thing. After significant gnashing of teeth I just did the bare minimum to make work by caching a common parent and exposing an `Action?` on it. If anyone has better ideas, I'm all ears. --- .../Screens/Edit/Setup/MetadataSection.cs | 53 ++++++++++++------- .../Screens/Edit/Setup/ResourcesSection.cs | 36 ++++++++++--- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 4 ++ 3 files changed, 67 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 20c0a74d84..6926b6631f 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -28,33 +28,29 @@ namespace osu.Game.Screens.Edit.Setup public override LocalisableString Title => EditorSetupStrings.MetadataHeader; [BackgroundDependencyLoader] - private void load() + private void load(SetupScreen setupScreen) { - var metadata = Beatmap.Metadata; - Children = new[] { - ArtistTextBox = createTextBox(EditorSetupStrings.Artist, - !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist), - RomanisedArtistTextBox = createTextBox(EditorSetupStrings.RomanisedArtist, - !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), - TitleTextBox = createTextBox(EditorSetupStrings.Title, - !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title), - RomanisedTitleTextBox = createTextBox(EditorSetupStrings.RomanisedTitle, - !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), - creatorTextBox = createTextBox(EditorSetupStrings.Creator, metadata.Author.Username), - difficultyTextBox = createTextBox(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName), - sourceTextBox = createTextBox(BeatmapsetsStrings.ShowInfoSource, metadata.Source), - tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags, metadata.Tags) + ArtistTextBox = createTextBox(EditorSetupStrings.Artist), + RomanisedArtistTextBox = createTextBox(EditorSetupStrings.RomanisedArtist), + TitleTextBox = createTextBox(EditorSetupStrings.Title), + RomanisedTitleTextBox = createTextBox(EditorSetupStrings.RomanisedTitle), + creatorTextBox = createTextBox(EditorSetupStrings.Creator), + difficultyTextBox = createTextBox(EditorSetupStrings.DifficultyName), + sourceTextBox = createTextBox(BeatmapsetsStrings.ShowInfoSource), + tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags) }; + + setupScreen.MetadataChanged += reloadMetadata; + reloadMetadata(); } - private TTextBox createTextBox(LocalisableString label, string initialValue) + private TTextBox createTextBox(LocalisableString label) where TTextBox : FormTextBox, new() => new TTextBox { Caption = label, - Current = { Value = initialValue }, TabbableContentContainer = this }; @@ -94,10 +90,29 @@ namespace osu.Game.Screens.Edit.Setup // for now, update on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - updateMetadata(); + setMetadata(); } - private void updateMetadata() + private void reloadMetadata() + { + var metadata = Beatmap.Metadata; + + RomanisedArtistTextBox.ReadOnly = false; + RomanisedTitleTextBox.ReadOnly = false; + + ArtistTextBox.Current.Value = !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist; + RomanisedArtistTextBox.Current.Value = !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode); + TitleTextBox.Current.Value = !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title; + RomanisedTitleTextBox.Current.Value = !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode); + creatorTextBox.Current.Value = metadata.Author.Username; + difficultyTextBox.Current.Value = Beatmap.BeatmapInfo.DifficultyName; + sourceTextBox.Current.Value = metadata.Source; + tagsTextBox.Current.Value = metadata.Tags; + + updateReadOnlyState(); + } + + private void setMetadata() { Beatmap.Metadata.ArtistUnicode = ArtistTextBox.Current.Value; Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value; diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 7fcd09d7e7..5bc95dd824 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -35,6 +35,9 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] private Editor? editor { get; set; } + [Resolved] + private SetupScreen setupScreen { get; set; } = null!; + private SetupScreenHeaderBackground headerBackground = null!; [BackgroundDependencyLoader] @@ -93,15 +96,37 @@ namespace osu.Game.Screens.Edit.Setup if (!source.Exists) return false; + var tagSource = TagLib.File.Create(source.FullName); + changeResource(source, applyToAllDifficulties, @"audio", metadata => metadata.AudioFile, - (metadata, name) => metadata.AudioFile = name); + (metadata, name) => + { + metadata.AudioFile = name; + + string artist = tagSource.Tag.JoinedAlbumArtists; + + if (!string.IsNullOrWhiteSpace(artist)) + { + metadata.ArtistUnicode = artist; + metadata.Artist = MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode); + } + + string title = tagSource.Tag.Title; + + if (!string.IsNullOrEmpty(title)) + { + metadata.TitleUnicode = title; + metadata.Title = MetadataUtils.StripNonRomanisedCharacters(metadata.TitleUnicode); + } + }); music.ReloadCurrentTrack(); + setupScreen.MetadataChanged?.Invoke(); return true; } - private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func readFilename, Action writeFilename) + private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func readFilename, Action writeMetadata) { var set = working.Value.BeatmapSetInfo; var beatmap = working.Value.BeatmapInfo; @@ -148,10 +173,7 @@ namespace osu.Game.Screens.Edit.Setup { foreach (var b in otherBeatmaps) { - // This operation is quite expensive, so only perform it if required. - if (readFilename(b.Metadata) == newFilename) continue; - - writeFilename(b.Metadata, newFilename); + writeMetadata(b.Metadata, newFilename); // save the difficulty to re-encode the .osu file, updating any reference of the old filename. // @@ -162,7 +184,7 @@ namespace osu.Game.Screens.Edit.Setup } } - writeFilename(beatmap.Metadata, newFilename); + writeMetadata(beatmap.Metadata, newFilename); // editor change handler cannot be aware of any file changes or other difficulties having their metadata modified. // for simplicity's sake, trigger a save when changing any resource to ensure the change is correctly saved. diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index f8c4998263..97e12ae096 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -13,12 +14,15 @@ using osuTK; namespace osu.Game.Screens.Edit.Setup { + [Cached] public partial class SetupScreen : EditorScreen { public const float COLUMN_WIDTH = 450; public const float SPACING = 28; public const float MAX_WIDTH = 2 * COLUMN_WIDTH + SPACING; + public Action? MetadataChanged { get; set; } + public SetupScreen() : base(EditorScreenMode.SongSetup) { From 1b2a223a5f5c3cc3523d0b7446cd2a1cea04e510 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 28 Dec 2024 01:02:15 +0900 Subject: [PATCH 099/620] Fix failing test scene due to new dependency --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 6926b6631f..7b74aa7642 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Setup public override LocalisableString Title => EditorSetupStrings.MetadataHeader; [BackgroundDependencyLoader] - private void load(SetupScreen setupScreen) + private void load(SetupScreen? setupScreen) { Children = new[] { @@ -42,7 +42,9 @@ namespace osu.Game.Screens.Edit.Setup tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags) }; - setupScreen.MetadataChanged += reloadMetadata; + if (setupScreen != null) + setupScreen.MetadataChanged += reloadMetadata; + reloadMetadata(); } From 8be500535d651e0ed17e4ab996cbb063773b4634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 30 Dec 2024 03:13:22 +0100 Subject: [PATCH 100/620] Speed up metronome when holding control --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 29e730c865..44553a92d4 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Timing; using osu.Framework.Utils; @@ -232,6 +233,19 @@ namespace osu.Game.Screens.Edit.Timing private ScheduledDelegate? latchDelegate; + private bool divisorChanged; + + private void setDivisor(int divisor) + { + if (divisor == Divisor) + return; + + divisorChanged = true; + + Divisor = divisor; + metronomeTick.Divisor = divisor; + } + protected override void LoadComplete() { base.LoadComplete(); @@ -250,13 +264,13 @@ namespace osu.Game.Screens.Edit.Timing timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime); - if (beatLength != timingPoint.BeatLength) + if (beatLength != timingPoint.BeatLength || divisorChanged) { beatLength = timingPoint.BeatLength; EarlyActivationMilliseconds = timingPoint.BeatLength / 2; - float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((timingPoint.BPM - 30) / 480, 0, 1)); + float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((timingPoint.BPM - 30) / 480 * Divisor, 0, 1)); weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint); @@ -286,6 +300,8 @@ namespace osu.Game.Screens.Edit.Timing latchDelegate = Schedule(() => sampleLatch?.Play()); } } + + divisorChanged = false; } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) @@ -316,6 +332,22 @@ namespace osu.Game.Screens.Edit.Timing stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); } + protected override bool OnKeyDown(KeyDownEvent e) + { + updateDivisorFromKey(e); + + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + base.OnKeyUp(e); + + updateDivisorFromKey(e); + } + + private void updateDivisorFromKey(UIEvent e) => setDivisor(e.ControlPressed ? 2 : 1); + private partial class MetronomeTick : BeatSyncedContainer { public bool EnableClicking; From aa6763785c00a50d1624b1aebe2a400d63273fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 30 Dec 2024 03:21:52 +0100 Subject: [PATCH 101/620] Use 3x speed instead when beat snap divisor is divisible by 3 --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 44553a92d4..553eacab46 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -42,6 +42,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private OverlayColourProvider overlayColourProvider { get; set; } = null!; + [Resolved] + private BindableBeatDivisor beatDivisor { get; set; } = null!; + public bool EnableClicking { get => metronomeTick.EnableClicking; @@ -233,10 +236,17 @@ namespace osu.Game.Screens.Edit.Timing private ScheduledDelegate? latchDelegate; + private bool spedUp; + private bool divisorChanged; - private void setDivisor(int divisor) + private void updateDivisor() { + int divisor = 1; + + if (spedUp) + divisor = beatDivisor.Value % 3 == 0 ? 3 : 2; + if (divisor == Divisor) return; @@ -264,6 +274,8 @@ namespace osu.Game.Screens.Edit.Timing timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime); + updateDivisor(); + if (beatLength != timingPoint.BeatLength || divisorChanged) { beatLength = timingPoint.BeatLength; @@ -346,7 +358,7 @@ namespace osu.Game.Screens.Edit.Timing updateDivisorFromKey(e); } - private void updateDivisorFromKey(UIEvent e) => setDivisor(e.ControlPressed ? 2 : 1); + private void updateDivisorFromKey(UIEvent e) => spedUp = e.ControlPressed; private partial class MetronomeTick : BeatSyncedContainer { From 9ea7afb38edb455f07771191481bd47e53bf9c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 30 Dec 2024 03:59:54 +0100 Subject: [PATCH 102/620] Use return value instead of field to force weight position update --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 553eacab46..58d461b3a5 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -238,9 +238,7 @@ namespace osu.Game.Screens.Edit.Timing private bool spedUp; - private bool divisorChanged; - - private void updateDivisor() + private bool updateDivisor() { int divisor = 1; @@ -248,12 +246,12 @@ namespace osu.Game.Screens.Edit.Timing divisor = beatDivisor.Value % 3 == 0 ? 3 : 2; if (divisor == Divisor) - return; - - divisorChanged = true; + return false; Divisor = divisor; metronomeTick.Divisor = divisor; + + return true; } protected override void LoadComplete() @@ -274,9 +272,7 @@ namespace osu.Game.Screens.Edit.Timing timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime); - updateDivisor(); - - if (beatLength != timingPoint.BeatLength || divisorChanged) + if (updateDivisor() || beatLength != timingPoint.BeatLength) { beatLength = timingPoint.BeatLength; @@ -312,8 +308,6 @@ namespace osu.Game.Screens.Edit.Timing latchDelegate = Schedule(() => sampleLatch?.Play()); } } - - divisorChanged = false; } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) From 7563a18c7fdcc40c33a1ef0e0ab5342ba8e879d1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 24 Dec 2024 09:23:52 -0500 Subject: [PATCH 103/620] Allow locking orientation on iOS in certain circumstances --- osu.Game/OsuGame.cs | 12 ++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 5 ++ osu.Game/Screens/IOsuScreen.cs | 10 ++++ osu.Game/Screens/OsuScreen.cs | 2 + osu.Game/Screens/Play/Player.cs | 2 + osu.iOS/AppDelegate.cs | 49 +++++++++++++++- osu.iOS/IOSOrientationHandler.cs | 76 +++++++++++++++++++++++++ osu.iOS/OsuGameIOS.cs | 12 ++++ 8 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 osu.iOS/IOSOrientationHandler.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 06e30e3fab..4352eb2a71 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,6 +174,16 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); + /// + /// On mobile devices, this specifies whether the device should be set and locked to portrait orientation. + /// + /// + /// Implementations can be viewed in mobile projects. + /// + public IBindable RequiresPortraitOrientation => requiresPortraitOrientation; + + private readonly Bindable requiresPortraitOrientation = new BindableBool(); + /// /// Whether the back button is currently displayed. /// @@ -1623,6 +1633,8 @@ namespace osu.Game GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; + requiresPortraitOrientation.Value = newOsuScreen.RequiresPortraitOrientation; + if (newOsuScreen.HideOverlaysOnEnter) CloseAllOverlays(); else diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index ebd84fd91b..13d4b67132 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -577,6 +577,11 @@ namespace osu.Game.Rulesets.UI /// public virtual bool AllowGameplayOverlays => true; + /// + /// On mobile devices, this specifies whether this ruleset requires the device to be in portrait orientation. + /// + public virtual bool RequiresPortraitOrientation => false; + /// /// Sets a replay to be used, overriding local input. /// diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 9e474ed0c6..8b3ff4306f 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -61,6 +61,16 @@ namespace osu.Game.Screens /// bool HideMenuCursorOnNonMouseInput { get; } + /// + /// On mobile devices, this specifies whether this requires the device to be in portrait orientation. + /// + /// + /// By default, all screens in the game display in landscape orientation. + /// Setting this to true will display this screen in portrait orientation instead, + /// and switch back to landscape when transitioning back to a regular non-portrait screen. + /// + bool RequiresPortraitOrientation { get; } + /// /// Whether overlays should be able to be opened when this screen is current. /// diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index ab66241a77..e1d1ac38da 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -47,6 +47,8 @@ namespace osu.Game.Screens public virtual bool HideMenuCursorOnNonMouseInput => false; + public virtual bool RequiresPortraitOrientation => false; + /// /// The initial overlay activation mode to use when this screen is entered for the first time. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 228b77b780..e50f97f912 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,6 +68,8 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; + public override bool RequiresPortraitOrientation => DrawableRuleset.RequiresPortraitOrientation; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; // We are managing our own adjustments (see OnEntering/OnExiting). diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs index e88b39f710..5d309f2fc1 100644 --- a/osu.iOS/AppDelegate.cs +++ b/osu.iOS/AppDelegate.cs @@ -1,14 +1,61 @@ // 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 Foundation; using osu.Framework.iOS; +using UIKit; namespace osu.iOS { [Register("AppDelegate")] public class AppDelegate : GameApplicationDelegate { - protected override Framework.Game CreateGame() => new OsuGameIOS(); + private UIInterfaceOrientationMask? defaultOrientationsMask; + private UIInterfaceOrientationMask? orientations; + + /// + /// The current orientation the game is displayed in. + /// + public UIInterfaceOrientation CurrentOrientation => Host.Window.UIWindow.WindowScene!.InterfaceOrientation; + + /// + /// Controls the orientations allowed for the device to rotate to, overriding the default allowed orientations. + /// + public UIInterfaceOrientationMask? Orientations + { + get => orientations; + set + { + if (orientations == value) + return; + + orientations = value; + + if (OperatingSystem.IsIOSVersionAtLeast(16)) + Host.Window.ViewController.SetNeedsUpdateOfSupportedInterfaceOrientations(); + else + UIViewController.AttemptRotationToDeviceOrientation(); + } + } + + protected override Framework.Game CreateGame() => new OsuGameIOS(this); + + public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow) + { + if (orientations != null) + return orientations.Value; + + if (defaultOrientationsMask == null) + { + defaultOrientationsMask = 0; + var defaultOrientations = (NSArray)NSBundle.MainBundle.ObjectForInfoDictionary("UISupportedInterfaceOrientations"); + + foreach (var value in defaultOrientations.ToArray()) + defaultOrientationsMask |= Enum.Parse(value.ToString().Replace("UIInterfaceOrientation", string.Empty)); + } + + return defaultOrientationsMask.Value; + } } } diff --git a/osu.iOS/IOSOrientationHandler.cs b/osu.iOS/IOSOrientationHandler.cs new file mode 100644 index 0000000000..9b60497be8 --- /dev/null +++ b/osu.iOS/IOSOrientationHandler.cs @@ -0,0 +1,76 @@ +// 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.Graphics; +using osu.Game; +using osu.Game.Screens.Play; +using UIKit; + +namespace osu.iOS +{ + public partial class IOSOrientationHandler : Component + { + private readonly AppDelegate appDelegate; + + [Resolved] + private OsuGame game { get; set; } = null!; + + [Resolved] + private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; + + private IBindable requiresPortraitOrientation = null!; + private IBindable localUserPlaying = null!; + + public IOSOrientationHandler(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); + requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); + + localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); + localUserPlaying.BindValueChanged(_ => updateOrientations()); + + updateOrientations(); + } + + private void updateOrientations() + { + UIInterfaceOrientation currentOrientation = appDelegate.CurrentOrientation; + bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; + bool lockToPortrait = requiresPortraitOrientation.Value; + bool isPhone = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; + + if (lockCurrentOrientation) + { + if (lockToPortrait && !currentOrientation.IsPortrait()) + currentOrientation = UIInterfaceOrientation.Portrait; + else if (!lockToPortrait && currentOrientation.IsPortrait() && isPhone) + currentOrientation = UIInterfaceOrientation.LandscapeRight; + + appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)currentOrientation); + return; + } + + if (lockToPortrait) + { + UIInterfaceOrientationMask portraitOrientations = UIInterfaceOrientationMask.Portrait; + + if (!isPhone) + portraitOrientations |= UIInterfaceOrientationMask.PortraitUpsideDown; + + appDelegate.Orientations = portraitOrientations; + return; + } + + appDelegate.Orientations = null; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a9ca1778a0..6a3d0d0ba4 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -15,10 +15,22 @@ namespace osu.iOS { public partial class OsuGameIOS : OsuGame { + private readonly AppDelegate appDelegate; public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); public override bool HideUnlicensedContent => true; + public OsuGameIOS(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Add(new IOSOrientationHandler(appDelegate)); + } + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From aa67f87fe95af769c66e5329b30212d07b8e3ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 09:42:24 +0100 Subject: [PATCH 104/620] Add failing test coverage --- .../Editor/TestSceneOsuComposerSelection.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs index 345965b912..5aa7d6865f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -10,6 +10,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; @@ -261,6 +262,90 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1)); } + [Test] + public void TestQuickDeleteOnUnselectedControlPointOnlyRemovesThatControlPoint() + { + var slider = new Slider + { + StartTime = 0, + Position = new Vector2(100, 100), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint { Type = PathType.LINEAR }, + new PathControlPoint(new Vector2(100, 0)), + new PathControlPoint(new Vector2(100)), + new PathControlPoint(new Vector2(0, 100)) + } + } + }; + AddStep("add slider", () => EditorBeatmap.Add(slider)); + AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); + + AddStep("select second node", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType>().ElementAt(1)); + InputManager.Click(MouseButton.Left); + }); + AddStep("also select third node", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.MoveMouseTo(this.ChildrenOfType>().ElementAt(2)); + InputManager.Click(MouseButton.Left); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddStep("quick-delete fourth node", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType>().ElementAt(3)); + InputManager.Click(MouseButton.Middle); + }); + AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType().Count(), () => Is.EqualTo(1)); + AddUntilStep("slider path has 3 nodes", () => EditorBeatmap.HitObjects.OfType().Single().Path.ControlPoints.Count, () => Is.EqualTo(3)); + } + + [Test] + public void TestQuickDeleteOnSelectedControlPointRemovesEntireSelection() + { + var slider = new Slider + { + StartTime = 0, + Position = new Vector2(100, 100), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint { Type = PathType.LINEAR }, + new PathControlPoint(new Vector2(100, 0)), + new PathControlPoint(new Vector2(100)), + new PathControlPoint(new Vector2(0, 100)) + } + } + }; + AddStep("add slider", () => EditorBeatmap.Add(slider)); + AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); + + AddStep("select second node", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType>().ElementAt(1)); + InputManager.Click(MouseButton.Left); + }); + AddStep("also select third node", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.MoveMouseTo(this.ChildrenOfType>().ElementAt(2)); + InputManager.Click(MouseButton.Left); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddStep("quick-delete second node", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType>().ElementAt(1)); + InputManager.Click(MouseButton.Middle); + }); + AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType().Count(), () => Is.EqualTo(1)); + AddUntilStep("slider path has 2 nodes", () => EditorBeatmap.HitObjects.OfType().Single().Path.ControlPoints.Count, () => Is.EqualTo(2)); + } + private ComposeBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); From 182f998f9b9069e52ab2b76e70bc47d4f4a0101c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 09:42:48 +0100 Subject: [PATCH 105/620] Fix quick-deleting unselected slider path control point also deleting all selected control points Closes https://github.com/ppy/osu/issues/31308. Logic matches corresponding quick-delete logic in https://github.com/ppy/osu/blob/130802e48048c134c6c8f19c77e3e032834acf72/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs#L307-L316. --- .../Components/PathControlPointVisualiser.cs | 23 ++++++++++++++----- .../Sliders/SliderSelectionBlueprint.cs | 7 ++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f114516300..f98117c0fa 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -137,11 +137,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// /// Delete all visually selected s. /// - /// + /// Whether any change actually took place. public bool DeleteSelected() { List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList(); + if (!Delete(toRemove)) + return false; + + // Since pieces are re-used, they will not point to the deleted control points while remaining selected + foreach (var piece in Pieces) + piece.IsSelected.Value = false; + + return true; + } + + /// + /// Delete the specified s. + /// + /// Whether any change actually took place. + public bool Delete(List toRemove) + { // Ensure that there are any points to be deleted if (toRemove.Count == 0) return false; @@ -149,11 +165,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components changeHandler?.BeginChange(); RemoveControlPointsRequested?.Invoke(toRemove); changeHandler?.EndChange(); - - // Since pieces are re-used, they will not point to the deleted control points while remaining selected - foreach (var piece in Pieces) - piece.IsSelected.Value = false; - return true; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 02f76b51b0..3504954bec 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (hoveredControlPoint == null) return false; - hoveredControlPoint.IsSelected.Value = true; - ControlPointVisualiser?.DeleteSelected(); + if (hoveredControlPoint.IsSelected.Value) + ControlPointVisualiser?.DeleteSelected(); + else + ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]); + return true; } From 2a758bc3df34d1fe309720e0f5eae56f8ac5f856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 10:47:55 +0100 Subject: [PATCH 106/620] Add failing test case --- .../Editor/TestSceneOsuComposerSelection.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs index 5aa7d6865f..f3e76da9c9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -346,6 +346,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddUntilStep("slider path has 2 nodes", () => EditorBeatmap.HitObjects.OfType().Single().Path.ControlPoints.Count, () => Is.EqualTo(2)); } + [Test] + public void TestSliderDragMarkerDoesNotBlockControlPointContextMenu() + { + var slider = new Slider + { + StartTime = 0, + Position = new Vector2(100, 100), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint { Type = PathType.LINEAR }, + new PathControlPoint(new Vector2(50, 100)), + new PathControlPoint(new Vector2(145, 100)), + }, + ExpectedDistance = { Value = 162.62 } + }, + }; + AddStep("add slider", () => EditorBeatmap.Add(slider)); + AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); + + AddStep("select last node", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType>().Last()); + InputManager.Click(MouseButton.Left); + }); + AddStep("right click node", () => InputManager.Click(MouseButton.Right)); + AddUntilStep("context menu open", () => this.ChildrenOfType().Single().ChildrenOfType().All(m => m.State == MenuState.Open)); + } + private ComposeBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); From a4c6f221c2ecfabf8d970969f7200da2c2bee7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 10:56:42 +0100 Subject: [PATCH 107/620] Add extra test coverage to prevent regressions Covers scenario described in https://github.com/ppy/osu/issues/31176 and fixed in https://github.com/ppy/osu/pull/31184. --- .../Editor/TestSceneOsuComposerSelection.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs index f3e76da9c9..4e6cad1dca 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -376,6 +377,49 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddUntilStep("context menu open", () => this.ChildrenOfType().Single().ChildrenOfType().All(m => m.State == MenuState.Open)); } + [Test] + public void TestSliderDragMarkerBlocksSelectionOfObjectsUnderneath() + { + var firstSlider = new Slider + { + StartTime = 0, + Position = new Vector2(10, 50), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + } + } + }; + var secondSlider = new Slider + { + StartTime = 500, + Position = new Vector2(200, 0), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(-100, 100)) + } + } + }; + + AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider })); + AddStep("select second slider", () => EditorBeatmap.SelectedHitObjects.Add(secondSlider)); + + AddStep("move to marker", () => + { + var marker = this.ChildrenOfType().First(); + var position = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2; + InputManager.MoveMouseTo(position); + }); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second slider still selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondSlider)); + } + private ComposeBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); From 4d326ec31f06068a85a83dfe08fe7f3e67c45d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 10:57:57 +0100 Subject: [PATCH 108/620] Fix slider end drag marker blocking open of control point piece context menus Closes https://github.com/ppy/osu/issues/31323. --- .../Edit/Blueprints/Sliders/SliderEndDragMarker.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs index 326dd82fc6..9cc5394191 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs @@ -10,6 +10,7 @@ using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Osu.Objects; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { @@ -76,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.OnDragEnd(e); } - protected override bool OnMouseDown(MouseDownEvent e) => true; + protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left; - protected override bool OnClick(ClickEvent e) => true; + protected override bool OnClick(ClickEvent e) => e.Button == MouseButton.Left; private void updateState() { From 693db097ee7dc90e2fda6d4d5cdcbc27a1191064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 12:04:41 +0100 Subject: [PATCH 109/620] Take custom bank name length into account when collapsing sample point indicators Would close https://github.com/ppy/osu/issues/31312. Not super happy with the performance overhead of this, but this is already a heuristic-based implementation to avoid every-frame `.ChildrenOfType<>()` calls or similar, so not super sure how to do better. The `Array.Contains()` check stands out in profiling, but without it the indicators can collapse *too* eagerly sometimes. --- .../Timeline/TimelineBlueprintContainer.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index a4083f58b6..578e945c64 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -131,7 +132,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateSamplePointContractedState() { - const double minimum_gap = 28; + const double absolute_minimum_gap = 31; // assumes single letter bank name for default banks + double minimumGap = absolute_minimum_gap; if (timeline == null || editorClock == null) return; @@ -153,9 +155,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2) break; + foreach (var sample in hitObject.Samples) + { + if (!HitSampleInfo.AllBanks.Contains(sample.Bank)) + minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3); + } + if (hitObject is IHasRepeats hasRepeats) + { smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); + foreach (var sample in hasRepeats.NodeSamples.SelectMany(s => s)) + { + if (!HitSampleInfo.AllBanks.Contains(sample.Bank)) + minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3); + } + } + double gap = lastTime - hitObject.GetEndTime(); // If the gap is less than 1ms, we can assume that the objects are stacked on top of each other @@ -167,7 +183,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap; - SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap; + SamplePointContracted.Value = smallestAbsoluteGap < minimumGap; } private readonly Stack currentConcurrentObjects = new Stack(); From 06879eee394bcf1a06b3b3b0b7e30fadfba182d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 13:52:50 +0100 Subject: [PATCH 110/620] Fix slider repeats not properly respecting "show hit markers" setting Closes https://github.com/ppy/osu/issues/31286. Curious on thoughts about how the instant arrow fade looks on non-classic skins. On argon it's probably fine, but it does look a little off on triangles... --- .../Objects/Drawables/DrawableSlider.cs | 8 +++++ .../Objects/Drawables/DrawableSliderRepeat.cs | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index eacd2b3e75..0fcfdef4ee 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -377,6 +377,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { UpdateState(ArmedState.Idle); HeadCircle.SuppressHitAnimations(); + + foreach (var repeat in repeatContainer) + repeat.SuppressHitAnimations(); + TailCircle.SuppressHitAnimations(); } @@ -384,6 +388,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { UpdateState(ArmedState.Hit); HeadCircle.RestoreHitAnimations(); + + foreach (var repeat in repeatContainer) + repeat.RestoreHitAnimations(); + TailCircle.RestoreHitAnimations(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 27c5278614..bc48f34828 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -163,5 +164,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint); } } + + #region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE + + internal void SuppressHitAnimations() + { + UpdateState(ArmedState.Idle); + UpdateComboColour(); + + // This method is called every frame in editor contexts, thus the lack of need for transforms. + + bool hit = Time.Current >= HitStateUpdateTime; + + if (hit) + { + // More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + AccentColour.Value = Color4.White; + Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700); + } + + Arrow.Alpha = hit ? 0 : 1; + + LifetimeEnd = HitStateUpdateTime + 700; + } + + internal void RestoreHitAnimations() + { + UpdateState(ArmedState.Hit); + UpdateComboColour(); + Arrow.Alpha = 1; + } + + #endregion } } From 0641d2b51000b953628cbad480f7b50cf251d4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 30 Dec 2024 19:12:21 +0100 Subject: [PATCH 111/620] Remove turboweird function and update displayed bpm text --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 58d461b3a5..5e5b740b62 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Edit.Timing Clock = new FramedClock(metronomeClock = new StopwatchClock(true)); } - private double beatLength; + private double effectiveBeatLength; private TimingControlPoint timingPoint = null!; @@ -238,27 +238,24 @@ namespace osu.Game.Screens.Edit.Timing private bool spedUp; - private bool updateDivisor() + private int computeSpedUpDivisor() { - int divisor = 1; + if (!spedUp) + return 1; - if (spedUp) - divisor = beatDivisor.Value % 3 == 0 ? 3 : 2; + if (beatDivisor.Value % 3 == 0) + return 3; + if (beatDivisor.Value % 2 == 0) + return 2; - if (divisor == Divisor) - return false; - - Divisor = divisor; - metronomeTick.Divisor = divisor; - - return true; + return 1; } protected override void LoadComplete() { base.LoadComplete(); - interpolatedBpm.BindValueChanged(bpm => bpmText.Text = bpm.NewValue.ToLocalisableString()); + interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString()); } protected override void Update() @@ -272,16 +269,20 @@ namespace osu.Game.Screens.Edit.Timing timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime); - if (updateDivisor() || beatLength != timingPoint.BeatLength) + Divisor = metronomeTick.Divisor = computeSpedUpDivisor(); + + if (effectiveBeatLength != timingPoint.BeatLength / Divisor) { - beatLength = timingPoint.BeatLength; + effectiveBeatLength = timingPoint.BeatLength / Divisor; EarlyActivationMilliseconds = timingPoint.BeatLength / 2; - float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((timingPoint.BPM - 30) / 480 * Divisor, 0, 1)); + double effectiveBpm = 60000 / effectiveBeatLength; + + float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((effectiveBpm - 30) / 480, 0, 1)); weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); - this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint); + this.TransformBindableTo(interpolatedBpm, (int)Math.Round(effectiveBpm), 600, Easing.OutQuint); } if (!BeatSyncSource.Clock.IsRunning && isSwinging) @@ -327,7 +328,7 @@ namespace osu.Game.Screens.Edit.Timing float currentAngle = swing.Rotation; float targetAngle = currentAngle > 0 ? -angle : angle; - swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad); + swing.RotateTo(targetAngle, effectiveBeatLength, Easing.InOutQuad); } private void onTickPlayed() @@ -335,7 +336,7 @@ namespace osu.Game.Screens.Edit.Timing // Originally, this flash only occurred when the pendulum correctly passess the centre. // Mappers weren't happy with the metronome tick not playing immediately after starting playback // so now this matches the actual tick sample. - stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); + stick.FlashColour(overlayColourProvider.Content1, effectiveBeatLength, Easing.OutQuint); } protected override bool OnKeyDown(KeyDownEvent e) From 22c82299930e3618ede159464bc06fb89c741911 Mon Sep 17 00:00:00 2001 From: CuNO3 Date: Tue, 31 Dec 2024 10:43:48 +0800 Subject: [PATCH 112/620] Ignore whitespace while 2FA authentication --- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 77835b1f09..dd79a962f0 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -121,9 +121,9 @@ namespace osu.Game.Overlays.Login codeTextBox.Current.BindValueChanged(code => { - if (code.NewValue.Length == 8) + if (code.NewValue.Trim().Length == 8) { - api.AuthenticateSecondFactor(code.NewValue); + api.AuthenticateSecondFactor(code.NewValue.Trim()); codeTextBox.Current.Disabled = true; } }); From 333ae75a8278e746a89588f05feca905ffe7a6ca Mon Sep 17 00:00:00 2001 From: aychar <58487401+hrfarmer@users.noreply.github.com> Date: Tue, 31 Dec 2024 00:29:36 -0600 Subject: [PATCH 113/620] Add game mode key to plist --- osu.iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 29410938a3..02f8462fbc 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -157,5 +157,7 @@ public.app-category.music-games LSSupportsOpeningDocumentsInPlace + GCSupportsGameMode + From 6ff31104336f13877a872366ef03068e37dd14d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Dec 2024 21:14:15 +0900 Subject: [PATCH 114/620] Consolidate variable --- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index dd79a962f0..3022233e9c 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -121,9 +121,11 @@ namespace osu.Game.Overlays.Login codeTextBox.Current.BindValueChanged(code => { - if (code.NewValue.Trim().Length == 8) + string trimmedCode = code.NewValue.Trim(); + + if (trimmedCode.Length == 8) { - api.AuthenticateSecondFactor(code.NewValue.Trim()); + api.AuthenticateSecondFactor(trimmedCode); codeTextBox.Current.Disabled = true; } }); From 21dba621f00af1b488b64fafd70592900ffcf677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Dec 2024 13:57:50 +0100 Subject: [PATCH 115/620] Display storyboard in editor background Fixes the main part of https://github.com/ppy/osu/issues/31144. Support for selecting a video will come later. Making this work was an absolutely awful time full of dealing with delightfully kooky issues, and yielded in a very weird-shaped contraption. There is at least one issue remaining wherein storyboard videos do not actually display until the track is started in editor, but that is 99% a framework issue and I do not currently have the mental fortitude to diagnose further. --- osu.Game/Configuration/OsuConfigManager.cs | 2 + .../Backgrounds/EditorBackgroundScreen.cs | 117 ++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 24 ++-- .../Screens/Edit/Setup/ResourcesSection.cs | 3 +- 4 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index deac1a5128..f050a2338a 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -218,6 +218,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false); + SetDefault(OsuSetting.EditorShowStoryboard, true); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -452,5 +453,6 @@ namespace osu.Game.Configuration AlwaysRequireHoldingForPause, MultiplayerShowInProgressFilter, BeatmapListingFeaturedArtistFilter, + EditorShowStoryboard, } } diff --git a/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs b/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs new file mode 100644 index 0000000000..9982357157 --- /dev/null +++ b/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs @@ -0,0 +1,117 @@ +// 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 System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Screens.Backgrounds +{ + public partial class EditorBackgroundScreen : BackgroundScreen + { + private readonly WorkingBeatmap beatmap; + private readonly Container dimContainer; + + private CancellationTokenSource? cancellationTokenSource; + private Bindable dimLevel = null!; + private Bindable showStoryboard = null!; + + private BeatmapBackground background = null!; + private Container storyboardContainer = null!; + + private IFrameBasedClock? clockSource; + + public EditorBackgroundScreen(WorkingBeatmap beatmap) + { + this.beatmap = beatmap; + + InternalChild = dimContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + dimContainer.AddRange(createContent()); + background = dimContainer.OfType().Single(); + storyboardContainer = dimContainer.OfType().Single(); + + dimLevel = config.GetBindable(OsuSetting.EditorDim); + showStoryboard = config.GetBindable(OsuSetting.EditorShowStoryboard); + } + + private IEnumerable createContent() => + [ + new BeatmapBackground(beatmap) { RelativeSizeAxes = Axes.Both, }, + // this kooky container nesting is here because the storyboard needs a custom clock + // but also needs it on an isolated-enough level that doesn't break screen stack expiry logic (which happens if the clock was put on `this`), + // or doesn't make it literally impossible to fade the storyboard in/out in real time (which happens if the fade transforms were to be applied directly to the storyboard). + new Container + { + RelativeSizeAxes = Axes.Both, + Child = new DrawableStoryboard(beatmap.Storyboard) + { + Clock = clockSource ?? Clock, + } + } + ]; + + protected override void LoadComplete() + { + base.LoadComplete(); + + dimLevel.BindValueChanged(_ => dimContainer.FadeColour(OsuColour.Gray(1 - dimLevel.Value), 500, Easing.OutQuint), true); + showStoryboard.BindValueChanged(_ => updateState()); + updateState(0); + } + + private void updateState(double duration = 500) + { + storyboardContainer.FadeTo(showStoryboard.Value ? 1 : 0, duration, Easing.OutQuint); + // yes, this causes overdraw, but is also a (crude) fix for bad-looking transitions on screen entry + // caused by the previous background on the background stack poking out from under this one and then instantly fading out + background.FadeColour(beatmap.Storyboard.ReplacesBackground && showStoryboard.Value ? Colour4.Black : Colour4.White, duration, Easing.OutQuint); + } + + public void ChangeClockSource(IFrameBasedClock frameBasedClock) + { + clockSource = frameBasedClock; + if (IsLoaded) + storyboardContainer.Child.Clock = frameBasedClock; + } + + public void RefreshBackground() + { + cancellationTokenSource?.Cancel(); + LoadComponentsAsync(createContent(), loaded => + { + dimContainer.Clear(); + dimContainer.AddRange(loaded); + + background = dimContainer.OfType().Single(); + storyboardContainer = dimContainer.OfType().Single(); + updateState(0); + }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); + } + + public override bool Equals(BackgroundScreen? other) + { + if (other is not EditorBackgroundScreen otherBeatmapBackground) + return false; + + return base.Equals(other) && beatmap == otherBeatmapBackground.beatmap; + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index f6875a7aa4..a102e76353 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -45,6 +45,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -54,7 +55,6 @@ using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.Play; using osu.Game.Users; using osuTK.Input; using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] [Cached] - public partial class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider + public partial class Editor : OsuScreen, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider { /// /// An offset applied to waveform visuals to align them with expectations. @@ -210,6 +210,7 @@ namespace osu.Game.Screens.Edit private OnScreenDisplay onScreenDisplay { get; set; } private Bindable editorBackgroundDim; + private Bindable editorShowStoryboard; private Bindable editorHitMarkers; private Bindable editorAutoSeekOnPlacement; private Bindable editorLimitedDistanceSnap; @@ -320,6 +321,7 @@ namespace osu.Game.Screens.Edit OsuMenuItem redoMenuItem; editorBackgroundDim = config.GetBindable(OsuSetting.EditorDim); + editorShowStoryboard = config.GetBindable(OsuSetting.EditorShowStoryboard); editorHitMarkers = config.GetBindable(OsuSetting.EditorShowHitMarkers); editorAutoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); @@ -398,7 +400,13 @@ namespace osu.Game.Screens.Edit }, ] }, + new OsuMenuItemSpacer(), new BackgroundDimMenuItem(editorBackgroundDim), + new ToggleMenuItem("Show storyboard") + { + State = { BindTarget = editorShowStoryboard }, + }, + new OsuMenuItemSpacer(), new ToggleMenuItem(EditorStrings.ShowHitMarkers) { State = { BindTarget = editorHitMarkers }, @@ -472,6 +480,8 @@ namespace osu.Game.Screens.Edit [Resolved] private MusicController musicController { get; set; } + protected override BackgroundScreen CreateBackground() => new EditorBackgroundScreen(Beatmap.Value); + protected override void LoadComplete() { base.LoadComplete(); @@ -867,9 +877,8 @@ namespace osu.Game.Screens.Edit { ApplyToBackground(b => { - b.IgnoreUserSettings.Value = true; - b.DimWhenUserSettingsIgnored.Value = editorBackgroundDim.Value; - b.BlurAmount.Value = 0; + var editorBackground = (EditorBackgroundScreen)b; + editorBackground.ChangeClockSource(clock); }); } @@ -908,11 +917,6 @@ namespace osu.Game.Screens.Edit beatmap.EditorTimestamp = clock.CurrentTime; }); - ApplyToBackground(b => - { - b.DimWhenUserSettingsIgnored.Value = 0; - }); - resetTrack(); refetchBeatmap(); diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 5bc95dd824..408292c2d0 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; using osu.Game.Models; +using osu.Game.Screens.Backgrounds; using osu.Game.Utils; namespace osu.Game.Screens.Edit.Setup @@ -87,7 +88,7 @@ namespace osu.Game.Screens.Edit.Setup (metadata, name) => metadata.BackgroundFile = name); headerBackground.UpdateBackground(); - editor?.ApplyToBackground(bg => bg.RefreshBackground()); + editor?.ApplyToBackground(bg => ((EditorBackgroundScreen)bg).RefreshBackground()); return true; } From 88311f5442e9fd6c711913aa090361deeedec380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Dec 2024 14:02:07 +0100 Subject: [PATCH 116/620] Remove unused method --- .../Screens/Backgrounds/BackgroundScreenBeatmap.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 185e2cab99..5f80c2cd96 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -101,18 +101,6 @@ namespace osu.Game.Screens.Backgrounds } } - /// - /// Reloads beatmap's background. - /// - public void RefreshBackground() - { - Schedule(() => - { - cancellationSource?.Cancel(); - LoadComponentAsync(new BeatmapBackground(beatmap), switchBackground, (cancellationSource = new CancellationTokenSource()).Token); - }); - } - private void switchBackground(BeatmapBackground b) { float newDepth = 0; From cd07ddfe28250d9c5422e4946aae5aecfdf23331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Dec 2024 14:08:41 +0100 Subject: [PATCH 117/620] Update outdated assertions --- .../Editing/TestSceneEditorTestGameplay.cs | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 765ffb4549..21c414cc21 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; +using osu.Game.Screens; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.Timelines.Summary; @@ -80,15 +81,7 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); AddStep("exit player", () => editorPlayer.Exit()); AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); - AddUntilStep("background has correct params", () => - { - // the test gameplay player's beatmap may be the "same" beatmap as the one being edited, *but* the `BeatmapInfo` references may differ - // due to the beatmap refetch logic ran on editor suspend. - // this test cares about checking the background belonging to the editor specifically, so check that using reference equality - // (as `.Equals()` cannot discern between the two, as they technically share the same database GUID). - var background = this.ChildrenOfType().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo)); - return background.DimWhenUserSettingsIgnored.Value == editorDim.Value && background.BlurAmount.Value == 0; - }); + AddUntilStep("background is correct", () => this.ChildrenOfType().Single().CurrentScreen is EditorBackgroundScreen); AddAssert("no mods selected", () => SelectedMods.Value.Count == 0); } @@ -113,15 +106,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("exit player", () => editorPlayer.Exit()); AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); - AddUntilStep("background has correct params", () => - { - // the test gameplay player's beatmap may be the "same" beatmap as the one being edited, *but* the `BeatmapInfo` references may differ - // due to the beatmap refetch logic ran on editor suspend. - // this test cares about checking the background belonging to the editor specifically, so check that using reference equality - // (as `.Equals()` cannot discern between the two, as they technically share the same database GUID). - var background = this.ChildrenOfType().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo)); - return background.DimWhenUserSettingsIgnored.Value == editorDim.Value && background.BlurAmount.Value == 0; - }); + AddUntilStep("background is correct", () => this.ChildrenOfType().Single().CurrentScreen is EditorBackgroundScreen); AddStep("start track", () => EditorClock.Start()); AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value); From 1803ee4025a2e99386d7e5b1528009f33898451d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Dec 2024 14:09:36 +0100 Subject: [PATCH 118/620] Rename method --- osu.Game/Screens/Edit/Editor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a102e76353..48befbdcc0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -474,7 +474,7 @@ namespace osu.Game.Screens.Edit changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); - editorBackgroundDim.BindValueChanged(_ => dimBackground()); + editorBackgroundDim.BindValueChanged(_ => setUpBackground()); } [Resolved] @@ -863,17 +863,17 @@ namespace osu.Game.Screens.Edit public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); - dimBackground(); + setUpBackground(); resetTrack(true); } public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - dimBackground(); + setUpBackground(); } - private void dimBackground() + private void setUpBackground() { ApplyToBackground(b => { From 78c7ee1fff6e2349337b3b391055b1ce91b17803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Dec 2024 14:21:21 +0100 Subject: [PATCH 119/620] Fix code quality --- .../Visual/Editing/TestSceneEditorTestGameplay.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 21c414cc21..60781d6f0a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -7,12 +7,10 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -43,14 +41,6 @@ namespace osu.Game.Tests.Visual.Editing private BeatmapSetInfo importedBeatmapSet; - private Bindable editorDim; - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - editorDim = config.GetBindable(OsuSetting.EditorDim); - } - public override void SetUpSteps() { AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely()); From 9d08bc2b50d9e5b80f38f0ebad2b72c6f3855361 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 28 Dec 2024 19:22:45 -0500 Subject: [PATCH 120/620] Improve osu!mania gameplay scaling on portrait orientation --- .../UI/DrawableManiaRuleset.cs | 2 + .../UI/ManiaPlayfieldAdjustmentContainer.cs | 51 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index d173ae4143..136b172a59 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -51,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; + public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1; + protected override bool RelativeScaleBeatLengths => true; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index 1183b616f5..d7cb211d4a 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -1,17 +1,64 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer { + protected override Container Content { get; } + + private readonly DrawSizePreservingFillContainer scalingContainer; + public ManiaPlayfieldAdjustmentContainer() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + InternalChild = scalingContainer = new DrawSizePreservingFillContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = Content = new Container + { + RelativeSizeAxes = Axes.Both, + } + }; + } + + [Resolved] + private DrawableManiaRuleset drawableManiaRuleset { get; set; } = null!; + + protected override void Update() + { + base.Update(); + + float aspectRatio = DrawWidth / DrawHeight; + bool isPortrait = aspectRatio < 4 / 3f; + + if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) + { + // Scale playfield up by 25% to become playable on mobile devices, + // and leave a 10% horizontal gap if the playfield is scaled down due to being too wide. + const float base_scale = 1.25f; + const float base_width = 768f / base_scale; + const float side_gap = 0.9f; + + scalingContainer.Strategy = DrawSizePreservationStrategy.Maximum; + float stageWidth = drawableManiaRuleset.Playfield.Stages[0].DrawWidth; + scalingContainer.TargetDrawSize = new Vector2(1024, base_width * Math.Max(stageWidth / aspectRatio / (base_width * side_gap), 1f)); + } + else + { + scalingContainer.Strategy = DrawSizePreservationStrategy.Minimum; + scalingContainer.Scale = new Vector2(1f); + scalingContainer.Size = new Vector2(1f); + scalingContainer.TargetDrawSize = new Vector2(1024, 768); + } } } } From d7e4038f4ae75645a6f074e7c49c9265ac9f04e2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 29 Dec 2024 23:54:04 -0500 Subject: [PATCH 121/620] Keep game in portrait mode when restarting --- osu.Game/Screens/Play/PlayerLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 837974a8f2..b258de0e9e 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -54,6 +54,9 @@ namespace osu.Game.Screens.Play public override bool? AllowGlobalTrackControl => false; + // this makes the game stay in portrait mode when restarting gameplay rather than switching back to landscape. + public override bool RequiresPortraitOrientation => CurrentPlayer?.RequiresPortraitOrientation == true; + public override float BackgroundParallaxAmount => quickRestart ? 0 : 1; // Here because IsHovered will not update unless we do so. From 0cd7f1b2d4f138443260042cb04ca6cbf2988184 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 30 Dec 2024 15:04:21 -0500 Subject: [PATCH 122/620] Abstractify orientation handling and add Android support --- osu.Android/AndroidOrientationManager.cs | 39 ++++++++++ osu.Android/GameplayScreenRotationLocker.cs | 34 --------- osu.Android/OsuGameActivity.cs | 6 +- osu.Android/OsuGameAndroid.cs | 2 +- osu.Game/Mobile/GameOrientation.cs | 34 +++++++++ osu.Game/Mobile/OrientationManager.cs | 84 +++++++++++++++++++++ osu.iOS/IOSOrientationHandler.cs | 76 ------------------- osu.iOS/IOSOrientationManager.cs | 41 ++++++++++ osu.iOS/OsuGameIOS.cs | 2 +- 9 files changed, 204 insertions(+), 114 deletions(-) create mode 100644 osu.Android/AndroidOrientationManager.cs delete mode 100644 osu.Android/GameplayScreenRotationLocker.cs create mode 100644 osu.Game/Mobile/GameOrientation.cs create mode 100644 osu.Game/Mobile/OrientationManager.cs delete mode 100644 osu.iOS/IOSOrientationHandler.cs create mode 100644 osu.iOS/IOSOrientationManager.cs diff --git a/osu.Android/AndroidOrientationManager.cs b/osu.Android/AndroidOrientationManager.cs new file mode 100644 index 0000000000..76d2fc24cb --- /dev/null +++ b/osu.Android/AndroidOrientationManager.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Android.Content.PM; +using Android.Content.Res; +using osu.Framework.Allocation; +using osu.Game.Mobile; + +namespace osu.Android +{ + public partial class AndroidOrientationManager : OrientationManager + { + [Resolved] + private OsuGameActivity gameActivity { get; set; } = null!; + + protected override bool IsCurrentOrientationPortrait => gameActivity.Resources!.Configuration!.Orientation == Orientation.Portrait; + protected override bool IsTablet => gameActivity.IsTablet; + + protected override void SetAllowedOrientations(GameOrientation? orientation) + => gameActivity.RequestedOrientation = orientation == null ? gameActivity.DefaultOrientation : toScreenOrientation(orientation.Value); + + private static ScreenOrientation toScreenOrientation(GameOrientation orientation) + { + if (orientation == GameOrientation.Locked) + return ScreenOrientation.Locked; + + if (orientation == GameOrientation.Portrait) + return ScreenOrientation.Portrait; + + if (orientation == GameOrientation.Landscape) + return ScreenOrientation.Landscape; + + if (orientation == GameOrientation.FullPortrait) + return ScreenOrientation.SensorPortrait; + + return ScreenOrientation.SensorLandscape; + } + } +} diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs deleted file mode 100644 index 42583b5dc2..0000000000 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Android.Content.PM; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Screens.Play; - -namespace osu.Android -{ - public partial class GameplayScreenRotationLocker : Component - { - private IBindable localUserPlaying = null!; - - [Resolved] - private OsuGameActivity gameActivity { get; set; } = null!; - - [BackgroundDependencyLoader] - private void load(ILocalUserPlayInfo localUserPlayInfo) - { - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(updateLock, true); - } - - private void updateLock(ValueChangedEvent userPlaying) - { - gameActivity.RunOnUiThread(() => - { - gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation; - }); - } - } -} diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index bbee491d90..b3717791da 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -50,6 +50,8 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; + public bool IsTablet { get; private set; } + private OsuGameAndroid game = null!; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); @@ -76,9 +78,9 @@ namespace osu.Android WindowManager.DefaultDisplay.GetSize(displaySize); #pragma warning restore CA1422 float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; - bool isTablet = smallestWidthDp >= 600f; + IsTablet = smallestWidthDp >= 600f; - RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; + RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; // Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android. // The assembly files are not available as files either after native AOT. diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index ffab7dd86d..4143c8cae6 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -71,7 +71,7 @@ namespace osu.Android protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new GameplayScreenRotationLocker(), Add); + LoadComponentAsync(new AndroidOrientationManager(), Add); } public override void SetHost(GameHost host) diff --git a/osu.Game/Mobile/GameOrientation.cs b/osu.Game/Mobile/GameOrientation.cs new file mode 100644 index 0000000000..0022c8fefb --- /dev/null +++ b/osu.Game/Mobile/GameOrientation.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Mobile +{ + public enum GameOrientation + { + /// + /// Lock the game orientation. + /// + Locked, + + /// + /// Display the game in regular portrait orientation. + /// + Portrait, + + /// + /// Display the game in landscape-right orientation. + /// + Landscape, + + /// + /// Display the game in landscape-right/landscape-left orientations. + /// + FullLandscape, + + /// + /// Display the game in portrait/portrait-upside-down orientations. + /// This is exclusive to tablet mobile devices. + /// + FullPortrait, + } +} diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs new file mode 100644 index 0000000000..b78bf8e760 --- /dev/null +++ b/osu.Game/Mobile/OrientationManager.cs @@ -0,0 +1,84 @@ +// 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.Graphics; +using osu.Game.Screens.Play; + +namespace osu.Game.Mobile +{ + /// + /// A that manages the device orientations a game can display in. + /// + public abstract partial class OrientationManager : Component + { + /// + /// Whether the current orientation of the game is portrait. + /// + protected abstract bool IsCurrentOrientationPortrait { get; } + + /// + /// Whether the mobile device is considered a tablet. + /// + protected abstract bool IsTablet { get; } + + [Resolved] + private OsuGame game { get; set; } = null!; + + [Resolved] + private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; + + private IBindable requiresPortraitOrientation = null!; + private IBindable localUserPlaying = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); + requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); + + localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); + localUserPlaying.BindValueChanged(_ => updateOrientations()); + + updateOrientations(); + } + + private void updateOrientations() + { + bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; + bool lockToPortrait = requiresPortraitOrientation.Value; + + if (lockCurrentOrientation) + { + if (lockToPortrait && !IsCurrentOrientationPortrait) + SetAllowedOrientations(GameOrientation.Portrait); + else if (!lockToPortrait && IsCurrentOrientationPortrait && !IsTablet) + SetAllowedOrientations(GameOrientation.Landscape); + else + SetAllowedOrientations(GameOrientation.Locked); + + return; + } + + if (lockToPortrait) + { + if (IsTablet) + SetAllowedOrientations(GameOrientation.FullPortrait); + else + SetAllowedOrientations(GameOrientation.Portrait); + + return; + } + + SetAllowedOrientations(null); + } + + /// + /// Sets the allowed orientations the device can rotate to. + /// + /// The allowed orientations, or null to return back to default. + protected abstract void SetAllowedOrientations(GameOrientation? orientation); + } +} diff --git a/osu.iOS/IOSOrientationHandler.cs b/osu.iOS/IOSOrientationHandler.cs deleted file mode 100644 index 9b60497be8..0000000000 --- a/osu.iOS/IOSOrientationHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game; -using osu.Game.Screens.Play; -using UIKit; - -namespace osu.iOS -{ - public partial class IOSOrientationHandler : Component - { - private readonly AppDelegate appDelegate; - - [Resolved] - private OsuGame game { get; set; } = null!; - - [Resolved] - private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; - - private IBindable requiresPortraitOrientation = null!; - private IBindable localUserPlaying = null!; - - public IOSOrientationHandler(AppDelegate appDelegate) - { - this.appDelegate = appDelegate; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); - requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); - - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(_ => updateOrientations()); - - updateOrientations(); - } - - private void updateOrientations() - { - UIInterfaceOrientation currentOrientation = appDelegate.CurrentOrientation; - bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortrait = requiresPortraitOrientation.Value; - bool isPhone = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; - - if (lockCurrentOrientation) - { - if (lockToPortrait && !currentOrientation.IsPortrait()) - currentOrientation = UIInterfaceOrientation.Portrait; - else if (!lockToPortrait && currentOrientation.IsPortrait() && isPhone) - currentOrientation = UIInterfaceOrientation.LandscapeRight; - - appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)currentOrientation); - return; - } - - if (lockToPortrait) - { - UIInterfaceOrientationMask portraitOrientations = UIInterfaceOrientationMask.Portrait; - - if (!isPhone) - portraitOrientations |= UIInterfaceOrientationMask.PortraitUpsideDown; - - appDelegate.Orientations = portraitOrientations; - return; - } - - appDelegate.Orientations = null; - } - } -} diff --git a/osu.iOS/IOSOrientationManager.cs b/osu.iOS/IOSOrientationManager.cs new file mode 100644 index 0000000000..6d5bb990c2 --- /dev/null +++ b/osu.iOS/IOSOrientationManager.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Mobile; +using UIKit; + +namespace osu.iOS +{ + public partial class IOSOrientationManager : OrientationManager + { + private readonly AppDelegate appDelegate; + + protected override bool IsCurrentOrientationPortrait => appDelegate.CurrentOrientation.IsPortrait(); + protected override bool IsTablet => UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; + + public IOSOrientationManager(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void SetAllowedOrientations(GameOrientation? orientation) + => appDelegate.Orientations = orientation == null ? null : toUIInterfaceOrientationMask(orientation.Value); + + private UIInterfaceOrientationMask toUIInterfaceOrientationMask(GameOrientation orientation) + { + if (orientation == GameOrientation.Locked) + return (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); + + if (orientation == GameOrientation.Portrait) + return UIInterfaceOrientationMask.Portrait; + + if (orientation == GameOrientation.Landscape) + return UIInterfaceOrientationMask.LandscapeRight; + + if (orientation == GameOrientation.FullPortrait) + return UIInterfaceOrientationMask.Portrait | UIInterfaceOrientationMask.PortraitUpsideDown; + + return UIInterfaceOrientationMask.Landscape; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 6a3d0d0ba4..ed47a1e8b8 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -28,7 +28,7 @@ namespace osu.iOS protected override void LoadComplete() { base.LoadComplete(); - Add(new IOSOrientationHandler(appDelegate)); + LoadComponentAsync(new IOSOrientationManager(appDelegate), Add); } protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 1e08b3dbdac1ed07fd56c0d55d83ce200053c336 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 29 Dec 2024 23:33:32 -0500 Subject: [PATCH 123/620] Make mania judgements relative to the hit target position This improves display in portrait screen, where the stage is scaled up. --- .../Mods/ManiaModWithPlayfieldCover.cs | 2 +- .../Skinning/Argon/ArgonJudgementPiece.cs | 2 +- .../Skinning/Legacy/LegacyManiaJudgementPiece.cs | 12 +++++------- .../UI/Components/ColumnHitObjectArea.cs | 2 +- ...bjectArea.cs => HitPositionPaddedContainer.cs} | 15 ++++----------- .../UI/DrawableManiaJudgement.cs | 3 +++ osu.Game.Rulesets.Mania/UI/Stage.cs | 12 ++++++------ 7 files changed, 21 insertions(+), 27 deletions(-) rename osu.Game.Rulesets.Mania/UI/Components/{HitObjectArea.cs => HitPositionPaddedContainer.cs} (74%) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index 864ef6c3d6..1bc16112c5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { - HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; + HitObjectContainer hoc = column.HitObjectContainer; Container hocParent = (Container)hoc.Parent!; hocParent.Remove(hoc, false); diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index 0052fd8b78..a1c81d3a6a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon { public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement { - private const float judgement_y_position = 160; + private const float judgement_y_position = -180f; private RingExplosion? ringExplosion; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index d21a8cd140..4b0cc482d9 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -23,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy this.result = result; this.animation = animation; - Anchor = Anchor.Centre; + Anchor = Anchor.BottomCentre; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -32,12 +31,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy [BackgroundDependencyLoader] private void load(ISkinSource skin) { - float? scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value; + float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; + float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; - if (scorePosition != null) - scorePosition -= Stage.HIT_TARGET_POSITION + 150; - - Y = scorePosition ?? 0; + float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; + Y = scorePosition - absoluteHitPosition; InternalChild = animation.With(d => { diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 91e0f2c19b..2d719ef764 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class ColumnHitObjectArea : HitObjectArea + public partial class ColumnHitObjectArea : HitPositionPaddedContainer { public readonly Container Explosions; diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs similarity index 74% rename from osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs rename to osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index 2ad6e4f076..f591102f6c 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -1,29 +1,22 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class HitObjectArea : SkinReloadableDrawable + public partial class HitPositionPaddedContainer : SkinReloadableDrawable { protected readonly IBindable Direction = new Bindable(); - public readonly HitObjectContainer HitObjectContainer; - public HitObjectArea(HitObjectContainer hitObjectContainer) + public HitPositionPaddedContainer(Drawable child) { - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Child = HitObjectContainer = hitObjectContainer - }; + InternalChild = child; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 9f25a44e21..5b87c74bbe 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -15,9 +15,12 @@ namespace osu.Game.Rulesets.Mania.UI private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece { + private const float judgement_y_position = -180f; + public DefaultManiaJudgementPiece(HitResult result) : base(result) { + Y = judgement_y_position; } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 9fb77a4995..2d73e7bcbe 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.UI Width = 1366, // Bar lines should only be masked on the vertical axis BypassAutoSizeAxes = Axes.Both, Masking = true, - Child = barLineContainer = new HitObjectArea(HitObjectContainer) + Child = barLineContainer = new HitPositionPaddedContainer(HitObjectContainer) { Name = "Bar lines", Anchor = Anchor.TopCentre, @@ -119,12 +119,12 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Both }, - judgements = new JudgementContainer + new HitPositionPaddedContainer(judgements = new JudgementContainer + { + RelativeSizeAxes = Axes.Both, + }) { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Y = HIT_TARGET_POSITION + 150 }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.UI { j.Apply(result, judgedObject); - j.Anchor = Anchor.Centre; + j.Anchor = Anchor.BottomCentre; j.Origin = Anchor.Centre; })!); } From bea61d24835e31af9821bce4e96e1cfd33c9f988 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 12:28:04 -0500 Subject: [PATCH 124/620] Replace `ManiaTouchInputArea` with touchable columns --- .../TestSceneManiaTouchInput.cs | 68 ++++++ .../TestSceneManiaTouchInputArea.cs | 49 ----- osu.Game.Rulesets.Mania/UI/Column.cs | 24 +++ .../UI/DrawableManiaRuleset.cs | 2 - .../UI/ManiaTouchInputArea.cs | 199 ------------------ 5 files changed, 92 insertions(+), 250 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs delete mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs delete mode 100644 osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs new file mode 100644 index 0000000000..dc95cd9ca0 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -0,0 +1,68 @@ +// 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.Input; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneManiaTouchInput : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestTouchInput() + { + for (int i = 0; i < 4; i++) + { + int index = i; + + AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(index).Action.Value)); + + AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(index).Action.Value)); + } + } + + [Test] + public void TestOneColumnMultipleTouches() + { + AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action still pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action still pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(0).Action.Value)); + } + + private Column getColumn(int index) => this.ChildrenOfType().ElementAt(index); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs deleted file mode 100644 index 30c0113bff..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs +++ /dev/null @@ -1,49 +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.Linq; -using NUnit.Framework; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Testing; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Mania.Tests -{ - public partial class TestSceneManiaTouchInputArea : PlayerTestScene - { - protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); - - [Test] - public void TestTouchAreaNotInitiallyVisible() - { - AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); - } - - [Test] - public void TestPressReceptors() - { - AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); - - for (int i = 0; i < 4; i++) - { - int index = i; - - AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); - - AddAssert("action sent", - () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), - () => Does.Contain(getReceptor(index).Action.Value)); - - AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); - - AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible); - } - } - - private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); - - private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); - } -} diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index c05a8f2a29..99d952ef1f 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -180,5 +180,29 @@ namespace osu.Game.Rulesets.Mania.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); + + #region Touch Input + + [Resolved(canBeNull: true)] + private ManiaInputManager maniaInputManager { get; set; } + + private int touchActivationCount; + + protected override bool OnTouchDown(TouchDownEvent e) + { + maniaInputManager.KeyBindingContainer.TriggerPressed(Action.Value); + touchActivationCount++; + return true; + } + + protected override void OnTouchUp(TouchUpEvent e) + { + touchActivationCount--; + + if (touchActivationCount == 0) + maniaInputManager.KeyBindingContainer.TriggerReleased(Action.Value); + } + + #endregion } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 136b172a59..65841af5de 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -112,8 +112,6 @@ namespace osu.Game.Rulesets.Mania.UI configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); - - KeyBindingInputManager.Add(new ManiaTouchInputArea()); } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs deleted file mode 100644 index 8c4a71cf24..0000000000 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osuTK; - -namespace osu.Game.Rulesets.Mania.UI -{ - /// - /// An overlay that captures and displays osu!mania mouse and touch input. - /// - public partial class ManiaTouchInputArea : VisibilityContainer - { - // visibility state affects our child. we always want to handle input. - public override bool PropagatePositionalInputSubTree => true; - public override bool PropagateNonPositionalInputSubTree => true; - - [SettingSource("Spacing", "The spacing between receptors.")] - public BindableFloat Spacing { get; } = new BindableFloat(10) - { - Precision = 1, - MinValue = 0, - MaxValue = 100, - }; - - [SettingSource("Opacity", "The receptor opacity.")] - public BindableFloat Opacity { get; } = new BindableFloat(1) - { - Precision = 0.1f, - MinValue = 0, - MaxValue = 1 - }; - - [Resolved] - private DrawableManiaRuleset drawableRuleset { get; set; } = null!; - - private GridContainer gridContainer = null!; - - public ManiaTouchInputArea() - { - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - - RelativeSizeAxes = Axes.Both; - Height = 0.5f; - } - - [BackgroundDependencyLoader] - private void load() - { - List receptorGridContent = new List(); - List receptorGridDimensions = new List(); - - bool first = true; - - foreach (var stage in drawableRuleset.Playfield.Stages) - { - foreach (var column in stage.Columns) - { - if (!first) - { - receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } }); - receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize)); - } - - receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } }); - receptorGridDimensions.Add(new Dimension()); - - first = false; - } - } - - InternalChild = gridContainer = new GridContainer - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Content = new[] { receptorGridContent.ToArray() }, - ColumnDimensions = receptorGridDimensions.ToArray() - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Opacity.BindValueChanged(o => Alpha = o.NewValue, true); - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - // Hide whenever the keyboard is used. - Hide(); - return false; - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - Show(); - return true; - } - - protected override void PopIn() - { - gridContainer.FadeIn(500, Easing.OutQuint); - } - - protected override void PopOut() - { - gridContainer.FadeOut(300); - } - - public partial class ColumnInputReceptor : CompositeDrawable - { - public readonly IBindable Action = new Bindable(); - - private readonly Box highlightOverlay; - - [Resolved] - private ManiaInputManager? inputManager { get; set; } - - private bool isPressed; - - public ColumnInputReceptor() - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.15f, - }, - highlightOverlay = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Blending = BlendingParameters.Additive, - } - } - } - }; - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - updateButton(true); - return false; // handled by parent container to show overlay. - } - - protected override void OnTouchUp(TouchUpEvent e) - { - updateButton(false); - } - - private void updateButton(bool press) - { - if (press == isPressed) - return; - - isPressed = press; - - if (press) - { - inputManager?.KeyBindingContainer.TriggerPressed(Action.Value); - highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); - } - else - { - inputManager?.KeyBindingContainer.TriggerReleased(Action.Value); - highlightOverlay.FadeTo(0, 400, Easing.OutQuint); - } - } - } - - private partial class Gutter : Drawable - { - public readonly IBindable Spacing = new Bindable(); - - public Gutter() - { - Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue)); - } - } - } -} From 64e557d00f98728e5a67d84c3158a8a11478c168 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 20:01:21 -0500 Subject: [PATCH 125/620] Simplify portrait check --- osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index d7cb211d4a..f7c4850a94 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Update(); float aspectRatio = DrawWidth / DrawHeight; - bool isPortrait = aspectRatio < 4 / 3f; + bool isPortrait = aspectRatio < 1f; if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { From 3ac2d90f19a1da783a45f721fdf4d9046dfe3886 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 20:44:50 -0500 Subject: [PATCH 126/620] Add explanatory note --- osu.iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 02f8462fbc..70747fc9c8 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -157,6 +157,8 @@ public.app-category.music-games LSSupportsOpeningDocumentsInPlace + GCSupportsGameMode From e5713e52392066a1430ebce460d07d8af01ad29f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 21:31:52 -0500 Subject: [PATCH 127/620] Fix triangles judgement mispositioned on a miss Similar to mania's `ArgonJudgementPiece`. --- .../UI/DrawableManiaJudgement.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 5b87c74bbe..a1dabd66bc 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -35,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: + this.FadeOutFromOne(800); + break; + case HitResult.Miss: - base.PlayAnimation(); + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); break; default: From c221a0c9f93c20949f26459405cfcc5047a39e0b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 1 Jan 2025 01:43:43 -0500 Subject: [PATCH 128/620] Improve UI scale on iOS devices --- osu.Game/Graphics/Containers/ScalingContainer.cs | 6 ++++++ osu.Game/OsuGame.cs | 5 +++++ osu.iOS/OsuGameIOS.cs | 3 +++ 3 files changed, 14 insertions(+) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index c47aba2f0c..ac76c0546b 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -99,6 +100,10 @@ namespace osu.Game.Graphics.Containers this.applyUIScale = applyUIScale; } + [Resolved(canBeNull: true)] + [CanBeNull] + private OsuGame game { get; set; } + [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) { @@ -111,6 +116,7 @@ namespace osu.Game.Graphics.Containers protected override void Update() { + TargetDrawSize = new Vector2(1024, 1024 / (game?.BaseAspectRatio ?? 1f)); Scale = new Vector2(CurrentScale); Size = new Vector2(1 / CurrentScale); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c20536a1ec..5227400694 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -831,6 +831,11 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); + /// + /// The base aspect ratio to use in all s. + /// + protected internal virtual float BaseAspectRatio => 4f / 3f; + protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); #region Beatmap progression diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a9ca1778a0..b3d9be04a1 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Game; using osu.Game.Updater; using osu.Game.Utils; +using UIKit; namespace osu.iOS { @@ -19,6 +20,8 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; + protected override float BaseAspectRatio => (float)(UIScreen.MainScreen.Bounds.Width / UIScreen.MainScreen.Bounds.Height); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From 1211f6cf4cfc7a214e026e584ba6f704ea3471e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 13:06:34 +0900 Subject: [PATCH 129/620] Add auto-start setting for 10 seconds As touched on in https://github.com/ppy/osu/discussions/31205#discussioncomment-11671185. Doesn't require server-side changes as the server just uses a `TimeSpan`. --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 79617f172c..1372054149 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -568,6 +568,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Description("Off")] Off = 0, + [Description("10 seconds")] + Seconds10 = 10, + [Description("30 seconds")] Seconds30 = 30, From cca63b599eb3b0f57ef23abf582884003ae7d3af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 14:31:24 +0900 Subject: [PATCH 130/620] Always block scroll input above editor toolbox areas Originally this was an intentional choice (see https://github.com/ppy/osu/pull/18088) when these controls were more transparent and didn't for a solid toolbox area. But this is no longer the case, so for now let's always block scroll to match user expectations. Closes #31262. --- osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index 8af795f880..2a94ae6017 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -55,12 +55,6 @@ namespace osu.Game.Rulesets.Edit } } - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos); - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos); - - private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.ScreenSpaceDrawQuad.Contains(screenSpacePos); - protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnClick(ClickEvent e) => true; From 58dcb25bd5606e803bc6fee654339cd5b8969f4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 15:59:00 +0900 Subject: [PATCH 131/620] Revert "Clear previous `LastLocalUserScore` when returning to song select" This reverts commit ced8dda1a29da0697bf5e47c7ab0734f473b6892. --- osu.Game/Configuration/SessionStatics.cs | 4 +--- osu.Game/Screens/Play/PlayerLoader.cs | 7 ------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 18631f5d00..225f209380 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -10,7 +10,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Scoring; -using osu.Game.Screens.Play; namespace osu.Game.Configuration { @@ -78,8 +77,7 @@ namespace osu.Game.Configuration TouchInputActive, /// - /// Contains the local user's last score (can be completed or aborted) after exiting . - /// Will be cleared to null when leaving . + /// Stores the local user's last score (can be completed or aborted). /// LastLocalUserScore, diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 837974a8f2..06086c1004 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -29,7 +29,6 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Volume; using osu.Game.Performance; -using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Skinning; @@ -80,8 +79,6 @@ namespace osu.Game.Screens.Play private FillFlowContainer disclaimers = null!; private OsuScrollContainer settingsScroll = null!; - private Bindable lastScore = null!; - private Bindable showStoryboards = null!; private bool backgroundBrightnessReduction; @@ -183,8 +180,6 @@ namespace osu.Game.Screens.Play { muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); - lastScore = sessionStatics.GetBindable(Static.LastLocalUserScore); - showStoryboards = config.GetBindable(OsuSetting.ShowStoryboard); const float padding = 25; @@ -354,8 +349,6 @@ namespace osu.Game.Screens.Play highPerformanceSession?.Dispose(); highPerformanceSession = null; - lastScore.Value = null; - return base.OnExiting(e); } From 2d3595f7688ae4d66e112ca26915e8151c6f496a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 16:17:34 +0900 Subject: [PATCH 132/620] Add test covering required behaviour See https://github.com/ppy/osu/issues/30885. --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 0f47c3cd27..aa99b22701 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -27,18 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUpSteps] public void SetUpSteps() { - AddStep("Create control", () => - { - Child = new PlayerSettingsGroup("Some settings") - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - offsetControl = new BeatmapOffsetControl() - } - }; - }); + recreateControl(); } [Test] @@ -123,13 +112,14 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestCalibrationFromZero() { + ScoreInfo referenceScore = null!; const double average_error = -4.5; AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); AddStep("Set reference score", () => { - offsetControl.ReferenceScore.Value = new ScoreInfo + offsetControl.ReferenceScore.Value = referenceScore = new ScoreInfo { HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), BeatmapInfo = Beatmap.Value.BeatmapInfo, @@ -143,6 +133,10 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + + recreateControl(); + AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } /// @@ -251,5 +245,21 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } + + private void recreateControl() + { + AddStep("Create control", () => + { + Child = new PlayerSettingsGroup("Some settings") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + offsetControl = new BeatmapOffsetControl() + } + }; + }); + } } } From 2a28c5f4de158ef1e57d5dd1aa80bbcdfcdb2449 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 16:20:21 +0900 Subject: [PATCH 133/620] Add static memory of last applied offset score I don't really like adding this new session static, but we don't have a better place to put this. --- osu.Game/Configuration/SessionStatics.cs | 6 ++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 225f209380..c55a597c32 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -29,6 +29,7 @@ namespace osu.Game.Configuration SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); SetDefault(Static.LastLocalUserScore, null); + SetDefault(Static.LastAppliedOffsetScore, null); } /// @@ -81,6 +82,11 @@ namespace osu.Game.Configuration /// LastLocalUserScore, + /// + /// Stores the local user's last score which was used to apply an offset. + /// + LastAppliedOffsetScore, + /// /// Whether the intro animation for the daily challenge screen has been played once. /// This is reset when a new challenge is up. diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 74b887481f..f93fa1b3c5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -15,6 +15,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -36,6 +37,8 @@ namespace osu.Game.Screens.Play.PlayerSettings { public Bindable ReferenceScore { get; } = new Bindable(); + private Bindable lastAppliedScore { get; } = new Bindable(); + public BindableDouble Current { get; } = new BindableDouble { MinValue = -50, @@ -100,6 +103,12 @@ namespace osu.Game.Screens.Play.PlayerSettings }; } + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + statics.BindWith(Static.LastAppliedOffsetScore, lastAppliedScore); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -176,6 +185,9 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; + if (score.NewValue.Equals(lastAppliedScore.Value)) + return; + if (!score.NewValue.BeatmapInfo.AsNonNull().Equals(beatmap.Value.BeatmapInfo)) return; @@ -230,7 +242,11 @@ namespace osu.Game.Screens.Play.PlayerSettings useAverageButton = new SettingsButton { Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, - Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage, + Action = () => + { + Current.Value = lastPlayBeatmapOffset - lastPlayAverage; + lastAppliedScore.Value = ReferenceScore.Value; + }, Enabled = { Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2) } }, globalOffsetText = new LinkFlowContainer From 794765ba853dda7b08f5e970516619a21318d115 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 18:36:58 +0900 Subject: [PATCH 134/620] Remove use of `Loop` (and transforms) for slider repeat arrow animations Less transforms in gameplay is always better. This fixes repeat arrows animating completely incorrectly in the editor (and probably gameplay when rewinding). --- .../Skinning/Argon/ArgonReverseArrow.cs | 52 ++++++++----------- .../Skinning/Default/DefaultReverseArrow.cs | 42 +++++++-------- .../Skinning/Legacy/LegacyReverseArrow.cs | 46 ++++++---------- 3 files changed, 58 insertions(+), 82 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index 87b89a07cf..9f15e8e177 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -5,12 +5,12 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -75,44 +75,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon accentColour = drawableRepeat.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true); - - drawableRepeat.ApplyCustomUpdateState += updateStateTransforms; } - private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + protected override void Update() { + base.Update(); + + if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit) + { + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out)); + } + else + Scale = Vector2.One; + const float move_distance = -12; + const float scale_amount = 1.3f; + const double move_out_duration = 35; const double move_in_duration = 250; const double total = 300; - switch (state) - { - case ArmedState.Idle: - main.ScaleTo(1.3f, move_out_duration, Easing.Out) - .Then() - .ScaleTo(1f, move_in_duration, Easing.Out) - .Loop(total - (move_in_duration + move_out_duration)); - side - .MoveToX(move_distance, move_out_duration, Easing.Out) - .Then() - .MoveToX(0, move_in_duration, Easing.Out) - .Loop(total - (move_in_duration + move_out_duration)); - break; + double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total; - case ArmedState.Hit: - double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); - this.ScaleTo(1.5f, animDuration, Easing.Out); - break; - } - } + if (loopCurrentTime < move_out_duration) + main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out)); + else + main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out)); - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (drawableRepeat.IsNotNull()) - drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; + if (loopCurrentTime < move_out_duration) + side.X = Interpolation.ValueAt(loopCurrentTime, 1, move_distance, 0, move_out_duration, Easing.Out); + else + side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs index ad49150d81..5e2d04700d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs @@ -3,10 +3,10 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -40,37 +40,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private void load(DrawableHitObject drawableObject) { drawableRepeat = (DrawableSliderRepeat)drawableObject; - drawableRepeat.ApplyCustomUpdateState += updateStateTransforms; } - private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + protected override void Update() { - const double move_out_duration = 35; - const double move_in_duration = 250; - const double total = 300; + base.Update(); - switch (state) + if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit) { - case ArmedState.Idle: - InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out) - .Then() - .ScaleTo(1f, move_in_duration, Easing.Out) - .Loop(total - (move_in_duration + move_out_duration)); - break; - - case ArmedState.Hit: - double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); - InternalChild.ScaleTo(1.5f, animDuration, Easing.Out); - break; + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out)); } - } + else + { + const float scale_amount = 1.3f; - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); + const double move_out_duration = 35; + const double move_in_duration = 250; + const double total = 300; - if (drawableRepeat.IsNotNull()) - drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; + double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total; + if (loopCurrentTime < move_out_duration) + Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out)); + else + Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out)); + } } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index ad1fb98aef..940e068da0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -9,10 +9,12 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -51,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; - drawableObject.ApplyCustomUpdateState += updateStateTransforms; - shouldRotate = skinSource.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value <= 1; } @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy accentColour = drawableRepeat.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(c => { - arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White; + arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > 600 / 255f ? Color4.Black : Color4.White; }, true); } @@ -80,36 +80,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy); } - private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + protected override void Update() { - const double duration = 300; - const float rotation = 5.625f; + base.Update(); - switch (state) + if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit) { - case ArmedState.Idle: - if (shouldRotate) - { - InternalChild.ScaleTo(1.3f) - .RotateTo(rotation) - .Then() - .ScaleTo(1f, duration) - .RotateTo(-rotation, duration) - .Loop(); - } - else - { - InternalChild.ScaleTo(1.3f).Then() - .ScaleTo(1f, duration, Easing.Out) - .Loop(); - } + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + arrow.Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.4f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out)); + } + else + { + const double duration = 300; + const float rotation = 5.625f; - break; + double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration; - case ArmedState.Hit: - double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); - InternalChild.ScaleTo(1.4f, animDuration, Easing.Out); - break; + if (shouldRotate) + arrow.Rotation = Interpolation.ValueAt(loopCurrentTime, rotation, -rotation, 0, duration); + arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration)); } } @@ -120,7 +109,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (drawableRepeat.IsNotNull()) { drawableRepeat.HitObjectApplied -= onHitObjectApplied; - drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; } } } From e7b80167cd1773587670159b9ef5da320e4090f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 18:54:28 +0900 Subject: [PATCH 135/620] Fix slider end circles not remaining for long enough when hit animations disabled --- .../Objects/Drawables/DrawableSlider.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 0fcfdef4ee..e22e1d2001 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -382,6 +382,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables repeat.SuppressHitAnimations(); TailCircle.SuppressHitAnimations(); + + // This method is called every frame in editor contexts, thus the lack of need for transforms. + + if (Time.Current >= HitStateUpdateTime) + { + // Apply the slider's alpha to *only* the body. + // This allows start and – more importantly – end circles to fade slower than the overall slider. + if (Alpha < 1) + Body.Alpha = Alpha; + Alpha = 1; + } + + LifetimeEnd = HitStateUpdateTime + 700; } internal void RestoreHitAnimations() From 039800550c336bded55ebbb2d475d5fd23965134 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 3 Jan 2025 00:20:23 -0500 Subject: [PATCH 136/620] Display popup disclaimer about game state and performance on mobile platforms --- osu.Game/Configuration/OsuConfigManager.cs | 3 ++ osu.Game/Screens/Menu/MainMenu.cs | 43 +++++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index deac1a5128..dd3abb6f81 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using osu.Framework; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; @@ -163,6 +164,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Version, string.Empty); SetDefault(OsuSetting.ShowFirstRunSetup, true); + SetDefault(OsuSetting.ShowMobileDisclaimer, RuntimeInfo.IsMobile); SetDefault(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); SetDefault(OsuSetting.ScreenshotCaptureMenuCursor, false); @@ -452,5 +454,6 @@ namespace osu.Game.Configuration AlwaysRequireHoldingForPause, MultiplayerShowInProgressFilter, BeatmapListingFeaturedArtistFilter, + ShowMobileDisclaimer, } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 99bc1825f5..4f6e55d13b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -87,6 +88,7 @@ namespace osu.Game.Screens.Menu private Bindable holdDelay; private Bindable loginDisplayed; + private Bindable showMobileDisclaimer; private HoldToExitGameOverlay holdToExitGameOverlay; @@ -111,6 +113,7 @@ namespace osu.Game.Screens.Menu { holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); loginDisplayed = statics.GetBindable(Static.LoginOverlayDisplayed); + showMobileDisclaimer = config.GetBindable(OsuSetting.ShowMobileDisclaimer); if (host.CanExit) { @@ -275,26 +278,54 @@ namespace osu.Game.Screens.Menu sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint); } - else if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth) + else { // copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop. var previousAction = logo.Action; - // we want to hook into logo.Action to display the login overlay, but also preserve the return value of the old action. + // we want to hook into logo.Action to display certain overlays, but also preserve the return value of the old action. // therefore pass the old action to displayLogin, so that it can return that value. // this ensures that the OsuLogo sample does not play when it is not desired. - logo.Action = () => displayLogin(previousAction); + logo.Action = () => onLogoClick(previousAction); } + } - bool displayLogin(Func originalAction) + private bool onLogoClick(Func originalAction) + { + if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth) { if (!loginDisplayed.Value) { - Scheduler.AddDelayed(() => login?.Show(), 500); + this.Delay(500).Schedule(() => login?.Show()); loginDisplayed.Value = true; } + } - return originalAction.Invoke(); + if (showMobileDisclaimer.Value) + { + this.Delay(500).Schedule(() => dialogOverlay.Push(new MobileDisclaimerDialog())); + showMobileDisclaimer.Value = false; + } + + return originalAction.Invoke(); + } + + internal partial class MobileDisclaimerDialog : PopupDialog + { + public MobileDisclaimerDialog() + { + HeaderText = "Mobile disclaimer"; + BodyText = "We're releasing this for your enjoyment, but PC is still our focus and mobile is hard to support.\n\nPlease bear with us as we continue to improve the experience!"; + + Icon = FontAwesome.Solid.Mobile; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = "Alright!", + }, + }; } } From c40371c052f474b89c263a6d6674d66fd4caf9a3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 3 Jan 2025 00:27:21 -0500 Subject: [PATCH 137/620] Move dialog class location --- osu.Game/Screens/Menu/MainMenu.cs | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 4f6e55d13b..ba8c1ae517 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -310,25 +310,6 @@ namespace osu.Game.Screens.Menu return originalAction.Invoke(); } - internal partial class MobileDisclaimerDialog : PopupDialog - { - public MobileDisclaimerDialog() - { - HeaderText = "Mobile disclaimer"; - BodyText = "We're releasing this for your enjoyment, but PC is still our focus and mobile is hard to support.\n\nPlease bear with us as we continue to improve the experience!"; - - Icon = FontAwesome.Solid.Mobile; - - Buttons = new PopupDialogButton[] - { - new PopupDialogOkButton - { - Text = "Alright!", - }, - }; - } - } - protected override void LogoSuspending(OsuLogo logo) { var seq = logo.FadeOut(300, Easing.InSine) @@ -474,5 +455,24 @@ namespace osu.Game.Screens.Menu public void OnReleased(KeyBindingReleaseEvent e) { } + + private partial class MobileDisclaimerDialog : PopupDialog + { + public MobileDisclaimerDialog() + { + HeaderText = "Mobile disclaimer"; + BodyText = "We're releasing this for your enjoyment, but PC is still our focus and mobile is hard to support.\n\nPlease bear with us as we continue to improve the experience!"; + + Icon = FontAwesome.Solid.Mobile; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = "Alright!", + }, + }; + } + } } } From 1161b7b3c0f79e8a4bb616029d57f3d41142eece Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 00:55:12 +0900 Subject: [PATCH 138/620] Flip navigation test expectations in line with new behaviour --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 5646649d33..58e780cf16 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -355,18 +355,18 @@ namespace osu.Game.Tests.Visual.Navigation } [Test] - public void TestLastScoreNullAfterExitingPlayer() + public void TestLastScoreNotNullAfterExitingPlayer() { - AddUntilStep("wait for last play null", getLastPlay, () => Is.Null); + AddUntilStep("last play null", getLastPlay, () => Is.Null); var getOriginalPlayer = playToCompletion(); AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action()); - AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo)); + AddUntilStep("last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo)); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit()); - AddUntilStep("wait for last play null", getLastPlay, () => Is.Null); + AddUntilStep("last play not null", getLastPlay, () => Is.Not.Null); ScoreInfo getLastPlay() => Game.Dependencies.Get().Get(Static.LastLocalUserScore); } From 97d065d88799d2f24dfcb95e019208dc39a31a1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 00:58:19 +0900 Subject: [PATCH 139/620] Only flip value if popup was definitely shown --- osu.Game/Screens/Menu/MainMenu.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index ba8c1ae517..692e6e2110 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -303,8 +303,11 @@ namespace osu.Game.Screens.Menu if (showMobileDisclaimer.Value) { - this.Delay(500).Schedule(() => dialogOverlay.Push(new MobileDisclaimerDialog())); - showMobileDisclaimer.Value = false; + this.Delay(500).Schedule(() => + { + dialogOverlay.Push(new MobileDisclaimerDialog()); + showMobileDisclaimer.Value = false; + }); } return originalAction.Invoke(); From 1d81dade25d68f44b196e8e4c5ed447c16abdf52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 01:06:33 +0900 Subject: [PATCH 140/620] Update copy and require actually clicking button to confirm --- osu.Game/Screens/Menu/MainMenu.cs | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 692e6e2110..ff5e81a609 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -19,6 +19,7 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -258,6 +259,9 @@ namespace osu.Game.Screens.Menu [CanBeNull] private Drawable proxiedLogo; + [CanBeNull] + private ScheduledDelegate mobileDisclaimerSchedule; + protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); @@ -296,18 +300,21 @@ namespace osu.Game.Screens.Menu { if (!loginDisplayed.Value) { - this.Delay(500).Schedule(() => login?.Show()); + Scheduler.AddDelayed(() => login?.Show(), 500); loginDisplayed.Value = true; } } if (showMobileDisclaimer.Value) { - this.Delay(500).Schedule(() => + mobileDisclaimerSchedule?.Cancel(); + mobileDisclaimerSchedule = Scheduler.AddDelayed(() => { - dialogOverlay.Push(new MobileDisclaimerDialog()); - showMobileDisclaimer.Value = false; - }); + dialogOverlay.Push(new MobileDisclaimerDialog(() => + { + showMobileDisclaimer.Value = false; + })); + }, 500); } return originalAction.Invoke(); @@ -461,10 +468,11 @@ namespace osu.Game.Screens.Menu private partial class MobileDisclaimerDialog : PopupDialog { - public MobileDisclaimerDialog() + public MobileDisclaimerDialog(Action confirmed) { - HeaderText = "Mobile disclaimer"; - BodyText = "We're releasing this for your enjoyment, but PC is still our focus and mobile is hard to support.\n\nPlease bear with us as we continue to improve the experience!"; + HeaderText = "A few important words from your dev team!"; + BodyText = + "While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version.\n\nYour experience will not be perfect, and may even feel subpar compared to games which are made mobile-first.\n\nPlease bear with us as we continue to improve the game for you!"; Icon = FontAwesome.Solid.Mobile; @@ -472,7 +480,8 @@ namespace osu.Game.Screens.Menu { new PopupDialogOkButton { - Text = "Alright!", + Text = "Understood", + Action = confirmed, }, }; } From 60fd0be48124cac5997ffb1b43e507a1edd20e07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 01:19:56 +0900 Subject: [PATCH 141/620] Make popup body text left aligned when multiple lines of text are provided --- osu.Game/Overlays/Dialog/PopupDialog.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index a23c394c9f..4cdd51327f 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -75,7 +75,9 @@ namespace osu.Game.Overlays.Dialog return; bodyText = value; + body.Text = value; + body.TextAnchor = bodyText.ToString().Contains('\n') ? Anchor.TopLeft : Anchor.TopCentre; } } @@ -210,13 +212,12 @@ namespace osu.Game.Overlays.Dialog RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.TopCentre, - Padding = new MarginPadding { Horizontal = 15 }, + Padding = new MarginPadding { Horizontal = 15, Bottom = 10 }, }, body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18)) { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, - TextAnchor = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 15 }, From da855170369efa046f779b0f8db14c1251bf5fb5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 01:28:09 +0900 Subject: [PATCH 142/620] Adjust popup icon animation slightly --- osu.Game/Overlays/Dialog/PopupDialog.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 4cdd51327f..0fec1625eb 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -302,6 +302,7 @@ namespace osu.Game.Overlays.Dialog { content.ScaleTo(0.7f); ring.ResizeTo(ringMinifiedSize); + icon.ScaleTo(0f); } content @@ -309,6 +310,7 @@ namespace osu.Game.Overlays.Dialog .FadeIn(ENTER_DURATION, Easing.OutQuint); ring.ResizeTo(ringSize, ENTER_DURATION * 1.5f, Easing.OutQuint); + icon.Delay(100).ScaleTo(1, ENTER_DURATION * 1.5f, Easing.OutQuint); } protected override void PopOut() From 2cd86cbf9161df2e84b0be5346bfc32648a898c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 01:28:33 +0900 Subject: [PATCH 143/620] Localise text --- osu.Game/Localisation/ButtonSystemStrings.cs | 19 +++++++++++++++++++ osu.Game/Screens/Menu/MainMenu.cs | 8 ++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/ButtonSystemStrings.cs b/osu.Game/Localisation/ButtonSystemStrings.cs index b0a205eebe..a9bc3068da 100644 --- a/osu.Game/Localisation/ButtonSystemStrings.cs +++ b/osu.Game/Localisation/ButtonSystemStrings.cs @@ -59,6 +59,25 @@ namespace osu.Game.Localisation /// public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge"); + /// + /// "A few important words from your dev team!" + /// + public static LocalisableString MobileDisclaimerHeader => new TranslatableString(getKey(@"mobile_disclaimer_header"), @"A few important words from your dev team!"); + + /// + /// "While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version. + /// + /// Your experience will not be perfect, and may even feel subpar compared to games which are made mobile-first. + /// + /// Please bear with us as we continue to improve the game for you!" + /// + public static LocalisableString MobileDisclaimerBody => new TranslatableString(getKey(@"mobile_disclaimer_body"), + @"While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version. + +Your experience will not be perfect, and may even feel subpar compared to games which are made mobile-first. + +Please bear with us as we continue to improve the game for you!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index ff5e81a609..583351438c 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -41,6 +41,7 @@ using osu.Game.Screens.Select; using osu.Game.Seasonal; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Screens.Menu { @@ -470,11 +471,10 @@ namespace osu.Game.Screens.Menu { public MobileDisclaimerDialog(Action confirmed) { - HeaderText = "A few important words from your dev team!"; - BodyText = - "While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version.\n\nYour experience will not be perfect, and may even feel subpar compared to games which are made mobile-first.\n\nPlease bear with us as we continue to improve the game for you!"; + HeaderText = ButtonSystemStrings.MobileDisclaimerHeader; + BodyText = ButtonSystemStrings.MobileDisclaimerBody; - Icon = FontAwesome.Solid.Mobile; + Icon = FontAwesome.Solid.SmileBeam; Buttons = new PopupDialogButton[] { From 3fc86f60ee344d3c6c86e9e4afc42d89a4368c2b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 3 Jan 2025 21:36:00 -0500 Subject: [PATCH 144/620] Fix mobile release dialog obstructed by the software keyboard --- osu.Game/Screens/Menu/MainMenu.cs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 583351438c..ab72dd7e69 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -297,15 +297,6 @@ namespace osu.Game.Screens.Menu private bool onLogoClick(Func originalAction) { - if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth) - { - if (!loginDisplayed.Value) - { - Scheduler.AddDelayed(() => login?.Show(), 500); - loginDisplayed.Value = true; - } - } - if (showMobileDisclaimer.Value) { mobileDisclaimerSchedule?.Cancel(); @@ -314,13 +305,28 @@ namespace osu.Game.Screens.Menu dialogOverlay.Push(new MobileDisclaimerDialog(() => { showMobileDisclaimer.Value = false; + displayLoginIfApplicable(); })); }, 500); } + else + displayLoginIfApplicable(); return originalAction.Invoke(); } + private void displayLoginIfApplicable() + { + if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth) + { + if (!loginDisplayed.Value) + { + Scheduler.AddDelayed(() => login?.Show(), 500); + loginDisplayed.Value = true; + } + } + } + protected override void LogoSuspending(OsuLogo logo) { var seq = logo.FadeOut(300, Easing.InSine) From e15978cc65d98d322785e5c2b7da4c7370193a79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 15:26:42 +0900 Subject: [PATCH 145/620] Add test coverage of user deleting intro files --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 48 +++++++++++-------- .../Visual/Menus/TestSceneIntroIntegrity.cs | 37 ++++++++++++++ osu.Game/OsuGameBase.cs | 1 + 3 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index b09dbc1a91..2b0717c1e3 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Menus protected OsuScreenStack IntroStack; - private IntroScreen intro; + protected IntroScreen Intro { get; private set; } [Cached(typeof(INotificationOverlay))] private NotificationOverlay notifications; @@ -62,22 +62,9 @@ namespace osu.Game.Tests.Visual.Menus [Test] public virtual void TestPlayIntro() { - AddStep("restart sequence", () => - { - logo.FinishTransforms(); - logo.IsTracking = false; + RestartIntro(); - IntroStack?.Expire(); - - Add(IntroStack = new OsuScreenStack - { - RelativeSizeAxes = Axes.Both, - }); - - IntroStack.Push(intro = CreateScreen()); - }); - - AddUntilStep("wait for menu", () => intro.DidLoadMenu); + WaitForMenu(); } [Test] @@ -103,18 +90,18 @@ namespace osu.Game.Tests.Visual.Menus RelativeSizeAxes = Axes.Both, }); - IntroStack.Push(intro = CreateScreen()); + IntroStack.Push(Intro = CreateScreen()); }); AddStep("trigger failure", () => { trackResetDelegate = Scheduler.AddDelayed(() => { - intro.Beatmap.Value.Track.Seek(0); + Intro.Beatmap.Value.Track.Seek(0); }, 0, true); }); - AddUntilStep("wait for menu", () => intro.DidLoadMenu); + WaitForMenu(); if (IntroReliesOnTrack) AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1); @@ -122,6 +109,29 @@ namespace osu.Game.Tests.Visual.Menus AddStep("uninstall delegate", () => trackResetDelegate?.Cancel()); } + protected void RestartIntro() + { + AddStep("restart sequence", () => + { + logo.FinishTransforms(); + logo.IsTracking = false; + + IntroStack?.Expire(); + + Add(IntroStack = new OsuScreenStack + { + RelativeSizeAxes = Axes.Both, + }); + + IntroStack.Push(Intro = CreateScreen()); + }); + } + + protected void WaitForMenu() + { + AddUntilStep("wait for menu", () => Intro.DidLoadMenu); + } + protected abstract IntroScreen CreateScreen(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs new file mode 100644 index 0000000000..ea70b3fe7f --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs @@ -0,0 +1,37 @@ +// 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.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [HeadlessTest] + [TestFixture] + public partial class TestSceneIntroIntegrity : IntroTestScene + { + [Test] + public virtual void TestDeletedFilesRestored() + { + RestartIntro(); + WaitForMenu(); + + AddStep("delete game files unexpectedly", () => LocalStorage.DeleteDirectory("files")); + AddStep("reset game beatmap", () => Dependencies.Get>().Value = new DummyWorkingBeatmap(Audio, null)); + AddStep("invalidate beatmap from cache", () => Dependencies.Get().Invalidate(Intro.Beatmap.Value.BeatmapSetInfo)); + + RestartIntro(); + WaitForMenu(); + + AddUntilStep("wait for track playing", () => Intro.Beatmap.Value.Track is TrackBass trackBass && trackBass.IsRunning); + } + + protected override bool IntroReliesOnTrack => true; + protected override IntroScreen CreateScreen() => new IntroTriangles(); + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8027b6bfbc..5e247ca877 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -315,6 +315,7 @@ namespace osu.Game dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true)); + dependencies.CacheAs(BeatmapManager); dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API)); From 72dfdac2e2478108a30bcf9098bc2bf0876e84c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 15:27:49 +0900 Subject: [PATCH 146/620] Ensure intro files exist in storage Guards against user interdiction. See [https://discord.com/channels/188630481301012481/1097318920991559880/1324765503012601927](recent) but not only case of this occurring. --- osu.Game/Screens/Menu/IntroScreen.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index c110c53df8..7b23cc7538 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -20,6 +20,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Overlays; @@ -170,7 +171,14 @@ namespace osu.Game.Screens.Menu if (s.Beatmaps.Count == 0) return; - initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); + var working = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); + + // Ensure files area actually present on disk. + // This is to handle edge cases like users deleting files outside the game and breaking the world. + if (!hasAllFiles(working)) + return; + + initialBeatmap = working; }); return UsingThemedIntro = initialBeatmap != null; @@ -188,6 +196,20 @@ namespace osu.Game.Screens.Menu [Resolved] private INotificationOverlay notifications { get; set; } + private bool hasAllFiles(WorkingBeatmap working) + { + foreach (var f in working.BeatmapSetInfo.Files) + { + using (var str = working.GetStream(f.File.GetStoragePath())) + { + if (str == null) + return false; + } + } + + return true; + } + private void ensureEventuallyArrivingAtMenu() { // This intends to handle the case where an intro may get stuck. From 21389820c5f415dab2db00530a860f1eb93ee270 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 4 Jan 2025 02:35:48 -0500 Subject: [PATCH 147/620] Fix player no longer handling non-loaded beatmaps --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e50f97f912..02a8a6d2cc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; - public override bool RequiresPortraitOrientation => DrawableRuleset.RequiresPortraitOrientation; + public override bool RequiresPortraitOrientation => DrawableRuleset?.RequiresPortraitOrientation == true; protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; From a241d1f5032f453d7a83e0b6fb0a8502bd42e431 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 4 Jan 2025 02:36:06 -0500 Subject: [PATCH 148/620] Fix `DrawableManiaRuleset` not cached as itself in subtypes i.e. editor mania ruleset --- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 65841af5de..d6794d0b4f 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -32,7 +32,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { - [Cached] + [Cached(typeof(DrawableManiaRuleset))] public partial class DrawableManiaRuleset : DrawableScrollingRuleset { /// From 37da72d764896b6678738bf9ea175b8a3ae2bed5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 Jan 2025 00:32:06 +0900 Subject: [PATCH 149/620] Reduce nesting slightly --- osu.Game/Screens/Menu/MainMenu.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index ab72dd7e69..135b3dba17 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -317,13 +317,12 @@ namespace osu.Game.Screens.Menu private void displayLoginIfApplicable() { + if (loginDisplayed.Value) return; + if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth) { - if (!loginDisplayed.Value) - { - Scheduler.AddDelayed(() => login?.Show(), 500); - loginDisplayed.Value = true; - } + Scheduler.AddDelayed(() => login?.Show(), 500); + loginDisplayed.Value = true; } } From 4f1a6b468895b03c2be20a3e33e5bd810ba2bb60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Jan 2025 17:51:04 +0900 Subject: [PATCH 150/620] Always show dialog when clicking supporter icon before opening browser I managed to do this by accident three times today while testing using the dashboard display, so it's time to action on it. Touched on in https://github.com/ppy/osu/discussions/30740#discussioncomment-11345996. Was also mentioned recently in discord or another discussion explicitly but I can't find that. --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 57 ++++++++++++++++++- osu.Game/Online/Chat/LinkWarnMode.cs | 23 ++++++++ osu.Game/OsuGame.cs | 30 +--------- .../Overlays/AccountCreation/ScreenEntry.cs | 3 +- .../Header/Components/SupporterIcon.cs | 4 +- 5 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 osu.Game/Online/Chat/LinkWarnMode.cs diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 75b161d57b..f76d42c96d 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -4,13 +4,16 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; +using osu.Game.Overlays.Notifications; using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Online.Chat @@ -23,9 +26,15 @@ namespace osu.Game.Online.Chat [Resolved] private Clipboard clipboard { get; set; } = null!; - [Resolved(CanBeNull = true)] + [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + private Bindable externalLinkWarning = null!; [BackgroundDependencyLoader(true)] @@ -34,9 +43,51 @@ namespace osu.Game.Online.Chat externalLinkWarning = config.GetBindable(OsuSetting.ExternalLinkWarning); } - public void OpenUrlExternally(string url, bool bypassWarning = false) + public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) { - if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null) + bool isTrustedDomain; + + if (url.StartsWith('/')) + { + url = $"{api.WebsiteRootUrl}{url}"; + isTrustedDomain = true; + } + else + { + isTrustedDomain = url.StartsWith(api.WebsiteRootUrl, StringComparison.Ordinal); + } + + if (!url.CheckIsValidUrl()) + { + notificationOverlay?.Post(new SimpleErrorNotification + { + Text = NotificationsStrings.UnsupportedOrDangerousUrlProtocol(url), + }); + + return; + } + + bool shouldWarn; + + switch (warnMode) + { + case LinkWarnMode.Default: + shouldWarn = externalLinkWarning.Value && !isTrustedDomain; + break; + + case LinkWarnMode.AlwaysWarn: + shouldWarn = true; + break; + + case LinkWarnMode.NeverWarn: + shouldWarn = false; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(warnMode), warnMode, null); + } + + if (dialogOverlay != null && shouldWarn) dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url))); else host.OpenUrlExternally(url); diff --git a/osu.Game/Online/Chat/LinkWarnMode.cs b/osu.Game/Online/Chat/LinkWarnMode.cs new file mode 100644 index 0000000000..0acd3994d8 --- /dev/null +++ b/osu.Game/Online/Chat/LinkWarnMode.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Chat +{ + public enum LinkWarnMode + { + /// + /// Will show a dialog when opening a URL that is not on a trusted domain. + /// + Default, + + /// + /// Will always show a dialog when opening a URL. + /// + AlwaysWarn, + + /// + /// Will never show a dialog when opening a URL. + /// + NeverWarn, + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c20536a1ec..0d86bdecde 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -18,7 +18,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Configuration; -using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; @@ -516,32 +515,7 @@ namespace osu.Game onScreenDisplay.Display(new CopyUrlToast()); }); - public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => - { - bool isTrustedDomain; - - if (url.StartsWith('/')) - { - url = $"{API.WebsiteRootUrl}{url}"; - isTrustedDomain = true; - } - else - { - isTrustedDomain = url.StartsWith(API.WebsiteRootUrl, StringComparison.Ordinal); - } - - if (!url.CheckIsValidUrl()) - { - Notifications.Post(new SimpleErrorNotification - { - Text = NotificationsStrings.UnsupportedOrDangerousUrlProtocol(url), - }); - - return; - } - - externalLinkOpener.OpenUrlExternally(url, forceBypassExternalUrlWarning || isTrustedDomain); - }); + public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) => waitForReady(() => externalLinkOpener, _ => externalLinkOpener.OpenUrlExternally(url, warnMode)); /// /// Open a specific channel in chat. @@ -1340,7 +1314,7 @@ namespace osu.Game IconColour = Colours.YellowDark, Activated = () => { - OpenUrlExternally("https://opentabletdriver.net/Tablets", true); + OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn); return true; } })); diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index fb6a5796a1..b2b672342e 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Chat; using osu.Game.Overlays.Settings; using osu.Game.Resources.Localisation.Web; using osuTK; @@ -213,7 +214,7 @@ namespace osu.Game.Overlays.AccountCreation if (!string.IsNullOrEmpty(errors.Message)) passwordDescription.AddErrors(new[] { errors.Message }); - game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true); + game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", LinkWarnMode.NeverWarn); } } else diff --git a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs index 92e2017659..74abb0af2a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs +++ b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Header.Components @@ -87,7 +88,8 @@ namespace osu.Game.Overlays.Profile.Header.Components { background.Colour = colours.Pink; - Action = () => game?.OpenUrlExternally(@"/home/support"); + // Easy to accidentally click so let's always show the open URL popup. + Action = () => game?.OpenUrlExternally(@"/home/support", LinkWarnMode.AlwaysWarn); } protected override bool OnHover(HoverEvent e) From ca9e16387ab1f4c724c0e63296c694e1df980dff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Jan 2025 18:27:00 +0900 Subject: [PATCH 151/620] Don't require track to be playing to fix test failures on some platforms --- osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs index ea70b3fe7f..a5590c79ae 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Menus RestartIntro(); WaitForMenu(); - AddUntilStep("wait for track playing", () => Intro.Beatmap.Value.Track is TrackBass trackBass && trackBass.IsRunning); + AddUntilStep("ensure track is not virtual", () => Intro.Beatmap.Value.Track is TrackBass); } protected override bool IntroReliesOnTrack => true; From 3a4497af32d3d793f3ba01b329281a7e97270271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Jan 2025 14:04:47 +0100 Subject: [PATCH 152/620] Constrain range of usable characters in romanised metadata to ASCII only Closes https://github.com/ppy/osu/issues/31398. Rationale given in issue. Compare stable logic: - https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameModes/Edit/Forms/SongSetup.cs#L118-L122 - https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!common/Helpers/GeneralHelper.cs#L410-L423 The control character check is a bit gratuitous (text boxes will already not allow insertion of those, see https://github.com/ppy/osu-framework/blob/e05cb86ff64abd343de49a143ada9734fd160a0a/osu.Framework/Graphics/UserInterface/TextBox.cs#L92), but as it's a general helper I figured might as well. --- osu.Game/Beatmaps/MetadataUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/MetadataUtils.cs b/osu.Game/Beatmaps/MetadataUtils.cs index 89c821c16c..1d2a3b5d01 100644 --- a/osu.Game/Beatmaps/MetadataUtils.cs +++ b/osu.Game/Beatmaps/MetadataUtils.cs @@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps /// Returns if the character can be used in and fields. /// Characters not matched by this method can be placed in and . /// - public static bool IsRomanised(char c) => c <= 0xFF; + public static bool IsRomanised(char c) => char.IsAscii(c) && !char.IsControl(c); /// /// Returns if the string can be used in and fields. From e8dc09f5bc66642b21e0a2bae8645f20904870d2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jan 2025 00:36:58 +0300 Subject: [PATCH 153/620] Reduce HitSampleInfo constants allocations --- osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs | 2 +- osu.Game/Audio/HitSampleInfo.cs | 4 ++-- osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs | 4 ++-- osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs | 2 +- .../Edit/Compose/Components/EditorSelectionHandler.cs | 6 +++--- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 4 ++-- .../Components/Timeline/TimelineBlueprintContainer.cs | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs index a5846efdfe..72422a0ae8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs @@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Mods // If samples aren't available at the exact start time of the object, // use samples (without additions) in the closest original hit object instead - obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); + obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.ALL_ADDITIONS.Contains(s.Name)).ToList(); } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 19273e3714..b6819a0f16 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -33,12 +33,12 @@ namespace osu.Game.Audio /// /// All valid sample addition constants. /// - public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP }; + public static readonly string[] ALL_ADDITIONS = new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP }; /// /// All valid bank constants. /// - public static IEnumerable AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM }; + public static readonly string[] ALL_BANKS = new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM }; /// /// The name of the sample to load. diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs index d6cd4f4caa..ee950248db 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs @@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Edit.Checks string bank = parts[0]; string sampleSet = parts[1]; - return HitSampleInfo.AllBanks.Contains(bank) - && HitSampleInfo.AllAdditions.Append(HitSampleInfo.HIT_NORMAL).Any(sampleSet.StartsWith); + return HitSampleInfo.ALL_BANKS.Contains(bank) + && HitSampleInfo.ALL_ADDITIONS.Append(HitSampleInfo.HIT_NORMAL).Any(sampleSet.StartsWith); } public class IssueTemplateConsequentDelay : IssueTemplate diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs index 3358e81d5f..97c1519c24 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Edit.Checks ++objectsWithoutHitsounds; } - private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains); + private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.ALL_ADDITIONS.Any(sample.Name.Contains); private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL); public abstract class IssueTemplateLongPeriod : IssueTemplate diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 78cee2c1cf..cd6e25734a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -79,7 +79,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private void createStateBindables() { - foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO)) + foreach (string bankName in HitSampleInfo.ALL_BANKS.Prepend(HIT_BANK_AUTO)) { var bindable = new Bindable { @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionBankStates[bankName] = bindable; } - foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO)) + foreach (string bankName in HitSampleInfo.ALL_BANKS.Prepend(HIT_BANK_AUTO)) { var bindable = new Bindable { @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components resetTernaryStates(); - foreach (string sampleName in HitSampleInfo.AllAdditions) + foreach (string sampleName in HitSampleInfo.ALL_ADDITIONS) { var bindable = new Bindable { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index c3a56c8df9..4ca3f93f13 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -409,7 +409,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void createStateBindables() { - foreach (string sampleName in HitSampleInfo.AllAdditions) + foreach (string sampleName in HitSampleInfo.ALL_ADDITIONS) { var bindable = new Bindable { @@ -433,7 +433,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline selectionSampleStates[sampleName] = bindable; } - banks.AddRange(HitSampleInfo.AllBanks.Prepend(EditorSelectionHandler.HIT_BANK_AUTO)); + banks.AddRange(HitSampleInfo.ALL_BANKS.Prepend(EditorSelectionHandler.HIT_BANK_AUTO)); } private void updateTernaryStates() diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 578e945c64..3825e280f1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -157,7 +157,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var sample in hitObject.Samples) { - if (!HitSampleInfo.AllBanks.Contains(sample.Bank)) + if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank)) minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3); } @@ -167,7 +167,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var sample in hasRepeats.NodeSamples.SelectMany(s => s)) { - if (!HitSampleInfo.AllBanks.Contains(sample.Bank)) + if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank)) minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3); } } From 791ca915e44c566789cfd77e4378ebfedfa30d6d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jan 2025 00:48:58 +0300 Subject: [PATCH 154/620] Fix allocations in updateSamplePointContractedState --- .../Timeline/TimelineBlueprintContainer.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 3825e280f1..2b5667ff9c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -155,8 +155,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2) break; - foreach (var sample in hitObject.Samples) + for (int i = 0; i < hitObject.Samples.Count; i++) { + var sample = hitObject.Samples[i]; + if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank)) minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3); } @@ -165,10 +167,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2); - foreach (var sample in hasRepeats.NodeSamples.SelectMany(s => s)) + for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) { - if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank)) - minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3); + var node = hasRepeats.NodeSamples[i]; + + for (int j = 0; j < node.Count; j++) + { + var sample = node[j]; + + if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank)) + minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3); + } } } From d35b308745bd9cdc2e5bf502705b2b7c4c8c72a8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jan 2025 01:23:19 +0300 Subject: [PATCH 155/620] Use cleaner array creation expression --- osu.Game/Audio/HitSampleInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index b6819a0f16..5a7c28d024 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -33,12 +33,12 @@ namespace osu.Game.Audio /// /// All valid sample addition constants. /// - public static readonly string[] ALL_ADDITIONS = new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP }; + public static readonly string[] ALL_ADDITIONS = [HIT_WHISTLE, HIT_FINISH, HIT_CLAP]; /// /// All valid bank constants. /// - public static readonly string[] ALL_BANKS = new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM }; + public static readonly string[] ALL_BANKS = [BANK_NORMAL, BANK_SOFT, BANK_DRUM]; /// /// The name of the sample to load. From 804fe0013d256ba64e3945b0c895103a5bad99ce Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Jan 2025 23:34:17 +0000 Subject: [PATCH 156/620] Make `ProgramId` public --- .../Windows/WindowsAssociationManager.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index 6f53c65ca9..0561c488d8 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -176,7 +176,7 @@ namespace osu.Desktop.Windows private record FileAssociation(string Extension, LocalisableString Description, string IconPath) { - private string programId => $@"{program_id_prefix}{Extension}"; + public string ProgramId => $@"{program_id_prefix}{Extension}"; /// /// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key @@ -187,7 +187,7 @@ namespace osu.Desktop.Windows if (classes == null) return; // register a program id for the given extension - using (var programKey = classes.CreateSubKey(programId)) + using (var programKey = classes.CreateSubKey(ProgramId)) { using (var defaultIconKey = programKey.CreateSubKey(default_icon)) defaultIconKey.SetValue(null, IconPath); @@ -199,12 +199,12 @@ namespace osu.Desktop.Windows using (var extensionKey = classes.CreateSubKey(Extension)) { // set ourselves as the default program - extensionKey.SetValue(null, programId); + extensionKey.SetValue(null, ProgramId); // add to the open with dialog // https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds")) - openWithKey.SetValue(programId, string.Empty); + openWithKey.SetValue(ProgramId, string.Empty); } } @@ -213,7 +213,7 @@ namespace osu.Desktop.Windows using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); if (classes == null) return; - using (var programKey = classes.OpenSubKey(programId, true)) + using (var programKey = classes.OpenSubKey(ProgramId, true)) programKey?.SetValue(null, description); } @@ -227,16 +227,16 @@ namespace osu.Desktop.Windows using (var extensionKey = classes.OpenSubKey(Extension, true)) { - // clear our default association so that Explorer doesn't show the raw programId to users + // clear our default association so that Explorer doesn't show the raw ProgramId to users // the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons - if (extensionKey?.GetValue(null) is string s && s == programId) + if (extensionKey?.GetValue(null) is string s && s == ProgramId) extensionKey.SetValue(null, string.Empty); using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds")) - openWithKey?.DeleteValue(programId, throwOnMissingValue: false); + openWithKey?.DeleteValue(ProgramId, throwOnMissingValue: false); } - classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false); + classes.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false); } } From 56eec929ca75bee95c33ae8c93bf7ab4d73d9398 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Jan 2025 23:41:44 +0000 Subject: [PATCH 157/620] Register application capability with file extensions https://learn.microsoft.com/en-us/windows/win32/shell/default-programs#registering-an-application-for-use-with-default-programs --- .../Windows/WindowsAssociationManager.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index 0561c488d8..b2ae39d837 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -17,6 +17,7 @@ namespace osu.Desktop.Windows public static class WindowsAssociationManager { private const string software_classes = @"Software\Classes"; + private const string software_registered_applications = @"Software\RegisteredApplications"; /// /// Sub key for setting the icon. @@ -38,6 +39,8 @@ namespace osu.Desktop.Windows /// private const string program_id_prefix = "osu.File"; + private static readonly ApplicationCapability application_capability = new ApplicationCapability(@"osu", @"Software\ppy\osu\Capabilities", "osu!(lazer)"); + private static readonly FileAssociation[] file_associations = { new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), @@ -112,6 +115,8 @@ namespace osu.Desktop.Windows { try { + application_capability.Uninstall(); + foreach (var association in file_associations) association.Uninstall(); @@ -133,15 +138,21 @@ namespace osu.Desktop.Windows /// private static void updateAssociations() { + application_capability.Install(); + foreach (var association in file_associations) association.Install(); foreach (var association in uri_associations) association.Install(); + + application_capability.RegisterFileAssociations(file_associations); } private static void updateDescriptions(LocalisationManager? localisation) { + application_capability.UpdateDescription(getLocalisedString(application_capability.Description)); + foreach (var association in file_associations) association.UpdateDescription(getLocalisedString(association.Description)); @@ -174,6 +185,51 @@ namespace osu.Desktop.Windows #endregion + private record ApplicationCapability(string UniqueName, string CapabilityPath, LocalisableString Description) + { + /// + /// Registers an application capability according to + /// Registering an Application for Use with Default Programs. + /// + public void Install() + { + using (Registry.CurrentUser.CreateSubKey(CapabilityPath)) + { + // create an empty "capability" key, other methods will fill it with information + } + + using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true)) + registeredApplications?.SetValue(UniqueName, CapabilityPath); + } + + public void RegisterFileAssociations(FileAssociation[] associations) + { + using var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true); + if (capability == null) return; + + using var fileAssociations = capability.CreateSubKey(@"FileAssociations"); + + foreach (var association in associations) + fileAssociations.SetValue(association.Extension, association.ProgramId); + } + + public void UpdateDescription(string description) + { + using (var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true)) + { + capability?.SetValue(@"ApplicationDescription", description); + } + } + + public void Uninstall() + { + using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true)) + registeredApplications?.DeleteValue(UniqueName, throwOnMissingValue: false); + + Registry.CurrentUser.DeleteSubKeyTree(CapabilityPath, throwOnMissingSubKey: false); + } + } + private record FileAssociation(string Extension, LocalisableString Description, string IconPath) { public string ProgramId => $@"{program_id_prefix}{Extension}"; From 64843a5e83aeee8abb745c6e91a641ed68dfccad Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Jan 2025 23:55:35 +0000 Subject: [PATCH 158/620] Clear out old way of specifying default association If we're the only app for a filetype, windows will automatically associate us. And if a new app is installed, it'll prompt the user to choose a default. --- osu.Desktop/Windows/WindowsAssociationManager.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index b2ae39d837..425468ef51 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -254,8 +254,10 @@ namespace osu.Desktop.Windows using (var extensionKey = classes.CreateSubKey(Extension)) { - // set ourselves as the default program - extensionKey.SetValue(null, ProgramId); + // Clear out our existing default ProgramID. Default programs in Windows are handled internally by Explorer, + // so having it here is just confusing and may override user preferences. + if (extensionKey.GetValue(null) is string s && s == ProgramId) + extensionKey.SetValue(null, string.Empty); // add to the open with dialog // https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box @@ -283,11 +285,6 @@ namespace osu.Desktop.Windows using (var extensionKey = classes.OpenSubKey(Extension, true)) { - // clear our default association so that Explorer doesn't show the raw ProgramId to users - // the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons - if (extensionKey?.GetValue(null) is string s && s == ProgramId) - extensionKey.SetValue(null, string.Empty); - using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds")) openWithKey?.DeleteValue(ProgramId, throwOnMissingValue: false); } From 31bf162db64b0f4602ab298b78e0991e61127248 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Jan 2025 23:59:52 +0000 Subject: [PATCH 159/620] Register URI handler as ProgID and add that to Capabilities --- .../Windows/WindowsAssociationManager.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index 425468ef51..af96067ec6 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -37,7 +37,9 @@ namespace osu.Desktop.Windows /// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit, /// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key. /// - private const string program_id_prefix = "osu.File"; + private const string program_id_file_prefix = "osu.File"; + + private const string program_id_protocol_prefix = "osu.Uri"; private static readonly ApplicationCapability application_capability = new ApplicationCapability(@"osu", @"Software\ppy\osu\Capabilities", "osu!(lazer)"); @@ -147,6 +149,7 @@ namespace osu.Desktop.Windows association.Install(); application_capability.RegisterFileAssociations(file_associations); + application_capability.RegisterUriAssociations(uri_associations); } private static void updateDescriptions(LocalisationManager? localisation) @@ -213,6 +216,17 @@ namespace osu.Desktop.Windows fileAssociations.SetValue(association.Extension, association.ProgramId); } + public void RegisterUriAssociations(UriAssociation[] associations) + { + using var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true); + if (capability == null) return; + + using var urlAssociations = capability.CreateSubKey(@"UrlAssociations"); + + foreach (var association in associations) + urlAssociations.SetValue(association.Protocol, association.ProgramId); + } + public void UpdateDescription(string description) { using (var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true)) @@ -232,7 +246,7 @@ namespace osu.Desktop.Windows private record FileAssociation(string Extension, LocalisableString Description, string IconPath) { - public string ProgramId => $@"{program_id_prefix}{Extension}"; + public string ProgramId => $@"{program_id_file_prefix}{Extension}"; /// /// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key @@ -301,6 +315,8 @@ namespace osu.Desktop.Windows /// public const string URL_PROTOCOL = @"URL Protocol"; + public string ProgramId => $@"{program_id_protocol_prefix}.{Protocol}"; + /// /// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85). /// @@ -319,6 +335,16 @@ namespace osu.Desktop.Windows using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND)) openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); } + + // register a program id for the given protocol + using (var programKey = classes.CreateSubKey(ProgramId)) + { + using (var defaultIconKey = programKey.CreateSubKey(default_icon)) + defaultIconKey.SetValue(null, IconPath); + + using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND)) + openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); + } } public void UpdateDescription(string description) @@ -333,6 +359,7 @@ namespace osu.Desktop.Windows public void Uninstall() { using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); + classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false); classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false); } } From 238197535918091b7f109f0b6aa97e4687d07269 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 7 Jan 2025 00:07:04 +0000 Subject: [PATCH 160/620] Clear out old protocol data when installing If we're the only capable app, windows will open us by default. --- osu.Desktop/Windows/WindowsAssociationManager.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index af96067ec6..a0d96c7bb4 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -329,11 +329,9 @@ namespace osu.Desktop.Windows { protocolKey.SetValue(URL_PROTOCOL, string.Empty); - using (var defaultIconKey = protocolKey.CreateSubKey(default_icon)) - defaultIconKey.SetValue(null, IconPath); - - using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND)) - openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); + // clear out old data + protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false); + protocolKey.DeleteSubKeyTree(@"Shell", throwOnMissingSubKey: false); } // register a program id for the given protocol @@ -360,7 +358,6 @@ namespace osu.Desktop.Windows { using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false); - classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false); } } } From 1648f2efa306f587714178f113e69d8ad8c4ac02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jan 2025 16:38:22 +0900 Subject: [PATCH 161/620] Ensure slider is not selectable when body is not visible --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 3504954bec..740862c9fd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - if (BodyPiece.ReceivePositionalInputAt(screenSpacePos)) + if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0) return true; if (ControlPointVisualiser == null) From a0496c60a47f9a8bfcfdc80905e36f6f163c2dad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jan 2025 02:49:06 +0900 Subject: [PATCH 162/620] Refactor `StarRatingRangeDisplay` test to be more usable --- .../TestSceneStarRatingRangeDisplay.cs | 72 +++++++++++++++---- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs index 88afef7de2..ecdbfc411a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs @@ -3,29 +3,71 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Resources; +using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public partial class TestSceneStarRatingRangeDisplay : OnlinePlayTestScene + public partial class TestSceneStarRatingRangeDisplay : OsuTestScene { - public override void SetUpSteps() + private readonly Room room = new Room(); + + protected override void LoadComplete() { - base.SetUpSteps(); + base.LoadComplete(); - AddStep("create display", () => + Child = new FillFlowContainer { - SelectedRoom.Value = new Room(); - - Child = new StarRatingRangeDisplay(SelectedRoom.Value) + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }; - }); + new StarRatingRangeDisplay(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5), + }, + new StarRatingRangeDisplay(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2), + }, + new StarRatingRangeDisplay(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1), + }, + new StarRatingRangeDisplay(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.2f, + Scale = new Vector2(5), + }, + new StarRatingRangeDisplay(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.2f, + Scale = new Vector2(2), + }, + new StarRatingRangeDisplay(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.2f, + Scale = new Vector2(1), + }, + } + }; } [Test] @@ -33,10 +75,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ - new PlaylistItem(new BeatmapInfo { StarRating = min }), - new PlaylistItem(new BeatmapInfo { StarRating = max }), + new PlaylistItem(new BeatmapInfo { StarRating = min }) { ID = TestResources.GetNextTestID() }, + new PlaylistItem(new BeatmapInfo { StarRating = max }) { ID = TestResources.GetNextTestID() }, ]; }); } From 383fda7431df206e3f3c518d2f99a5d2becb3bc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jan 2025 02:48:53 +0900 Subject: [PATCH 163/620] Fix star range display looking a bit bad when changing opacity --- .../Components/StarRatingRangeDisplay.cs | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index 2bdb41ce12..e2aecb6781 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -14,7 +14,6 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Online.Rooms; using osuTK; -using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Components { @@ -30,6 +29,8 @@ namespace osu.Game.Screens.OnlinePlay.Components private StarRatingDisplay maxDisplay = null!; private Drawable maxBackground = null!; + private BufferedContainer bufferedContent = null!; + public StarRatingRangeDisplay(Room room) { this.room = room; @@ -41,38 +42,43 @@ namespace osu.Game.Screens.OnlinePlay.Components { InternalChildren = new Drawable[] { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 1, - Children = new[] - { - minBackground = new Box - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - }, - maxBackground = new Box - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - }, - } - }, - new FillFlowContainer + new CircularContainer { AutoSizeAxes = Axes.Both, - Children = new Drawable[] + Masking = true, + // Stops artifacting from boxes drawn behind wrong colour boxes (and edge pixels adding up to higher opacity). + Padding = new MarginPadding(-0.1f), + Child = bufferedContent = new BufferedContainer(pixelSnapping: true, cachedFrameBuffer: true) { - minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range), - maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range) + AutoSizeAxes = Axes.Both, + Children = new[] + { + minBackground = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.5f), + }, + maxBackground = new Box + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.5f), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range), + maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range) + } + } + } } - } + }, }; } @@ -121,6 +127,8 @@ namespace osu.Game.Screens.OnlinePlay.Components minBackground.Colour = colours.ForStarDifficulty(minDifficulty.Stars); maxBackground.Colour = colours.ForStarDifficulty(maxDifficulty.Stars); + + bufferedContent.ForceRedraw(); } protected override void Dispose(bool isDisposing) From 8d913e8971ab827a0d47a434f1ded439d6251c36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jan 2025 16:54:11 +0900 Subject: [PATCH 164/620] Fix multiple animation inconsistencies pointed out in review --- .../Skinning/Argon/ArgonReverseArrow.cs | 4 ++-- .../Skinning/Legacy/LegacyReverseArrow.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index 9f15e8e177..1fbdbafec4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -104,9 +104,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out)); if (loopCurrentTime < move_out_duration) - side.X = Interpolation.ValueAt(loopCurrentTime, 1, move_distance, 0, move_out_duration, Easing.Out); + side.X = Interpolation.ValueAt(loopCurrentTime, 0, move_distance, 0, move_out_duration, Easing.Out); else - side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out); + side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 0, move_out_duration, move_out_duration + move_in_duration, Easing.Out); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index 940e068da0..85c895006b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -96,9 +96,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration; + // Reference: https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameplayElements/HitObjects/Osu/HitCircleSliderEnd.cs#L79-L96 if (shouldRotate) + { arrow.Rotation = Interpolation.ValueAt(loopCurrentTime, rotation, -rotation, 0, duration); - arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration)); + arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration)); + } + else + { + arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration, Easing.Out)); + } } } From b8a10d9b0e82f6da2db182f53321531ab3d1ae54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jan 2025 17:57:09 +0900 Subject: [PATCH 165/620] Mark recommendation test as flaky Will revisit during song select refactoring no doubt. --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index aa452101bf..5c89e8a02c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -12,7 +12,6 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -85,6 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestPresentedBeatmapIsRecommended() { List beatmapSets = null; @@ -106,6 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestCurrentRulesetIsRecommended() { BeatmapSetInfo catchSet = null, mixedSet = null; @@ -142,6 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestSecondBestRulesetIsRecommended() { BeatmapSetInfo osuSet = null, mixedSet = null; @@ -159,6 +161,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestCorrectStarRatingIsUsed() { BeatmapSetInfo osuSet = null, maniaSet = null; @@ -176,6 +179,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestBeatmapListingFilter() { AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko"); @@ -245,7 +249,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); - AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1])); + AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(getImport().Beatmaps[expectedDiff - 1].OnlineID)); } protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API); From 51b62a6d8e6877131542d2869f91158c000dcb50 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Jan 2025 19:12:31 +0900 Subject: [PATCH 166/620] Display notification on friend presence changes --- .../TestSceneFriendPresenceNotifier.cs | 129 +++++++++++++++ osu.Game/Online/API/APIAccess.cs | 9 ++ osu.Game/Online/API/DummyAPIAccess.cs | 3 + osu.Game/Online/API/IAPIProvider.cs | 7 + osu.Game/Online/FriendPresenceNotifier.cs | 148 ++++++++++++++++++ osu.Game/OsuGame.cs | 1 + .../Visual/Metadata/TestMetadataClient.cs | 3 +- 7 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs create mode 100644 osu.Game/Online/FriendPresenceNotifier.cs diff --git a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs new file mode 100644 index 0000000000..851c1141db --- /dev/null +++ b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs @@ -0,0 +1,129 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Online.Metadata; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Tests.Visual.Metadata; +using osu.Game.Users; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Components +{ + public partial class TestSceneFriendPresenceNotifier : OsuManualInputManagerTestScene + { + private ChannelManager channelManager = null!; + private NotificationOverlay notificationOverlay = null!; + private ChatOverlay chatOverlay = null!; + private TestMetadataClient metadataClient = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(ChannelManager), channelManager = new ChannelManager(API)), + (typeof(INotificationOverlay), notificationOverlay = new NotificationOverlay()), + (typeof(ChatOverlay), chatOverlay = new ChatOverlay()), + (typeof(MetadataClient), metadataClient = new TestMetadataClient()), + ], + Children = new Drawable[] + { + channelManager, + notificationOverlay, + chatOverlay, + metadataClient, + new FriendPresenceNotifier() + } + }; + + for (int i = 1; i <= 100; i++) + ((DummyAPIAccess)API).Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } }); + }); + + [Test] + public void TestNotifications() + { + AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + AddStep("bring friend 1 offline", () => metadataClient.UserPresenceUpdated(1, null)); + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestSingleUserNotificationOpensChat() + { + AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + + AddStep("click notification", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("chat overlay opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("user channel selected", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo(((DummyAPIAccess)API).Friends[0].TargetUser!.Username)); + } + + [Test] + public void TestMultipleUserNotificationDoesNotOpenChat() + { + AddStep("bring friends 1 & 2 online", () => + { + metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }); + metadataClient.UserPresenceUpdated(2, new UserPresence { Status = UserStatus.Online }); + }); + + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + + AddStep("click notification", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("chat overlay not opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden)); + } + + [Test] + public void TestNonFriendsDoNotNotify() + { + AddStep("bring non-friend 1000 online", () => metadataClient.UserPresenceUpdated(1000, new UserPresence { Status = UserStatus.Online })); + AddWaitStep("wait for possible notification", 10); + AddAssert("no notification", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + } + + [Test] + public void TestPostManyDebounced() + { + AddStep("bring friends 1-10 online", () => + { + for (int i = 1; i <= 10; i++) + metadataClient.UserPresenceUpdated(i, new UserPresence { Status = UserStatus.Online }); + }); + + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + + AddStep("bring friends 1-10 offline", () => + { + for (int i = 1; i <= 10; i++) + metadataClient.UserPresenceUpdated(i, null); + }); + + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2)); + } + } +} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ec48fa2436..39c09f2a5d 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -75,6 +75,7 @@ namespace osu.Game.Online.API protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); + private readonly Dictionary friendsMapping = new Dictionary(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; @@ -403,6 +404,8 @@ namespace osu.Game.Online.API public IChatClient GetChatClient() => new WebSocketChatClient(this); + public APIRelation GetFriend(int userId) => friendsMapping.GetValueOrDefault(userId); + public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { Debug.Assert(State.Value == APIState.Offline); @@ -594,6 +597,8 @@ namespace osu.Game.Online.API Schedule(() => { setLocalUser(createGuestUser()); + + friendsMapping.Clear(); friends.Clear(); }); @@ -610,7 +615,11 @@ namespace osu.Game.Online.API friendsReq.Failure += _ => state.Value = APIState.Failing; friendsReq.Success += res => { + friendsMapping.Clear(); friends.Clear(); + + foreach (var u in res) + friendsMapping[u.TargetID] = u; friends.AddRange(res); }; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 5d63c04925..ca4edb3d8f 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; @@ -194,6 +195,8 @@ namespace osu.Game.Online.API public IChatClient GetChatClient() => new TestChatClientConnector(this); + public APIRelation? GetFriend(int userId) => Friends.FirstOrDefault(r => r.TargetID == userId); + public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password) { Thread.Sleep(200); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 1c4b2da742..4655b26f84 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -152,6 +152,13 @@ namespace osu.Game.Online.API /// IChatClient GetChatClient(); + /// + /// Retrieves a friend from a given user ID. + /// + /// The friend's user ID. + /// The object representing the friend, if any. + APIRelation? GetFriend(int userId); + /// /// Create a new user account. This is a blocking operation. /// diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs new file mode 100644 index 0000000000..8fcf1a9f69 --- /dev/null +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -0,0 +1,148 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Online.Metadata; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Users; + +namespace osu.Game.Online +{ + public partial class FriendPresenceNotifier : Component + { + [Resolved] + private INotificationOverlay notifications { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + [Resolved] + private ChannelManager channelManager { get; set; } = null!; + + [Resolved] + private ChatOverlay chatOverlay { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + private readonly IBindableDictionary userStates = new BindableDictionary(); + private readonly HashSet onlineAlertQueue = new HashSet(); + private readonly HashSet offlineAlertQueue = new HashSet(); + + private double? lastOnlineAlertTime; + private double? lastOfflineAlertTime; + + protected override void LoadComplete() + { + base.LoadComplete(); + + userStates.BindTo(metadataClient.UserStates); + userStates.BindCollectionChanged((_, args) => + { + switch (args.Action) + { + case NotifyDictionaryChangedAction.Add: + foreach ((int userId, var _) in args.NewItems!) + { + if (api.GetFriend(userId)?.TargetUser is APIUser user) + { + if (!offlineAlertQueue.Remove(user)) + { + onlineAlertQueue.Add(user); + lastOnlineAlertTime ??= Time.Current; + } + } + } + + break; + + case NotifyDictionaryChangedAction.Remove: + foreach ((int userId, var _) in args.OldItems!) + { + if (api.GetFriend(userId)?.TargetUser is APIUser user) + { + if (!onlineAlertQueue.Remove(user)) + { + offlineAlertQueue.Add(user); + lastOfflineAlertTime ??= Time.Current; + } + } + } + + break; + } + }); + } + + protected override void Update() + { + base.Update(); + + alertOnlineUsers(); + alertOfflineUsers(); + } + + private void alertOnlineUsers() + { + if (onlineAlertQueue.Count == 0) + return; + + if (lastOnlineAlertTime == null || Time.Current - lastOnlineAlertTime < 1000) + return; + + APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; + + notifications.Post(new SimpleNotification + { + Icon = FontAwesome.Solid.UserPlus, + Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", + IconColour = colours.Green, + Activated = () => + { + if (singleUser != null) + { + channelManager.OpenPrivateChannel(singleUser); + chatOverlay.Show(); + } + + return true; + } + }); + + onlineAlertQueue.Clear(); + lastOnlineAlertTime = null; + } + + private void alertOfflineUsers() + { + if (offlineAlertQueue.Count == 0) + return; + + if (lastOfflineAlertTime == null || Time.Current - lastOfflineAlertTime < 1000) + return; + + notifications.Post(new SimpleNotification + { + Icon = FontAwesome.Solid.UserMinus, + Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", + IconColour = colours.Red + }); + + offlineAlertQueue.Clear(); + lastOfflineAlertTime = null; + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c20536a1ec..329ac89a6c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1151,6 +1151,7 @@ namespace osu.Game Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); + Add(new FriendPresenceNotifier()); // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay }; diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 4a862750bc..6dd6392b3a 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -66,7 +67,7 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UserPresenceUpdated(int userId, UserPresence? presence) { - if (isWatchingUserPresence.Value) + if (isWatchingUserPresence.Value || api.Friends.Any(f => f.TargetID == userId)) { if (presence.HasValue) userStates[userId] = presence.Value; From 3c03406b45f2c2e707eab5a1a61e7ab1fa4f4815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 11:23:47 +0100 Subject: [PATCH 167/620] Add failing test --- .../Editing/TestSceneEditorTestGameplay.cs | 30 +++++++++++++++++++ .../Edit/Components/PlaybackControl.cs | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 765ffb4549..04dae38668 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Play; @@ -127,6 +128,35 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value); } + [Test] + public void TestGameplayTestResetsPlaybackSpeedAdjustment() + { + AddStep("start track", () => EditorClock.Start()); + AddStep("change playback speed", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("track playback rate is 0.25x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25)); + + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + AddAssert("editor track stopped", () => !EditorClock.IsRunning); + AddAssert("track playback rate is 1x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(1)); + + AddStep("exit player", () => editorPlayer.Exit()); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + AddAssert("track playback rate is 0.25x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25)); + } + [TestCase(2000)] // chosen to be after last object in the map [TestCase(22000)] // chosen to be in the middle of the last spinner public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 9fe6160ab4..6e624fe69b 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -148,7 +148,7 @@ namespace osu.Game.Screens.Edit.Components public LocalisableString TooltipText { get; set; } } - private partial class PlaybackTabControl : OsuTabControl + public partial class PlaybackTabControl : OsuTabControl { private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 }; From a5036cd092b0bb020982c6606d2ed110de25f387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 11:25:00 +0100 Subject: [PATCH 168/620] Re-route editor tempo adjustment via `EditorClock` and remove it on gameplay test --- .../Screens/Edit/Components/PlaybackControl.cs | 6 ++++-- osu.Game/Screens/Edit/Editor.cs | 5 +++++ osu.Game/Screens/Edit/EditorClock.cs | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 6e624fe69b..01d777cdc6 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -75,7 +76,7 @@ namespace osu.Game.Screens.Edit.Components } }; - Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment), true); + editorClock.AudioAdjustments.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment); if (editor != null) currentScreenMode.BindTo(editor.Mode); @@ -105,7 +106,8 @@ namespace osu.Game.Screens.Edit.Components protected override void Dispose(bool isDisposing) { - Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempoAdjustment); + if (editorClock.IsNotNull()) + editorClock.AudioAdjustments.RemoveAdjustment(AdjustableProperty.Tempo, tempoAdjustment); base.Dispose(isDisposing); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index f6875a7aa4..a77696bc45 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -861,6 +861,7 @@ namespace osu.Game.Screens.Edit { base.OnResuming(e); dimBackground(); + clock.BindAdjustments(); } private void dimBackground() @@ -925,6 +926,10 @@ namespace osu.Game.Screens.Edit base.OnSuspending(e); clock.Stop(); refetchBeatmap(); + // unfortunately ordering matters here. + // this unbind MUST happen after `refetchBeatmap()`, because along other things, `refetchBeatmap()` causes a global working beatmap change, + // which causes `EditorClock` to reload the track and automatically reapply adjustments to it. + clock.UnbindAdjustments(); } private void refetchBeatmap() diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 5b9c662c95..7214854b52 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Linq; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -29,6 +30,8 @@ namespace osu.Game.Screens.Edit public double TrackLength => track.Value?.IsLoaded == true ? track.Value.Length : 60000; + public AudioAdjustments AudioAdjustments { get; } = new AudioAdjustments(); + public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo; public IBeatmap Beatmap { get; set; } @@ -208,7 +211,16 @@ namespace osu.Game.Screens.Edit } } - public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments(); + public void BindAdjustments() => track.Value?.BindAdjustments(AudioAdjustments); + + public void UnbindAdjustments() => track.Value?.UnbindAdjustments(AudioAdjustments); + + public void ResetSpeedAdjustments() + { + AudioAdjustments.RemoveAllAdjustments(AdjustableProperty.Frequency); + AudioAdjustments.RemoveAllAdjustments(AdjustableProperty.Tempo); + underlyingClock.ResetSpeedAdjustments(); + } double IAdjustableClock.Rate { @@ -231,8 +243,12 @@ namespace osu.Game.Screens.Edit public void ChangeSource(IClock source) { + UnbindAdjustments(); + track.Value = source as Track; underlyingClock.ChangeSource(source); + + BindAdjustments(); } public IClock Source => underlyingClock.Source; From 275e8ce7b79d03173b018d86e99bcbd656891dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 11:26:08 +0100 Subject: [PATCH 169/620] Remove unused protected field --- osu.Game/Screens/Edit/Components/BottomBarContainer.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index da71457004..37337bc79f 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components protected readonly IBindable Beatmap = new Bindable(); - protected readonly IBindable Track = new Bindable(); - public readonly Drawable Background; private readonly Container content; @@ -45,10 +42,9 @@ namespace osu.Game.Screens.Edit.Components } [BackgroundDependencyLoader] - private void load(IBindable beatmap, EditorClock clock) + private void load(IBindable beatmap) { Beatmap.BindTo(beatmap); - Track.BindTo(clock.Track); } } } From 45e0adcd253f1dfa922723c502dab365b76f51cd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Jan 2025 19:32:30 +0900 Subject: [PATCH 170/620] Add config option --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../Localisation/OnlineSettingsStrings.cs | 12 +++++++++++- osu.Game/Online/FriendPresenceNotifier.cs | 19 +++++++++++++++++++ .../Online/AlertsAndPrivacySettings.cs | 6 ++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index dd3abb6f81..3c463f6f0c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -96,6 +96,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.NotifyOnUsernameMentioned, true); SetDefault(OsuSetting.NotifyOnPrivateMessage, true); + SetDefault(OsuSetting.NotifyOnFriendPresenceChange, true); // Audio SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); @@ -417,6 +418,7 @@ namespace osu.Game.Configuration IntroSequence, NotifyOnUsernameMentioned, NotifyOnPrivateMessage, + NotifyOnFriendPresenceChange, UIHoldActivationDelay, HitLighting, StarFountains, diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index 8e8c81cf59..98364a3f5a 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -29,6 +29,16 @@ namespace osu.Game.Localisation /// public static LocalisableString NotifyOnPrivateMessage => new TranslatableString(getKey(@"notify_on_private_message"), @"Show a notification when you receive a private message"); + /// + /// "Show notification popups when friends change status" + /// + public static LocalisableString NotifyOnFriendPresenceChange => new TranslatableString(getKey(@"notify_on_friend_presence_change"), @"Show notification popups when friends change status"); + + /// + /// "Notifications will be shown when friends go online/offline." + /// + public static LocalisableString NotifyOnFriendPresenceChangeTooltip => new TranslatableString(getKey(@"notify_on_friend_presence_change_tooltip"), @"Notifications will be shown when friends go online/offline."); + /// /// "Integrations" /// @@ -84,6 +94,6 @@ namespace osu.Game.Localisation /// public static LocalisableString HideCountryFlags => new TranslatableString(getKey(@"hide_country_flags"), @"Hide country flags"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 8fcf1a9f69..655a004d3e 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -38,6 +39,10 @@ namespace osu.Game.Online [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + private readonly Bindable notifyOnFriendPresenceChange = new BindableBool(); private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly HashSet onlineAlertQueue = new HashSet(); private readonly HashSet offlineAlertQueue = new HashSet(); @@ -49,6 +54,8 @@ namespace osu.Game.Online { base.LoadComplete(); + config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange); + userStates.BindTo(metadataClient.UserStates); userStates.BindCollectionChanged((_, args) => { @@ -103,6 +110,12 @@ namespace osu.Game.Online if (lastOnlineAlertTime == null || Time.Current - lastOnlineAlertTime < 1000) return; + if (!notifyOnFriendPresenceChange.Value) + { + lastOnlineAlertTime = null; + return; + } + APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; notifications.Post(new SimpleNotification @@ -134,6 +147,12 @@ namespace osu.Game.Online if (lastOfflineAlertTime == null || Time.Current - lastOfflineAlertTime < 1000) return; + if (!notifyOnFriendPresenceChange.Value) + { + lastOfflineAlertTime = null; + return; + } + notifications.Post(new SimpleNotification { Icon = FontAwesome.Solid.UserMinus, diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index 7bd0829add..608c6ef1b2 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -29,6 +29,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online Current = config.GetBindable(OsuSetting.NotifyOnPrivateMessage) }, new SettingsCheckbox + { + LabelText = OnlineSettingsStrings.NotifyOnFriendPresenceChange, + TooltipText = OnlineSettingsStrings.NotifyOnFriendPresenceChangeTooltip, + Current = config.GetBindable(OsuSetting.NotifyOnFriendPresenceChange), + }, + new SettingsCheckbox { LabelText = OnlineSettingsStrings.HideCountryFlags, Current = config.GetBindable(OsuSetting.HideCountryFlags) From 98bb723438c0ce37311451e52529e86f2386777a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 11:37:06 +0100 Subject: [PATCH 171/620] Do not expose track directly in `EditorClock` Intends to stop people from mutating it directly, and going through `EditorClock` members like `AudioAdjustments` instead. --- .../Timelines/Summary/Parts/TimelinePart.cs | 26 +++++++++------- .../Compose/Components/Timeline/Timeline.cs | 31 +++++++++++++------ osu.Game/Screens/Edit/EditorClock.cs | 6 +++- .../Edit/Timing/WaveformComparisonDisplay.cs | 24 ++++++++++---- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index ee7e759ebc..bec9e275cb 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -3,8 +3,8 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -26,7 +26,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts [Resolved] protected EditorBeatmap EditorBeatmap { get; private set; } = null!; - protected readonly IBindable Track = new Bindable(); + [Resolved] + private EditorClock editorClock { get; set; } = null!; private readonly Container content; @@ -35,22 +36,17 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts public TimelinePart(Container? content = null) { AddInternal(this.content = content ?? new Container { RelativeSizeAxes = Axes.Both }); - - beatmap.ValueChanged += _ => - { - updateRelativeChildSize(); - }; - - Track.ValueChanged += _ => updateRelativeChildSize(); } [BackgroundDependencyLoader] - private void load(IBindable beatmap, EditorClock clock) + private void load(IBindable beatmap) { this.beatmap.BindTo(beatmap); LoadBeatmap(EditorBeatmap); - Track.BindTo(clock.Track); + this.beatmap.ValueChanged += _ => updateRelativeChildSize(); + editorClock.TrackChanged += updateRelativeChildSize; + updateRelativeChildSize(); } private void updateRelativeChildSize() @@ -68,5 +64,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { content.Clear(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorClock.IsNotNull()) + editorClock.TrackChanged -= updateRelativeChildSize; + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 66621afa21..e5360e2eeb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -3,9 +3,9 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; @@ -49,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; + [Resolved] + private IBindable beatmap { get; set; } = null!; + /// /// The timeline's scroll position in the last frame. /// @@ -86,8 +89,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private double trackLengthForZoom; - private readonly IBindable track = new Bindable(); - public Timeline(Drawable userContent) { this.userContent = userContent; @@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config) + private void load(OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config) { CentreMarker centreMarker; @@ -150,16 +151,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline controlPointsVisible = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); ticksVisible = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - track.BindTo(editorClock.Track); - track.BindValueChanged(_ => - { - waveform.Waveform = beatmap.Value.Waveform; - Scheduler.AddOnce(applyVisualOffset, beatmap); - }, true); + editorClock.TrackChanged += updateWaveform; + updateWaveform(); Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom); } + private void updateWaveform() + { + waveform.Waveform = beatmap.Value.Waveform; + Scheduler.AddOnce(applyVisualOffset, beatmap); + } + private void applyVisualOffset(IBindable beatmap) { waveform.RelativePositionAxes = Axes.X; @@ -334,5 +337,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorClock.IsNotNull()) + editorClock.TrackChanged -= updateWaveform; + } } } diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 7214854b52..8b9bdb595d 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -24,7 +25,8 @@ namespace osu.Game.Screens.Edit /// public partial class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { - public IBindable Track => track; + [CanBeNull] + public event Action TrackChanged; private readonly Bindable track = new Bindable(); @@ -59,6 +61,8 @@ namespace osu.Game.Screens.Edit underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true); AddInternal(underlyingClock); + + track.BindValueChanged(_ => TrackChanged?.Invoke()); } /// diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index 45213b7bdb..2df2dd7c5b 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -4,8 +4,8 @@ using System; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; @@ -305,7 +305,8 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private IBindable beatmap { get; set; } = null!; - private readonly IBindable track = new Bindable(); + [Resolved] + private EditorClock editorClock { get; set; } = null!; public WaveformRow(bool isMainRow) { @@ -313,7 +314,7 @@ namespace osu.Game.Screens.Edit.Timing } [BackgroundDependencyLoader] - private void load(EditorClock clock) + private void load() { InternalChildren = new Drawable[] { @@ -343,13 +344,16 @@ namespace osu.Game.Screens.Edit.Timing Colour = colourProvider.Content2 } }; - - track.BindTo(clock.Track); } protected override void LoadComplete() { - track.ValueChanged += _ => waveformGraph.Waveform = beatmap.Value.Waveform; + editorClock.TrackChanged += updateWaveform; + } + + private void updateWaveform() + { + waveformGraph.Waveform = beatmap.Value.Waveform; } public int BeatIndex { set => beatIndexText.Text = value.ToString(); } @@ -363,6 +367,14 @@ namespace osu.Game.Screens.Edit.Timing get => waveformGraph.X; set => waveformGraph.X = value; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorClock.IsNotNull()) + editorClock.TrackChanged -= updateWaveform; + } } } } From 8f4eafea4eab7a1a2e7d4b3571732477509ba0cf Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jan 2025 14:00:31 +0300 Subject: [PATCH 172/620] Fix combo properties multiple reassignments --- .../Objects/CatchHitObject.cs | 36 ++++++++++--------- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 36 ++++++++++--------- .../Objects/Types/IHasComboInformation.cs | 16 +++++---- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 2018fd5ea9..3c7ead09af 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -159,27 +159,29 @@ namespace osu.Game.Rulesets.Catch.Objects { // Note that this implementation is shared with the osu! ruleset's implementation. // If a change is made here, OsuHitObject.cs should also be updated. - ComboIndex = lastObj?.ComboIndex ?? 0; - ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; - IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + int index = lastObj?.ComboIndex ?? 0; + int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; - if (this is BananaShower) + // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + if (this is not BananaShower) { - // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. - return; + // At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (NewCombo || lastObj == null || lastObj is BananaShower) + { + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; + + if (lastObj != null) + lastObj.LastInCombo = true; + } } - // At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, - // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. - if (NewCombo || lastObj == null || lastObj is BananaShower) - { - IndexInCurrentCombo = 0; - ComboIndex++; - ComboIndexWithOffsets += ComboOffset + 1; - - if (lastObj != null) - lastObj.LastInCombo = true; - } + ComboIndex = index; + ComboIndexWithOffsets = indexWithOffsets; + IndexInCurrentCombo = inCurrentCombo; } protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 8c1bd6302e..937e0bda23 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -184,27 +184,29 @@ namespace osu.Game.Rulesets.Osu.Objects { // Note that this implementation is shared with the osu!catch ruleset's implementation. // If a change is made here, CatchHitObject.cs should also be updated. - ComboIndex = lastObj?.ComboIndex ?? 0; - ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; - IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + int index = lastObj?.ComboIndex ?? 0; + int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; - if (this is Spinner) + // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + if (this is not Spinner) { - // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. - return; + // At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (NewCombo || lastObj == null || lastObj is Spinner) + { + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; + + if (lastObj != null) + lastObj.LastInCombo = true; + } } - // At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, - // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. - if (NewCombo || lastObj == null || lastObj is Spinner) - { - IndexInCurrentCombo = 0; - ComboIndex++; - ComboIndexWithOffsets += ComboOffset + 1; - - if (lastObj != null) - lastObj.LastInCombo = true; - } + ComboIndex = index; + ComboIndexWithOffsets = indexWithOffsets; + IndexInCurrentCombo = inCurrentCombo; } protected override HitWindows CreateHitWindows() => new OsuHitWindows(); diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 3aa68197ec..98519de981 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -84,19 +84,23 @@ namespace osu.Game.Rulesets.Objects.Types /// The previous hitobject, or null if this is the first object in the beatmap. void UpdateComboInformation(IHasComboInformation? lastObj) { - ComboIndex = lastObj?.ComboIndex ?? 0; - ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; - IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + int index = lastObj?.ComboIndex ?? 0; + int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; if (NewCombo || lastObj == null) { - IndexInCurrentCombo = 0; - ComboIndex++; - ComboIndexWithOffsets += ComboOffset + 1; + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; if (lastObj != null) lastObj.LastInCombo = true; } + + ComboIndex = index; + ComboIndexWithOffsets = indexWithOffsets; + IndexInCurrentCombo = inCurrentCombo; } } } From 973f606a9e48fb5d43cbbff03af514ca8a48766a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 13:59:26 +0100 Subject: [PATCH 173/620] Add test coverage for expected behaviour --- .../TestSceneEditorBeatmapProcessor.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index bbcf6aac2c..1df8f96f93 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -539,5 +539,78 @@ namespace osu.Game.Tests.Editing Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX)); }); } + + [Test] + public void TestPuttingObjectBetweenBreakEndAndAnotherObjectForcesNewCombo() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new EditorBeatmap(new Beatmap + { + ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, + Difficulty = + { + ApproachRate = 10, + }, + HitObjects = + { + new HitCircle { StartTime = 1000, NewCombo = true }, + new HitCircle { StartTime = 4500 }, + new HitCircle { StartTime = 5000, NewCombo = true }, + }, + Breaks = + { + new BreakPeriod(2000, 4000), + } + }); + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True); + Assert.That(((HitCircle)beatmap.HitObjects[2]).NewCombo, Is.True); + }); + } + + [Test] + public void TestAutomaticallyInsertedBreakForcesNewCombo() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new EditorBeatmap(new Beatmap + { + ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, + Difficulty = + { + ApproachRate = 10, + }, + HitObjects = + { + new HitCircle { StartTime = 1000, NewCombo = true }, + new HitCircle { StartTime = 5000 }, + }, + }); + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True); + }); + } } } From c93b87583ac33bc9dc0bd8efc05ebc8f683fea70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 13:59:53 +0100 Subject: [PATCH 174/620] Force new combo on objects succeeding a break No issue thread for this again. Reported internally on discord: https://discord.com/channels/90072389919997952/1259818301517725707/1320420768814727229 Placing this logic in the beatmap processor, as a post-processing step, means that the new combo force won't be visible until a placement has been committed. That can be seen as subpar, but I tried putting this logic in the placement and it sucked anyway: - While the combo number was correct, the colour looked off, because it would use the same combo colour as the already-placed objects after said break, which would only cycle to the next, correct one on placement - Not all scenarios can be handled in the placement. Refer to one of the test cases added in the preceding commit, wherein two objects are placed far apart from each other, and an automated break is inserted between them - the placement has no practical way of knowing whether it's going to have a break inserted automatically before it or not. --- .../Screens/Edit/EditorBeatmapProcessor.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 4fe431498f..8108f51ad1 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit rulesetBeatmapProcessor?.PostProcess(); autoGenerateBreaks(); + ensureNewComboAfterBreaks(); } private void autoGenerateBreaks() @@ -100,5 +101,31 @@ namespace osu.Game.Screens.Edit Beatmap.Breaks.Add(breakPeriod); } } + + private void ensureNewComboAfterBreaks() + { + var breakEnds = Beatmap.Breaks.Select(b => b.EndTime).OrderBy(t => t).ToList(); + + if (breakEnds.Count == 0) + return; + + int currentBreak = 0; + + for (int i = 0; i < Beatmap.HitObjects.Count; ++i) + { + var hitObject = Beatmap.HitObjects[i]; + + if (hitObject is not IHasComboInformation hasCombo) + continue; + + if (currentBreak < breakEnds.Count && hitObject.StartTime >= breakEnds[currentBreak]) + { + hasCombo.NewCombo = true; + currentBreak += 1; + } + + hasCombo.UpdateComboInformation(i > 0 ? Beatmap.HitObjects[i - 1] as IHasComboInformation : null); + } + } } } From 125d652dd82b9baa69c55f4b9234a03270d51769 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jan 2025 01:35:56 +0900 Subject: [PATCH 175/620] Update realm xmldoc references --- osu.Game/Database/RealmObjectExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index df725505fc..538ac1dff7 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -266,7 +266,7 @@ namespace osu.Game.Database /// /// If a write transaction did not modify any objects in this , the callback is not invoked at all. /// If an error occurs the callback will be invoked with null for the sender parameter and a non-null error. - /// Currently the only errors that can occur are when opening the on the background worker thread. + /// Currently, the only errors that can occur are when opening the on the background worker thread. /// /// /// At the time when the block is called, the object will be fully evaluated @@ -285,8 +285,8 @@ namespace osu.Game.Database /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// - /// - /// + /// + /// #pragma warning restore RS0030 public static IDisposable QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase From 6f42b59e31628eb6e3d384d3be210f487abfdc32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jan 2025 01:43:38 +0900 Subject: [PATCH 176/620] Upgrade more packages again This also downgrades nunit to be aligned across all projects. Getting it up-to-date is a bit high effort. --- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 6 +++--- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 6 +++--- ...osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 6 +++--- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 6 +++--- osu.Desktop/osu.Desktop.csproj | 4 ++-- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 4 ++-- .../osu.Game.Rulesets.Catch.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Mania.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Osu.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Taiko.Tests.csproj | 4 ++-- osu.Game.Tests/osu.Game.Tests.csproj | 4 ++-- .../osu.Game.Tournament.Tests.csproj | 4 ++-- osu.Game/osu.Game.csproj | 16 ++++++++-------- 13 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index d0f4db5ed1..1d368e9bd1 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -9,9 +9,9 @@ false - - - + + + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 7ced68ebf5..d69bc78b8f 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,9 +9,9 @@ false - - - + + + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 6fb1574403..7ac269f65f 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -9,9 +9,9 @@ false - - - + + + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 7ced68ebf5..d69bc78b8f 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,9 +9,9 @@ false - - - + + + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index d06c4dd41b..21c570a7b2 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,9 +24,9 @@ - + - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 8a56a3df79..8a353eb2f5 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index b434d6aaf9..56ee208670 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index e7abd47881..5e4bad279b 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 5ea231e606..267dc98985 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -1,10 +1,10 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 2170009ae8..523df4c259 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 01d2241650..e78a3ea4f3 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,11 +1,11 @@  - + - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 04683cd83b..1daf5a446e 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -4,9 +4,9 @@ osu.Game.Tournament.Tests.TournamentTestRunner - + - + WinExe diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f53f25a8d3..bcca1eee35 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,14 +20,14 @@ - + - - - - - - + + + + + + @@ -37,7 +37,7 @@ - + From d5f2bdf6cd8dcb434f4233763a36da88526567ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jan 2025 02:54:13 +0900 Subject: [PATCH 177/620] Appease message pack new inspections --- CodeAnalysis/osu.globalconfig | 5 ++++- osu.Game/Online/API/ModSettingsDictionaryFormatter.cs | 6 ++++-- .../MatchTypes/TeamVersus/TeamVersusUserState.cs | 1 + osu.Game/Users/UserActivity.cs | 4 ++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CodeAnalysis/osu.globalconfig b/CodeAnalysis/osu.globalconfig index 247a825033..8012c31eca 100644 --- a/CodeAnalysis/osu.globalconfig +++ b/CodeAnalysis/osu.globalconfig @@ -51,8 +51,11 @@ dotnet_diagnostic.IDE1006.severity = warning # Too many noisy warnings for parsing/formatting numbers dotnet_diagnostic.CA1305.severity = none +# messagepack complains about "osu" not being title cased due to reserved words +dotnet_diagnostic.CS8981.severity = none + # CA1507: Use nameof to express symbol names -# Flaggs serialization name attributes +# Flags serialization name attributes dotnet_diagnostic.CA1507.severity = suggestion # CA1806: Do not ignore method results diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index 3fad032531..8da83d2aad 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -10,10 +10,12 @@ using osu.Game.Configuration; namespace osu.Game.Online.API { - public class ModSettingsDictionaryFormatter : IMessagePackFormatter> + public class ModSettingsDictionaryFormatter : IMessagePackFormatter?> { - public void Serialize(ref MessagePackWriter writer, Dictionary value, MessagePackSerializerOptions options) + public void Serialize(ref MessagePackWriter writer, Dictionary? value, MessagePackSerializerOptions options) { + if (value == null) return; + var primitiveFormatter = PrimitiveObjectFormatter.Instance; writer.WriteArrayHeader(value.Count); diff --git a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs index ac3b9724cc..bf11713663 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs @@ -5,6 +5,7 @@ using MessagePack; namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus { + [MessagePackObject] public class TeamVersusUserState : MatchUserState { [Key(0)] diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index a8e0fc9030..a792424562 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -54,6 +54,10 @@ namespace osu.Game.Users } [MessagePackObject] + [Union(12, typeof(InSoloGame))] + [Union(23, typeof(InMultiplayerGame))] + [Union(24, typeof(SpectatingMultiplayerGame))] + [Union(31, typeof(InPlaylistGame))] public abstract class InGame : UserActivity { [Key(0)] From d04947d400b0900fec4625e2828e4fb4434b4f53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jan 2025 15:42:30 +0900 Subject: [PATCH 178/620] Don't use `record`s they are ugly Refactor `WindowsAssociationManager` to be usable --- .../Windows/WindowsAssociationManager.cs | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index 6f53c65ca9..f8702732e7 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -174,9 +174,20 @@ namespace osu.Desktop.Windows #endregion - private record FileAssociation(string Extension, LocalisableString Description, string IconPath) + private class FileAssociation { - private string programId => $@"{program_id_prefix}{Extension}"; + private string programId => $@"{program_id_prefix}{extension}"; + + private string extension { get; } + private LocalisableString description { get; } + private string iconPath { get; } + + public FileAssociation(string extension, LocalisableString description, string iconPath) + { + this.extension = extension; + this.description = description; + this.iconPath = iconPath; + } /// /// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key @@ -190,13 +201,13 @@ namespace osu.Desktop.Windows using (var programKey = classes.CreateSubKey(programId)) { using (var defaultIconKey = programKey.CreateSubKey(default_icon)) - defaultIconKey.SetValue(null, IconPath); + defaultIconKey.SetValue(null, iconPath); using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND)) openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); } - using (var extensionKey = classes.CreateSubKey(Extension)) + using (var extensionKey = classes.CreateSubKey(extension)) { // set ourselves as the default program extensionKey.SetValue(null, programId); @@ -225,7 +236,7 @@ namespace osu.Desktop.Windows using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); if (classes == null) return; - using (var extensionKey = classes.OpenSubKey(Extension, true)) + using (var extensionKey = classes.OpenSubKey(extension, true)) { // clear our default association so that Explorer doesn't show the raw programId to users // the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons @@ -240,13 +251,24 @@ namespace osu.Desktop.Windows } } - private record UriAssociation(string Protocol, LocalisableString Description, string IconPath) + private class UriAssociation { /// /// "The URL Protocol string value indicates that this key declares a custom pluggable protocol handler." /// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85). /// - public const string URL_PROTOCOL = @"URL Protocol"; + private const string url_protocol = @"URL Protocol"; + + private string protocol { get; } + private LocalisableString description { get; } + private string iconPath { get; } + + public UriAssociation(string protocol, LocalisableString description, string iconPath) + { + this.protocol = protocol; + this.description = description; + this.iconPath = iconPath; + } /// /// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85). @@ -256,12 +278,12 @@ namespace osu.Desktop.Windows using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); if (classes == null) return; - using (var protocolKey = classes.CreateSubKey(Protocol)) + using (var protocolKey = classes.CreateSubKey(protocol)) { - protocolKey.SetValue(URL_PROTOCOL, string.Empty); + protocolKey.SetValue(url_protocol, string.Empty); using (var defaultIconKey = protocolKey.CreateSubKey(default_icon)) - defaultIconKey.SetValue(null, IconPath); + defaultIconKey.SetValue(null, iconPath); using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND)) openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); @@ -273,14 +295,14 @@ namespace osu.Desktop.Windows using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); if (classes == null) return; - using (var protocolKey = classes.OpenSubKey(Protocol, true)) + using (var protocolKey = classes.OpenSubKey(protocol, true)) protocolKey?.SetValue(null, $@"URL:{description}"); } public void Uninstall() { using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); - classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false); + classes?.DeleteSubKeyTree(protocol, throwOnMissingSubKey: false); } } } From b6288802145828429ac27ea8cf634d7af0b64b00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jan 2025 15:55:04 +0900 Subject: [PATCH 179/620] Change association localisation flow to make logical sense --- .../Windows/WindowsAssociationManager.cs | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index f8702732e7..98e77b1ff6 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -56,14 +56,13 @@ namespace osu.Desktop.Windows /// Installs file and URI associations. /// /// - /// Call in a timely fashion to keep descriptions up-to-date and localised. + /// Call in a timely fashion to keep descriptions up-to-date and localised. /// public static void InstallAssociations() { try { updateAssociations(); - updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called. NotifyShellUpdate(); } catch (Exception e) @@ -76,17 +75,13 @@ namespace osu.Desktop.Windows /// Updates associations with latest definitions. /// /// - /// Call in a timely fashion to keep descriptions up-to-date and localised. + /// Call in a timely fashion to keep descriptions up-to-date and localised. /// public static void UpdateAssociations() { try { updateAssociations(); - - // TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc. - updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed - NotifyShellUpdate(); } catch (Exception e) @@ -95,11 +90,17 @@ namespace osu.Desktop.Windows } } - public static void UpdateDescriptions(LocalisationManager localisationManager) + // TODO: call this sometime. + public static void LocaliseDescriptions(LocalisationManager localisationManager) { try { - updateDescriptions(localisationManager); + foreach (var association in file_associations) + association.LocaliseDescription(localisationManager); + + foreach (var association in uri_associations) + association.LocaliseDescription(localisationManager); + NotifyShellUpdate(); } catch (Exception e) @@ -140,17 +141,6 @@ namespace osu.Desktop.Windows association.Install(); } - private static void updateDescriptions(LocalisationManager? localisation) - { - foreach (var association in file_associations) - association.UpdateDescription(getLocalisedString(association.Description)); - - foreach (var association in uri_associations) - association.UpdateDescription(getLocalisedString(association.Description)); - - string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString(); - } - #region Native interop [DllImport("Shell32.dll")] @@ -200,6 +190,8 @@ namespace osu.Desktop.Windows // register a program id for the given extension using (var programKey = classes.CreateSubKey(programId)) { + programKey.SetValue(null, description); + using (var defaultIconKey = programKey.CreateSubKey(default_icon)) defaultIconKey.SetValue(null, iconPath); @@ -219,13 +211,13 @@ namespace osu.Desktop.Windows } } - public void UpdateDescription(string description) + public void LocaliseDescription(LocalisationManager localisationManager) { using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); if (classes == null) return; using (var programKey = classes.OpenSubKey(programId, true)) - programKey?.SetValue(null, description); + programKey?.SetValue(null, localisationManager.GetLocalisedString(description)); } /// @@ -280,6 +272,7 @@ namespace osu.Desktop.Windows using (var protocolKey = classes.CreateSubKey(protocol)) { + protocolKey.SetValue(null, $@"URL:{description}"); protocolKey.SetValue(url_protocol, string.Empty); using (var defaultIconKey = protocolKey.CreateSubKey(default_icon)) @@ -290,13 +283,13 @@ namespace osu.Desktop.Windows } } - public void UpdateDescription(string description) + public void LocaliseDescription(LocalisationManager localisationManager) { using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); if (classes == null) return; using (var protocolKey = classes.OpenSubKey(protocol, true)) - protocolKey?.SetValue(null, $@"URL:{description}"); + protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}"); } public void Uninstall() From fbfda2e04425296c8f8fb73557cc724da0ee0e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jan 2025 10:28:04 +0100 Subject: [PATCH 180/620] Extend test coverage with combo index correctness checks --- osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 1df8f96f93..c625346645 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -576,6 +576,10 @@ namespace osu.Game.Tests.Editing { Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True); Assert.That(((HitCircle)beatmap.HitObjects[2]).NewCombo, Is.True); + + Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1)); + Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2)); + Assert.That(((HitCircle)beatmap.HitObjects[2]).ComboIndex, Is.EqualTo(3)); }); } @@ -610,6 +614,9 @@ namespace osu.Game.Tests.Editing { Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True); + + Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1)); + Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2)); }); } } From 7c70dc4dc305d7bcd421c0e1f8d83d1ab3bfd67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jan 2025 10:28:06 +0100 Subject: [PATCH 181/620] Only update combo information when any changes happened --- .../Screens/Edit/EditorBeatmapProcessor.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 8108f51ad1..957c1d0969 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -111,20 +111,29 @@ namespace osu.Game.Screens.Edit int currentBreak = 0; - for (int i = 0; i < Beatmap.HitObjects.Count; ++i) - { - var hitObject = Beatmap.HitObjects[i]; + IHasComboInformation? lastObj = null; + bool comboInformationUpdateRequired = false; + foreach (var hitObject in Beatmap.HitObjects) + { if (hitObject is not IHasComboInformation hasCombo) continue; if (currentBreak < breakEnds.Count && hitObject.StartTime >= breakEnds[currentBreak]) { - hasCombo.NewCombo = true; + if (!hasCombo.NewCombo) + { + hasCombo.NewCombo = true; + comboInformationUpdateRequired = true; + } + currentBreak += 1; } - hasCombo.UpdateComboInformation(i > 0 ? Beatmap.HitObjects[i - 1] as IHasComboInformation : null); + if (comboInformationUpdateRequired) + hasCombo.UpdateComboInformation(lastObj); + + lastObj = hasCombo; } } } From 9c05837b3a36e26b4cbe6cdb6b364b03d99b585c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 18:45:35 +0900 Subject: [PATCH 182/620] Change to using a 'FreeStyle' boolean --- .../Online/Rooms/MultiplayerPlaylistItem.cs | 5 +-- osu.Game/Online/Rooms/PlaylistItem.cs | 18 ++++---- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 41 ++++++------------- .../Multiplayer/MultiplayerMatchSongSelect.cs | 4 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 3 ++ .../OnlinePlay/OnlinePlaySongSelect.cs | 5 +-- .../OnlinePlay/OnlinePlayStyleSelect.cs | 13 ++++-- .../Playlists/PlaylistsRoomSubScreen.cs | 8 ++++ .../Playlists/PlaylistsSongSelect.cs | 2 +- 9 files changed, 49 insertions(+), 50 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4a15fd9690..4dfb3b389d 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -57,11 +57,10 @@ namespace osu.Game.Online.Rooms public double StarRating { get; set; } /// - /// A non-null value indicates "freestyle" mode where players are able to individually select - /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [Key(11)] - public int? BeatmapSetID { get; set; } + public bool FreeStyle { get; set; } [SerializationConstructor] public MultiplayerPlaylistItem() diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 16c252befc..e8725b6792 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -68,11 +68,10 @@ namespace osu.Game.Online.Rooms } /// - /// A non-null value indicates "freestyle" mode where players are able to individually select - /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// - [JsonProperty("beatmapset_id")] - public int? BeatmapSetId { get; set; } + [JsonProperty("freestyle")] + public bool FreeStyle { get; set; } /// /// A beatmap representing this playlist item. @@ -108,7 +107,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); - BeatmapSetId = item.BeatmapSetID; + FreeStyle = item.FreeStyle; } public void MarkInvalid() => valid.Value = false; @@ -128,8 +127,7 @@ namespace osu.Game.Online.Rooms #endregion - public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, - Optional ruleset = default) + public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, Optional ruleset = default) { return new PlaylistItem(beatmap.GetOr(Beatmap)) { @@ -141,19 +139,19 @@ namespace osu.Game.Online.Rooms PlayedAt = PlayedAt, AllowedMods = AllowedMods, RequiredMods = RequiredMods, + FreeStyle = FreeStyle, valid = { Value = Valid.Value }, - BeatmapSetId = BeatmapSetId }; } public bool Equals(PlaylistItem? other) => ID == other?.ID && Beatmap.OnlineID == other.Beatmap.OnlineID - && BeatmapSetId == other.BeatmapSetId && RulesetID == other.RulesetID && Expired == other.Expired && PlaylistOrder == other.PlaylistOrder && AllowedMods.SequenceEqual(other.AllowedMods) - && RequiredMods.SequenceEqual(other.RequiredMods); + && RequiredMods.SequenceEqual(other.RequiredMods) + && FreeStyle == other.FreeStyle; } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index b51679ded6..ec2ed90eca 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -272,21 +272,9 @@ namespace osu.Game.Screens.OnlinePlay.Match base.LoadComplete(); SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - - UserMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); - - UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(() => - { - updateBeatmap(); - updateUserStyle(); - })); - - UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(() => - { - updateUserMods(); - updateRuleset(); - updateUserStyle(); - })); + UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); @@ -458,14 +446,6 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - // Reset user style if no longer valid. - // Todo: In the future this can be made more lenient, such as allowing a non-null ruleset as the set changes. - if (item.BeatmapSetId == null || item.BeatmapSetId != UserBeatmap.Value?.BeatmapSet!.OnlineID) - { - UserBeatmap.Value = null; - UserRuleset.Value = null; - } - updateUserMods(); updateBeatmap(); updateMods(); @@ -487,10 +467,10 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } - if (item.BeatmapSetId == null) - UserStyleSection?.Hide(); - else + if (item.FreeStyle) UserStyleSection?.Show(); + else + UserStyleSection?.Hide(); } private void updateUserMods() @@ -499,8 +479,13 @@ namespace osu.Game.Screens.OnlinePlay.Match return; // Remove any user mods that are no longer allowed. - var rulesetInstance = GetGameplayRuleset().CreateInstance(); - var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + Ruleset rulesetInstance = GetGameplayRuleset().CreateInstance(); + Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + + if (newUserMods.SequenceEqual(UserMods.Value)) + return; + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 9f9e6349a6..5754bcb963 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -83,11 +83,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { ID = itemToEdit?.ID ?? 0, BeatmapID = item.Beatmap.OnlineID, - BeatmapSetID = item.BeatmapSetId, BeatmapChecksum = item.Beatmap.MD5Hash, RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), - AllowedMods = item.AllowedMods.ToArray() + AllowedMods = item.AllowedMods.ToArray(), + FreeStyle = item.FreeStyle }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edfb059c77..34a1eb70a9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -403,7 +403,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void updateCurrentItem() { Debug.Assert(client.Room != null); + SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); + UserBeatmap.Value = client.LocalUser?.BeatmapId == null ? null : UserBeatmap.Value; + UserRuleset.Value = client.LocalUser?.RulesetId == null ? null : UserRuleset.Value; } private void handleRoomLost() => Schedule(() => diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index a91f43635b..9df01ead42 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -111,8 +111,7 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } - if (initialItem.BeatmapSetId != null) - FreeStyle.Value = true; + FreeStyle.Value = initialItem.FreeStyle; } Mods.BindValueChanged(onModsChanged); @@ -162,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null + FreeStyle = FreeStyle.Value }; return SelectItem(item); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 029ca68e36..d1fcf94152 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -63,6 +63,7 @@ namespace osu.Game.Screens.OnlinePlay { private readonly PlaylistItem item; private double itemLength; + private int beatmapSetId; public DifficultySelectFilterControl(PlaylistItem item) { @@ -72,8 +73,14 @@ namespace osu.Game.Screens.OnlinePlay [BackgroundDependencyLoader] private void load(RealmAccess realm) { - int beatmapId = item.Beatmap.OnlineID; - itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + realm.Run(r => + { + int beatmapId = item.Beatmap.OnlineID; + BeatmapInfo? beatmap = r.All().FirstOrDefault(b => b.OnlineID == beatmapId); + + itemLength = beatmap?.Length ?? 0; + beatmapSetId = beatmap?.BeatmapSet?.OnlineID ?? 0; + }); } public override FilterCriteria CreateCriteria() @@ -81,7 +88,7 @@ namespace osu.Game.Screens.OnlinePlay var criteria = base.CreateCriteria(); // Must be from the same set as the playlist item. - criteria.BeatmapSetId = item.BeatmapSetId; + criteria.BeatmapSetId = beatmapSetId; // Must be within 30s of the playlist item. criteria.Length.Min = itemLength - 30000; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index b941bbd290..eaadfb6507 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -67,6 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); isIdle.BindValueChanged(_ => updatePollingRate(), true); Room.PropertyChanged += onRoomPropertyChanged; @@ -75,6 +76,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists updateRoomPlaylist(); } + private void onSelectedItemChanged(ValueChangedEvent item) + { + // Simplest for now. + UserBeatmap.Value = null; + UserRuleset.Value = null; + } + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index a3b8a1575e..abf80c0d44 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,10 +37,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, - BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), + FreeStyle = FreeStyle.Value }; } } From be33addae16f589dda941d27d2e49a25ec61d0bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 18:57:22 +0900 Subject: [PATCH 183/620] Fix possible null reference --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 34a1eb70a9..b5fe8bf631 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -274,7 +274,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override APIMod[] GetGameplayMods() { // Using the room's reported status makes the server authoritative. - return client.LocalUser?.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray()!; + return client.LocalUser?.Mods != null ? client.LocalUser.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray() : base.GetGameplayMods(); } protected override RulesetInfo GetGameplayRuleset() From ac19124632616dfff072bcff83b77aa4ce8b136b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jan 2025 11:39:48 +0100 Subject: [PATCH 184/620] Add failing test --- .../Editor/TestSceneJuiceStreamSelectionBlueprint.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 7b665b1ff9..9e2c87af25 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -193,6 +193,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor addVertexCheckStep(1, 0, times[0], positions[0]); } + [Test] + public void TestDeletingSecondVertexDeletesEntireJuiceStream() + { + double[] times = { 100, 400 }; + float[] positions = { 100, 150 }; + addBlueprintStep(times, positions); + + addDeleteVertexSteps(times[1], positions[1]); + AddAssert("juice stream deleted", () => EditorBeatmap.HitObjects, () => Is.Empty); + } + [Test] public void TestVertexResampling() { From 9058fd97395338674eda340895b1589f709ecf4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jan 2025 11:39:49 +0100 Subject: [PATCH 185/620] Delete entire juice stream when only one vertex remains after deleting another vertex Closes https://github.com/ppy/osu/issues/31425. --- .../Edit/Blueprints/Components/EditablePath.cs | 2 +- .../Edit/Blueprints/Components/SelectionEditablePath.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index e626392234..6a671458f0 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components })); } - public void UpdateHitObjectFromPath(JuiceStream hitObject) + public virtual void UpdateHitObjectFromPath(JuiceStream hitObject) { // The SV setting may need to be changed for the current path. var svBindable = hitObject.SliderVelocityMultiplierBindable; diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index b2ee43ba16..26b26641d3 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -138,5 +138,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components EditorBeatmap?.EndChange(); } + + public override void UpdateHitObjectFromPath(JuiceStream hitObject) + { + base.UpdateHitObjectFromPath(hitObject); + + if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength) + EditorBeatmap?.Remove(hitObject); + } } } From 87866d1b96d0190579b9a0abf734dd0346d4fc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jan 2025 11:41:00 +0100 Subject: [PATCH 186/620] Enable NRT in test scene --- .../Editor/TestSceneJuiceStreamSelectionBlueprint.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 9e2c87af25..278c7b1bde 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene { - private JuiceStream hitObject; + private JuiceStream hitObject = null!; private readonly ManualClock manualClock = new ManualClock(); From e131a6c39f1f26542f249d5b183747aaf8b70432 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jan 2025 20:19:38 +0900 Subject: [PATCH 187/620] Add explicit `ToString()` to avoid sending `LocalisableString` to registry function --- osu.Desktop/Windows/WindowsAssociationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index 98e77b1ff6..43c3e5a947 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -190,7 +190,7 @@ namespace osu.Desktop.Windows // register a program id for the given extension using (var programKey = classes.CreateSubKey(programId)) { - programKey.SetValue(null, description); + programKey.SetValue(null, description.ToString()); using (var defaultIconKey = programKey.CreateSubKey(default_icon)) defaultIconKey.SetValue(null, iconPath); From 5a2024777dec1eba69fbc2b5e8256bb99c29c5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jan 2025 14:19:50 +0100 Subject: [PATCH 188/620] Select closest timing point every time the timing screen is changed to No issue thread for this, was pointed out internally: https://discord.com/channels/90072389919997952/1259818301517725707/1316604605777444905 Due to the custom setup that editor has with its nested "screens-that-aren't-screens", the logic that selects the closest timing point to the current time would only fire on the first open of the screen. Seems like a good idea to have it fire every time instead. --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 33 +++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 67d4429be8..cddde34aca 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -15,6 +15,8 @@ namespace osu.Game.Screens.Edit.Timing [Cached] public readonly Bindable SelectedGroup = new Bindable(); + private readonly Bindable currentEditorMode = new Bindable(); + [Resolved] private EditorClock? editorClock { get; set; } @@ -41,18 +43,35 @@ namespace osu.Game.Screens.Edit.Timing } }; + [BackgroundDependencyLoader] + private void load(Editor? editor) + { + if (editor != null) + currentEditorMode.BindTo(editor.Mode); + } + protected override void LoadComplete() { base.LoadComplete(); - if (editorClock != null) + // When entering the timing screen, let's choose the closest valid timing point. + // This will emulate the osu-stable behaviour where a metronome and timing information + // are presented on entering the screen. + currentEditorMode.BindValueChanged(mode => { - // When entering the timing screen, let's choose the closest valid timing point. - // This will emulate the osu-stable behaviour where a metronome and timing information - // are presented on entering the screen. - var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime); - SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); - } + if (mode.NewValue == EditorScreenMode.Timing) + selectClosestTimingPoint(); + }); + selectClosestTimingPoint(); + } + + private void selectClosestTimingPoint() + { + if (editorClock == null) + return; + + var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime); + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); } protected override void ConfigureTimeline(TimelineArea timelineArea) From f4d83fe6851272375f2382ffc2dd0c0d89721f93 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Jan 2025 13:23:16 +0900 Subject: [PATCH 189/620] Keep friend states when stopping watching global activity --- .../Online/Metadata/OnlineMetadataClient.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index a3041c6753..ef748f0b49 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -31,10 +32,11 @@ namespace osu.Game.Online.Metadata private readonly string endpoint; + [Resolved] + private IAPIProvider api { get; set; } = null!; + private IHubClientConnector? connector; - private Bindable lastQueueId = null!; - private IBindable localUser = null!; private IBindable userActivity = null!; private IBindable? userStatus; @@ -47,7 +49,7 @@ namespace osu.Game.Online.Metadata } [BackgroundDependencyLoader] - private void load(IAPIProvider api, OsuConfigManager config) + private void load(OsuConfigManager config) { // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. @@ -226,7 +228,15 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userStates.Clear()); + Schedule(() => + { + foreach (int userId in userStates.Keys.ToArray()) + { + if (api.GetFriend(userId) == null) + userStates.Remove(userId); + } + }); + Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); From 2a7a3d932edebd82d2a2fa26f20957a88ea5edc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jan 2025 13:24:12 +0900 Subject: [PATCH 190/620] Add test showing that rate adjustments cause discrepancies in replay frame precision --- .../Gameplay/TestSceneReplayRecorder.cs | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index a7ab021884..31af96bdf8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -15,6 +15,7 @@ using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; using osu.Framework.Testing; using osu.Framework.Threading; +using osu.Framework.Timing; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; @@ -42,6 +43,8 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState; + private Drawable content; + [SetUpSteps] public void SetUpSteps() { @@ -58,7 +61,7 @@ namespace osu.Game.Tests.Visual.Gameplay { RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] { (typeof(GameplayState), gameplayState) }, - Child = createContent(), + Child = content = createContent(), }; }); } @@ -67,10 +70,32 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestBasic() { AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("at least one frame recorded", () => replay.Frames.Count > 0); + AddUntilStep("at least one frame recorded", () => replay.Frames.Count, () => Is.GreaterThanOrEqualTo(0)); AddUntilStep("position matches", () => playbackManager.ChildrenOfType().First().Position == recordingManager.ChildrenOfType().First().Position); } + [Test] + [Explicit] + public void TestSlowClockStillRecordsFramesInRealtime() + { + ScheduledDelegate moveFunction = null; + + AddStep("set slow running clock", () => + { + var stopwatchClock = new StopwatchClock(true) { Rate = 0.01 }; + stopwatchClock.Seek(Clock.CurrentTime); + + content.Clock = new FramedClock(stopwatchClock); + }); + + AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre)); + AddStep("much move", () => moveFunction = Scheduler.AddDelayed(() => + InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true)); + AddWaitStep("move", 10); + AddStep("stop move", () => moveFunction.Cancel()); + AddAssert("at least 60 frames recorded", () => replay.Frames.Count, () => Is.GreaterThanOrEqualTo(60)); + } + [Test] public void TestHighFrameRate() { @@ -81,7 +106,7 @@ namespace osu.Game.Tests.Visual.Gameplay InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true)); AddWaitStep("move", 10); AddStep("stop move", () => moveFunction.Cancel()); - AddAssert("at least 60 frames recorded", () => replay.Frames.Count > 60); + AddAssert("at least 60 frames recorded", () => replay.Frames.Count, () => Is.GreaterThanOrEqualTo(60)); } [Test] @@ -97,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true)); AddWaitStep("move", 10); AddStep("stop move", () => moveFunction.Cancel()); - AddAssert("less than 10 frames recorded", () => replay.Frames.Count - initialFrameCount < 10); + AddAssert("less than 10 frames recorded", () => replay.Frames.Count - initialFrameCount, () => Is.LessThan(10)); } [Test] @@ -114,7 +139,7 @@ namespace osu.Game.Tests.Visual.Gameplay }, 10, true)); AddWaitStep("move", 10); AddStep("stop move", () => moveFunction.Cancel()); - AddAssert("at least 60 frames recorded", () => replay.Frames.Count > 60); + AddAssert("at least 60 frames recorded", () => replay.Frames.Count, () => Is.GreaterThanOrEqualTo(60)); } protected override void Update() From c8f72fdbe920f8f2fe4b2eaf88db9f7c9a2e41e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jan 2025 13:24:27 +0900 Subject: [PATCH 191/620] Fix rate adjustments changing the spacing between replay frames --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 28e25c72e1..1f91e2c5f0 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -27,7 +27,10 @@ namespace osu.Game.Rulesets.UI private InputManager inputManager; - public int RecordFrameRate = 60; + /// + /// The frame rate to record replays at. + /// + public int RecordFrameRate { get; set; } = 60; [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -76,7 +79,7 @@ namespace osu.Game.Rulesets.UI { var last = target.Replay.Frames.LastOrDefault(); - if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate)) + if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate) * Clock.Rate) return; var position = ScreenSpaceToGamefield?.Invoke(inputManager.CurrentState.Mouse.Position) ?? inputManager.CurrentState.Mouse.Position; From 0fe6b4be0dd7f4295adf3f379d4c6bb997c185e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jan 2025 13:33:55 +0900 Subject: [PATCH 192/620] Add reason for making test interactive-only --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 31af96bdf8..4ad6bc66e3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - [Explicit] + [Explicit("Making this test work in a headless context is high effort due to rate adjustment requirements not aligning with the global fast clock. StopwatchClock usage would need to be replace with a rate adjusting clock that still reads from the parent clock. High effort for a test which likely will not see any changes to covered code for some years.")] public void TestSlowClockStillRecordsFramesInRealtime() { ScheduledDelegate moveFunction = null; From 7268b2e077ab95347a12d5374cbdf505ff8538d1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Jan 2025 17:31:01 +0900 Subject: [PATCH 193/620] Add separate path for friend presence notifications It proved to be too difficult to deal with the flow that clears user states on stopping the watching of global presence updates. It's not helped in the least that friends are updated via the API, so there's a third flow to consider (and the timings therein - both server-spectator and friends are updated concurrently). Simplest is to separate the friends flow, though this does mean some logic and state duplication. --- .../TestSceneFriendPresenceNotifier.cs | 14 +- osu.Game/Online/API/APIAccess.cs | 19 ++- osu.Game/Online/API/DummyAPIAccess.cs | 3 - osu.Game/Online/API/IAPIProvider.cs | 7 - osu.Game/Online/FriendPresenceNotifier.cs | 121 ++++++++++++------ osu.Game/Online/Metadata/IMetadataClient.cs | 5 + osu.Game/Online/Metadata/MetadataClient.cs | 8 ++ .../Online/Metadata/OnlineMetadataClient.cs | 34 +++-- .../Visual/Metadata/TestMetadataClient.cs | 16 ++- 9 files changed, 148 insertions(+), 79 deletions(-) diff --git a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs index 851c1141db..2fe2326508 100644 --- a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs +++ b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs @@ -56,16 +56,16 @@ namespace osu.Game.Tests.Visual.Components [Test] public void TestNotifications() { - AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); + AddStep("bring friend 1 online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); - AddStep("bring friend 1 offline", () => metadataClient.UserPresenceUpdated(1, null)); + AddStep("bring friend 1 offline", () => metadataClient.FriendPresenceUpdated(1, null)); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2)); } [Test] public void TestSingleUserNotificationOpensChat() { - AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); + AddStep("bring friend 1 online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); AddStep("click notification", () => @@ -83,8 +83,8 @@ namespace osu.Game.Tests.Visual.Components { AddStep("bring friends 1 & 2 online", () => { - metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }); - metadataClient.UserPresenceUpdated(2, new UserPresence { Status = UserStatus.Online }); + metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }); + metadataClient.FriendPresenceUpdated(2, new UserPresence { Status = UserStatus.Online }); }); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Visual.Components AddStep("bring friends 1-10 online", () => { for (int i = 1; i <= 10; i++) - metadataClient.UserPresenceUpdated(i, new UserPresence { Status = UserStatus.Online }); + metadataClient.FriendPresenceUpdated(i, new UserPresence { Status = UserStatus.Online }); }); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Components AddStep("bring friends 1-10 offline", () => { for (int i = 1; i <= 10; i++) - metadataClient.UserPresenceUpdated(i, null); + metadataClient.FriendPresenceUpdated(i, null); }); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2)); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 39c09f2a5d..46476ab7f0 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Sockets; @@ -18,6 +19,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Localisation; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -75,7 +77,6 @@ namespace osu.Game.Online.API protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); - private readonly Dictionary friendsMapping = new Dictionary(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; @@ -404,8 +405,6 @@ namespace osu.Game.Online.API public IChatClient GetChatClient() => new WebSocketChatClient(this); - public APIRelation GetFriend(int userId) => friendsMapping.GetValueOrDefault(userId); - public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { Debug.Assert(State.Value == APIState.Offline); @@ -597,8 +596,6 @@ namespace osu.Game.Online.API Schedule(() => { setLocalUser(createGuestUser()); - - friendsMapping.Clear(); friends.Clear(); }); @@ -615,12 +612,14 @@ namespace osu.Game.Online.API friendsReq.Failure += _ => state.Value = APIState.Failing; friendsReq.Success += res => { - friendsMapping.Clear(); - friends.Clear(); + // Add new friends into local list. + HashSet friendsSet = friends.Select(f => f.TargetID).ToHashSet(); + friends.AddRange(res.Where(f => !friendsSet.Contains(f.TargetID))); - foreach (var u in res) - friendsMapping[u.TargetID] = u; - friends.AddRange(res); + // Remove non-friends from local lists. + friendsSet.Clear(); + friendsSet.AddRange(res.Select(f => f.TargetID)); + friends.RemoveAll(f => !friendsSet.Contains(f.TargetID)); }; Queue(friendsReq); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index ca4edb3d8f..5d63c04925 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; @@ -195,8 +194,6 @@ namespace osu.Game.Online.API public IChatClient GetChatClient() => new TestChatClientConnector(this); - public APIRelation? GetFriend(int userId) => Friends.FirstOrDefault(r => r.TargetID == userId); - public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password) { Thread.Sleep(200); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 4655b26f84..1c4b2da742 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -152,13 +152,6 @@ namespace osu.Game.Online.API /// IChatClient GetChatClient(); - /// - /// Retrieves a friend from a given user ID. - /// - /// The friend's user ID. - /// The object representing the friend, if any. - APIRelation? GetFriend(int userId); - /// /// Create a new user account. This is a blocking operation. /// diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 655a004d3e..330e0a908f 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -43,7 +44,10 @@ namespace osu.Game.Online private OsuConfigManager config { get; set; } = null!; private readonly Bindable notifyOnFriendPresenceChange = new BindableBool(); - private readonly IBindableDictionary userStates = new BindableDictionary(); + + private readonly IBindableList friends = new BindableList(); + private readonly IBindableDictionary friendStates = new BindableDictionary(); + private readonly HashSet onlineAlertQueue = new HashSet(); private readonly HashSet offlineAlertQueue = new HashSet(); @@ -56,42 +60,11 @@ namespace osu.Game.Online config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange); - userStates.BindTo(metadataClient.UserStates); - userStates.BindCollectionChanged((_, args) => - { - switch (args.Action) - { - case NotifyDictionaryChangedAction.Add: - foreach ((int userId, var _) in args.NewItems!) - { - if (api.GetFriend(userId)?.TargetUser is APIUser user) - { - if (!offlineAlertQueue.Remove(user)) - { - onlineAlertQueue.Add(user); - lastOnlineAlertTime ??= Time.Current; - } - } - } + friends.BindTo(api.Friends); + friends.BindCollectionChanged(onFriendsChanged, true); - break; - - case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, var _) in args.OldItems!) - { - if (api.GetFriend(userId)?.TargetUser is APIUser user) - { - if (!onlineAlertQueue.Remove(user)) - { - offlineAlertQueue.Add(user); - lastOfflineAlertTime ??= Time.Current; - } - } - } - - break; - } - }); + friendStates.BindTo(metadataClient.FriendStates); + friendStates.BindCollectionChanged(onFriendStatesChanged, true); } protected override void Update() @@ -102,6 +75,82 @@ namespace osu.Game.Online alertOfflineUsers(); } + private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (APIRelation friend in e.NewItems!.Cast()) + { + if (friend.TargetUser is not APIUser user) + continue; + + if (friendStates.TryGetValue(friend.TargetID, out _)) + markUserOnline(user); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (APIRelation friend in e.OldItems!.Cast()) + { + if (friend.TargetUser is not APIUser user) + continue; + + onlineAlertQueue.Remove(user); + offlineAlertQueue.Remove(user); + } + + break; + } + } + + private void onFriendStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + { + switch (e.Action) + { + case NotifyDictionaryChangedAction.Add: + foreach ((int friendId, _) in e.NewItems!) + { + APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId); + + if (friend?.TargetUser is APIUser user) + markUserOnline(user); + } + + break; + + case NotifyDictionaryChangedAction.Remove: + foreach ((int friendId, _) in e.OldItems!) + { + APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId); + + if (friend?.TargetUser is APIUser user) + markUserOffline(user); + } + + break; + } + } + + private void markUserOnline(APIUser user) + { + if (!offlineAlertQueue.Remove(user)) + { + onlineAlertQueue.Add(user); + lastOnlineAlertTime ??= Time.Current; + } + } + + private void markUserOffline(APIUser user) + { + if (!onlineAlertQueue.Remove(user)) + { + offlineAlertQueue.Add(user); + lastOfflineAlertTime ??= Time.Current; + } + } + private void alertOnlineUsers() { if (onlineAlertQueue.Count == 0) diff --git a/osu.Game/Online/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs index 97c1bbde5f..a4251fae80 100644 --- a/osu.Game/Online/Metadata/IMetadataClient.cs +++ b/osu.Game/Online/Metadata/IMetadataClient.cs @@ -21,6 +21,11 @@ namespace osu.Game.Online.Metadata /// Task UserPresenceUpdated(int userId, UserPresence? status); + /// + /// Delivers and update of the of a friend with the supplied . + /// + Task FriendPresenceUpdated(int userId, UserPresence? presence); + /// /// Delivers an update of the current "daily challenge" status. /// Null value means there is no "daily challenge" currently active. diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 8a5fe1733e..6578f70f74 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -42,6 +42,11 @@ namespace osu.Game.Online.Metadata /// public abstract IBindableDictionary UserStates { get; } + /// + /// Dictionary keyed by user ID containing all of the information about currently online friends received from the server. + /// + public abstract IBindableDictionary FriendStates { get; } + /// public abstract Task UpdateActivity(UserActivity? activity); @@ -57,6 +62,9 @@ namespace osu.Game.Online.Metadata /// public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); + /// + public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); + #endregion #region Daily Challenge diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index ef748f0b49..a8a14b1c78 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -27,14 +26,14 @@ namespace osu.Game.Online.Metadata public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindableDictionary FriendStates => friendStates; + private readonly BindableDictionary friendStates = new BindableDictionary(); + public override IBindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); private readonly string endpoint; - [Resolved] - private IAPIProvider api { get; set; } = null!; - private IHubClientConnector? connector; private Bindable lastQueueId = null!; private IBindable localUser = null!; @@ -49,7 +48,7 @@ namespace osu.Game.Online.Metadata } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(IAPIProvider api, OsuConfigManager config) { // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. @@ -63,6 +62,7 @@ namespace osu.Game.Online.Metadata // https://github.com/dotnet/aspnetcore/issues/15198 connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated); connection.On(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated); + connection.On(nameof(IMetadataClient.FriendPresenceUpdated), ((IMetadataClient)this).FriendPresenceUpdated); connection.On(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated); connection.On(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested); @@ -108,6 +108,7 @@ namespace osu.Game.Online.Metadata { isWatchingUserPresence.Value = false; userStates.Clear(); + friendStates.Clear(); dailyChallengeInfo.Value = null; }); return; @@ -209,6 +210,19 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } + public override Task FriendPresenceUpdated(int userId, UserPresence? presence) + { + Schedule(() => + { + if (presence?.Status != null) + friendStates[userId] = presence.Value; + else + friendStates.Remove(userId); + }); + + return Task.CompletedTask; + } + public override async Task BeginWatchingUserPresence() { if (connector?.IsConnected.Value != true) @@ -228,15 +242,7 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => - { - foreach (int userId in userStates.Keys.ToArray()) - { - if (api.GetFriend(userId) == null) - userStates.Remove(userId); - } - }); - + Schedule(() => userStates.Clear()); Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 6dd6392b3a..36f79a5adc 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -23,6 +22,9 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindableDictionary FriendStates => friendStates; + private readonly BindableDictionary friendStates = new BindableDictionary(); + public override Bindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); @@ -67,7 +69,7 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UserPresenceUpdated(int userId, UserPresence? presence) { - if (isWatchingUserPresence.Value || api.Friends.Any(f => f.TargetID == userId)) + if (isWatchingUserPresence.Value) { if (presence.HasValue) userStates[userId] = presence.Value; @@ -78,6 +80,16 @@ namespace osu.Game.Tests.Visual.Metadata return Task.CompletedTask; } + public override Task FriendPresenceUpdated(int userId, UserPresence? presence) + { + if (presence.HasValue) + friendStates[userId] = presence.Value; + else + friendStates.Remove(userId); + + return Task.CompletedTask; + } + public override Task GetChangesSince(int queueId) => Task.FromResult(new BeatmapUpdates(Array.Empty(), queueId)); From 18f1d62182b02cecca7f8fff118c287cde6109fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jan 2025 13:40:42 +0100 Subject: [PATCH 194/620] Fix juice stream placement blueprint being initially visually offset - Closes https://github.com/ppy/osu/issues/31423. - Regressed in https://github.com/ppy/osu/pull/30411. Admittedly, I don't completely understand all of the pieces here, because code quality of this placement blueprint code is ALL-CAPS ATROCIOUS, but I believe the failure mode to be something along the lines of: - User activates juice stream tool, blueprint gets created in initial state. It reads in a mouse position far outside of the playfield, and sets internal positioning appropriately. - When the user moves the mouse into the bounds of the playfield, some positions update (the ones inside `UpdateTimeAndPosition()`, but the fruit markers are for *nested* objects, and `updateHitObjectFromPath()` is responsible for updating those... however, it only fires if the `editablePath.PathId` changes, which it won't here, because there is only one path vertex until the user commits the starting point of the juice stream and it's always at (0,0). - Therefore the position of the starting fruit marker remains bogus until left click, at which point the path changes and everything returns to *relative* sanity. The solution essentially relies on inlining the broken method and only guarding the relevant part of processing behind the path version check (which is actually updating the path). Everything else that can touch positions of nesteds (like default application, and the drawable piece updates) is allowed to happen unconditionally. --- .../JuiceStreamPlacementBlueprint.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 7b57dac36e..21cc260462 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -88,10 +88,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints switch (PlacementActive) { case PlacementState.Waiting: - if (!(result.Time is double snappedTime)) return; - HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X; - HitObject.StartTime = snappedTime; + if (result.Time is double snappedTime) + HitObject.StartTime = snappedTime; break; case PlacementState.Active: @@ -107,21 +106,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject); editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition; - updateHitObjectFromPath(); - } + if (lastEditablePathId != editablePath.PathId) + editablePath.UpdateHitObjectFromPath(HitObject); + lastEditablePathId = editablePath.PathId; - private void updateHitObjectFromPath() - { - if (lastEditablePathId == editablePath.PathId) - return; - - editablePath.UpdateHitObjectFromPath(HitObject); ApplyDefaultsToHitObject(); - scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject); nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject); - - lastEditablePathId = editablePath.PathId; } private double positionToTime(float relativeYPosition) From 5c8ae6f851b681ff06dc1e778ac48c73b4092ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Jan 2025 13:04:13 +0100 Subject: [PATCH 195/620] Simplify editor "ternary button" structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As I look into re-implementing the ability to choose combo colour for an object (also known as "colourhax") from the editor UI, I stumble upon these wretched ternary items again and sigh a deep sigh of annoyance. The structure is overly rigid. `TernaryItem` does nothing that `DrawableTernaryItem` couldn't, except make it more annoying to add specific sub-variants of `DrawableTernaryItem` that could do more things. Yes you could sprinkle more levels of virtuals to `CreateDrawableButton()` or something, but after all, as Saint Exupéry says, "perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away." So I'm leaning for taking one step towards perfection. --- .../Edit/CatchHitObjectComposer.cs | 2 +- .../Edit/OsuHitObjectComposer.cs | 9 ++- .../Edit/ComposerDistanceSnapProvider.cs | 9 ++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 14 ++--- .../Edit/ScrollingHitObjectComposer.cs | 7 ++- .../TernaryButtons/DrawableTernaryButton.cs | 62 ++++++++++++++----- .../TernaryButtons/SampleBankTernaryButton.cs | 38 ++++++++---- .../TernaryButtons/TernaryButton.cs | 48 -------------- .../Components/ComposeBlueprintContainer.cs | 58 ++++++++++------- .../Components/Timeline/SamplePointPiece.cs | 17 +++-- 10 files changed, 147 insertions(+), 117 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index aae3369d40..e0d80e0e64 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider); - protected override IEnumerable CreateTernaryButtons() + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() .Concat(DistanceSnapProvider.CreateTernaryButtons()); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7c50558b92..e8b9d0544e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -53,9 +53,14 @@ namespace osu.Game.Rulesets.Osu.Edit protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector(); - protected override IEnumerable CreateTernaryButtons() + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() - .Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })) + .Append(new DrawableTernaryButton + { + Current = rectangularGridSnapToggle, + Description = "Grid Snap", + CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }, + }) .Concat(DistanceSnapProvider.CreateTernaryButtons()); private BindableList selectedHitObjects; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 7337a75509..0ca01ccee6 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -191,9 +191,14 @@ namespace osu.Game.Rulesets.Edit } } - public IEnumerable CreateTernaryButtons() => new[] + public IEnumerable CreateTernaryButtons() => new[] { - new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap }) + new DrawableTernaryButton + { + Current = DistanceSnapToggle, + Description = "Distance Snap", + CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap }, + } }; public void HandleToggleViaKey(KeyboardEvent key) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 4b64548f9c..9f277b6190 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -269,10 +269,9 @@ namespace osu.Game.Rulesets.Edit }; } - TernaryStates = CreateTernaryButtons().ToArray(); - togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b))); + togglesCollection.AddRange(CreateTernaryButtons().ToArray()); - sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Zip(BlueprintContainer.SampleAdditionBankTernaryStates).Select(b => new SampleBankTernaryButton(b.First, b.Second))); + sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates); SetSelectTool(); @@ -368,15 +367,10 @@ namespace osu.Game.Rulesets.Edit /// protected abstract IReadOnlyList CompositionTools { get; } - /// - /// A collection of states which will be displayed to the user in the toolbox. - /// - public TernaryButton[] TernaryStates { get; private set; } - /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; + protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. @@ -437,7 +431,7 @@ namespace osu.Game.Rulesets.Edit { if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button) { - button.Button.Toggle(); + button.Toggle(); return true; } } diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index 223b770b48..e7161ce36c 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -56,7 +56,12 @@ namespace osu.Game.Rulesets.Edit Spacing = new Vector2(0, 5), Children = new[] { - new DrawableTernaryButton(new TernaryButton(showSpeedChanges, "Show speed changes", () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt })) + new DrawableTernaryButton + { + Current = showSpeedChanges, + Description = "Show speed changes", + CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt }, + } } }, }); diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs index fcbc719f46..326fdbc731 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs @@ -1,12 +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.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -16,8 +19,29 @@ using osuTK.Graphics; namespace osu.Game.Screens.Edit.Components.TernaryButtons { - public partial class DrawableTernaryButton : OsuButton, IHasTooltip + public partial class DrawableTernaryButton : OsuButton, IHasTooltip, IHasCurrentValue { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public required LocalisableString Description + { + get => Text; + set => Text = value; + } + + public LocalisableString TooltipText { get; set; } + + /// + /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. + /// + public Func? CreateIcon { get; init; } + private Color4 defaultBackgroundColour; private Color4 defaultIconColour; private Color4 selectedBackgroundColour; @@ -25,14 +49,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons protected Drawable Icon { get; private set; } = null!; - public readonly TernaryButton Button; - - public DrawableTernaryButton(TernaryButton button) + public DrawableTernaryButton() { - Button = button; - - Text = button.Description; - RelativeSizeAxes = Axes.X; } @@ -45,7 +63,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons defaultIconColour = defaultBackgroundColour.Darken(0.5f); selectedIconColour = selectedBackgroundColour.Lighten(0.5f); - Add(Icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b => + Add(Icon = (CreateIcon?.Invoke() ?? new Circle()).With(b => { b.Blending = BlendingParameters.Additive; b.Anchor = Anchor.CentreLeft; @@ -59,18 +77,32 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { base.LoadComplete(); - Button.Bindable.BindValueChanged(_ => updateSelectionState(), true); - Button.Enabled.BindTo(Enabled); + current.BindValueChanged(_ => updateSelectionState(), true); Action = onAction; } private void onAction() { - if (!Button.Enabled.Value) + if (!Enabled.Value) return; - Button.Toggle(); + Toggle(); + } + + public void Toggle() + { + switch (Current.Value) + { + case TernaryState.False: + case TernaryState.Indeterminate: + Current.Value = TernaryState.True; + break; + + case TernaryState.True: + Current.Value = TernaryState.False; + break; + } } private void updateSelectionState() @@ -78,7 +110,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons if (!IsLoaded) return; - switch (Button.Bindable.Value) + switch (Current.Value) { case TernaryState.Indeterminate: Icon.Colour = selectedIconColour.Darken(0.5f); @@ -104,7 +136,5 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons Anchor = Anchor.CentreLeft, X = 40f }; - - public LocalisableString TooltipText => Button.Tooltip; } } diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/SampleBankTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/SampleBankTernaryButton.cs index 33eb2ac0b4..a9aa4b4227 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/SampleBankTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/SampleBankTernaryButton.cs @@ -1,23 +1,32 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using Humanizer; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; namespace osu.Game.Screens.Edit.Components.TernaryButtons { public partial class SampleBankTernaryButton : CompositeDrawable { - public readonly TernaryButton NormalButton; - public readonly TernaryButton AdditionsButton; + public string BankName { get; } + public Func? CreateIcon { get; init; } - public SampleBankTernaryButton(TernaryButton normalButton, TernaryButton additionsButton) + public readonly BindableWithCurrent NormalState = new BindableWithCurrent(); + public readonly BindableWithCurrent AdditionsState = new BindableWithCurrent(); + + public DrawableTernaryButton NormalButton { get; private set; } = null!; + public DrawableTernaryButton AdditionsButton { get; private set; } = null!; + + public SampleBankTernaryButton(string bankName) { - NormalButton = normalButton; - AdditionsButton = additionsButton; + BankName = bankName; } [BackgroundDependencyLoader] @@ -36,7 +45,12 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons AutoSizeAxes = Axes.Y, Width = 0.5f, Padding = new MarginPadding { Right = 1 }, - Child = new InlineDrawableTernaryButton(NormalButton), + Child = NormalButton = new InlineDrawableTernaryButton + { + Current = NormalState, + Description = BankName.Titleize(), + CreateIcon = CreateIcon, + }, }, new Container { @@ -46,18 +60,18 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons AutoSizeAxes = Axes.Y, Width = 0.5f, Padding = new MarginPadding { Left = 1 }, - Child = new InlineDrawableTernaryButton(AdditionsButton), + Child = AdditionsButton = new InlineDrawableTernaryButton + { + Current = AdditionsState, + Description = BankName.Titleize(), + CreateIcon = CreateIcon, + }, }, }; } private partial class InlineDrawableTernaryButton : DrawableTernaryButton { - public InlineDrawableTernaryButton(TernaryButton button) - : base(button) - { - } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs deleted file mode 100644 index b7aaf517f5..0000000000 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs +++ /dev/null @@ -1,48 +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.Bindables; -using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Screens.Edit.Components.TernaryButtons -{ - public class TernaryButton - { - public readonly Bindable Bindable; - - public readonly Bindable Enabled = new Bindable(true); - - public readonly string Description; - - /// - /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. - /// - public readonly Func? CreateIcon; - - public string Tooltip { get; set; } = string.Empty; - - public TernaryButton(Bindable bindable, string description, Func? createIcon = null) - { - Bindable = bindable; - Description = description; - CreateIcon = createIcon; - } - - public void Toggle() - { - switch (Bindable.Value) - { - case TernaryState.False: - case TernaryState.Indeterminate: - Bindable.Value = TernaryState.True; - break; - - case TernaryState.True: - Bindable.Value = TernaryState.False; - break; - } - } - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 0ffd1072cd..bbb4095206 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -65,11 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void load() { MainTernaryStates = CreateTernaryButtons().ToArray(); - SampleBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionBankStates).ToArray(); - SampleAdditionBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionAdditionBankStates).ToArray(); - - SelectionHandler.AutoSelectionBankEnabled.BindValueChanged(_ => updateAutoBankTernaryButtonTooltip(), true); - SelectionHandler.SelectionAdditionBanksEnabled.BindValueChanged(_ => updateAdditionBankTernaryButtonTooltips(), true); + SampleBankTernaryStates = createSampleBankTernaryButtons().ToArray(); AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset) { @@ -98,6 +94,9 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach (var kvp in SelectionHandler.SelectionAdditionBankStates) kvp.Value.BindValueChanged(_ => updatePlacementSamples()); + + SelectionHandler.AutoSelectionBankEnabled.BindValueChanged(_ => updateAutoBankTernaryButtonTooltip(), true); + SelectionHandler.SelectionAdditionBanksEnabled.BindValueChanged(_ => updateAdditionBankTernaryButtonTooltips(), true); } protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject) @@ -238,28 +237,45 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A collection of states which will be displayed to the user in the toolbox. /// - public TernaryButton[] MainTernaryStates { get; private set; } + public DrawableTernaryButton[] MainTernaryStates { get; private set; } - public TernaryButton[] SampleBankTernaryStates { get; private set; } - - public TernaryButton[] SampleAdditionBankTernaryStates { get; private set; } + public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; } /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() + protected virtual IEnumerable CreateTernaryButtons() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }); + yield return new DrawableTernaryButton + { + Current = NewCombo, + Description = "New combo", + CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }, + }; foreach (var kvp in SelectionHandler.SelectionSampleStates) - yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => GetIconForSample(kvp.Key)); + { + yield return new DrawableTernaryButton + { + Current = kvp.Value, + Description = kvp.Key.Replace(@"hit", string.Empty).Titleize(), + CreateIcon = () => GetIconForSample(kvp.Key), + }; + } } - private IEnumerable createSampleBankTernaryButtons(Dictionary> sampleBankStates) + private IEnumerable createSampleBankTernaryButtons() { - foreach (var kvp in sampleBankStates) - yield return new TernaryButton(kvp.Value, kvp.Key.Titleize(), () => getIconForBank(kvp.Key)); + foreach (string bankName in HitSampleInfo.ALL_BANKS.Prepend(EditorSelectionHandler.HIT_BANK_AUTO)) + { + yield return new SampleBankTernaryButton(bankName) + { + NormalState = { Current = SelectionHandler.SelectionBankStates[bankName], }, + AdditionsState = { Current = SelectionHandler.SelectionAdditionBankStates[bankName], }, + CreateIcon = () => getIconForBank(bankName) + }; + } } private Drawable getIconForBank(string sampleName) @@ -295,19 +311,19 @@ namespace osu.Game.Screens.Edit.Compose.Components { bool enabled = SelectionHandler.AutoSelectionBankEnabled.Value; - var autoBankButton = SampleBankTernaryStates.Single(t => t.Bindable == SelectionHandler.SelectionBankStates[EditorSelectionHandler.HIT_BANK_AUTO]); - autoBankButton.Enabled.Value = enabled; - autoBankButton.Tooltip = !enabled ? "Auto normal bank can only be used during hit object placement" : string.Empty; + var autoBankButton = SampleBankTernaryStates.Single(t => t.BankName == EditorSelectionHandler.HIT_BANK_AUTO); + autoBankButton.NormalButton.Enabled.Value = enabled; + autoBankButton.NormalButton.TooltipText = !enabled ? "Auto normal bank can only be used during hit object placement" : string.Empty; } private void updateAdditionBankTernaryButtonTooltips() { bool enabled = SelectionHandler.SelectionAdditionBanksEnabled.Value; - foreach (var ternaryButton in SampleAdditionBankTernaryStates) + foreach (var ternaryButton in SampleBankTernaryStates) { - ternaryButton.Enabled.Value = enabled; - ternaryButton.Tooltip = !enabled ? "Add an addition sample first to be able to set a bank" : string.Empty; + ternaryButton.AdditionsButton.Enabled.Value = enabled; + ternaryButton.AdditionsButton.TooltipText = !enabled ? "Add an addition sample first to be able to set a bank" : string.Empty; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 4ca3f93f13..5e8637c1ac 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -300,7 +300,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline createStateBindables(); updateTernaryStates(); - togglesCollection.AddRange(createTernaryButtons().Select(b => new DrawableTernaryButton(b) { RelativeSizeAxes = Axes.None, Size = new Vector2(40, 40) })); + togglesCollection.AddRange(createTernaryButtons()); } private string? getCommonBank() => allRelevantSamples.Select(h => GetBankValue(h.samples)).Distinct().Count() == 1 @@ -444,10 +444,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - private IEnumerable createTernaryButtons() + private IEnumerable createTernaryButtons() { foreach ((string sampleName, var bindable) in selectionSampleStates) - yield return new TernaryButton(bindable, string.Empty, () => ComposeBlueprintContainer.GetIconForSample(sampleName)); + { + yield return new DrawableTernaryButton + { + Current = bindable, + Description = string.Empty, + CreateIcon = () => ComposeBlueprintContainer.GetIconForSample(sampleName), + RelativeSizeAxes = Axes.None, + Size = new Vector2(40, 40), + }; + } } private void addHitSample(string sampleName) @@ -516,7 +525,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (item is not DrawableTernaryButton button) return base.OnKeyDown(e); - button.Button.Toggle(); + button.Toggle(); } return true; From 253b9cbbdd3ef5a3e78ec4401a44096315874956 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 9 Jan 2025 16:51:52 +0000 Subject: [PATCH 196/620] Add new osu!stable registry ProgId --- osu.Desktop/OsuGameDesktop.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 2d3f4e0ed6..c33608832f 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -67,7 +67,12 @@ namespace osu.Desktop { try { - stableInstallPath = getStableInstallPathFromRegistry(); + stableInstallPath = getStableInstallPathFromRegistry("osustable.File.osz"); + + if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath)) + return stableInstallPath; + + stableInstallPath = getStableInstallPathFromRegistry("osu!"); if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath)) return stableInstallPath; @@ -89,9 +94,9 @@ namespace osu.Desktop } [SupportedOSPlatform("windows")] - private string? getStableInstallPathFromRegistry() + private string? getStableInstallPathFromRegistry(string progId) { - using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!")) + using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey(progId)) return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); } From 0509623ef662e9d6e0f5149cb1dba3cd6cc20f51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 14:48:18 +0900 Subject: [PATCH 197/620] Ignore realm `List` type --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index ccd6db354b..8f5e642f94 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -840,6 +840,7 @@ See the LICENCE file in the repository root for full licence text. True True True + True True True True From 48196949e080e1f0057d20e3bb637cfc9b4989fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Jan 2025 15:29:40 +0100 Subject: [PATCH 198/620] Add combo colour override control to editor Closes https://github.com/ppy/osu/issues/25608. Logic mostly matching stable. All operations are done on `ComboOffset` which still makes overridden combo colours weirdly relatively dependent on each other rather than them be an "absolute" choice, but alas... As per stable, two consecutive new combos can use the same colour only if they are separated by a break: https://github.com/peppy/osu-stable-reference/blob/52f3f75ed7efd7b9eb56e1e45c95bb91504337be/osu!/GameModes/Edit/Modes/EditorModeCompose.cs#L4564-L4571 This control is only available once the user has changed the combo colours from defaults; additionally, only a single new combo object must be selected for the colour selector to show up. --- .../Edit/CatchHitObjectComposer.cs | 3 +- .../Edit/OsuHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 5 +- .../Objects/Types/IHasComboInformation.cs | 3 + .../TernaryButtons/NewComboTernaryButton.cs | 278 ++++++++++++++++++ .../Components/ComposeBlueprintContainer.cs | 11 +- 6 files changed, 289 insertions(+), 13 deletions(-) create mode 100644 osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index e0d80e0e64..7bb5539963 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -18,7 +18,6 @@ using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -72,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Edit protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider); - protected override IEnumerable CreateTernaryButtons() + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() .Concat(DistanceSnapProvider.CreateTernaryButtons()); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e8b9d0544e..f5e7ff6004 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector(); - protected override IEnumerable CreateTernaryButtons() + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() .Append(new DrawableTernaryButton { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 9f277b6190..15b60114af 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -370,7 +371,7 @@ namespace osu.Game.Rulesets.Edit /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; + protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. @@ -429,7 +430,7 @@ namespace osu.Game.Rulesets.Edit } else { - if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button) + if (togglesCollection.ChildrenOfType().ElementAtOrDefault(rightIndex) is DrawableTernaryButton button) { button.Toggle(); return true; diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 3aa68197ec..cc521aeab7 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -50,6 +50,9 @@ namespace osu.Game.Rulesets.Objects.Types /// new bool NewCombo { get; set; } + /// + new int ComboOffset { get; set; } + /// /// Bindable exposure of . /// diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs new file mode 100644 index 0000000000..effe35c0c3 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -0,0 +1,278 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.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.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Screens.Edit.Components.TernaryButtons +{ + public partial class NewComboTernaryButton : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + private readonly BindableList selectedHitObjects = new BindableList(); + private readonly BindableList comboColours = new BindableList(); + + private Container mainButtonContainer = null!; + private ColourPickerButton pickerButton = null!; + + [BackgroundDependencyLoader] + private void load(EditorBeatmap editorBeatmap) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChildren = new Drawable[] + { + mainButtonContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 30 }, + Child = new DrawableTernaryButton + { + Current = Current, + Description = "New combo", + CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }, + }, + }, + pickerButton = new ColourPickerButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 25, + ComboColours = { BindTarget = comboColours } + } + }; + + selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects); + if (editorBeatmap.BeatmapSkin != null) + comboColours.BindTo(editorBeatmap.BeatmapSkin.ComboColours); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedHitObjects.BindCollectionChanged((_, _) => updateState()); + comboColours.BindCollectionChanged((_, _) => updateState()); + Current.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + if (Current.Value == TernaryState.True && selectedHitObjects.Count == 1 && selectedHitObjects.Single() is IHasComboInformation hasCombo && comboColours.Count > 1) + { + mainButtonContainer.Padding = new MarginPadding { Right = 30 }; + pickerButton.SelectedHitObject.Value = hasCombo; + pickerButton.Alpha = 1; + } + else + { + mainButtonContainer.Padding = new MarginPadding(); + pickerButton.Alpha = 0; + } + } + + private partial class ColourPickerButton : OsuButton, IHasPopover + { + public BindableList ComboColours { get; } = new BindableList(); + public Bindable SelectedHitObject { get; } = new Bindable(); + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private SpriteIcon icon = null!; + + [BackgroundDependencyLoader] + private void load() + { + Add(icon = new SpriteIcon + { + Icon = FontAwesome.Solid.Palette, + Size = new Vector2(16), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + Action = this.ShowPopover; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + ComboColours.BindCollectionChanged((_, _) => updateState()); + SelectedHitObject.BindValueChanged(val => + { + if (val.OldValue != null) + val.OldValue.ComboIndexWithOffsetsBindable.ValueChanged -= onComboIndexChanged; + + updateState(); + + if (val.NewValue != null) + val.NewValue.ComboIndexWithOffsetsBindable.ValueChanged += onComboIndexChanged; + }, true); + } + + private void onComboIndexChanged(ValueChangedEvent _) => updateState(); + + private void updateState() + { + if (SelectedHitObject.Value == null) + { + BackgroundColour = colourProvider.Background3; + icon.Colour = BackgroundColour.Darken(0.5f); + icon.Blending = BlendingParameters.Additive; + Enabled.Value = false; + } + else + { + BackgroundColour = ComboColours[comboIndexFor(SelectedHitObject.Value, ComboColours)]; + icon.Colour = OsuColour.ForegroundTextColourFor(BackgroundColour); + icon.Blending = BlendingParameters.Inherit; + Enabled.Value = true; + } + } + + public Popover GetPopover() => new ComboColourPalettePopover(ComboColours, SelectedHitObject.Value.AsNonNull(), editorBeatmap); + } + + private partial class ComboColourPalettePopover : OsuPopover + { + private readonly IReadOnlyList comboColours; + private readonly IHasComboInformation hasComboInformation; + private readonly EditorBeatmap editorBeatmap; + + public ComboColourPalettePopover(IReadOnlyList comboColours, IHasComboInformation hasComboInformation, EditorBeatmap editorBeatmap) + { + this.comboColours = comboColours; + this.hasComboInformation = hasComboInformation; + this.editorBeatmap = editorBeatmap; + + AllowableAnchors = [Anchor.CentreRight]; + } + + [BackgroundDependencyLoader] + private void load() + { + Debug.Assert(comboColours.Count > 0); + var hitObject = hasComboInformation as HitObject; + Debug.Assert(hitObject != null); + + FillFlowContainer container; + + Child = container = new FillFlowContainer + { + Width = 230, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + }; + + int selectedColourIndex = comboIndexFor(hasComboInformation, comboColours); + + for (int i = 0; i < comboColours.Count; i++) + { + int index = i; + + if (getPreviousHitObjectWithCombo(editorBeatmap, hitObject) is IHasComboInformation previousHasCombo + && index == comboIndexFor(previousHasCombo, comboColours) + && !canReuseLastComboColour(editorBeatmap, hitObject)) + { + continue; + } + + container.Add(new OsuClickableContainer + { + Size = new Vector2(50), + Masking = true, + CornerRadius = 25, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = comboColours[index], + }, + selectedColourIndex == index + ? new SpriteIcon + { + Icon = FontAwesome.Solid.Check, + Size = new Vector2(24), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = OsuColour.ForegroundTextColourFor(comboColours[index]), + } + : Empty() + }, + Action = () => + { + int comboDifference = index - selectedColourIndex; + if (comboDifference == 0) + return; + + int newOffset = hasComboInformation.ComboOffset + comboDifference; + // `newOffset` must be positive to serialise correctly - this implements the true math "modulus" rather than the built-in "remainder" % op + // which can return negative results when the first operand is negative + newOffset -= (int)Math.Floor((double)newOffset / comboColours.Count) * comboColours.Count; + + hasComboInformation.ComboOffset = newOffset; + editorBeatmap.BeginChange(); + editorBeatmap.Update((HitObject)hasComboInformation); + editorBeatmap.EndChange(); + this.HidePopover(); + } + }); + } + } + + private static IHasComboInformation? getPreviousHitObjectWithCombo(EditorBeatmap editorBeatmap, HitObject hitObject) + => editorBeatmap.HitObjects.TakeWhile(ho => ho != hitObject).LastOrDefault() as IHasComboInformation; + + private static bool canReuseLastComboColour(EditorBeatmap editorBeatmap, HitObject hitObject) + { + double? closestBreakEnd = editorBeatmap.Breaks.Select(b => b.EndTime) + .Where(t => t <= hitObject.StartTime) + .OrderBy(t => t) + .LastOrDefault(); + + if (closestBreakEnd == null) + return false; + + return editorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= closestBreakEnd) == hitObject; + } + } + + // compare `EditorBeatmapSkin.updateColours()` et al. for reasoning behind the off-by-one index rotation + private static int comboIndexFor(IHasComboInformation hasComboInformation, IReadOnlyCollection comboColours) + => (hasComboInformation.ComboIndexWithOffsets + comboColours.Count - 1) % comboColours.Count; + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index bbb4095206..5d93c4ea9d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -237,22 +237,17 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A collection of states which will be displayed to the user in the toolbox. /// - public DrawableTernaryButton[] MainTernaryStates { get; private set; } + public Drawable[] MainTernaryStates { get; private set; } public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; } /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() + protected virtual IEnumerable CreateTernaryButtons() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - yield return new DrawableTernaryButton - { - Current = NewCombo, - Description = "New combo", - CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }, - }; + yield return new NewComboTernaryButton { Current = NewCombo }; foreach (var kvp in SelectionHandler.SelectionSampleStates) { From 0d9a3428ae4b447d72e908f7fdb4f617525c0905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Jan 2025 14:13:03 +0100 Subject: [PATCH 199/620] Merge conditionals --- .../Objects/CatchHitObject.cs | 21 ++++++++----------- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 21 ++++++++----------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 3c7ead09af..deaa566864 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -163,20 +163,17 @@ namespace osu.Game.Rulesets.Catch.Objects int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; - // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. - if (this is not BananaShower) + // - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + // - At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (this is not BananaShower && (NewCombo || lastObj == null || lastObj is BananaShower)) { - // At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, - // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. - if (NewCombo || lastObj == null || lastObj is BananaShower) - { - inCurrentCombo = 0; - index++; - indexWithOffsets += ComboOffset + 1; + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; - if (lastObj != null) - lastObj.LastInCombo = true; - } + if (lastObj != null) + lastObj.LastInCombo = true; } ComboIndex = index; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 937e0bda23..9623d1999b 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -188,20 +188,17 @@ namespace osu.Game.Rulesets.Osu.Objects int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; - // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. - if (this is not Spinner) + // - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + // - At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (this is not Spinner && (NewCombo || lastObj == null || lastObj is Spinner)) { - // At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, - // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. - if (NewCombo || lastObj == null || lastObj is Spinner) - { - inCurrentCombo = 0; - index++; - indexWithOffsets += ComboOffset + 1; + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; - if (lastObj != null) - lastObj.LastInCombo = true; - } + if (lastObj != null) + lastObj.LastInCombo = true; } ComboIndex = index; From 94ea003d90f0d96ebe82ab1a80abb6e2672f060a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Jan 2025 01:42:54 +0900 Subject: [PATCH 200/620] Update game `ScrollContainer` usage in line with framework changes See https://github.com/ppy/osu-framework/pull/6467. --- .../UserInterface/TestSceneSectionsContainer.cs | 2 +- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 8 ++++---- osu.Game/Graphics/Containers/SectionsContainer.cs | 4 ++-- .../Containers/UserTrackingScrollContainer.cs | 4 ++-- osu.Game/Online/Leaderboards/Leaderboard.cs | 4 ++-- osu.Game/Overlays/Chat/ChannelScrollContainer.cs | 4 ++-- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 ++++---- osu.Game/Overlays/NewsOverlay.cs | 2 +- osu.Game/Overlays/OnlineOverlay.cs | 2 +- osu.Game/Overlays/OverlayScrollContainer.cs | 6 +++--- osu.Game/Overlays/WikiOverlay.cs | 2 +- .../Edit/Compose/Components/Timeline/Timeline.cs | 4 ++-- .../Components/Timeline/ZoomableScrollContainer.cs | 2 +- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 6 +++--- osu.Game/Screens/Ranking/ScorePanelList.cs | 4 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 12 ++++++------ 17 files changed, 38 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs index 3a1eb554ab..7ec57c9e5e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("section top is visible", () => { var scrollContainer = container.ChildrenOfType().Single(); - float sectionPosition = scrollContainer.GetChildPosInContent(container.Children[scrollIndex]); + double sectionPosition = scrollContainer.GetChildPosInContent(container.Children[scrollIndex]); return scrollContainer.Current < sectionPosition; }); } diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index a3cd5a4902..f40c91e27e 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -59,11 +59,11 @@ namespace osu.Game.Graphics.Containers /// An added amount to scroll beyond the requirement to bring the target into view. public void ScrollIntoView(Drawable d, bool animated = true, float extraScroll = 0) { - float childPos0 = GetChildPosInContent(d); - float childPos1 = GetChildPosInContent(d, d.DrawSize); + double childPos0 = GetChildPosInContent(d); + double childPos1 = GetChildPosInContent(d, d.DrawSize); - float minPos = Math.Min(childPos0, childPos1); - float maxPos = Math.Max(childPos0, childPos1); + double minPos = Math.Min(childPos0, childPos1); + double maxPos = Math.Max(childPos0, childPos1); if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent)) ScrollTo(minPos - extraScroll, animated); diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 9f41c4eff2..828fc9704c 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -208,7 +208,7 @@ namespace osu.Game.Graphics.Containers private float getScrollTargetForDrawable(Drawable target) { // implementation similar to ScrollIntoView but a bit more nuanced. - return scrollContainer.GetChildPosInContent(target) - scrollContainer.DisplayableContent * scroll_y_centre; + return (float)(scrollContainer.GetChildPosInContent(target) - scrollContainer.DisplayableContent * scroll_y_centre); } public void ScrollToTop() => scrollContainer.ScrollTo(0); @@ -259,7 +259,7 @@ namespace osu.Game.Graphics.Containers updateSectionsMargin(); } - float currentScroll = scrollContainer.Current; + float currentScroll = (float)scrollContainer.Current; if (currentScroll != lastKnownScroll) { diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index 354a57b7d2..30b9eeb74c 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers { } - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) + protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = default) { UserScrolling = true; base.OnUserScroll(value, animated, distanceDecay); @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.Containers base.ScrollFromMouseEvent(e); } - public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null) + public new void ScrollTo(double value, bool animated = true, double? distanceDecay = null) { UserScrolling = false; base.ScrollTo(value, animated, distanceDecay); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index d76da54adf..3c25d6f789 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -375,8 +375,8 @@ namespace osu.Game.Online.Leaderboards { base.UpdateAfterChildren(); - float fadeBottom = scrollContainer.Current + scrollContainer.DrawHeight; - float fadeTop = scrollContainer.Current + LeaderboardScore.HEIGHT; + float fadeBottom = (float)(scrollContainer.Current + scrollContainer.DrawHeight); + float fadeTop = (float)(scrollContainer.Current + LeaderboardScore.HEIGHT); if (!scrollContainer.IsScrolledToEnd()) fadeBottom -= LeaderboardScore.HEIGHT; diff --git a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs index 6d8b21a7c5..b621b555b0 100644 --- a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs +++ b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs @@ -41,13 +41,13 @@ namespace osu.Game.Overlays.Chat #region Scroll handling - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) + protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = null) { base.OnUserScroll(value, animated, distanceDecay); updateTrackState(); } - public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null) + public new void ScrollTo(double value, bool animated = true, double? distanceDecay = null) { base.ScrollTo(value, animated, distanceDecay); updateTrackState(); diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index cb7cd03584..2f0461eb40 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.Chat if (chatLine == null) return; - float center = scroll.GetChildPosInContent(chatLine, chatLine.DrawSize / 2) - scroll.DisplayableContent / 2; + double center = scroll.GetChildPosInContent(chatLine, chatLine.DrawSize / 2) - scroll.DisplayableContent / 2; scroll.ScrollTo(Math.Clamp(center, 0, scroll.ScrollableExtent)); chatLine.Highlight(); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ed73340eeb..daac925dfb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -710,13 +710,13 @@ namespace osu.Game.Overlays.Mods // the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space. // note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns. - float leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent); - float rightVisibleBound = leftVisibleBound + DrawWidth; + double leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent); + double rightVisibleBound = leftVisibleBound + DrawWidth; // if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass. // this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past. - float leftMovementBound = Math.Min(Current, Target); - float rightMovementBound = Math.Max(Current, Target) + DrawWidth; + double leftMovementBound = Math.Min(Current, Target); + double rightMovementBound = Math.Max(Current, Target) + DrawWidth; foreach (var column in Child) { diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index cb9d940a05..81ac67bd89 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -136,7 +136,7 @@ namespace osu.Game.Overlays { base.UpdateAfterChildren(); sidebarContainer.Height = DrawHeight; - sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); + sidebarContainer.Y = (float)Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); } private void loadListing(int? year = null) diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 051873b394..cc5a1b9d2d 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -88,7 +88,7 @@ namespace osu.Game.Overlays base.UpdateAfterChildren(); // don't block header by applying padding equal to the visible header height - loadingContainer.Padding = new MarginPadding { Top = Math.Max(0, Header.Height - ScrollFlow.Current) }; + loadingContainer.Padding = new MarginPadding { Top = (float)Math.Max(0, Header.Height - ScrollFlow.Current) }; } } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 4328977a8d..66a8686a88 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays public ScrollBackButton Button { get; private set; } - private readonly Bindable lastScrollTarget = new Bindable(); + private readonly Bindable lastScrollTarget = new Bindable(); [BackgroundDependencyLoader] private void load() @@ -63,7 +63,7 @@ namespace osu.Game.Overlays Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden; } - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) + protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = default) { base.OnUserScroll(value, animated, distanceDecay); @@ -112,7 +112,7 @@ namespace osu.Game.Overlays private readonly Box background; private readonly SpriteIcon spriteIcon; - public Bindable LastScrollTarget = new Bindable(); + public Bindable LastScrollTarget = new Bindable(); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 14a25a909d..ef258da82b 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -100,7 +100,7 @@ namespace osu.Game.Overlays if (articlePage != null) { articlePage.SidebarContainer.Height = DrawHeight; - articlePage.SidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); + articlePage.SidebarContainer.Y = (float)Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index e5360e2eeb..5f46b3d937 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// /// The timeline's scroll position in the last frame. /// - private float lastScrollPosition; + private double lastScrollPosition; /// /// The track time in the last frame. @@ -322,7 +322,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => editorClock.TrackLength / Zoom; - public double TimeAtPosition(float x) + public double TimeAtPosition(double x) { return x / Content.DrawWidth * editorClock.TrackLength; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 31a0936eb4..9db14ce4c4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None) - => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing)); + => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, (float)Current), newZoom, duration, easing)); /// /// Invoked when has changed. diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index d2b6b834f8..f6694505dc 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -114,15 +114,15 @@ namespace osu.Game.Screens.Play.HUD if (requiresScroll && TrackedScore != null) { - float scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2; + double scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2; scroll.ScrollTo(scrollTarget); } const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT; - float fadeBottom = scroll.Current + scroll.DrawHeight; - float fadeTop = scroll.Current + panel_height; + float fadeBottom = (float)(scroll.Current + scroll.DrawHeight); + float fadeTop = (float)(scroll.Current + panel_height); if (scroll.IsScrolledToStart()) fadeTop -= panel_height; if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height; diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index e711bed729..b0e1c89121 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -334,7 +334,7 @@ namespace osu.Game.Screens.Ranking private partial class Scroll : OsuScrollContainer { - public new float Target => base.Target; + public new double Target => base.Target; public Scroll() : base(Direction.Horizontal) @@ -344,7 +344,7 @@ namespace osu.Game.Screens.Ranking /// /// The target that will be scrolled to instantaneously next frame. /// - public float? InstantScrollTarget; + public double? InstantScrollTarget; protected override void UpdateAfterChildren() { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 65c4133ea2..de12b36b17 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -611,12 +611,12 @@ namespace osu.Game.Screens.Select /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom; + private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom); /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => Scroll.Current - BleedTop; + private float visibleUpperBound => (float)(Scroll.Current - BleedTop); public void FlushPendingFilterOperations() { @@ -1006,7 +1006,7 @@ namespace osu.Game.Screens.Select // we take the difference in scroll height and apply to all visible panels. // this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer // to enter clamp-special-case mode where it animates completely differently to normal. - float scrollChange = scrollTarget.Value - Scroll.Current; + float scrollChange = (float)(scrollTarget.Value - Scroll.Current); Scroll.ScrollTo(scrollTarget.Value, false); foreach (var i in Scroll) i.Y += scrollChange; @@ -1217,12 +1217,12 @@ namespace osu.Game.Screens.Select private const float top_padding = 10; private const float bottom_padding = 70; - protected override float ToScrollbarPosition(float scrollPosition) + protected override float ToScrollbarPosition(double scrollPosition) { if (Precision.AlmostEquals(0, ScrollableExtent)) return 0; - return top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent); + return (float)(top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent)); } protected override float FromScrollbarPosition(float scrollbarPosition) @@ -1230,7 +1230,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) return 0; - return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))); + return (float)(ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)))); } } } From 5e9a7532d31d594a36013d19772e7ea4a95a0a46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 20:55:53 +0900 Subject: [PATCH 201/620] Add basic implementation of new beatmap carousel --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 189 +++++++++ .../Screens/SelectV2/BeatmapCarouselV2.cs | 205 ++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 371 ++++++++++++++++++ osu.Game/Screens/SelectV2/CarouselItem.cs | 41 ++ osu.Game/Screens/SelectV2/ICarouselFilter.cs | 23 ++ osu.Game/Screens/SelectV2/ICarouselPanel.cs | 23 ++ osu.Game/Tests/Beatmaps/TestBeatmapStore.cs | 2 +- 7 files changed, 853 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs create mode 100644 osu.Game/Screens/SelectV2/Carousel.cs create mode 100644 osu.Game/Screens/SelectV2/CarouselItem.cs create mode 100644 osu.Game/Screens/SelectV2/ICarouselFilter.cs create mode 100644 osu.Game/Screens/SelectV2/ICarouselPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs new file mode 100644 index 0000000000..75223adc2b --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -0,0 +1,189 @@ +// 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 System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2 : OsuManualInputManagerTestScene + { + private readonly BindableList beatmapSets = new BindableList(); + + [Cached(typeof(BeatmapStore))] + private BeatmapStore store; + + private OsuTextFlowContainer stats = null!; + private BeatmapCarouselV2 carousel = null!; + + private int beatmapCount; + + public TestSceneBeatmapCarouselV2() + { + store = new TestBeatmapStore + { + BeatmapSets = { BindTarget = beatmapSets } + }; + + beatmapSets.BindCollectionChanged((_, _) => + { + beatmapCount = beatmapSets.Sum(s => s.Beatmaps.Count); + }); + + Scheduler.AddDelayed(updateStats, 100, true); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create components", () => + { + beatmapSets.Clear(); + + Box topBox; + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 1), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 200), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 200), + }, + Content = new[] + { + new Drawable[] + { + topBox = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + }, + new Drawable[] + { + carousel = new BeatmapCarouselV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + RelativeSizeAxes = Axes.Y, + }, + }, + new[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + topBox.CreateProxy(), + } + } + }, + stats = new OsuTextFlowContainer(cp => cp.Font = FrameworkFont.Regular.With()) + { + Padding = new MarginPadding(10), + TextAnchor = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }; + }); + } + + [Test] + public void TestBasic() + { + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddStep("add 1 beatmap", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)))); + + AddStep("remove all beatmaps", () => beatmapSets.Clear()); + } + + [Test] + public void TestAddRemoveOneByOne() + { + AddRepeatStep("add beatmaps", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); + + AddRepeatStep("remove beatmaps", () => beatmapSets.RemoveAt(RNG.Next(0, beatmapSets.Count)), 20); + } + + [Test] + [Explicit] + public void TestInsane() + { + const int count = 200000; + + List generated = new List(); + + AddStep($"populate {count} test beatmaps", () => + { + generated.Clear(); + Task.Run(() => + { + for (int j = 0; j < count; j++) + generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }).ConfigureAwait(true); + }); + + AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3)); + AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2)); + AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count)); + + AddStep("add all beatmaps", () => beatmapSets.AddRange(generated)); + } + + private void updateStats() + { + if (carousel.IsNull()) + return; + + stats.Text = $""" + store + sets: {beatmapSets.Count} + beatmaps: {beatmapCount} + carousel: + sorting: {carousel.IsFiltering} + tracked: {carousel.ItemsTracked} + displayable: {carousel.DisplayableItems} + displayed: {carousel.VisibleItems} + """; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs new file mode 100644 index 0000000000..a54c2aceff --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs @@ -0,0 +1,205 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Select; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapCarouselV2 : Carousel + { + private IBindableList detachedBeatmaps = null!; + + private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + + public BeatmapCarouselV2() + { + DebounceDelay = 100; + DistanceOffscreenToPreload = 100; + + Filters = new ICarouselFilter[] + { + new Sorter(), + new Grouper(), + }; + + AddInternal(carouselPanelPool); + } + + [BackgroundDependencyLoader] + private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) + { + detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); + detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); + } + + protected override Drawable GetDrawableForDisplay(CarouselItem item) + { + var drawable = carouselPanelPool.Get(); + drawable.FlashColour(Color4.Red, 2000); + + return drawable; + } + + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + { + // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. + // right now we are managing this locally which is a bit of added overhead. + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps).Select(b => new BeatmapCarouselItem(b))); + break; + + case NotifyCollectionChangedAction.Remove: + + foreach (var set in beatmapSetInfos!) + { + foreach (var beatmap in set.Beatmaps) + Items.RemoveAll(i => i.Model is BeatmapInfo bi && beatmap.Equals(bi)); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + throw new NotImplementedException(); + + case NotifyCollectionChangedAction.Reset: + Items.Clear(); + break; + } + } + + public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); + + public void Filter(FilterCriteria criteria) + { + Criteria = criteria; + QueueFilter(); + } + } + + public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel + { + public CarouselItem? Item { get; set; } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + Size = new Vector2(500, Item.DrawHeight); + + InternalChildren = new Drawable[] + { + new Box + { + Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = Item.ToString() ?? string.Empty, + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + } + + public class BeatmapCarouselItem : CarouselItem + { + public readonly Guid ID; + + public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; + + public BeatmapCarouselItem(object model) + : base(model) + { + ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); + } + + public override string? ToString() + { + switch (Model) + { + case BeatmapInfo bi: + return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; + + case BeatmapSetInfo si: + return $"{si.Metadata}"; + } + + return Model.ToString(); + } + } + + public class Grouper : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + // TODO: perform grouping based on FilterCriteria + + CarouselItem? lastItem = null; + + var newItems = new List(); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (item.Model is BeatmapInfo b1) + { + // Add set header + if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) + newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); + } + + newItems.Add(item); + lastItem = item; + } + + return newItems; + }, cancellationToken).ConfigureAwait(false); + } + + public class Sorter : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + return items.OrderDescending(Comparer.Create((a, b) => + { + if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) + return ab.OnlineID.CompareTo(bb.OnlineID); + + if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) + return aItem.ID.CompareTo(bItem.ID); + + return 0; + })); + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs new file mode 100644 index 0000000000..2f3c47a0a3 --- /dev/null +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -0,0 +1,371 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// A highly efficient vertical list display that is used primarily for the song select screen, + /// but flexible enough to be used for other use cases. + /// + public abstract partial class Carousel : CompositeDrawable + { + /// + /// A collection of filters which should be run each time a is executed. + /// + public IEnumerable Filters { get; init; } = Enumerable.Empty(); + + /// + /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. + /// + public float BleedTop { get; set; } = 0; + + /// + /// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it. + /// + public float BleedBottom { get; set; } = 0; + + /// + /// The number of pixels outside the carousel's vertical bounds to manifest drawables. + /// This allows preloading content before it scrolls into view. + /// + public float DistanceOffscreenToPreload { get; set; } = 0; + + /// + /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. + /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. + /// + public int DebounceDelay { get; set; } = 0; + + /// + /// Whether an asynchronous filter / group operation is currently underway. + /// + public bool IsFiltering => !filterTask.IsCompleted; + + /// + /// The number of displayable items currently being tracked (before filtering). + /// + public int ItemsTracked => Items.Count; + + /// + /// The number of carousel items currently in rotation for display. + /// + public int DisplayableItems => displayedCarouselItems?.Count ?? 0; + + /// + /// The number of items currently actualised into drawables. + /// + public int VisibleItems => scroll.Panels.Count; + + /// + /// All items which are to be considered for display in this carousel. + /// Mutating this list will automatically queue a . + /// + protected readonly BindableList Items = new BindableList(); + + private List? displayedCarouselItems; + + private readonly DoublePrecisionScroll scroll; + + protected Carousel() + { + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + scroll = new DoublePrecisionScroll + { + RelativeSizeAxes = Axes.Both, + Masking = false, + } + }; + + Items.BindCollectionChanged((_, _) => QueueFilter()); + } + + /// + /// Queue an asynchronous filter operation. + /// + public void QueueFilter() => Scheduler.AddOnce(() => filterTask = performFilter()); + + /// + /// Create a drawable for the given carousel item so it can be displayed. + /// + /// + /// For efficiency, it is recommended the drawables are retrieved from a . + /// + /// The item which should be represented by the returned drawable. + /// The manifested drawable. + protected abstract Drawable GetDrawableForDisplay(CarouselItem item); + + #region Filtering and display preparation + + private Task filterTask = Task.CompletedTask; + private CancellationTokenSource cancellationSource = new CancellationTokenSource(); + + private async Task performFilter() + { + Debug.Assert(SynchronizationContext.Current != null); + + var cts = new CancellationTokenSource(); + + lock (this) + { + cancellationSource.Cancel(); + cancellationSource = cts; + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + IEnumerable items = new List(Items); + + await Task.Run(async () => + { + try + { + if (DebounceDelay > 0) + { + log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); + await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false); + } + + foreach (var filter in Filters) + { + log($"Performing {filter.GetType().ReadableName()}"); + items = await filter.Run(items, cts.Token).ConfigureAwait(false); + } + + log("Updating Y positions"); + await updateYPositions(items, cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + log("Cancelled due to newer request arriving"); + } + }, cts.Token).ConfigureAwait(true); + + if (cts.Token.IsCancellationRequested) + return; + + log("Items ready for display"); + displayedCarouselItems = items.ToList(); + displayedRange = null; + + void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString().Substring(0, 5)}] {stopwatch.ElapsedMilliseconds} ms: {text}"); + } + + private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => + { + const float spacing = 10; + float yPos = 0; + + foreach (var item in carouselItems) + { + item.CarouselYPosition = yPos; + yPos += item.DrawHeight + spacing; + } + }, cancellationToken).ConfigureAwait(false); + + #endregion + + #region Display handling + + private DisplayRange? displayedRange; + + private readonly CarouselItem carouselBoundsItem = new BoundsCarouselItem(); + + /// + /// The position of the lower visible bound with respect to the current scroll position. + /// + private float visibleBottomBound => (float)(scroll.Current + DrawHeight + BleedBottom); + + /// + /// The position of the upper visible bound with respect to the current scroll position. + /// + private float visibleUpperBound => (float)(scroll.Current - BleedTop); + + protected override void Update() + { + base.Update(); + + if (displayedCarouselItems == null) + return; + + var range = getDisplayRange(); + + if (range != displayedRange) + { + Logger.Log($"Updating displayed range of carousel from {displayedRange} to {range}"); + displayedRange = range; + + updateDisplayedRange(range); + } + } + + private DisplayRange getDisplayRange() + { + Debug.Assert(displayedCarouselItems != null); + + // Find index range of all items that should be on-screen + carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload; + int firstIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + if (firstIndex < 0) firstIndex = ~firstIndex; + + carouselBoundsItem.CarouselYPosition = visibleBottomBound + DistanceOffscreenToPreload; + int lastIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + if (lastIndex < 0) lastIndex = ~lastIndex; + + firstIndex = Math.Max(0, firstIndex - 1); + lastIndex = Math.Max(0, lastIndex - 1); + + return new DisplayRange(firstIndex, lastIndex); + } + + private void updateDisplayedRange(DisplayRange range) + { + Debug.Assert(displayedCarouselItems != null); + + List toDisplay = range.Last - range.First == 0 + ? new List() + : displayedCarouselItems.GetRange(range.First, range.Last - range.First + 1); + + // Iterate over all panels which are already displayed and figure which need to be displayed / removed. + foreach (var panel in scroll.Panels) + { + var carouselPanel = (ICarouselPanel)panel; + + // The case where we're intending to display this panel, but it's already displayed. + // Note that we **must compare the model here** as the CarouselItems may be fresh instances due to a filter operation. + var existing = toDisplay.FirstOrDefault(i => i.Model == carouselPanel.Item!.Model); + + if (existing != null) + { + carouselPanel.Item = existing; + toDisplay.Remove(existing); + continue; + } + + // If the new display range doesn't contain the panel, it's no longer required for display. + expirePanelImmediately(panel); + } + + // Add any new items which need to be displayed and haven't yet. + foreach (var item in toDisplay) + { + var drawable = GetDrawableForDisplay(item); + + if (drawable is not ICarouselPanel carouselPanel) + throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); + + carouselPanel.Item = item; + scroll.Add(drawable); + } + + // Update the total height of all items (to make the scroll container scrollable through the full height even though + // most items are not displayed / loaded). + if (displayedCarouselItems.Count > 0) + { + var lastItem = displayedCarouselItems[^1]; + scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight)); + } + else + scroll.SetLayoutHeight(0); + } + + private static void expirePanelImmediately(Drawable panel) + { + panel.FinishTransforms(); + panel.Expire(); + } + + #endregion + + #region Internal helper classes + + private record DisplayRange(int First, int Last); + + /// + /// Implementation of scroll container which handles very large vertical lists by internally using double precision + /// for pre-display Y values. + /// + private partial class DoublePrecisionScroll : OsuScrollContainer + { + public readonly Container Panels; + + public void SetLayoutHeight(float height) => Panels.Height = height; + + public DoublePrecisionScroll() + { + // Managing our own custom layout within ScrollContent causes feedback with public ScrollContainer calculations, + // so we must maintain one level of separation from ScrollContent. + base.Add(Panels = new Container + { + Name = "Layout content", + RelativeSizeAxes = Axes.X, + }); + } + + public override void Clear(bool disposeChildren) + { + Panels.Height = 0; + Panels.Clear(disposeChildren); + } + + public override void Add(Drawable drawable) + { + if (drawable is not ICarouselPanel) + throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); + + Panels.Add(drawable); + } + + public override double GetChildPosInContent(Drawable d, Vector2 offset) + { + if (d is not ICarouselPanel panel) + return base.GetChildPosInContent(d, offset); + + return panel.YPosition + offset.X; + } + + protected override void ApplyCurrentToContent() + { + Debug.Assert(ScrollDirection == Direction.Vertical); + + double scrollableExtent = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; + + foreach (var d in Panels) + d.Y = (float)(((ICarouselPanel)d).YPosition + scrollableExtent); + } + } + + private class BoundsCarouselItem : CarouselItem + { + public override float DrawHeight => 0; + + public BoundsCarouselItem() + : base(new object()) + { + } + } + + #endregion + } +} diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs new file mode 100644 index 0000000000..69abe86205 --- /dev/null +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// Represents a single display item for display in a . + /// This is used to house information related to the attached model that helps with display and tracking. + /// + public abstract class CarouselItem : IComparable + { + /// + /// The model this item is representing. + /// + public readonly object Model; + + /// + /// The current Y position in the carousel. This is managed by and should not be set manually. + /// + public double CarouselYPosition { get; set; } + + /// + /// The height this item will take when displayed. + /// + public abstract float DrawHeight { get; } + + protected CarouselItem(object model) + { + Model = model; + } + + public int CompareTo(CarouselItem? other) + { + if (other == null) return 1; + + return CarouselYPosition.CompareTo(other.CarouselYPosition); + } + } +} diff --git a/osu.Game/Screens/SelectV2/ICarouselFilter.cs b/osu.Game/Screens/SelectV2/ICarouselFilter.cs new file mode 100644 index 0000000000..82aca18b85 --- /dev/null +++ b/osu.Game/Screens/SelectV2/ICarouselFilter.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// An interface representing a filter operation which can be run on a . + /// + public interface ICarouselFilter + { + /// + /// Execute the filter operation. + /// + /// The items to be filtered. + /// A cancellation token. + /// The post-filtered items. + Task> Run(IEnumerable items, CancellationToken cancellationToken); + } +} diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs new file mode 100644 index 0000000000..2f03bd8e26 --- /dev/null +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// An interface to be attached to any s which are used for display inside a . + /// + public interface ICarouselPanel + { + /// + /// The Y position which should be used for displaying this item within the carousel. + /// + double YPosition => Item!.CarouselYPosition; + + /// + /// The carousel item this drawable is representing. This is managed by and should not be set manually. + /// + CarouselItem? Item { get; set; } + } +} diff --git a/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs b/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs index 1734f1397f..eaef2af7c8 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs @@ -11,6 +11,6 @@ namespace osu.Game.Tests.Beatmaps internal partial class TestBeatmapStore : BeatmapStore { public readonly BindableList BeatmapSets = new BindableList(); - public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) => BeatmapSets; + public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) => BeatmapSets.GetBoundCopy(); } } From 288be46b17d3c87347e2e8ed1df8f7af3df379e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 19:34:56 +0900 Subject: [PATCH 202/620] Add basic selection support --- .../Screens/SelectV2/BeatmapCarouselV2.cs | 54 ++++++++++++++++++- osu.Game/Screens/SelectV2/Carousel.cs | 40 ++++++++++++++ osu.Game/Screens/SelectV2/CarouselItem.cs | 7 ++- osu.Game/Screens/SelectV2/ICarouselFilter.cs | 2 +- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 4 +- 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs index a54c2aceff..37c33446da 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs @@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Sprites; @@ -23,6 +24,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { + [Cached] public partial class BeatmapCarouselV2 : Carousel { private IBindableList detachedBeatmaps = null!; @@ -102,7 +104,48 @@ namespace osu.Game.Screens.SelectV2 public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel { - public CarouselItem? Item { get; set; } + [Resolved] + private BeatmapCarouselV2 carousel { get; set; } = null!; + + public CarouselItem? Item + { + get => item; + set + { + item = value; + + selected.UnbindBindings(); + + if (item != null) + selected.BindTo(item.Selected); + } + } + + private readonly BindableBool selected = new BindableBool(); + private CarouselItem? item; + + [BackgroundDependencyLoader] + private void load() + { + selected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + Item = null; + } protected override void PrepareForUse() { @@ -111,6 +154,7 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); Size = new Vector2(500, Item.DrawHeight); + Masking = true; InternalChildren = new Drawable[] { @@ -128,6 +172,12 @@ namespace osu.Game.Screens.SelectV2 } }; } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } } public class BeatmapCarouselItem : CarouselItem @@ -165,7 +215,7 @@ namespace osu.Game.Screens.SelectV2 CarouselItem? lastItem = null; - var newItems = new List(); + var newItems = new List(items.Count()); foreach (var item in items) { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 2f3c47a0a3..45dadc3455 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -77,8 +77,28 @@ namespace osu.Game.Screens.SelectV2 /// All items which are to be considered for display in this carousel. /// Mutating this list will automatically queue a . /// + /// + /// Note that an may add new items which are displayed but not tracked in this list. + /// protected readonly BindableList Items = new BindableList(); + /// + /// The currently selected model. + /// + /// + /// Setting this will ensure is set to true only on the matching . + /// Of note, if no matching item exists all items will be deselected while waiting for potential new item which matches. + /// + public virtual object? CurrentSelection + { + get => currentSelection; + set + { + currentSelection = value; + updateSelection(); + } + } + private List? displayedCarouselItems; private readonly DoublePrecisionScroll scroll; @@ -169,6 +189,8 @@ namespace osu.Game.Screens.SelectV2 displayedCarouselItems = items.ToList(); displayedRange = null; + updateSelection(); + void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString().Substring(0, 5)}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } @@ -186,6 +208,24 @@ namespace osu.Game.Screens.SelectV2 #endregion + #region Selection handling + + private object? currentSelection; + + private void updateSelection() + { + if (displayedCarouselItems == null) return; + + // TODO: this is ugly, we probably should stop exposing CarouselItem externally. + foreach (var item in Items) + item.Selected.Value = item.Model == currentSelection; + + foreach (var item in displayedCarouselItems) + item.Selected.Value = item.Model == currentSelection; + } + + #endregion + #region Display handling private DisplayRange? displayedRange; diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 69abe86205..4636e8a32f 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -2,22 +2,25 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; namespace osu.Game.Screens.SelectV2 { /// - /// Represents a single display item for display in a . + /// Represents a single display item for display in a . /// This is used to house information related to the attached model that helps with display and tracking. /// public abstract class CarouselItem : IComparable { + public readonly BindableBool Selected = new BindableBool(); + /// /// The model this item is representing. /// public readonly object Model; /// - /// The current Y position in the carousel. This is managed by and should not be set manually. + /// The current Y position in the carousel. This is managed by and should not be set manually. /// public double CarouselYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/ICarouselFilter.cs b/osu.Game/Screens/SelectV2/ICarouselFilter.cs index 82aca18b85..f510a7cd4b 100644 --- a/osu.Game/Screens/SelectV2/ICarouselFilter.cs +++ b/osu.Game/Screens/SelectV2/ICarouselFilter.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace osu.Game.Screens.SelectV2 { /// - /// An interface representing a filter operation which can be run on a . + /// An interface representing a filter operation which can be run on a . /// public interface ICarouselFilter { diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 2f03bd8e26..97c585492c 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; namespace osu.Game.Screens.SelectV2 { /// - /// An interface to be attached to any s which are used for display inside a . + /// An interface to be attached to any s which are used for display inside a . /// public interface ICarouselPanel { @@ -16,7 +16,7 @@ namespace osu.Game.Screens.SelectV2 double YPosition => Item!.CarouselYPosition; /// - /// The carousel item this drawable is representing. This is managed by and should not be set manually. + /// The carousel item this drawable is representing. This is managed by and should not be set manually. /// CarouselItem? Item { get; set; } } From ad04681b2856d9e821a1e4a5f65a2b6b8ced0993 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 20:24:14 +0900 Subject: [PATCH 203/620] Add scroll position maintaining --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 30 ++++++++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 36 ++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 75223adc2b..dde4ef88bd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Framework.Utils; @@ -34,6 +35,8 @@ namespace osu.Game.Tests.Visual.SongSelect private OsuTextFlowContainer stats = null!; private BeatmapCarouselV2 carousel = null!; + private OsuScrollContainer scroll => carousel.ChildrenOfType().Single(); + private int beatmapCount; public TestSceneBeatmapCarouselV2() @@ -136,6 +139,33 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("remove all beatmaps", () => beatmapSets.Clear()); } + [Test] + public void TestScrollPositionVelocityMaintained() + { + Quad positionBefore = default; + + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + + AddStep("scroll to last item", () => scroll.ScrollToEnd(false)); + + AddStep("select last beatmap", () => carousel.CurrentSelection = beatmapSets.First()); + + AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); + + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + + AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); + AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + [Test] public void TestAddRemoveOneByOne() { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 45dadc3455..54a671949f 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -94,7 +94,13 @@ namespace osu.Game.Screens.SelectV2 get => currentSelection; set { + if (currentSelectionCarouselItem != null) + currentSelectionCarouselItem.Selected.Value = false; + currentSelection = value; + + currentSelectionCarouselItem = null; + currentSelectionYPosition = null; updateSelection(); } } @@ -211,17 +217,37 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling private object? currentSelection; + private CarouselItem? currentSelectionCarouselItem; + private double? currentSelectionYPosition; private void updateSelection() { + currentSelectionCarouselItem = null; + if (displayedCarouselItems == null) return; - // TODO: this is ugly, we probably should stop exposing CarouselItem externally. - foreach (var item in Items) - item.Selected.Value = item.Model == currentSelection; - foreach (var item in displayedCarouselItems) - item.Selected.Value = item.Model == currentSelection; + { + bool isSelected = item.Model == currentSelection; + + if (isSelected) + { + currentSelectionCarouselItem = item; + + if (currentSelectionYPosition != item.CarouselYPosition) + { + if (currentSelectionYPosition != null) + { + float adjustment = (float)(item.CarouselYPosition - currentSelectionYPosition.Value); + scroll.OffsetScrollPosition(adjustment); + } + + currentSelectionYPosition = item.CarouselYPosition; + } + } + + item.Selected.Value = isSelected; + } } #endregion From 6fbab1bbceb4d26838bb35a3c5cf824151320a37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 20:30:41 +0900 Subject: [PATCH 204/620] Stop exposing `CarouselItem` externally --- osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs | 6 ++++-- osu.Game/Screens/SelectV2/Carousel.cs | 11 +++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs index 37c33446da..dd4aaadfbb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs @@ -60,6 +60,8 @@ namespace osu.Game.Screens.SelectV2 return drawable; } + protected override CarouselItem CreateCarouselItemForModel(object model) => new BeatmapCarouselItem(model); + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. @@ -70,7 +72,7 @@ namespace osu.Game.Screens.SelectV2 switch (changed.Action) { case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps).Select(b => new BeatmapCarouselItem(b))); + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); break; case NotifyCollectionChangedAction.Remove: @@ -78,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 foreach (var set in beatmapSetInfos!) { foreach (var beatmap in set.Beatmaps) - Items.RemoveAll(i => i.Model is BeatmapInfo bi && beatmap.Equals(bi)); + Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); } break; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 54a671949f..9fab9d0bf6 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 /// /// Note that an may add new items which are displayed but not tracked in this list. /// - protected readonly BindableList Items = new BindableList(); + protected readonly BindableList Items = new BindableList(); /// /// The currently selected model. @@ -143,6 +143,13 @@ namespace osu.Game.Screens.SelectV2 /// The manifested drawable. protected abstract Drawable GetDrawableForDisplay(CarouselItem item); + /// + /// Create an internal carousel representation for the provided model object. + /// + /// The model. + /// A representing the model. + protected abstract CarouselItem CreateCarouselItemForModel(object model); + #region Filtering and display preparation private Task filterTask = Task.CompletedTask; @@ -161,7 +168,7 @@ namespace osu.Game.Screens.SelectV2 } Stopwatch stopwatch = Stopwatch.StartNew(); - IEnumerable items = new List(Items); + IEnumerable items = new List(Items.Select(CreateCarouselItemForModel)); await Task.Run(async () => { From cf55fe16abbb08ce8815c14a1a38c01be44235ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 20:32:07 +0900 Subject: [PATCH 205/620] Generic type instead of raw `object`? --- osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs | 4 ++-- osu.Game/Screens/SelectV2/Carousel.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs index dd4aaadfbb..23954da3a1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs @@ -25,7 +25,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { [Cached] - public partial class BeatmapCarouselV2 : Carousel + public partial class BeatmapCarouselV2 : Carousel { private IBindableList detachedBeatmaps = null!; @@ -60,7 +60,7 @@ namespace osu.Game.Screens.SelectV2 return drawable; } - protected override CarouselItem CreateCarouselItemForModel(object model) => new BeatmapCarouselItem(model); + protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 9fab9d0bf6..02e87c7704 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.SelectV2 /// A highly efficient vertical list display that is used primarily for the song select screen, /// but flexible enough to be used for other use cases. /// - public abstract partial class Carousel : CompositeDrawable + public abstract partial class Carousel : CompositeDrawable { /// /// A collection of filters which should be run each time a is executed. @@ -80,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 /// /// Note that an may add new items which are displayed but not tracked in this list. /// - protected readonly BindableList Items = new BindableList(); + protected readonly BindableList Items = new BindableList(); /// /// The currently selected model. @@ -148,7 +148,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The model. /// A representing the model. - protected abstract CarouselItem CreateCarouselItemForModel(object model); + protected abstract CarouselItem CreateCarouselItemForModel(T model); #region Filtering and display preparation From 83a2fe09c5cede3991615135c10e1853c8e22164 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jan 2025 13:07:20 +0900 Subject: [PATCH 206/620] Update readme with updated mobile release information --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6043497181..32c43995f4 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu! If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below. -**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024. +**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon to se don't have to live with this limitation. ## Developing a custom ruleset From f71869610292ff7be0025f149cb92664e7809aea Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 02:34:36 -0500 Subject: [PATCH 207/620] Allow landscape orientation on tablet devices in osu!mania --- osu.Game/Mobile/OrientationManager.cs | 19 ++++++++++--------- osu.Game/OsuGame.cs | 3 ++- osu.Game/Screens/IOsuScreen.cs | 5 +++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs index b78bf8e760..0f9b56d434 100644 --- a/osu.Game/Mobile/OrientationManager.cs +++ b/osu.Game/Mobile/OrientationManager.cs @@ -48,27 +48,28 @@ namespace osu.Game.Mobile private void updateOrientations() { bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortrait = requiresPortraitOrientation.Value; + bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; if (lockCurrentOrientation) { - if (lockToPortrait && !IsCurrentOrientationPortrait) + if (!IsTablet && lockToPortraitOnPhone && !IsCurrentOrientationPortrait) SetAllowedOrientations(GameOrientation.Portrait); - else if (!lockToPortrait && IsCurrentOrientationPortrait && !IsTablet) + else if (!IsTablet && !lockToPortraitOnPhone && IsCurrentOrientationPortrait) SetAllowedOrientations(GameOrientation.Landscape); else + { + // if the orientation is already portrait/landscape according to the game's specifications, + // then use Locked instead of Portrait/Landscape to handle the case where the device is + // in landscape-left or reverse-portrait. SetAllowedOrientations(GameOrientation.Locked); + } return; } - if (lockToPortrait) + if (!IsTablet && lockToPortraitOnPhone) { - if (IsTablet) - SetAllowedOrientations(GameOrientation.FullPortrait); - else - SetAllowedOrientations(GameOrientation.Portrait); - + SetAllowedOrientations(GameOrientation.Portrait); return; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e72d106928..0d725bf07c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,7 +174,8 @@ namespace osu.Game public readonly IBindable OverlayActivationMode = new Bindable(); /// - /// On mobile devices, this specifies whether the device should be set and locked to portrait orientation. + /// On mobile phones, this specifies whether the device should be set and locked to portrait orientation. + /// Tablet devices are unaffected by this property. /// /// /// Implementations can be viewed in mobile projects. diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 8b3ff4306f..0fd7299115 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -62,10 +62,11 @@ namespace osu.Game.Screens bool HideMenuCursorOnNonMouseInput { get; } /// - /// On mobile devices, this specifies whether this requires the device to be in portrait orientation. + /// On mobile phones, this specifies whether this requires the device to be in portrait orientation. + /// Tablet devices are unaffected by this property. /// /// - /// By default, all screens in the game display in landscape orientation. + /// By default, all screens in the game display in landscape orientation on phones. /// Setting this to true will display this screen in portrait orientation instead, /// and switch back to landscape when transitioning back to a regular non-portrait screen. /// From dfbc93c3dc99653bb221bc07e3647402505bb676 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jan 2025 19:16:53 +0900 Subject: [PATCH 208/620] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32c43995f4..d87ca31f72 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu! If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below. -**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon to se don't have to live with this limitation. +**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation. ## Developing a custom ruleset From 76e09586fd3951b7659d67bf1aefaa5a8cfbecb2 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 12 Jan 2025 23:33:04 +0000 Subject: [PATCH 209/620] Fix possible nullref in `handleIntent()` Could happen if we get a malformed intent without data --- osu.Android/OsuGameActivity.cs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index bbee491d90..fe11672767 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -13,7 +13,6 @@ using Android.Graphics; using Android.OS; using Android.Views; using osu.Framework.Android; -using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Database; using Debug = System.Diagnostics.Debug; using Uri = Android.Net.Uri; @@ -95,25 +94,38 @@ namespace osu.Android private void handleIntent(Intent? intent) { - switch (intent?.Action) + if (intent == null) + return; + + switch (intent.Action) { case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) - handleImportFromUris(intent.Data.AsNonNull()); + { + if (intent.Data != null) + handleImportFromUris(intent.Data); + } else if (osu_url_schemes.Contains(intent.Scheme)) - game.HandleLink(intent.DataString); + { + if (intent.DataString != null) + game.HandleLink(intent.DataString); + } + break; case Intent.ActionSend: case Intent.ActionSendMultiple: { + if (intent.ClipData == null) + break; + var uris = new List(); - for (int i = 0; i < intent.ClipData?.ItemCount; i++) + for (int i = 0; i < intent.ClipData.ItemCount; i++) { - var content = intent.ClipData?.GetItemAt(i); - if (content != null) - uris.Add(content.Uri.AsNonNull()); + var item = intent.ClipData.GetItemAt(i); + if (item?.Uri != null) + uris.Add(item.Uri); } handleImportFromUris(uris.ToArray()); From b0339a9d63252a56cea9a1ec1da187a530419183 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 13 Jan 2025 00:47:52 +0000 Subject: [PATCH 210/620] Create game as soon as possible --- osu.Android/OsuGameActivity.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index fe11672767..42065e61fd 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -49,9 +49,23 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; - private OsuGameAndroid game = null!; + private readonly OsuGameAndroid game; - protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); + private bool gameCreated; + + protected override Framework.Game CreateGame() + { + if (gameCreated) + throw new InvalidOperationException("Framework tried to create a game twice."); + + gameCreated = true; + return game; + } + + public OsuGameActivity() + { + game = new OsuGameAndroid(this); + } protected override void OnCreate(Bundle? savedInstanceState) { From c1ac27d65894b4418c9d700ec87972728f0c26d9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 22:56:28 -0500 Subject: [PATCH 211/620] Fix failing tests - Caches `DrawableRuleset` in editor compose screen for mania playfield adjustment container (because it's used to wrap the blueprint container as well) - Fixes `ManiaModWithPlayfieldCover` performing a no-longer-correct direct cast with a naive-but-working approach. --- .../Mods/ManiaModWithPlayfieldCover.cs | 4 ++-- .../Components/HitPositionPaddedContainer.cs | 10 +++++++++ .../UI/DrawableManiaRuleset.cs | 1 - .../UI/ManiaPlayfieldAdjustmentContainer.cs | 4 +++- .../Edit/DrawableEditorRulesetWrapper.cs | 22 +++++++++---------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 + 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index 1bc16112c5..b6e6ee7481 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -5,9 +5,9 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { HitObjectContainer hoc = column.HitObjectContainer; - Container hocParent = (Container)hoc.Parent!; + ColumnHitObjectArea hocParent = (ColumnHitObjectArea)hoc.Parent!; hocParent.Remove(hoc, false); hocParent.Add(CreateCover(hoc).With(c => diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index f591102f6c..f550e3b241 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -19,6 +19,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components InternalChild = child; } + internal void Add(Drawable drawable) + { + base.AddInternal(drawable); + } + + internal void Remove(Drawable drawable, bool disposeImmediately = true) + { + base.RemoveInternal(drawable, disposeImmediately); + } + [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index d6794d0b4f..a186d9aa7d 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -32,7 +32,6 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { - [Cached(typeof(DrawableManiaRuleset))] public partial class DrawableManiaRuleset : DrawableScrollingRuleset { /// diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index f7c4850a94..b0203643b0 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.UI } [Resolved] - private DrawableManiaRuleset drawableManiaRuleset { get; set; } = null!; + private DrawableRuleset drawableRuleset { get; set; } = null!; protected override void Update() { @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.UI float aspectRatio = DrawWidth / DrawHeight; bool isPortrait = aspectRatio < 1f; + var drawableManiaRuleset = (DrawableManiaRuleset)drawableRuleset; + if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { // Scale playfield up by 25% to become playable on mobile devices, diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 174b278d89..573eb8c42f 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -19,16 +19,16 @@ namespace osu.Game.Rulesets.Edit internal partial class DrawableEditorRulesetWrapper : CompositeDrawable where TObject : HitObject { - public Playfield Playfield => drawableRuleset.Playfield; + public Playfield Playfield => DrawableRuleset.Playfield; - private readonly DrawableRuleset drawableRuleset; + public readonly DrawableRuleset DrawableRuleset; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { - this.drawableRuleset = drawableRuleset; + DrawableRuleset = drawableRuleset; RelativeSizeAxes = Axes.Both; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - drawableRuleset.FrameStablePlayback = false; + DrawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } @@ -67,27 +67,27 @@ namespace osu.Game.Rulesets.Edit private void regenerateAutoplay() { - var autoplayMod = drawableRuleset.Mods.OfType().Single(); - drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods)); + var autoplayMod = DrawableRuleset.Mods.OfType().Single(); + DrawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(DrawableRuleset.Beatmap, DrawableRuleset.Mods)); } private void addHitObject(HitObject hitObject) { - drawableRuleset.AddHitObject((TObject)hitObject); - drawableRuleset.Playfield.PostProcess(); + DrawableRuleset.AddHitObject((TObject)hitObject); + DrawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - drawableRuleset.RemoveHitObject((TObject)hitObject); - drawableRuleset.Playfield.PostProcess(); + DrawableRuleset.RemoveHitObject((TObject)hitObject); + DrawableRuleset.Playfield.PostProcess(); } public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; - public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => drawableRuleset.CreatePlayfieldAdjustmentContainer(); + public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => DrawableRuleset.CreatePlayfieldAdjustmentContainer(); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 9f277b6190..8cc7072582 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -132,6 +132,7 @@ namespace osu.Game.Rulesets.Edit if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset) dependencies.CacheAs(scrollingRuleset.ScrollingInfo); + dependencies.CacheAs(drawableRulesetWrapper.DrawableRuleset); dependencies.CacheAs(Playfield); InternalChildren = new[] From 4774d9c9ae2652ceb002444dfcc37d176cdbfa45 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 22:56:39 -0500 Subject: [PATCH 212/620] Fix mania fade in test not actually testing the mod --- .../Mods/TestSceneManiaModFadeIn.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs index f403d67377..7b8156c74f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) }); } @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) }); @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) }); @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) }); @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), CreateBeatmap = () => new Beatmap { HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(), From fc069e060c69599285dcba82c657b2568c399674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 13 Jan 2025 12:38:28 +0100 Subject: [PATCH 213/620] Only show colour on new combo selector button if overridden As proposed in https://discord.com/channels/188630481301012481/188630652340404224/1327309179911929936. --- .../Edit/Components/TernaryButtons/NewComboTernaryButton.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs index effe35c0c3..8c64480b43 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -147,19 +147,19 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons private void updateState() { - if (SelectedHitObject.Value == null) + Enabled.Value = SelectedHitObject.Value != null; + + if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0) { BackgroundColour = colourProvider.Background3; icon.Colour = BackgroundColour.Darken(0.5f); icon.Blending = BlendingParameters.Additive; - Enabled.Value = false; } else { BackgroundColour = ComboColours[comboIndexFor(SelectedHitObject.Value, ComboColours)]; icon.Colour = OsuColour.ForegroundTextColourFor(BackgroundColour); icon.Blending = BlendingParameters.Inherit; - Enabled.Value = true; } } From 39a69d64548de357b2c408da774783f463d727ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 13 Jan 2025 13:04:17 +0100 Subject: [PATCH 214/620] Adjust test to pass What I think was happening here is that the dump of the accuracy counter's state was happening too early. The component is loaded synchronously into the `ISerialisableDrawableContainer` before its default position is set via the "apply defaults" `ArgonSkin` flow - so the test needs to wait for that to take place first. --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index b319c88fc2..622c85774a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Navigation string state = string.Empty; - AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.IsLoaded)); + AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.Position != new Vector2())); AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo())); AddStep("add any component", () => Game.ChildrenOfType().First().TriggerClick()); AddStep("undo", () => @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Navigation string state = string.Empty; - AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.IsLoaded)); + AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.Position != new Vector2())); AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo())); AddStep("add any component", () => Game.ChildrenOfType().First().TriggerClick()); AddStep("undo", () => From 7761a0c18a3080f49e6c7dda9bc467005af625a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 16:15:43 +0900 Subject: [PATCH 215/620] Add failing test coverage showing storyboard not being updated when dimmed --- .../Background/TestSceneUserDimBackgrounds.cs | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 693e1e48d4..96954f6984 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Linq; using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; @@ -15,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Framework.Input.States; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -31,6 +33,7 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Storyboards.Drawables; using osu.Game.Tests.Resources; using osuTK; using osuTK.Graphics; @@ -45,6 +48,7 @@ namespace osu.Game.Tests.Visual.Background private LoadBlockingTestPlayer player; private BeatmapManager manager; private RulesetStore rulesets; + private UpdateCounter storyboardUpdateCounter; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -194,6 +198,28 @@ namespace osu.Game.Tests.Visual.Background AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible); } + [Test] + public void TestStoryboardUpdatesWhenDimmed() + { + performFullSetup(); + createFakeStoryboard(); + + AddStep("Enable fully dimmed storyboard", () => + { + player.StoryboardReplacesBackground.Value = true; + player.StoryboardEnabled.Value = true; + player.DimmableStoryboard.IgnoreUserSettings.Value = false; + songSelect.DimLevel.Value = 1f; + }); + + AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible); + + AddWaitStep("wait some", 20); + + AddUntilStep("Storyboard is always present", () => player.ChildrenOfType().Single().AlwaysPresent, () => Is.True); + AddUntilStep("Dimmable storyboard content is being updated", () => storyboardUpdateCounter.StoryboardContentLastUpdated, () => Is.EqualTo(Time.Current).Within(100)); + } + [Test] public void TestStoryboardIgnoreUserSettings() { @@ -269,15 +295,19 @@ namespace osu.Game.Tests.Visual.Background { player.StoryboardEnabled.Value = false; player.StoryboardReplacesBackground.Value = false; - player.DimmableStoryboard.Add(new OsuSpriteText + player.DimmableStoryboard.AddRange(new Drawable[] { - Size = new Vector2(500, 50), - Alpha = 1, - Colour = Color4.White, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "THIS IS A STORYBOARD", - Font = new FontUsage(size: 50) + storyboardUpdateCounter = new UpdateCounter(), + new OsuSpriteText + { + Size = new Vector2(500, 50), + Alpha = 1, + Colour = Color4.White, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "THIS IS A STORYBOARD", + Font = new FontUsage(size: 50) + } }); }); @@ -353,7 +383,7 @@ namespace osu.Game.Tests.Visual.Background /// /// Make sure every time a screen gets pushed, the background doesn't get replaced /// - /// Whether or not the original background (The one created in DummySongSelect) is still the current background + /// Whether the original background (The one created in DummySongSelect) is still the current background public bool IsBackgroundCurrent() => background?.IsCurrentScreen() == true; } @@ -384,7 +414,7 @@ namespace osu.Game.Tests.Visual.Background public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; - // Whether or not the player should be allowed to load. + // Whether the player should be allowed to load. public bool BlockLoad; public Bindable StoryboardEnabled; @@ -451,6 +481,17 @@ namespace osu.Game.Tests.Visual.Background } } + private class UpdateCounter : Drawable + { + public double StoryboardContentLastUpdated; + + protected override void Update() + { + base.Update(); + StoryboardContentLastUpdated = Time.Current; + } + } + private partial class TestDimmableBackground : BackgroundScreenBeatmap.DimmableBackground { public Color4 CurrentColour => Content.Colour; From 77db35580900896fa46fca26b45780c21727e3af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 15:55:29 +0900 Subject: [PATCH 216/620] Ensure storyboards are still updated even when dim is 100% This avoids piled-up overhead when entering break time. It's not great, but it is what we need for now to avoid weirdness. --- osu.Game/Screens/Play/DimmableStoryboard.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index 84d99ea863..a096400fe0 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -69,7 +69,22 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { - ShowStoryboard.BindValueChanged(_ => initializeStoryboard(true), true); + ShowStoryboard.BindValueChanged(show => + { + initializeStoryboard(true); + + if (drawableStoryboard != null) + { + // Regardless of user dim setting, for the time being we need to ensure storyboards are still updated in the background (even if not displayed). + // If we don't do this, an intensive storyboard will have a lot of catch-up work to do at the start of a break, causing a huge stutter. + // + // This can be reconsidered when https://github.com/ppy/osu-framework/issues/6491 is resolved. + bool alwaysPresent = show.NewValue; + + Content.AlwaysPresent = alwaysPresent; + drawableStoryboard.AlwaysPresent = alwaysPresent; + } + }, true); base.LoadComplete(); } From 2c57cd59a5cbbb4c9d95a70e25a7d64d0bd3d9cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 16:26:56 +0900 Subject: [PATCH 217/620] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 84827ce76b..dbb0a6d610 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 349d6fa1d7..afbcf49d32 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 904a08af26b2c0ba9992365de56c6bb2f2a12a68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 16:29:56 +0900 Subject: [PATCH 218/620] Update textbox usage in line with framework changes --- osu.Game/Graphics/UserInterface/OsuNumberBox.cs | 6 ++++-- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 6 +++--- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 6 +++++- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 6 +++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index db4b7b2ab3..1742cb6bdd 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -1,14 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Input; + namespace osu.Game.Graphics.UserInterface { public partial class OsuNumberBox : OsuTextBox { - protected override bool AllowIme => false; - public OsuNumberBox() { + InputProperties = new TextInputProperties(TextInputType.Number, false); + SelectAllOnFocus = true; } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 0be7b4dc48..e2e273cfe1 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -18,7 +18,7 @@ using osu.Game.Localisation; namespace osu.Game.Graphics.UserInterface { - public partial class OsuPasswordTextBox : OsuTextBox, ISuppressKeyEventLogging + public partial class OsuPasswordTextBox : OsuTextBox { protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer { @@ -32,8 +32,6 @@ namespace osu.Game.Graphics.UserInterface protected override bool AllowWordNavigation => false; - protected override bool AllowIme => false; - private readonly CapsWarning warning; [Resolved] @@ -41,6 +39,8 @@ namespace osu.Game.Graphics.UserInterface public OsuPasswordTextBox() { + InputProperties = new TextInputProperties(TextInputType.Password, false); + Add(warning = new CapsWarning { Size = new Vector2(20), diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index fbcdb4a968..2548f3c87b 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; namespace osu.Game.Overlays.Settings { @@ -66,7 +67,10 @@ namespace osu.Game.Overlays.Settings private partial class OutlinedNumberBox : OutlinedTextBox { - protected override bool AllowIme => false; + public OutlinedNumberBox() + { + InputProperties = new TextInputProperties(TextInputType.Number, false); + } protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 7b74aa7642..85247bc15a 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; @@ -136,7 +137,10 @@ namespace osu.Game.Screens.Edit.Setup private partial class RomanisedTextBox : InnerTextBox { - protected override bool AllowIme => false; + public RomanisedTextBox() + { + InputProperties = new TextInputProperties(TextInputType.Text, false); + } protected override bool CanAddCharacter(char character) => MetadataUtils.IsRomanised(character); From 8ffd2547196d89123cb51566418f2aaa012f9793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 08:54:40 +0100 Subject: [PATCH 219/620] Adjust initialisation code to start with combo colour picker hidden --- .../Edit/Components/TernaryButtons/NewComboTernaryButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs index 8c64480b43..1f95d5f239 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -54,7 +54,6 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 30 }, Child = new DrawableTernaryButton { Current = Current, @@ -66,6 +65,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + Alpha = 0, Width = 25, ComboColours = { BindTarget = comboColours } } From 058ff8af7769cbc50438d0d6078b51c5902564fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 09:22:56 +0100 Subject: [PATCH 220/620] Make test class partial --- osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 96954f6984..eeaa68e2ee 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -481,7 +481,7 @@ namespace osu.Game.Tests.Visual.Background } } - private class UpdateCounter : Drawable + private partial class UpdateCounter : Drawable { public double StoryboardContentLastUpdated; From f6073d4ac09c499d7b828d01a7d04671fc252563 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 17:43:29 +0900 Subject: [PATCH 221/620] Ensure API starts up with `LocalUser` in correct state I noticed in passing that in a very edge case scenario where the API's `run` thread doesn't run before it is loaded into the game, something could access it and get a guest `LocalUser` when the local user actually has a valid login. Put another way, the `protected HasLogin` could be `true` while `LocalUser` is `Guest`. I think we want to avoid this, so I've moved the initial set of the local user earlier in the initialisation process. If this is controversial in any way, the PR can be closed and we can assume no one is ever going to run into this scenario (or that it doesn't matter enough even if they did). --- osu.Game/Online/API/APIAccess.cs | 43 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ec48fa2436..e0927dbc4e 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -110,6 +111,9 @@ namespace osu.Game.Online.API config.BindWith(OsuSetting.UserOnlineStatus, configStatus); + // Early call to ensure the local user / "logged in" state is correct immediately. + setPlaceholderLocalUser(); + localUser.BindValueChanged(u => { u.OldValue?.Activity.UnbindFrom(activity); @@ -193,7 +197,7 @@ namespace osu.Game.Online.API Debug.Assert(HasLogin); - // Ensure that we are in an online state. If not, attempt a connect. + // Ensure that we are in an online state. If not, attempt to connect. if (state.Value != APIState.Online) { attemptConnect(); @@ -247,17 +251,7 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - if (localUser.IsDefault) - { - // Show a placeholder user if saved credentials are available. - // This is useful for storing local scores and showing a placeholder username after starting the game, - // until a valid connection has been established. - setLocalUser(new APIUser - { - Username = ProvidedUsername, - Status = { Value = configStatus.Value ?? UserStatus.Online } - }); - } + Scheduler.Add(setPlaceholderLocalUser, false); // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); @@ -339,9 +333,11 @@ namespace osu.Game.Online.API userReq.Success += me => { + Debug.Assert(ThreadSafety.IsUpdateThread); + me.Status.Value = configStatus.Value ?? UserStatus.Online; - setLocalUser(me); + localUser.Value = me; state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; @@ -366,6 +362,23 @@ namespace osu.Game.Online.API Thread.Sleep(500); } + /// + /// Show a placeholder user if saved credentials are available. + /// This is useful for storing local scores and showing a placeholder username after starting the game, + /// until a valid connection has been established. + /// + private void setPlaceholderLocalUser() + { + if (!localUser.IsDefault) + return; + + localUser.Value = new APIUser + { + Username = ProvidedUsername, + Status = { Value = configStatus.Value ?? UserStatus.Online } + }; + } + public void Perform(APIRequest request) { try @@ -593,7 +606,7 @@ namespace osu.Game.Online.API // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => { - setLocalUser(createGuestUser()); + localUser.Value = createGuestUser(); friends.Clear(); }); @@ -619,8 +632,6 @@ namespace osu.Game.Online.API private static APIUser createGuestUser() => new GuestUser(); - private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false); - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 51c7c218bfc83c8b45c7b1853485877c6a7504dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Jan 2025 17:51:04 +0900 Subject: [PATCH 222/620] Simplify operations on local list --- osu.Game/Online/API/APIAccess.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 46476ab7f0..9d0ef06ebf 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -612,14 +612,14 @@ namespace osu.Game.Online.API friendsReq.Failure += _ => state.Value = APIState.Failing; friendsReq.Success += res => { - // Add new friends into local list. - HashSet friendsSet = friends.Select(f => f.TargetID).ToHashSet(); - friends.AddRange(res.Where(f => !friendsSet.Contains(f.TargetID))); + var existingFriends = friends.Select(f => f.TargetID).ToHashSet(); + var updatedFriends = res.Select(f => f.TargetID).ToHashSet(); - // Remove non-friends from local lists. - friendsSet.Clear(); - friendsSet.AddRange(res.Select(f => f.TargetID)); - friends.RemoveAll(f => !friendsSet.Contains(f.TargetID)); + // Add new friends into local list. + friends.AddRange(res.Where(r => !existingFriends.Contains(r.TargetID))); + + // Remove non-friends from local list. + friends.RemoveAll(f => !updatedFriends.Contains(f.TargetID)); }; Queue(friendsReq); From 156207d3472541422fe3b57fec0f05435b684e7f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Jan 2025 17:54:40 +0900 Subject: [PATCH 223/620] Remove unused using --- osu.Game/Online/API/APIAccess.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 9d0ef06ebf..d44ca90fa1 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -19,7 +19,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Configuration; -using osu.Game.Extensions; using osu.Game.Localisation; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; From 55ae0403d8ee2f4b37f78a4f9fcf185443d50832 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 18:18:53 +0900 Subject: [PATCH 224/620] Ensure API state is `Connecting` immediately on startup when credentials are present Currently, there's a period where the API is `Offline` even though it is about to connect (as soon as the `run` thread starts up). This can cause any `Queue`d requests to fail if they arrive too early. To avoid this, let's ensure the `Connecting` state is set as early as possible. --- osu.Game/Online/API/APIAccess.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index e0927dbc4e..49ba99daa9 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -111,8 +111,14 @@ namespace osu.Game.Online.API config.BindWith(OsuSetting.UserOnlineStatus, configStatus); - // Early call to ensure the local user / "logged in" state is correct immediately. - setPlaceholderLocalUser(); + if (HasLogin) + { + // Early call to ensure the local user / "logged in" state is correct immediately. + prepareForConnect(); + + // This is required so that Queue() requests during startup sequence don't fail due to "not logged in". + state.Value = APIState.Connecting; + } localUser.BindValueChanged(u => { @@ -251,7 +257,7 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - Scheduler.Add(setPlaceholderLocalUser, false); + Scheduler.Add(prepareForConnect, false); // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); @@ -367,7 +373,7 @@ namespace osu.Game.Online.API /// This is useful for storing local scores and showing a placeholder username after starting the game, /// until a valid connection has been established. /// - private void setPlaceholderLocalUser() + private void prepareForConnect() { if (!localUser.IsDefault) return; From 3ddff1933738c17911514306734c2f266b618a28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 19:03:58 +0900 Subject: [PATCH 225/620] Fix potential nullref due to silly null handling and too much OOP --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 095bd95314..5ef6b30a82 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -35,7 +35,7 @@ namespace osu.Game.Storyboards.Drawables protected override Container Content { get; } - protected override Vector2 DrawScale => new Vector2(Parent!.DrawHeight / 480); + protected override Vector2 DrawScale => new Vector2((Parent?.DrawHeight ?? 0) / 480); public override bool RemoveCompletedTransforms => false; From d97a3270a50154817c20d1f9f2b1e92016b868df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 19:18:02 +0900 Subject: [PATCH 226/620] Split out `BeatmapCarousel` classes and drop `V2` suffix --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 4 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 99 +++++++ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 40 +++ .../SelectV2/BeatmapCarouselFilterSorting.cs | 28 ++ .../Screens/SelectV2/BeatmapCarouselItem.cs | 36 +++ .../Screens/SelectV2/BeatmapCarouselPanel.cs | 96 +++++++ .../Screens/SelectV2/BeatmapCarouselV2.cs | 257 ------------------ 7 files changed, 301 insertions(+), 259 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarousel.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs delete mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index dde4ef88bd..6d54e13b6f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapStore store; private OsuTextFlowContainer stats = null!; - private BeatmapCarouselV2 carousel = null!; + private BeatmapCarousel carousel = null!; private OsuScrollContainer scroll => carousel.ChildrenOfType().Single(); @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelect }, new Drawable[] { - carousel = new BeatmapCarouselV2 + carousel = new BeatmapCarousel { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs new file mode 100644 index 0000000000..3c431a6003 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Screens.Select; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + [Cached] + public partial class BeatmapCarousel : Carousel + { + private IBindableList detachedBeatmaps = null!; + + private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + + public BeatmapCarousel() + { + DebounceDelay = 100; + DistanceOffscreenToPreload = 100; + + Filters = new ICarouselFilter[] + { + new BeatmapCarouselFilterSorting(), + new BeatmapCarouselFilterGrouping(), + }; + + AddInternal(carouselPanelPool); + } + + [BackgroundDependencyLoader] + private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) + { + detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); + detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); + } + + protected override Drawable GetDrawableForDisplay(CarouselItem item) + { + var drawable = carouselPanelPool.Get(); + drawable.FlashColour(Color4.Red, 2000); + + return drawable; + } + + protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); + + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + { + // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. + // right now we are managing this locally which is a bit of added overhead. + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); + break; + + case NotifyCollectionChangedAction.Remove: + + foreach (var set in beatmapSetInfos!) + { + foreach (var beatmap in set.Beatmaps) + Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + throw new NotImplementedException(); + + case NotifyCollectionChangedAction.Reset: + Items.Clear(); + break; + } + } + + public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); + + public void Filter(FilterCriteria criteria) + { + Criteria = criteria; + QueueFilter(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs new file mode 100644 index 0000000000..ee4b9ddb69 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -0,0 +1,40 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselFilterGrouping : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + // TODO: perform grouping based on FilterCriteria + + CarouselItem? lastItem = null; + + var newItems = new List(items.Count()); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (item.Model is BeatmapInfo b1) + { + // Add set header + if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) + newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); + } + + newItems.Add(item); + lastItem = item; + } + + return newItems; + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs new file mode 100644 index 0000000000..a2fd774cf0 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselFilterSorting : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + return items.OrderDescending(Comparer.Create((a, b) => + { + if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) + return ab.OnlineID.CompareTo(bb.OnlineID); + + if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) + return aItem.ID.CompareTo(bItem.ID); + + return 0; + })); + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs new file mode 100644 index 0000000000..adb5a19875 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs @@ -0,0 +1,36 @@ +// 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.Database; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselItem : CarouselItem + { + public readonly Guid ID; + + public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; + + public BeatmapCarouselItem(object model) + : base(model) + { + ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); + } + + public override string? ToString() + { + switch (Model) + { + case BeatmapInfo bi: + return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; + + case BeatmapSetInfo si: + return $"{si.Metadata}"; + } + + return Model.ToString(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs new file mode 100644 index 0000000000..a64d16a984 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel + { + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + public CarouselItem? Item + { + get => item; + set + { + item = value; + + selected.UnbindBindings(); + + if (item != null) + selected.BindTo(item.Selected); + } + } + + private readonly BindableBool selected = new BindableBool(); + private CarouselItem? item; + + [BackgroundDependencyLoader] + private void load() + { + selected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + Item = null; + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + Size = new Vector2(500, Item.DrawHeight); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = Item.ToString() ?? string.Empty, + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs deleted file mode 100644 index 23954da3a1..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Select; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - [Cached] - public partial class BeatmapCarouselV2 : Carousel - { - private IBindableList detachedBeatmaps = null!; - - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); - - public BeatmapCarouselV2() - { - DebounceDelay = 100; - DistanceOffscreenToPreload = 100; - - Filters = new ICarouselFilter[] - { - new Sorter(), - new Grouper(), - }; - - AddInternal(carouselPanelPool); - } - - [BackgroundDependencyLoader] - private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) - { - detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); - detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); - } - - protected override Drawable GetDrawableForDisplay(CarouselItem item) - { - var drawable = carouselPanelPool.Get(); - drawable.FlashColour(Color4.Red, 2000); - - return drawable; - } - - protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); - - private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) - { - // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. - // right now we are managing this locally which is a bit of added overhead. - IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); - IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); - - switch (changed.Action) - { - case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); - break; - - case NotifyCollectionChangedAction.Remove: - - foreach (var set in beatmapSetInfos!) - { - foreach (var beatmap in set.Beatmaps) - Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); - } - - break; - - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Replace: - throw new NotImplementedException(); - - case NotifyCollectionChangedAction.Reset: - Items.Clear(); - break; - } - } - - public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); - - public void Filter(FilterCriteria criteria) - { - Criteria = criteria; - QueueFilter(); - } - } - - public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel - { - [Resolved] - private BeatmapCarouselV2 carousel { get; set; } = null!; - - public CarouselItem? Item - { - get => item; - set - { - item = value; - - selected.UnbindBindings(); - - if (item != null) - selected.BindTo(item.Selected); - } - } - - private readonly BindableBool selected = new BindableBool(); - private CarouselItem? item; - - [BackgroundDependencyLoader] - private void load() - { - selected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); - } - - protected override void FreeAfterUse() - { - base.FreeAfterUse(); - Item = null; - } - - protected override void PrepareForUse() - { - base.PrepareForUse(); - - Debug.Assert(Item != null); - - Size = new Vector2(500, Item.DrawHeight); - Masking = true; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = Item.ToString() ?? string.Empty, - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - } - - protected override bool OnClick(ClickEvent e) - { - carousel.CurrentSelection = Item!.Model; - return true; - } - } - - public class BeatmapCarouselItem : CarouselItem - { - public readonly Guid ID; - - public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; - - public BeatmapCarouselItem(object model) - : base(model) - { - ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); - } - - public override string? ToString() - { - switch (Model) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return Model.ToString(); - } - } - - public class Grouper : ICarouselFilter - { - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => - { - // TODO: perform grouping based on FilterCriteria - - CarouselItem? lastItem = null; - - var newItems = new List(items.Count()); - - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.Model is BeatmapInfo b1) - { - // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); - } - - newItems.Add(item); - lastItem = item; - } - - return newItems; - }, cancellationToken).ConfigureAwait(false); - } - - public class Sorter : ICarouselFilter - { - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => - { - return items.OrderDescending(Comparer.Create((a, b) => - { - if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) - return ab.OnlineID.CompareTo(bb.OnlineID); - - if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) - return aItem.ID.CompareTo(bItem.ID); - - return 0; - })); - }, cancellationToken).ConfigureAwait(false); - } -} From b0c0c98c5dff7ac92e67d5f25a0c20749568adda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 11:19:17 +0100 Subject: [PATCH 227/620] Refetch local metadata cache if corruption is detected Addresses one of the points in https://github.com/ppy/osu/issues/31496. Not going to lie, this is mostly best-effort stuff (while the refetch is happening, metadata lookups using the local source *will* fail), but I see this as a marginal scenario anyways. --- .../LocalCachedBeatmapMetadataSource.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 66fad6c8d8..7495805cff 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -114,6 +114,15 @@ namespace osu.Game.Beatmaps } } } + catch (SqliteException sqliteException) when (sqliteException.SqliteErrorCode == 11 || sqliteException.SqliteErrorCode == 26) // SQLITE_CORRUPT, SQLITE_NOTADB + { + // only attempt purge & refetch if there is no other refetch in progress + if (cacheDownloadRequest == null) + { + tryPurgeCache(); + prepareLocalCache(); + } + } catch (Exception ex) { logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with {ex}."); @@ -125,6 +134,22 @@ namespace osu.Game.Beatmaps return false; } + private void tryPurgeCache() + { + log(@"Local metadata cache is corrupted; attempting purge."); + + try + { + File.Delete(storage.GetFullPath(cache_database_name)); + } + catch (Exception ex) + { + log($@"Failed to purge local metadata cache: {ex}"); + } + + log(@"Local metadata cache purged due to corruption."); + } + private SqliteConnection getConnection() => new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))); From 7e8a80a0e5e812a30df71687e91952def018aeeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 19:37:28 +0900 Subject: [PATCH 228/620] Add difficulty, artist and title sort examples Also: - Adds hinting at grouping and header status of items - Passes through criteria and prepare for grouping tests. - Makes `Filters` list `protected` because naming clash with `Filter()` on `BeatmapCarousel`. --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 28 +++++++++++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 4 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 28 +++++++++++-- .../SelectV2/BeatmapCarouselFilterSorting.cs | 39 ++++++++++++++++++- .../Screens/SelectV2/BeatmapCarouselItem.cs | 14 ++++++- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 6 files changed, 106 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 6d54e13b6f..1d7d6041ae 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -17,10 +17,13 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Containers; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK.Graphics; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; namespace osu.Game.Tests.Visual.SongSelect { @@ -123,6 +126,11 @@ namespace osu.Game.Tests.Visual.SongSelect }, }; }); + + AddStep("sort by title", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Title }); + }); } [Test] @@ -139,6 +147,26 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("remove all beatmaps", () => beatmapSets.Clear()); } + [Test] + public void TestSorting() + { + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddStep("sort by difficulty", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }); + }); + + AddStep("sort by artist", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }); + }); + } + [Test] public void TestScrollPositionVelocityMaintained() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 3c431a6003..582933bbaf 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -31,8 +31,8 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { - new BeatmapCarouselFilterSorting(), - new BeatmapCarouselFilterGrouping(), + new BeatmapCarouselFilterSorting(() => Criteria), + new BeatmapCarouselFilterGrouping(() => Criteria), }; AddInternal(carouselPanelPool); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index ee4b9ddb69..6cdd15d301 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -1,19 +1,36 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + private readonly Func getCriteria; + + public BeatmapCarouselFilterGrouping(Func getCriteria) + { + this.getCriteria = getCriteria; + } + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - // TODO: perform grouping based on FilterCriteria + var criteria = getCriteria(); + + if (criteria.SplitOutDifficulties) + { + foreach (var item in items) + ((BeatmapCarouselItem)item).HasGroupHeader = false; + + return items; + } CarouselItem? lastItem = null; @@ -23,15 +40,18 @@ namespace osu.Game.Screens.SelectV2 { cancellationToken.ThrowIfCancellationRequested(); - if (item.Model is BeatmapInfo b1) + if (item.Model is BeatmapInfo b) { // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); + if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) + newItems.Add(new BeatmapCarouselItem(b.BeatmapSet!) { IsGroupHeader = true }); } newItems.Add(item); lastItem = item; + + var beatmapCarouselItem = (BeatmapCarouselItem)item; + beatmapCarouselItem.HasGroupHeader = true; } return newItems; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index a2fd774cf0..df41aa3e86 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -1,22 +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 System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Utils; namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterSorting : ICarouselFilter { + private readonly Func getCriteria; + + public BeatmapCarouselFilterSorting(Func getCriteria) + { + this.getCriteria = getCriteria; + } + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + var criteria = getCriteria(); + return items.OrderDescending(Comparer.Create((a, b) => { + int comparison = 0; + if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) - return ab.OnlineID.CompareTo(bb.OnlineID); + { + switch (criteria.Sort) + { + case SortMode.Artist: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + if (comparison == 0) + goto case SortMode.Title; + break; + + case SortMode.Difficulty: + comparison = ab.StarRating.CompareTo(bb.StarRating); + break; + + case SortMode.Title: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + if (comparison != 0) return comparison; if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) return aItem.ID.CompareTo(bItem.ID); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs index adb5a19875..dd7aae3db9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs @@ -11,7 +11,19 @@ namespace osu.Game.Screens.SelectV2 { public readonly Guid ID; - public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; + /// + /// Whether this item has a header providing extra information for it. + /// When displaying items which don't have header, we should make sure enough information is included inline. + /// + public bool HasGroupHeader { get; set; } + + /// + /// Whether this item is a group header. + /// Group headers are generally larger in display. Setting this will account for the size difference. + /// + public bool IsGroupHeader { get; set; } + + public override float DrawHeight => IsGroupHeader ? 80 : 40; public BeatmapCarouselItem(object model) : base(model) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 02e87c7704..f0289d634d 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.SelectV2 /// /// A collection of filters which should be run each time a is executed. /// - public IEnumerable Filters { get; init; } = Enumerable.Empty(); + protected IEnumerable Filters { get; init; } = Enumerable.Empty(); /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. From cc8941a94a3522d3a4fc13d82b421bd7004d7ca3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 20:07:09 +0900 Subject: [PATCH 229/620] Add animation and depth control --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +------- .../Screens/SelectV2/BeatmapCarouselPanel.cs | 19 +++++++++++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 12 ++++++++++-- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 2 +- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 582933bbaf..a394cc894f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -45,13 +45,7 @@ namespace osu.Game.Screens.SelectV2 detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); } - protected override Drawable GetDrawableForDisplay(CarouselItem item) - { - var drawable = carouselPanelPool.Get(); - drawable.FlashColour(Color4.Red, 2000); - - return drawable; - } + protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index a64d16a984..5b8ae211d1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osuTK; @@ -67,6 +68,8 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); + DrawYPosition = Item.CarouselYPosition; + Size = new Vector2(500, Item.DrawHeight); Masking = true; @@ -85,6 +88,8 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.CentreLeft, } }; + + this.FadeInFromZero(500, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) @@ -92,5 +97,19 @@ namespace osu.Game.Screens.SelectV2 carousel.CurrentSelection = Item!.Model; return true; } + + protected override void Update() + { + base.Update(); + + Debug.Assert(Item != null); + + if (DrawYPosition != Item.CarouselYPosition) + { + DrawYPosition = Interpolation.DampContinuously(DrawYPosition, Item.CarouselYPosition, 50, Time.Elapsed); + } + } + + public double DrawYPosition { get; private set; } } } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index f0289d634d..f10ab1c1b0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -291,6 +291,14 @@ namespace osu.Game.Screens.SelectV2 updateDisplayedRange(range); } + + foreach (var panel in scroll.Panels) + { + var carouselPanel = (ICarouselPanel)panel; + + if (panel.Depth != carouselPanel.DrawYPosition) + scroll.Panels.ChangeChildDepth(panel, (float)carouselPanel.DrawYPosition); + } } private DisplayRange getDisplayRange() @@ -415,7 +423,7 @@ namespace osu.Game.Screens.SelectV2 if (d is not ICarouselPanel panel) return base.GetChildPosInContent(d, offset); - return panel.YPosition + offset.X; + return panel.DrawYPosition + offset.X; } protected override void ApplyCurrentToContent() @@ -425,7 +433,7 @@ namespace osu.Game.Screens.SelectV2 double scrollableExtent = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; foreach (var d in Panels) - d.Y = (float)(((ICarouselPanel)d).YPosition + scrollableExtent); + d.Y = (float)(((ICarouselPanel)d).DrawYPosition + scrollableExtent); } } diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 97c585492c..d729df7876 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The Y position which should be used for displaying this item within the carousel. /// - double YPosition => Item!.CarouselYPosition; + double DrawYPosition { get; } /// /// The carousel item this drawable is representing. This is managed by and should not be set manually. From 900237c1ed7dbf06040fa1f24c2c2c7a09fe9132 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 20:23:53 +0900 Subject: [PATCH 230/620] Add loading overlay and refine filter flow --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 17 ++++++++++++-- osu.Game/Screens/SelectV2/Carousel.cs | 24 +++++++++++--------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index a394cc894f..93d4c90be0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -6,14 +6,16 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Select; -using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -24,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + private readonly LoadingLayer loading; + public BeatmapCarousel() { DebounceDelay = 100; @@ -36,6 +40,8 @@ namespace osu.Game.Screens.SelectV2 }; AddInternal(carouselPanelPool); + + AddInternal(loading = new LoadingLayer(dimBackground: true)); } [BackgroundDependencyLoader] @@ -87,7 +93,14 @@ namespace osu.Game.Screens.SelectV2 public void Filter(FilterCriteria criteria) { Criteria = criteria; - QueueFilter(); + FilterAsync().FireAndForget(); + } + + protected override async Task FilterAsync() + { + loading.Show(); + await base.FilterAsync().ConfigureAwait(true); + loading.Hide(); } } } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index f10ab1c1b0..dbecfc6601 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.SelectV2 public abstract partial class Carousel : CompositeDrawable { /// - /// A collection of filters which should be run each time a is executed. + /// A collection of filters which should be run each time a is executed. /// protected IEnumerable Filters { get; init; } = Enumerable.Empty(); @@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2 /// /// All items which are to be considered for display in this carousel. - /// Mutating this list will automatically queue a . + /// Mutating this list will automatically queue a . /// /// /// Note that an may add new items which are displayed but not tracked in this list. @@ -125,13 +125,13 @@ namespace osu.Game.Screens.SelectV2 } }; - Items.BindCollectionChanged((_, _) => QueueFilter()); + Items.BindCollectionChanged((_, _) => FilterAsync()); } /// /// Queue an asynchronous filter operation. /// - public void QueueFilter() => Scheduler.AddOnce(() => filterTask = performFilter()); + protected virtual Task FilterAsync() => filterTask = performFilter(); /// /// Create a drawable for the given carousel item so it can be displayed. @@ -159,6 +159,7 @@ namespace osu.Game.Screens.SelectV2 { Debug.Assert(SynchronizationContext.Current != null); + Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); lock (this) @@ -167,19 +168,20 @@ namespace osu.Game.Screens.SelectV2 cancellationSource = cts; } - Stopwatch stopwatch = Stopwatch.StartNew(); + if (DebounceDelay > 0) + { + log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); + await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true); + } + + // Copy must be performed on update thread for now (see ConfigureAwait above). + // Could potentially be optimised in the future if it becomes an issue. IEnumerable items = new List(Items.Select(CreateCarouselItemForModel)); await Task.Run(async () => { try { - if (DebounceDelay > 0) - { - log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); - await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false); - } - foreach (var filter in Filters) { log($"Performing {filter.GetType().ReadableName()}"); From 91fa2e70d8e7d49d7143f62a393e68324f2fe7b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 20:41:18 +0900 Subject: [PATCH 231/620] Revert name change --- osu.Game/Online/API/APIAccess.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 1f9dffc605..00fe3bb005 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -115,7 +115,7 @@ namespace osu.Game.Online.API if (HasLogin) { // Early call to ensure the local user / "logged in" state is correct immediately. - prepareForConnect(); + setPlaceholderLocalUser(); // This is required so that Queue() requests during startup sequence don't fail due to "not logged in". state.Value = APIState.Connecting; @@ -258,7 +258,7 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - Scheduler.Add(prepareForConnect, false); + Scheduler.Add(setPlaceholderLocalUser, false); // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); @@ -374,7 +374,7 @@ namespace osu.Game.Online.API /// This is useful for storing local scores and showing a placeholder username after starting the game, /// until a valid connection has been established. /// - private void prepareForConnect() + private void setPlaceholderLocalUser() { if (!localUser.IsDefault) return; From e871f0235020e294b7cfa35d82da0bdb25d403d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 20:43:03 +0900 Subject: [PATCH 232/620] Fix inspections that don't show in rider --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index dbecfc6601..12f520d6c4 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -45,13 +45,13 @@ namespace osu.Game.Screens.SelectV2 /// The number of pixels outside the carousel's vertical bounds to manifest drawables. /// This allows preloading content before it scrolls into view. /// - public float DistanceOffscreenToPreload { get; set; } = 0; + public float DistanceOffscreenToPreload { get; set; } /// /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. /// - public int DebounceDelay { get; set; } = 0; + public int DebounceDelay { get; set; } /// /// Whether an asynchronous filter / group operation is currently underway. From 20108e3b74084692b34643d4e61124b079c0aa44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Jan 2025 23:44:14 +0900 Subject: [PATCH 233/620] Remove Status and Activity bindables from APIUser As for the tests, I'm (ab)using the `IsOnline` state for the time being to restore functionality. --- osu.Desktop/DiscordRichPresence.cs | 14 ++++------- .../Visual/Menus/TestSceneLoginOverlay.cs | 2 +- .../Online/TestSceneUserClickableAvatar.cs | 5 +--- .../Visual/Online/TestSceneUserPanel.cs | 2 +- osu.Game/Online/API/APIAccess.cs | 21 ++++------------- osu.Game/Online/API/DummyAPIAccess.cs | 15 ++++-------- osu.Game/Online/API/IAPIProvider.cs | 7 +++++- .../Online/API/Requests/Responses/APIUser.cs | 5 ---- .../Online/Metadata/OnlineMetadataClient.cs | 17 +++++++------- .../Dashboard/CurrentlyOnlineDisplay.cs | 23 +++++++++---------- osu.Game/Overlays/Login/LoginPanel.cs | 19 ++++----------- 11 files changed, 46 insertions(+), 84 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 32a8ba51a3..94804ad1cc 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -54,8 +54,8 @@ namespace osu.Desktop [Resolved] private OsuConfigManager config { get; set; } = null!; - private readonly IBindable status = new Bindable(); - private readonly IBindable activity = new Bindable(); + private readonly IBindable status = new Bindable(); + private readonly IBindable activity = new Bindable(); private readonly Bindable privacyMode = new Bindable(); private readonly RichPresence presence = new RichPresence @@ -108,14 +108,8 @@ namespace osu.Desktop config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); user = api.LocalUser.GetBoundCopy(); - user.BindValueChanged(u => - { - status.UnbindBindings(); - status.BindTo(u.NewValue.Status); - - activity.UnbindBindings(); - activity.BindTo(u.NewValue.Activity); - }, true); + status.BindTo(api.Status); + activity.BindTo(api.Activity); ruleset.BindValueChanged(_ => schedulePresenceUpdate()); status.BindValueChanged(_ => schedulePresenceUpdate()); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 609bc6e166..5c12e0c102 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("clear handler", () => dummyAPI.HandleRequest = null); assertDropdownState(UserAction.Online); - AddStep("change user state", () => dummyAPI.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb); + AddStep("change user state", () => dummyAPI.Status.Value = UserStatus.DoNotDisturb); assertDropdownState(UserAction.DoNotDisturb); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 4539eae25f..fce888094d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -62,10 +62,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = countryCode, CoverUrl = cover, Colour = color ?? "000000", - Status = - { - Value = UserStatus.Online - }, + IsOnline = true }; return new ClickableAvatar(user, showPanel) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 3f1d961588..4c2e47d336 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3103765, CountryCode = CountryCode.JP, CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - Status = { Value = UserStatus.Online } + IsOnline = true }) { Width = 300 }, boundPanel1 = new UserGridPanel(new APIUser { diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 00fe3bb005..4f8c5dcb22 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -60,6 +60,7 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; + public Bindable Status { get; } = new Bindable(UserStatus.Online); public IBindable Activity => activity; public INotificationsClient NotificationsClient { get; } @@ -73,7 +74,6 @@ namespace osu.Game.Online.API private Bindable activity { get; } = new Bindable(); private Bindable configStatus { get; } = new Bindable(); - private Bindable localUserStatus { get; } = new Bindable(); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); @@ -121,17 +121,6 @@ namespace osu.Game.Online.API state.Value = APIState.Connecting; } - localUser.BindValueChanged(u => - { - u.OldValue?.Activity.UnbindFrom(activity); - u.NewValue.Activity.BindTo(activity); - - u.OldValue?.Status.UnbindFrom(localUserStatus); - u.NewValue.Status.BindTo(localUserStatus); - }, true); - - localUserStatus.BindTo(configStatus); - var thread = new Thread(run) { Name = "APIAccess", @@ -342,9 +331,8 @@ namespace osu.Game.Online.API { Debug.Assert(ThreadSafety.IsUpdateThread); - me.Status.Value = configStatus.Value ?? UserStatus.Online; - localUser.Value = me; + Status.Value = configStatus.Value ?? UserStatus.Online; state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; @@ -381,9 +369,10 @@ namespace osu.Game.Online.API localUser.Value = new APIUser { - Username = ProvidedUsername, - Status = { Value = configStatus.Value ?? UserStatus.Online } + Username = ProvidedUsername }; + + Status.Value = configStatus.Value ?? UserStatus.Online; } public void Perform(APIRequest request) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 5d63c04925..b338f4e8cb 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,9 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public Bindable Activity { get; } = new Bindable(); + public Bindable Status { get; } = new Bindable(UserStatus.Online); + + public Bindable Activity { get; } = new Bindable(); public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; @@ -69,15 +71,6 @@ namespace osu.Game.Online.API /// public IBindable State => state; - public DummyAPIAccess() - { - LocalUser.BindValueChanged(u => - { - u.OldValue?.Activity.UnbindFrom(Activity); - u.NewValue.Activity.BindTo(Activity); - }, true); - } - public virtual void Queue(APIRequest request) { request.AttachAPI(this); @@ -204,7 +197,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; - IBindable IAPIProvider.Activity => Activity; + IBindable IAPIProvider.Activity => Activity; /// /// Skip 2FA requirement for next login. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 1c4b2da742..cc065a659a 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -24,10 +24,15 @@ namespace osu.Game.Online.API /// IBindableList Friends { get; } + /// + /// The current user's status. + /// + Bindable Status { get; } + /// /// The current user's activity. /// - IBindable Activity { get; } + IBindable Activity { get; } /// /// The language supplied by this provider to API requests. diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index a829484506..30fceab852 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; -using osu.Framework.Bindables; using osu.Game.Extensions; using osu.Game.Users; @@ -56,10 +55,6 @@ namespace osu.Game.Online.API.Requests.Responses set => countryCodeString = value.ToString(); } - public readonly Bindable Status = new Bindable(); - - public readonly Bindable Activity = new Bindable(); - [JsonProperty(@"profile_colour")] public string Colour; diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index a8a14b1c78..b3204a7cd1 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -37,8 +37,9 @@ namespace osu.Game.Online.Metadata private IHubClientConnector? connector; private Bindable lastQueueId = null!; private IBindable localUser = null!; + + private IBindable userStatus = null!; private IBindable userActivity = null!; - private IBindable? userStatus; private HubConnection? connection => connector?.CurrentConnection; @@ -75,22 +76,20 @@ namespace osu.Game.Online.Metadata lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId); localUser = api.LocalUser.GetBoundCopy(); + userStatus = api.Status.GetBoundCopy(); userActivity = api.Activity.GetBoundCopy()!; } protected override void LoadComplete() { base.LoadComplete(); - localUser.BindValueChanged(_ => + + userStatus.BindValueChanged(status => { if (localUser.Value is not GuestUser) - { - userStatus = localUser.Value.Status.GetBoundCopy(); - userStatus.BindValueChanged(status => UpdateStatus(status.NewValue), true); - } - else - userStatus = null; + UpdateStatus(status.NewValue); }, true); + userActivity.BindValueChanged(activity => { if (localUser.Value is not GuestUser) @@ -117,7 +116,7 @@ namespace osu.Game.Online.Metadata if (localUser.Value is not GuestUser) { UpdateActivity(userActivity.Value); - UpdateStatus(userStatus?.Value); + UpdateStatus(userStatus.Value); } if (lastQueueId.Value >= 0) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index ee277ff538..2ca548fdf5 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -140,15 +140,11 @@ namespace osu.Game.Overlays.Dashboard Schedule(() => { - // explicitly refetch the user's status. - // things may have changed in between the time of scheduling and the time of actual execution. - if (onlineUsers.TryGetValue(userId, out var updatedStatus)) + userFlow.Add(userPanels[userId] = createUserPanel(user).With(p => { - user.Activity.Value = updatedStatus.Activity; - user.Status.Value = updatedStatus.Status; - } - - userFlow.Add(userPanels[userId] = createUserPanel(user)); + p.Status.Value = onlineUsers.GetValueOrDefault(userId).Status; + p.Activity.Value = onlineUsers.GetValueOrDefault(userId).Activity; + })); }); }); } @@ -162,8 +158,8 @@ namespace osu.Game.Overlays.Dashboard { if (userPanels.TryGetValue(kvp.Key, out var panel)) { - panel.User.Activity.Value = kvp.Value.Activity; - panel.User.Status.Value = kvp.Value.Status; + panel.Activity.Value = kvp.Value.Activity; + panel.Status.Value = kvp.Value.Status; } } @@ -223,6 +219,9 @@ namespace osu.Game.Overlays.Dashboard { public readonly APIUser User; + public readonly Bindable Status = new Bindable(); + public readonly Bindable Activity = new Bindable(); + public BindableBool CanSpectate { get; } = new BindableBool(); public IEnumerable FilterTerms { get; } @@ -271,8 +270,8 @@ namespace osu.Game.Overlays.Dashboard Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, // this is SHOCKING - Activity = { BindTarget = User.Activity }, - Status = { BindTarget = User.Status }, + Activity = { BindTarget = Activity }, + Status = { BindTarget = Status }, }, new PurpleRoundedButton { diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 84bd0c36b9..b947731f8b 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Settings; using osu.Game.Users; using osuTK; @@ -38,9 +37,7 @@ namespace osu.Game.Overlays.Login /// public Action? RequestHide; - private IBindable user = null!; - private readonly Bindable status = new Bindable(); - + private readonly Bindable status = new Bindable(); private readonly IBindable apiState = new Bindable(); [Resolved] @@ -71,13 +68,7 @@ namespace osu.Game.Overlays.Login apiState.BindTo(api.State); apiState.BindValueChanged(onlineStateChanged, true); - user = api.LocalUser.GetBoundCopy(); - user.BindValueChanged(u => - { - status.UnbindBindings(); - status.BindTo(u.NewValue.Status); - }, true); - + status.BindTo(api.Status); status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); } @@ -163,17 +154,17 @@ namespace osu.Game.Overlays.Login switch (action.NewValue) { case UserAction.Online: - api.LocalUser.Value.Status.Value = UserStatus.Online; + status.Value = UserStatus.Online; dropdown.StatusColour = colours.Green; break; case UserAction.DoNotDisturb: - api.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb; + status.Value = UserStatus.DoNotDisturb; dropdown.StatusColour = colours.Red; break; case UserAction.AppearOffline: - api.LocalUser.Value.Status.Value = UserStatus.Offline; + status.Value = UserStatus.Offline; dropdown.StatusColour = colours.Gray7; break; From b7a9b77efef2590a6f47e013165c95c71d837bb3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 00:01:19 +0900 Subject: [PATCH 234/620] Make config the definitive status value --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Online/API/APIAccess.cs | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index d4a75334a9..642da16d2d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -211,7 +211,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.LastProcessedMetadataId, -1); SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f); - SetDefault(OsuSetting.UserOnlineStatus, null); + SetDefault(OsuSetting.UserOnlineStatus, UserStatus.Online); SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true); SetDefault(OsuSetting.EditorTimelineShowBreaks, true); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 4f8c5dcb22..a4ac577a02 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -73,8 +73,6 @@ namespace osu.Game.Online.API private Bindable activity { get; } = new Bindable(); - private Bindable configStatus { get; } = new Bindable(); - protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -110,7 +108,7 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; - config.BindWith(OsuSetting.UserOnlineStatus, configStatus); + config.BindWith(OsuSetting.UserOnlineStatus, Status); if (HasLogin) { @@ -332,8 +330,6 @@ namespace osu.Game.Online.API Debug.Assert(ThreadSafety.IsUpdateThread); localUser.Value = me; - Status.Value = configStatus.Value ?? UserStatus.Online; - state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; }; @@ -371,8 +367,6 @@ namespace osu.Game.Online.API { Username = ProvidedUsername }; - - Status.Value = configStatus.Value ?? UserStatus.Online; } public void Perform(APIRequest request) @@ -597,7 +591,7 @@ namespace osu.Game.Online.API password = null; SecondFactorCode = null; authentication.Clear(); - configStatus.Value = UserStatus.Online; + Status.Value = UserStatus.Online; // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => From 208824e9f47de863860ac8a010cae9deabb0f20b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jan 2025 21:40:14 +0300 Subject: [PATCH 235/620] Add ability for cursor trail to spin --- .../Skinning/Legacy/LegacyCursorTrail.cs | 1 + .../Skinning/OsuSkinConfiguration.cs | 1 + .../UI/Cursor/CursorTrail.cs | 22 +++++++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index ca0002d8c0..4c21b94326 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void load(OsuConfigManager config, ISkinSource skinSource) { cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); + Spin = skin.GetConfig(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true; Texture = skin.GetTexture("cursortrail"); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 9685ab685d..81488ca1a3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Skinning CursorCentre, CursorExpand, CursorRotate, + CursorTrailRotate, HitCircleOverlayAboveNumber, // ReSharper disable once IdentifierTypo diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 5132dc2859..920a8c372f 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private IShader shader; private double timeOffset; private float time; + protected bool Spin { get; set; } /// /// The scale used on creation of a new trail part. @@ -220,6 +221,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private float time; private float fadeExponent; + private float angle; private readonly TrailPart[] parts = new TrailPart[max_sprites]; private Vector2 originPosition; @@ -239,6 +241,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; time = Source.time; fadeExponent = Source.FadeExponent; + angle = Source.Spin ? time / 10 : 0; originPosition = Vector2.Zero; @@ -279,6 +282,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor renderer.PushLocalMatrix(DrawInfo.Matrix); + float sin = MathF.Sin(angle); + float cos = MathF.Cos(angle); + foreach (var part in parts) { if (part.InvalidationID == -1) @@ -289,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + Position = rotateAround(new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -298,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + Position = rotateAround(new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -307,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + Position = rotateAround(new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -316,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + Position = rotateAround(new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, @@ -330,6 +336,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader.Unbind(); } + private static Vector2 rotateAround(Vector2 input, Vector2 origin, float sin, float cos) + { + float xTranslated = input.X - origin.X; + float yTranslated = input.Y - origin.Y; + + return new Vector2(xTranslated * cos - yTranslated * sin, xTranslated * sin + yTranslated * cos) + origin; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 7a6355d7cfe61abaaf4167ecda84755f4da9c9a4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jan 2025 22:51:17 +0300 Subject: [PATCH 236/620] Sync cursor trail rotation with the cursor --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs | 4 +++- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs index 375d81049d..e526c4f14c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public partial class LegacyCursor : SkinnableCursor { + public static readonly int REVOLUTION_DURATION = 10000; + private const float pressed_scale = 1.3f; private const float released_scale = 1f; @@ -52,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void LoadComplete() { if (spin) - ExpandTarget.Spin(10000, RotationDirection.Clockwise); + ExpandTarget.Spin(REVOLUTION_DURATION, RotationDirection.Clockwise); } public override void Expand() diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 920a8c372f..5b7d2d40d3 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -18,6 +18,7 @@ using osu.Framework.Graphics.Visualisation; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Timing; +using osu.Game.Rulesets.Osu.Skinning.Legacy; using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; @@ -79,9 +80,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); } + private double loadCompleteTime; + protected override void LoadComplete() { base.LoadComplete(); + loadCompleteTime = Parent!.Clock.CurrentTime; // using parent's clock since our is overridden resetTime(); } @@ -241,7 +245,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; time = Source.time; fadeExponent = Source.FadeExponent; - angle = Source.Spin ? time / 10 : 0; + // The goal is to sync trail rotation with the cursor. Cursor uses spin transform which starts rotation at LoadComplete time. + angle = Source.Spin ? (float)((Source.Parent!.Clock.CurrentTime - Source.loadCompleteTime) * 2 * Math.PI / LegacyCursor.REVOLUTION_DURATION) : 0; originPosition = Vector2.Zero; From 57a9911b22e29979f1bd55c16e1e911c8ab748a5 Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Wed, 15 Jan 2025 04:12:54 +0100 Subject: [PATCH 237/620] Apply beatmap offset on every beatmap set difficulty if they have the same audio --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index f93fa1b3c5..ac224794ea 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -165,13 +165,14 @@ namespace osu.Game.Screens.Play.PlayerSettings if (setInfo == null) // only the case for tests. return; - // Apply to all difficulties in a beatmap set for now (they generally always share timing). + // Apply to all difficulties in a beatmap set if they have the same audio + // (they generally always share timing). foreach (var b in setInfo.Beatmaps) { BeatmapUserSettings userSettings = b.UserSettings; double val = Current.Value; - if (userSettings.Offset != val) + if (userSettings.Offset != val && b.AudioEquals(beatmap.Value.BeatmapInfo)) userSettings.Offset = val; } }); From 0b764e63720a03867f7fb1ab183410e84ba6bf29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 16:18:34 +0900 Subject: [PATCH 238/620] Fix substring of `GetHashCode` potentially failing --- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 12f520d6c4..aeab6a96d0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -206,7 +206,7 @@ namespace osu.Game.Screens.SelectV2 updateSelection(); - void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString().Substring(0, 5)}] {stopwatch.ElapsedMilliseconds} ms: {text}"); + void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => From 8985a387344b91ce8ec48da0bfc183db67b14b4f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 16:53:55 +0900 Subject: [PATCH 239/620] Display up-to-date online status in user panels --- .../Visual/Online/TestSceneUserPanel.cs | 221 +++++++++--------- osu.Game/Online/Metadata/MetadataClient.cs | 16 ++ .../Dashboard/CurrentlyOnlineDisplay.cs | 59 ++--- osu.Game/Users/ExtendedUserPanel.cs | 105 +++++---- 4 files changed, 202 insertions(+), 199 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 4c2e47d336..b4dafd3107 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -4,17 +4,18 @@ using System; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.Metadata; using osu.Game.Users; using osuTK; @@ -23,144 +24,138 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public partial class TestSceneUserPanel : OsuTestScene { - private readonly Bindable activity = new Bindable(); - private readonly Bindable status = new Bindable(); - - private UserGridPanel boundPanel1 = null!; - private TestUserListPanel boundPanel2 = null!; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - [Cached(typeof(LocalUserStatisticsProvider))] - private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider(); - [Resolved] private IRulesetStore rulesetStore { get; set; } = null!; + private TestUserStatisticsProvider statisticsProvider = null!; + private TestMetadataClient metadataClient = null!; + private TestUserListPanel panel = null!; + [SetUp] public void SetUp() => Schedule(() => { - activity.Value = null; - status.Value = null; - - Remove(statisticsProvider, false); - Clear(); - Add(statisticsProvider); - - Add(new FillFlowContainer + Child = new DependencyProvidingContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(10f), + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(LocalUserStatisticsProvider), statisticsProvider = new TestUserStatisticsProvider()), + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], Children = new Drawable[] { - new UserBrickPanel(new APIUser + statisticsProvider, + metadataClient, + new FillFlowContainer { - Username = @"flyte", - Id = 3103765, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - }), - new UserBrickPanel(new APIUser - { - Username = @"peppy", - Id = 2, - Colour = "99EB47", - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - }), - new UserGridPanel(new APIUser - { - Username = @"flyte", - Id = 3103765, - CountryCode = CountryCode.JP, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - IsOnline = true - }) { Width = 300 }, - boundPanel1 = new UserGridPanel(new APIUser - { - Username = @"peppy", - Id = 2, - CountryCode = CountryCode.AU, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsSupporter = true, - SupportLevel = 3, - }) { Width = 300 }, - boundPanel2 = new TestUserListPanel(new APIUser - { - Username = @"Evast", - Id = 8195163, - CountryCode = CountryCode.BY, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, - LastVisit = DateTimeOffset.Now - }), - new UserRankPanel(new APIUser - { - Username = @"flyte", - Id = 3103765, - CountryCode = CountryCode.JP, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } - }) { Width = 300 }, - new UserRankPanel(new APIUser - { - Username = @"peppy", - Id = 2, - Colour = "99EB47", - CountryCode = CountryCode.AU, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } - }) { Width = 300 } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(10f), + Children = new Drawable[] + { + new UserBrickPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + }), + new UserBrickPanel(new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + }), + new UserGridPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + IsOnline = true + }) { Width = 300 }, + new UserGridPanel(new APIUser + { + Username = @"peppy", + Id = 2, + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsSupporter = true, + SupportLevel = 3, + }) { Width = 300 }, + panel = new TestUserListPanel(new APIUser + { + Username = @"peppy", + Id = 2, + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + LastVisit = DateTimeOffset.Now + }), + new UserRankPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } + }) { Width = 300 }, + new UserRankPanel(new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } + }) { Width = 300 } + } + } } - }); + }; - boundPanel1.Status.BindTo(status); - boundPanel1.Activity.BindTo(activity); - - boundPanel2.Status.BindTo(status); - boundPanel2.Activity.BindTo(activity); + metadataClient.BeginWatchingUserPresence(); }); [Test] public void TestUserStatus() { - AddStep("online", () => status.Value = UserStatus.Online); - AddStep("do not disturb", () => status.Value = UserStatus.DoNotDisturb); - AddStep("offline", () => status.Value = UserStatus.Offline); - AddStep("null status", () => status.Value = null); + AddStep("online", () => setPresence(UserStatus.Online, null)); + AddStep("do not disturb", () => setPresence(UserStatus.DoNotDisturb, null)); + AddStep("offline", () => setPresence(UserStatus.Offline, null)); } [Test] public void TestUserActivity() { - AddStep("set online status", () => status.Value = UserStatus.Online); - - AddStep("idle", () => activity.Value = null); - AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats"))); - AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk"))); - AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0)); - AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1)); - AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); - AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); - AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo())); - AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo())); - AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo())); + AddStep("idle", () => setPresence(UserStatus.Online, null)); + AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); + AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); + AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0))); + AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1))); + AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2))); + AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3))); + AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); + AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); + AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); } [Test] public void TestUserActivityChange() { - AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = UserStatus.Online); - AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); - AddStep("set choosing activity", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("set offline status", () => status.Value = UserStatus.Offline); - AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = UserStatus.Online); - AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); + AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent); + AddStep("set online status", () => setPresence(UserStatus.Online, null)); + AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent); + AddStep("set choosing activity", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("set offline status", () => setPresence(UserStatus.Offline, null)); + AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent); + AddStep("set online status", () => setPresence(UserStatus.Online, null)); + AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent); } [Test] @@ -185,6 +180,14 @@ namespace osu.Game.Tests.Visual.Online AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); } + private void setPresence(UserStatus status, UserActivity? activity) + { + if (status == UserStatus.Offline) + metadataClient.UserPresenceUpdated(panel.User.OnlineID, null); + else + metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); + } + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 6578f70f74..8f1fe0641f 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -47,6 +47,22 @@ namespace osu.Game.Online.Metadata /// public abstract IBindableDictionary FriendStates { get; } + /// + /// Attempts to retrieve the presence of a user. + /// + /// The user ID. + /// The user presence, or null if not available or the user's offline. + public UserPresence? GetPresence(int userId) + { + if (FriendStates.TryGetValue(userId, out UserPresence presence)) + return presence; + + if (UserStates.TryGetValue(userId, out presence)) + return presence; + + return null; + } + /// public abstract Task UpdateActivity(UserActivity? activity); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 2ca548fdf5..ef07f4538c 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; @@ -40,17 +38,20 @@ namespace osu.Game.Overlays.Dashboard private readonly IBindableDictionary onlineUsers = new BindableDictionary(); private readonly Dictionary userPanels = new Dictionary(); - private SearchContainer userFlow; - private BasicSearchTextBox searchTextBox; + private SearchContainer userFlow = null!; + private BasicSearchTextBox searchTextBox = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] - private MetadataClient metadataClient { get; set; } + private MetadataClient metadataClient { get; set; } = null!; + + [Resolved] + private UserLookupCache users { get; set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -99,9 +100,6 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue; } - [Resolved] - private UserLookupCache users { get; set; } - protected override void LoadComplete() { base.LoadComplete(); @@ -120,7 +118,7 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } - private void onUserUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserUpdated(object? sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { @@ -133,38 +131,13 @@ namespace osu.Game.Overlays.Dashboard users.GetUserAsync(userId).ContinueWith(task => { - APIUser user = task.GetResultSafely(); - - if (user == null) - return; - - Schedule(() => - { - userFlow.Add(userPanels[userId] = createUserPanel(user).With(p => - { - p.Status.Value = onlineUsers.GetValueOrDefault(userId).Status; - p.Activity.Value = onlineUsers.GetValueOrDefault(userId).Activity; - })); - }); + if (task.GetResultSafely() is APIUser user) + Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user))); }); } break; - case NotifyDictionaryChangedAction.Replace: - Debug.Assert(e.NewItems != null); - - foreach (var kvp in e.NewItems) - { - if (userPanels.TryGetValue(kvp.Key, out var panel)) - { - panel.Activity.Value = kvp.Value.Activity; - panel.Status.Value = kvp.Value.Status; - } - } - - break; - case NotifyDictionaryChangedAction.Remove: Debug.Assert(e.OldItems != null); @@ -179,7 +152,7 @@ namespace osu.Game.Overlays.Dashboard } }); - private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) + private void onPlayingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { @@ -219,9 +192,6 @@ namespace osu.Game.Overlays.Dashboard { public readonly APIUser User; - public readonly Bindable Status = new Bindable(); - public readonly Bindable Activity = new Bindable(); - public BindableBool CanSpectate { get; } = new BindableBool(); public IEnumerable FilterTerms { get; } @@ -268,10 +238,7 @@ namespace osu.Game.Overlays.Dashboard { RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - // this is SHOCKING - Activity = { BindTarget = Activity }, - Status = { BindTarget = Status }, + Origin = Anchor.TopCentre }, new PurpleRoundedButton { diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index e33fb7a44e..eb1115e296 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -1,10 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,44 +12,56 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users.Drawables; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; namespace osu.Game.Users { public abstract partial class ExtendedUserPanel : UserPanel { - public readonly Bindable Status = new Bindable(); + protected TextFlowContainer LastVisitMessage { get; private set; } = null!; - public readonly IBindable Activity = new Bindable(); + private StatusIcon statusIcon = null!; + private StatusText statusMessage = null!; - protected TextFlowContainer LastVisitMessage { get; private set; } + [Resolved] + private MetadataClient? metadata { get; set; } - private StatusIcon statusIcon; - private StatusText statusMessage; + [Resolved] + private IAPIProvider? api { get; set; } + + private UserStatus? lastStatus; + private UserActivity? lastActivity; + private DateTimeOffset? lastVisit; protected ExtendedUserPanel(APIUser user) : base(user) { + lastVisit = user.LastVisit; } [BackgroundDependencyLoader] private void load() { BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; - - Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value); - Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue); } protected override void LoadComplete() { base.LoadComplete(); - Status.TriggerChange(); + updatePresence(); // Colour should be applied immediately on first load. statusIcon.FinishTransforms(); } + protected override void Update() + { + base.Update(); + updatePresence(); + } + protected Container CreateStatusIcon() => statusIcon = new StatusIcon(); protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) @@ -70,15 +80,6 @@ namespace osu.Game.Users text.Origin = alignment; text.AutoSizeAxes = Axes.Both; text.Alpha = 0; - - if (User.LastVisit.HasValue) - { - text.AddText(@"Last seen "); - text.AddText(new DrawableDate(User.LastVisit.Value, italic: false) - { - Shadow = false - }); - } })); statusContainer.Add(statusMessage = new StatusText @@ -91,37 +92,53 @@ namespace osu.Game.Users return statusContainer; } - private void displayStatus(UserStatus? status, UserActivity activity = null) + private void updatePresence() { - if (status != null) + UserPresence? presence; + + if (User.Equals(api?.LocalUser.Value)) + presence = new UserPresence { Status = api.Status.Value, Activity = api.Activity.Value }; + else + presence = metadata?.GetPresence(User.OnlineID); + + UserStatus status = presence?.Status ?? UserStatus.Offline; + UserActivity? activity = presence?.Activity; + + if (status == lastStatus && activity == lastActivity) + return; + + if (status == UserStatus.Offline && lastVisit != null) { - LastVisitMessage.FadeTo(status == UserStatus.Offline && User.LastVisit.HasValue ? 1 : 0); - - // Set status message based on activity (if we have one) and status is not offline - if (activity != null && status != UserStatus.Offline) + LastVisitMessage.FadeTo(1); + LastVisitMessage.Clear(); + LastVisitMessage.AddText(@"Last seen "); + LastVisitMessage.AddText(new DrawableDate(lastVisit.Value, italic: false) { - statusMessage.Text = activity.GetStatus(); - statusMessage.TooltipText = activity.GetDetails(); - statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); - return; - } + Shadow = false + }); + } + else + LastVisitMessage.FadeTo(0); - // Otherwise use only status + // Set status message based on activity (if we have one) and status is not offline + if (activity != null && status != UserStatus.Offline) + { + statusMessage.Text = activity.GetStatus(); + statusMessage.TooltipText = activity.GetDetails() ?? string.Empty; + statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); + } + + // Otherwise use only status + else + { statusMessage.Text = status.GetLocalisableDescription(); statusMessage.TooltipText = string.Empty; - statusIcon.FadeColour(status.Value.GetAppropriateColour(Colours), 500, Easing.OutQuint); - - return; + statusIcon.FadeColour(status.GetAppropriateColour(Colours), 500, Easing.OutQuint); } - // Fallback to web status if local one is null - if (User.IsOnline) - { - Status.Value = UserStatus.Online; - return; - } - - Status.Value = UserStatus.Offline; + lastStatus = status; + lastActivity = activity; + lastVisit = status != UserStatus.Offline ? DateTimeOffset.Now : lastVisit; } protected override bool OnHover(HoverEvent e) From 60279476570a20b5a9bf40525c615078a83c5e6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 17:01:07 +0900 Subject: [PATCH 240/620] Move animation handling to `Carousel` implementation to better handle add/removes With the animation logic being external, it was going to make it very hard to apply the scroll offset when a new panel is added or removed before the current selection. There's no real reason for the animations to be local to beatmap carousel. If there's a usage in the future where the animation is to change, we can add more customisation to `Carousel` itself. --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 28 ++++++++++++++- .../Screens/SelectV2/BeatmapCarouselPanel.cs | 15 +------- osu.Game/Screens/SelectV2/Carousel.cs | 36 ++++++++++++++++--- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 4 +-- 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 1d7d6041ae..f99e0a418a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -168,7 +168,33 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestScrollPositionVelocityMaintained() + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + + AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); + AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Item!.Selected.Value))); + + AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); + + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + + AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); + AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() { Quad positionBefore = default; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 5b8ae211d1..27023b50be 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osuTK; @@ -98,18 +97,6 @@ namespace osu.Game.Screens.SelectV2 return true; } - protected override void Update() - { - base.Update(); - - Debug.Assert(Item != null); - - if (DrawYPosition != Item.CarouselYPosition) - { - DrawYPosition = Interpolation.DampContinuously(DrawYPosition, Item.CarouselYPosition, 50, Time.Elapsed); - } - } - - public double DrawYPosition { get; private set; } + public double DrawYPosition { get; set; } } } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index aeab6a96d0..12a86be7b9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -107,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 private List? displayedCarouselItems; - private readonly DoublePrecisionScroll scroll; + private readonly CarouselScrollContainer scroll; protected Carousel() { @@ -118,7 +119,7 @@ namespace osu.Game.Screens.SelectV2 Colour = Color4.Black, RelativeSizeAxes = Axes.Both, }, - scroll = new DoublePrecisionScroll + scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, Masking = false, @@ -389,13 +390,13 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class DoublePrecisionScroll : OsuScrollContainer + private partial class CarouselScrollContainer : OsuScrollContainer { public readonly Container Panels; public void SetLayoutHeight(float height) => Panels.Height = height; - public DoublePrecisionScroll() + public CarouselScrollContainer() { // Managing our own custom layout within ScrollContent causes feedback with public ScrollContainer calculations, // so we must maintain one level of separation from ScrollContent. @@ -406,6 +407,33 @@ namespace osu.Game.Screens.SelectV2 }); } + public override void OffsetScrollPosition(double offset) + { + base.OffsetScrollPosition(offset); + + foreach (var panel in Panels) + { + var c = (ICarouselPanel)panel; + Debug.Assert(c.Item != null); + + c.DrawYPosition += offset; + } + } + + protected override void Update() + { + base.Update(); + + foreach (var panel in Panels) + { + var c = (ICarouselPanel)panel; + Debug.Assert(c.Item != null); + + if (c.DrawYPosition != c.Item.CarouselYPosition) + c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); + } + } + public override void Clear(bool disposeChildren) { Panels.Height = 0; diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index d729df7876..117feab621 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -11,9 +11,9 @@ namespace osu.Game.Screens.SelectV2 public interface ICarouselPanel { /// - /// The Y position which should be used for displaying this item within the carousel. + /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. /// - double DrawYPosition { get; } + double DrawYPosition { get; set; } /// /// The carousel item this drawable is representing. This is managed by and should not be set manually. From 2763cb0b4e9febbfc7f9d185c4acc737214e9a58 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 17:14:16 +0900 Subject: [PATCH 241/620] Fix inspection --- osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index ef07f4538c..e6e1850721 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -196,8 +196,8 @@ namespace osu.Game.Overlays.Dashboard public IEnumerable FilterTerms { get; } - [Resolved(canBeNull: true)] - private IPerformFromScreenRunner performer { get; set; } + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } public bool FilteringActive { set; get; } From 7ca3a6fc26f78c639ddefb725c25f40442c94dc6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 17:48:22 +0900 Subject: [PATCH 242/620] Clear Discord presence when logged out --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 94804ad1cc..6c7e7d393f 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -145,7 +145,7 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) + if (!api.IsLoggedIn || status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) { client.ClearPresence(); return; From e22dc09149097555fe81b66e5ff8ef36fca9caaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 20:42:46 +0900 Subject: [PATCH 243/620] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index dbb0a6d610..7ae16b8b70 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index afbcf49d32..ece42e87b4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 582c5180b9830e01a34a0d68db1dec850059aa43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 13:24:31 +0100 Subject: [PATCH 244/620] Implement spectator list display - First step for https://github.com/ppy/osu/issues/22087 - Supersedes / closes https://github.com/ppy/osu/pull/22795 Roughly uses design shown in https://github.com/ppy/osu/pull/22795#issuecomment-1579936284 with some modifications to better fit everything else, and some customisation options so it can fit better on other skins. --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 49 ++++ .../Localisation/HUD/SpectatorListStrings.cs | 19 ++ osu.Game/Online/Chat/DrawableLinkCompiler.cs | 16 +- osu.Game/Screens/Play/HUD/SpectatorList.cs | 219 ++++++++++++++++++ 4 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs create mode 100644 osu.Game/Localisation/HUD/SpectatorListStrings.cs create mode 100644 osu.Game/Screens/Play/HUD/SpectatorList.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs new file mode 100644 index 0000000000..3cd37baafd --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [TestFixture] + public partial class TestSceneSpectatorList : OsuTestScene + { + private readonly BindableList spectators = new BindableList(); + private readonly Bindable localUserPlayingState = new Bindable(); + + private int counter; + + [Test] + public void TestBasics() + { + SpectatorList list = null!; + AddStep("create spectator list", () => Child = list = new SpectatorList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spectators = { BindTarget = spectators }, + UserPlayingState = { BindTarget = localUserPlayingState } + }); + + AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); + AddStep("add a user", () => + { + int id = Interlocked.Increment(ref counter); + spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); + }); + AddStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count))); + AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); + AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); + AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); + AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); + AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); + } + } +} diff --git a/osu.Game/Localisation/HUD/SpectatorListStrings.cs b/osu.Game/Localisation/HUD/SpectatorListStrings.cs new file mode 100644 index 0000000000..8d82250526 --- /dev/null +++ b/osu.Game/Localisation/HUD/SpectatorListStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.HUD +{ + public static class SpectatorListStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SpectatorList"; + + /// + /// "Spectators ({0})" + /// + public static LocalisableString SpectatorCount(int arg0) => new TranslatableString(getKey(@"spectator_count"), @"Spectators ({0})", arg0); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs index fa107a0e43..f640a3dab5 100644 --- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs +++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Online.Chat { @@ -27,6 +28,18 @@ namespace osu.Game.Online.Chat /// public readonly SlimReadOnlyListWrapper Parts; + public new Color4 IdleColour + { + get => base.IdleColour; + set => base.IdleColour = value; + } + + public new Color4 HoverColour + { + get => base.HoverColour; + set => base.HoverColour = value; + } + [Resolved] private OverlayColourProvider? overlayColourProvider { get; set; } @@ -56,7 +69,8 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load(OsuColour colours) { - IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; + if (IdleColour == default) + IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; } protected override IEnumerable EffectTargets => Parts; diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs new file mode 100644 index 0000000000..ad94b23cd7 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -0,0 +1,219 @@ +// 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.Specialized; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osu.Game.Users; +using osu.Game.Localisation.HUD; +using osu.Game.Localisation.SkinComponents; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class SpectatorList : CompositeDrawable + { + private const int max_spectators_displayed = 10; + + public BindableList Spectators { get; } = new BindableList(); + public Bindable UserPlayingState { get; } = new Bindable(); + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] + public Bindable Font { get; } = new Bindable(Typeface.Torus); + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); + + protected OsuSpriteText Header { get; private set; } = null!; + + private FillFlowContainer mainFlow = null!; + private FillFlowContainer spectatorsFlow = null!; + private DrawablePool pool = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + mainFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 250, + AutoSizeEasing = Easing.OutQuint, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + Header = new OsuSpriteText + { + Colour = colours.Blue0, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + }, + spectatorsFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + } + } + }, + pool = new DrawablePool(max_spectators_displayed), + }; + + HeaderColour.Value = Header.Colour; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Spectators.BindCollectionChanged(onSpectatorsChanged, true); + UserPlayingState.BindValueChanged(_ => updateVisibility()); + + Font.BindValueChanged(_ => updateAppearance()); + HeaderColour.BindValueChanged(_ => updateAppearance(), true); + FinishTransforms(true); + } + + private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + { + var spectator = (Spectator)e.NewItems![i]!; + int index = e.NewStartingIndex + i; + + if (index >= max_spectators_displayed) + break; + + spectatorsFlow.Insert(e.NewStartingIndex + i, pool.Get(entry => + { + entry.Current.Value = spectator; + entry.UserPlayingState = UserPlayingState; + })); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + spectatorsFlow.RemoveAll(entry => e.OldItems!.Contains(entry.Current.Value), false); + + for (int i = 0; i < spectatorsFlow.Count; i++) + spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); + + if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + { + for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) + { + var spectator = Spectators[i]; + spectatorsFlow.Insert(i, pool.Get(entry => + { + entry.Current.Value = spectator; + entry.UserPlayingState = UserPlayingState; + })); + } + } + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + spectatorsFlow.Clear(false); + break; + } + + default: + throw new NotSupportedException(); + } + + Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); + updateVisibility(); + } + + private void updateVisibility() + { + mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + } + + private void updateAppearance() + { + Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + Header.Colour = HeaderColour.Value; + } + + private partial class SpectatorListEntry : PoolableDrawable + { + public Bindable Current { get; } = new Bindable(); + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable UserPlayingState + { + get => current.Current; + set => current.Current = value; + } + + private OsuSpriteText username = null!; + private DrawableLinkCompiler? linkCompiler; + + [Resolved] + private OsuGame? game { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + username = new OsuSpriteText(), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + UserPlayingState.BindValueChanged(_ => updateEnabledState()); + Current.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + username.Text = Current.Value.Username; + linkCompiler?.Expire(); + AddInternal(linkCompiler = new DrawableLinkCompiler([username]) + { + IdleColour = Colour4.White, + Action = () => game?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, Current.Value)), + }); + updateEnabledState(); + } + + private void updateEnabledState() + { + if (linkCompiler != null) + linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; + } + } + + public record Spectator(int OnlineID, string Username) : IUser + { + public CountryCode CountryCode => CountryCode.Unknown; + public bool IsBot => false; + } + } +} From 43fc48a3f300c13433f957ed99c65541e0c4f801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 14:44:13 +0100 Subject: [PATCH 245/620] Add client methods allowing users to be notified of who is watching them --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 9 ++++- osu.Game/Online/Spectator/ISpectatorClient.cs | 12 ++++++ .../Online/Spectator/OnlineSpectatorClient.cs | 2 + osu.Game/Online/Spectator/SpectatorClient.cs | 35 ++++++++++++++++- osu.Game/Online/Spectator/SpectatorUser.cs | 39 +++++++++++++++++++ osu.Game/Screens/Play/HUD/SpectatorList.cs | 14 ++----- 6 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Online/Spectator/SpectatorUser.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 3cd37baafd..5be1829b85 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Graphics; +using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; @@ -15,7 +16,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public partial class TestSceneSpectatorList : OsuTestScene { - private readonly BindableList spectators = new BindableList(); + private readonly BindableList spectators = new BindableList(); private readonly Bindable localUserPlayingState = new Bindable(); private int counter; @@ -36,7 +37,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add a user", () => { int id = Interlocked.Increment(ref counter); - spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); + spectators.Add(new SpectatorUser + { + OnlineID = id, + Username = $"User {id}" + }); }); AddStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count))); AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 2dc2283c23..2b73037cb8 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -37,5 +37,17 @@ namespace osu.Game.Online.Spectator /// The ID of the user who achieved the score. /// The ID of the score. Task UserScoreProcessed(int userId, long scoreId); + + /// + /// Signals that another user has started watching this client. + /// + /// The information about the user who started watching. + Task UserStartedWatching(SpectatorUser[] user); + + /// + /// Signals that another user has ended watching this client + /// + /// The ID of the user who ended watching. + Task UserEndedWatching(int userId); } } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 036cfa1d76..645d7054dc 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -42,6 +42,8 @@ namespace osu.Game.Online.Spectator connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.On(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed); + connection.On(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching); + connection.On(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested); }; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index fb7a3d13ca..ac11dad0f0 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -36,9 +36,14 @@ namespace osu.Game.Online.Spectator public abstract IBindable IsConnected { get; } /// - /// The states of all users currently being watched. + /// The states of all users currently being watched by the local user. /// - public virtual IBindableDictionary WatchedUserStates => watchedUserStates; + public IBindableDictionary WatchedUserStates => watchedUserStates; + + /// + /// All users who are currently watching the local user. + /// + public IBindableList WatchingUsers => watchingUsers; /// /// A global list of all players currently playing. @@ -82,6 +87,7 @@ namespace osu.Game.Online.Spectator private readonly BindableDictionary watchedUserStates = new BindableDictionary(); + private readonly BindableList watchingUsers = new BindableList(); private readonly BindableList playingUsers = new BindableList(); private readonly SpectatorState currentState = new SpectatorState(); @@ -127,6 +133,7 @@ namespace osu.Game.Online.Spectator { playingUsers.Clear(); watchedUserStates.Clear(); + watchingUsers.Clear(); } }), true); } @@ -179,6 +186,30 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } + Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users) + { + Schedule(() => + { + foreach (var user in users) + { + if (!watchingUsers.Contains(user)) + watchingUsers.Add(user); + } + }); + + return Task.CompletedTask; + } + + Task ISpectatorClient.UserEndedWatching(int userId) + { + Schedule(() => + { + watchingUsers.RemoveAll(u => u.OnlineID == userId); + }); + + return Task.CompletedTask; + } + Task IStatefulUserHubClient.DisconnectRequested() { Schedule(() => DisconnectInternal()); diff --git a/osu.Game/Online/Spectator/SpectatorUser.cs b/osu.Game/Online/Spectator/SpectatorUser.cs new file mode 100644 index 0000000000..9c9563be70 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatorUser.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; +using osu.Game.Users; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + [MessagePackObject] + public class SpectatorUser : IUser, IEquatable + { + [Key(0)] + public int OnlineID { get; set; } + + [Key(1)] + public string Username { get; set; } = string.Empty; + + [IgnoreMember] + public CountryCode CountryCode => CountryCode.Unknown; + + [IgnoreMember] + public bool IsBot => false; + + public bool Equals(SpectatorUser? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + + return OnlineID == other.OnlineID; + } + + public override bool Equals(object? obj) => Equals(obj as SpectatorUser); + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => OnlineID; + } +} diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ad94b23cd7..90b2ae0a3d 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -13,9 +13,9 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -using osu.Game.Users; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Spectator; namespace osu.Game.Screens.Play.HUD { @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - public BindableList Spectators { get; } = new BindableList(); + public BindableList Spectators { get; } = new BindableList(); public Bindable UserPlayingState { get; } = new Bindable(); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD { for (int i = 0; i < e.NewItems!.Count; i++) { - var spectator = (Spectator)e.NewItems![i]!; + var spectator = (SpectatorUser)e.NewItems![i]!; int index = e.NewStartingIndex + i; if (index >= max_spectators_displayed) @@ -157,7 +157,7 @@ namespace osu.Game.Screens.Play.HUD private partial class SpectatorListEntry : PoolableDrawable { - public Bindable Current { get; } = new Bindable(); + public Bindable Current { get; } = new Bindable(); private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -209,11 +209,5 @@ namespace osu.Game.Screens.Play.HUD linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; } } - - public record Spectator(int OnlineID, string Username) : IUser - { - public CountryCode CountryCode => CountryCode.Unknown; - public bool IsBot => false; - } } } From 12b2631e5e2b85f621866e87579ef69b218e2ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 15:03:37 +0100 Subject: [PATCH 246/620] Add a skinnable variant of spectator list & hook it up to online data --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 90b2ae0a3d..733f2d2514 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -16,6 +16,8 @@ using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; using osu.Game.Online.Spectator; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -43,8 +45,9 @@ namespace osu.Game.Screens.Play.HUD { AutoSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChildren = new[] { + Empty().With(t => t.Size = new Vector2(100, 50)), mainFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -210,4 +213,16 @@ namespace osu.Game.Screens.Play.HUD } } } + + public partial class SkinnableSpectatorList : SpectatorList, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [BackgroundDependencyLoader] + private void load(SpectatorClient client, Player player) + { + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(player.PlayingState); + } + } } From 99c7e164dc7465d2bd748b0c20d895e79087e429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 15 Jan 2025 13:08:32 +0100 Subject: [PATCH 247/620] Add skinnable spectator list to default skins --- .../Legacy/CatchLegacySkinTransformer.cs | 10 ++++++ .../Argon/ManiaArgonSkinTransformer.cs | 11 ++++++ .../Legacy/ManiaLegacySkinTransformer.cs | 11 ++++++ .../Legacy/OsuLegacySkinTransformer.cs | 14 ++++++++ osu.Game/Skinning/ArgonSkin.cs | 35 +++++++++++++++---- osu.Game/Skinning/LegacySkin.cs | 15 +++++++- osu.Game/Skinning/TrianglesSkin.cs | 24 +++++++++---- 7 files changed, 106 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 69efb7fbca..978a098990 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var keyCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (keyCounter != null) { @@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy keyCounter.Origin = Anchor.TopRight; keyCounter.Position = new Vector2(0, -40) * 1.6f; } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(10, -10); + } }) { Children = new Drawable[] { new LegacyKeyCounterDisplay(), + new SkinnableSpectatorList(), } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index c37c18081a..48c487e70d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -9,7 +9,9 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon @@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon combo.Origin = Anchor.Centre; combo.Y = 200; } + + if (spectatorList != null) + spectatorList.Position = new Vector2(36, -66); }) { new ArgonManiaComboCounter(), + new SkinnableSpectatorList + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 8f425edc44..359f21561f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -15,7 +15,9 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { @@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy combo.Origin = Anchor.Centre; combo.Y = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(10, -10); + } }) { new LegacyManiaComboCounter(), + new SkinnableSpectatorList(), }; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 636a9ecb21..03e4bb24f1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; @@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } var combo = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(); if (combo != null) { combo.Anchor = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft; combo.Scale = new Vector2(1.28f); + + pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X); + } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = pos; } }) { @@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { new LegacyDefaultComboCounter(), new LegacyKeyCounterDisplay(), + new SkinnableSpectatorList(), } }; } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 771d10d73b..c3319b738d 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -7,7 +7,6 @@ using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; @@ -110,15 +109,37 @@ namespace osu.Game.Skinning case GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) { - return new Container + return new DefaultSkinComponentsContainer(container => + { + var comboCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(36, -66); + + if (comboCounter != null) + { + comboCounter.Position = pos; + pos -= new Vector2(0, comboCounter.DrawHeight * 1.4f + 20); + } + + if (spectatorList != null) + spectatorList.Position = pos; + }) { RelativeSizeAxes = Axes.Both, - Child = new ArgonComboCounter + Children = new Drawable[] { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(36, -66), - Scale = new Vector2(1.3f), + new ArgonComboCounter + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Scale = new Vector2(1.3f), + }, + new SkinnableSpectatorList + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + } }, }; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 6faadfba9b..c607c57fcc 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -367,16 +367,29 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var combo = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(); if (combo != null) { combo.Anchor = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft; combo.Scale = new Vector2(1.28f); + + pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X); + } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = pos; } }) { - new LegacyDefaultComboCounter() + new LegacyDefaultComboCounter(), + new SkinnableSpectatorList(), }; } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index d562fd3256..8853a5c4ac 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; +using osu.Game.Graphics; using osu.Game.IO; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; @@ -90,6 +91,7 @@ namespace osu.Game.Skinning var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (score != null) { @@ -142,17 +144,26 @@ namespace osu.Game.Skinning } } + const float padding = 10; + + // Hard to find this at runtime, so taken from the most expanded state during replay. + const float song_progress_offset_height = 73; + if (songProgress != null && keyCounter != null) { - const float padding = 10; - - // Hard to find this at runtime, so taken from the most expanded state during replay. - const float song_progress_offset_height = 73; - keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding)); } + + if (spectatorList != null) + { + spectatorList.Font.Value = Typeface.Venera; + spectatorList.HeaderColour.Value = new OsuColour().BlueLighter; + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(padding, -(song_progress_offset_height + padding)); + } }) { Children = new Drawable[] @@ -165,7 +176,8 @@ namespace osu.Game.Skinning new DefaultKeyCounterDisplay(), new BarHitErrorMeter(), new BarHitErrorMeter(), - new TrianglesPerformancePointsCounter() + new TrianglesPerformancePointsCounter(), + new SkinnableSpectatorList(), } }; From 2eb63e6fe045f7e2b6087897669add86cc8932cf Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 15 Jan 2025 20:38:51 +0300 Subject: [PATCH 248/620] Simplify rotation sync with no clocks involved --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 8 ++------ osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 5 +++++ osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs | 3 +++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 5b7d2d40d3..7809a0bf05 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -18,7 +18,6 @@ using osu.Framework.Graphics.Visualisation; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Timing; -using osu.Game.Rulesets.Osu.Skinning.Legacy; using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; @@ -41,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private double timeOffset; private float time; protected bool Spin { get; set; } + public float PartRotation { get; set; } /// /// The scale used on creation of a new trail part. @@ -80,12 +80,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); } - private double loadCompleteTime; - protected override void LoadComplete() { base.LoadComplete(); - loadCompleteTime = Parent!.Clock.CurrentTime; // using parent's clock since our is overridden resetTime(); } @@ -245,8 +242,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; time = Source.time; fadeExponent = Source.FadeExponent; - // The goal is to sync trail rotation with the cursor. Cursor uses spin transform which starts rotation at LoadComplete time. - angle = Source.Spin ? (float)((Source.Parent!.Clock.CurrentTime - Source.loadCompleteTime) * 2 * Math.PI / LegacyCursor.REVOLUTION_DURATION) : 0; + angle = Source.Spin ? float.DegreesToRadians(Source.PartRotation) : 0; originPosition = Vector2.Zero; diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index c2f7d84f5e..e84fb9e2d6 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -36,6 +36,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One; + /// + /// The current rotation of the cursor. + /// + public float CurrentRotation => skinnableCursor.ExpandTarget?.Rotation ?? 0; + public IBindable CursorScale => cursorScale; /// diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 8c0871d54f..974d99d7c8 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -83,7 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor base.Update(); if (cursorTrail.Drawable is CursorTrail trail) + { trail.NewPartScale = ActiveCursor.CurrentExpandedScale; + trail.PartRotation = ActiveCursor.CurrentRotation; + } } public bool OnPressed(KeyBindingPressEvent e) From 6008c3138ead169b6586dfaf481afa832cda3bc6 Mon Sep 17 00:00:00 2001 From: Shawn Presser Date: Wed, 15 Jan 2025 19:29:41 -0600 Subject: [PATCH 249/620] Typo fix --- osu.Game/Rulesets/Scoring/HitResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index b6cfca58db..46c0371d9f 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Scoring /// /// /// This miss window should determine how early a hit can be before it is considered for judgement (as opposed to being ignored as - /// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time). + /// "too far in the future"). It should also define when a forced miss should be triggered (as a result of no user input in time). /// [Description(@"Miss")] [EnumMember(Value = "miss")] From 920648c267484c4e57386bbc39bd3a83c6f9ac35 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 14:00:27 +0900 Subject: [PATCH 250/620] Minor refactorings and xmldoc additions --- .../Skinning/Legacy/LegacyCursorTrail.cs | 2 +- .../UI/Cursor/CursorTrail.cs | 48 +++++++++++++------ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index 4c21b94326..375bef721d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void load(OsuConfigManager config, ISkinSource skinSource) { cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); - Spin = skin.GetConfig(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true; + AllowPartRotation = skin.GetConfig(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true; Texture = skin.GetTexture("cursortrail"); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 7809a0bf05..1c2d69fa00 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -34,21 +34,24 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// protected virtual float FadeExponent => 1.7f; - private readonly TrailPart[] parts = new TrailPart[max_sprites]; - private int currentIndex; - private IShader shader; - private double timeOffset; - private float time; - protected bool Spin { get; set; } - public float PartRotation { get; set; } - /// /// The scale used on creation of a new trail part. /// - public Vector2 NewPartScale = Vector2.One; + public Vector2 NewPartScale { get; set; } = Vector2.One; - private Anchor trailOrigin = Anchor.Centre; + /// + /// The rotation (in degrees) to apply to trail parts when is true. + /// + public float PartRotation { get; set; } + /// + /// Whether to rotate trail parts based on the value of . + /// + protected bool AllowPartRotation { get; set; } + + /// + /// The trail part texture origin. + /// protected Anchor TrailOrigin { get => trailOrigin; @@ -59,6 +62,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } } + private readonly TrailPart[] parts = new TrailPart[max_sprites]; + private Anchor trailOrigin = Anchor.Centre; + private int currentIndex; + private IShader shader; + private double timeOffset; + private float time; + public CursorTrail() { // as we are currently very dependent on having a running clock, let's make our own clock for the time being. @@ -242,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; time = Source.time; fadeExponent = Source.FadeExponent; - angle = Source.Spin ? float.DegreesToRadians(Source.PartRotation) : 0; + angle = Source.AllowPartRotation ? float.DegreesToRadians(Source.PartRotation) : 0; originPosition = Vector2.Zero; @@ -296,7 +306,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = rotateAround(new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), + Position = rotateAround( + new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -305,7 +317,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = rotateAround(new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), + Position = rotateAround( + new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, + part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -314,7 +328,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = rotateAround(new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), part.Position, sin, cos), + Position = rotateAround( + new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -323,7 +339,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = rotateAround(new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), part.Position, sin, cos), + Position = rotateAround( + new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, From fe8389bc2b0a65c39351275f3db4e79b6afc514c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 14:11:21 +0900 Subject: [PATCH 251/620] Add test --- .../TestSceneCursorTrail.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 17f365f820..a8a65f7edb 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -17,6 +18,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Framework.Testing.Input; using osu.Game.Audio; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -103,6 +105,23 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("contract", () => this.ChildrenOfType().Single().NewPartScale = Vector2.One); } + [Test] + public void TestRotation() + { + createTest(() => + { + var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true, enableRotation: true); + var legacyCursorTrail = new LegacyRotatingCursorTrail(skinContainer) + { + NewPartScale = new Vector2(10) + }; + + skinContainer.Child = legacyCursorTrail; + + return skinContainer; + }); + } + private void createTest(Func createContent) => AddStep("create trail", () => { Clear(); @@ -121,12 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly IRenderer renderer; private readonly bool provideMiddle; private readonly bool provideCursor; + private readonly bool enableRotation; - public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true) + public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true, bool enableRotation = false) { this.renderer = renderer; this.provideMiddle = provideMiddle; this.provideCursor = provideCursor; + this.enableRotation = enableRotation; RelativeSizeAxes = Axes.Both; } @@ -152,7 +173,19 @@ namespace osu.Game.Rulesets.Osu.Tests public ISample GetSample(ISampleInfo sampleInfo) => null; - public IBindable GetConfig(TLookup lookup) => null; + public IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + case OsuSkinConfiguration osuLookup: + if (osuLookup == OsuSkinConfiguration.CursorTrailRotate) + return SkinUtils.As(new BindableBool(enableRotation)); + + break; + } + + return null; + } public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null; @@ -185,5 +218,19 @@ namespace osu.Game.Rulesets.Osu.Tests MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos)); } } + + private partial class LegacyRotatingCursorTrail : LegacyCursorTrail + { + public LegacyRotatingCursorTrail([NotNull] ISkin skin) + : base(skin) + { + } + + protected override void Update() + { + base.Update(); + PartRotation += (float)(Time.Elapsed * 0.1); + } + } } } From 46e9da7960ef551d4127305d7ce66907bb47e774 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 15:34:20 +0900 Subject: [PATCH 252/620] Fix style display refreshing on all room updates --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ec2ed90eca..edb44a7666 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -523,19 +523,21 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - if (UserStyleDisplayContainer != null) - { - PlaylistItem gameplayItem = SelectedItem.Value.With( - ruleset: GetGameplayRuleset().OnlineID, - beatmap: new Optional(GetGameplayBeatmap())); + if (UserStyleDisplayContainer == null) + return; - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; - } + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; } protected virtual APIMod[] GetGameplayMods() From 409ea53ad96441104494bb73e75f6155bcd0be76 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 15:51:53 +0900 Subject: [PATCH 253/620] Send `beatmap_id` when creating score --- osu.Game/Online/Rooms/CreateRoomScoreRequest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs index e0f91032fd..eb2879ba6c 100644 --- a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs @@ -31,6 +31,7 @@ namespace osu.Game.Online.Rooms var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; req.AddParameter("version_hash", versionHash); + req.AddParameter("beatmap_id", beatmapInfo.OnlineID.ToString(CultureInfo.InvariantCulture)); req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash); req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture)); return req; From b54d95926329c0af71df64458196ec4339b66147 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 17:05:18 +0900 Subject: [PATCH 254/620] Expose as IBindable from IAPIProvider, writes via config --- .../Visual/Menus/TestSceneLoginOverlay.cs | 27 ++++++++++--------- osu.Game/Configuration/OsuConfigManager.cs | 5 ++++ osu.Game/Online/API/APIAccess.cs | 10 ++++--- osu.Game/Online/API/DummyAPIAccess.cs | 2 +- osu.Game/Online/API/IAPIProvider.cs | 6 ++--- .../Online/Metadata/OnlineMetadataClient.cs | 1 - osu.Game/Overlays/Login/LoginPanel.cs | 20 ++++++++------ 7 files changed, 41 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 5c12e0c102..3c97b291ee 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -29,9 +29,7 @@ namespace osu.Game.Tests.Visual.Menus private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; private LoginOverlay loginOverlay = null!; - - [Resolved] - private OsuConfigManager configManager { get; set; } = null!; + private OsuConfigManager localConfig = null!; [Cached(typeof(LocalUserStatisticsProvider))] private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider(); @@ -39,6 +37,8 @@ namespace osu.Game.Tests.Visual.Menus [BackgroundDependencyLoader] private void load() { + Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); + Child = loginOverlay = new LoginOverlay { Anchor = Anchor.Centre, @@ -49,6 +49,7 @@ namespace osu.Game.Tests.Visual.Menus [SetUpSteps] public void SetUpSteps() { + AddStep("reset online state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.Online)); AddStep("show login overlay", () => loginOverlay.Show()); } @@ -89,7 +90,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("clear handler", () => dummyAPI.HandleRequest = null); assertDropdownState(UserAction.Online); - AddStep("change user state", () => dummyAPI.Status.Value = UserStatus.DoNotDisturb); + AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb)); assertDropdownState(UserAction.DoNotDisturb); } @@ -188,31 +189,31 @@ namespace osu.Game.Tests.Visual.Menus public void TestUncheckingRememberUsernameClearsIt() { AddStep("logout", () => API.Logout()); - AddStep("set username", () => configManager.SetValue(OsuSetting.Username, "test_user")); - AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true)); + AddStep("set username", () => localConfig.SetValue(OsuSetting.Username, "test_user")); + AddStep("set remember password", () => localConfig.SetValue(OsuSetting.SavePassword, true)); AddStep("uncheck remember username", () => { InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); - AddAssert("remember username off", () => configManager.Get(OsuSetting.SaveUsername), () => Is.False); - AddAssert("remember password off", () => configManager.Get(OsuSetting.SavePassword), () => Is.False); - AddAssert("username cleared", () => configManager.Get(OsuSetting.Username), () => Is.Empty); + AddAssert("remember username off", () => localConfig.Get(OsuSetting.SaveUsername), () => Is.False); + AddAssert("remember password off", () => localConfig.Get(OsuSetting.SavePassword), () => Is.False); + AddAssert("username cleared", () => localConfig.Get(OsuSetting.Username), () => Is.Empty); } [Test] public void TestUncheckingRememberPasswordClearsToken() { AddStep("logout", () => API.Logout()); - AddStep("set token", () => configManager.SetValue(OsuSetting.Token, "test_token")); - AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true)); + AddStep("set token", () => localConfig.SetValue(OsuSetting.Token, "test_token")); + AddStep("set remember password", () => localConfig.SetValue(OsuSetting.SavePassword, true)); AddStep("uncheck remember token", () => { InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().Last()); InputManager.Click(MouseButton.Left); }); - AddAssert("remember password off", () => configManager.Get(OsuSetting.SavePassword), () => Is.False); - AddAssert("token cleared", () => configManager.Get(OsuSetting.Token), () => Is.Empty); + AddAssert("remember password off", () => localConfig.Get(OsuSetting.SavePassword), () => Is.False); + AddAssert("token cleared", () => localConfig.Get(OsuSetting.Token), () => Is.Empty); } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 642da16d2d..d4f5b2af76 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -443,7 +443,12 @@ namespace osu.Game.Configuration EditorShowSpeedChanges, TouchDisableGameplayTaps, ModSelectTextSearchStartsActive, + + /// + /// The status for the current user to broadcast to other players. + /// UserOnlineStatus, + MultiplayerRoomFilter, HideCountryFlags, EditorTimelineShowTimingChanges, diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index a4ac577a02..dcb8a193bc 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -60,7 +60,7 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; - public Bindable Status { get; } = new Bindable(UserStatus.Online); + public IBindable Status => configStatus; public IBindable Activity => activity; public INotificationsClient NotificationsClient { get; } @@ -75,8 +75,8 @@ namespace osu.Game.Online.API protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); + private readonly Bindable configStatus = new Bindable(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); - private readonly Logger log; public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash) @@ -108,7 +108,7 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; - config.BindWith(OsuSetting.UserOnlineStatus, Status); + config.BindWith(OsuSetting.UserOnlineStatus, configStatus); if (HasLogin) { @@ -591,7 +591,9 @@ namespace osu.Game.Online.API password = null; SecondFactorCode = null; authentication.Clear(); - Status.Value = UserStatus.Online; + + // Reset the status to be broadcast on the next login, in case multiple players share the same system. + configStatus.Value = UserStatus.Online; // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index b338f4e8cb..4cd3c02414 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public Bindable Status { get; } = new Bindable(UserStatus.Online); + public IBindable Status { get; } = new Bindable(UserStatus.Online); public Bindable Activity { get; } = new Bindable(); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index cc065a659a..9ac7343885 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -25,12 +25,12 @@ namespace osu.Game.Online.API IBindableList Friends { get; } /// - /// The current user's status. + /// The status for the current user that's broadcast to other players. /// - Bindable Status { get; } + IBindable Status { get; } /// - /// The current user's activity. + /// The activity for the current user that's broadcast to other players. /// IBindable Activity { get; } diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index b3204a7cd1..101307636a 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -37,7 +37,6 @@ namespace osu.Game.Online.Metadata private IHubClientConnector? connector; private Bindable lastQueueId = null!; private IBindable localUser = null!; - private IBindable userStatus = null!; private IBindable userActivity = null!; diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index b947731f8b..6d74fc442e 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -37,12 +38,15 @@ namespace osu.Game.Overlays.Login /// public Action? RequestHide; - private readonly Bindable status = new Bindable(); private readonly IBindable apiState = new Bindable(); + private readonly Bindable configUserStatus = new Bindable(); [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + public override RectangleF BoundingBox => bounding ? base.BoundingBox : RectangleF.Empty; public bool Bounding @@ -65,11 +69,11 @@ namespace osu.Game.Overlays.Login { base.LoadComplete(); + config.BindWith(OsuSetting.UserOnlineStatus, configUserStatus); + configUserStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); + apiState.BindTo(api.State); apiState.BindValueChanged(onlineStateChanged, true); - - status.BindTo(api.Status); - status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); } private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => @@ -148,23 +152,23 @@ namespace osu.Game.Overlays.Login }, }; - updateDropdownCurrent(status.Value); + updateDropdownCurrent(configUserStatus.Value); dropdown.Current.BindValueChanged(action => { switch (action.NewValue) { case UserAction.Online: - status.Value = UserStatus.Online; + configUserStatus.Value = UserStatus.Online; dropdown.StatusColour = colours.Green; break; case UserAction.DoNotDisturb: - status.Value = UserStatus.DoNotDisturb; + configUserStatus.Value = UserStatus.DoNotDisturb; dropdown.StatusColour = colours.Red; break; case UserAction.AppearOffline: - status.Value = UserStatus.Offline; + configUserStatus.Value = UserStatus.Offline; dropdown.StatusColour = colours.Gray7; break; From c1f0c47586a3816936a5148732ccd4545eaf0a9b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 17:06:54 +0900 Subject: [PATCH 255/620] Allow setting of DummyAPIAccess status --- osu.Game/Online/API/DummyAPIAccess.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 4cd3c02414..3fef2b59cf 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public IBindable Status { get; } = new Bindable(UserStatus.Online); + public Bindable Status { get; } = new Bindable(UserStatus.Online); public Bindable Activity { get; } = new Bindable(); @@ -197,6 +197,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; + IBindable IAPIProvider.Status => Status; IBindable IAPIProvider.Activity => Activity; /// From aa3ae8324e19769df585bb45fe8af3935f830113 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 17:29:43 +0900 Subject: [PATCH 256/620] Add test for local user presence --- .../Visual/Online/TestSceneUserPanel.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index b4dafd3107..684e8b7b86 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Overlays; @@ -112,7 +113,11 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.AU, CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } - }) { Width = 300 } + }) { Width = 300 }, + new UserGridPanel(API.LocalUser.Value) + { + Width = 300 + } } } } @@ -180,6 +185,23 @@ namespace osu.Game.Tests.Visual.Online AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); } + [Test] + public void TestLocalUserActivity() + { + AddStep("idle", () => setLocalUserPresence(UserStatus.Online, null)); + AddStep("watching replay", () => setLocalUserPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); + AddStep("spectating user", () => setLocalUserPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); + AddStep("solo (osu!)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(0))); + AddStep("solo (osu!taiko)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(1))); + AddStep("solo (osu!catch)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(2))); + AddStep("solo (osu!mania)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(3))); + AddStep("choosing", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("editing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); + AddStep("modding beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); + AddStep("testing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); + AddStep("set offline status", () => setLocalUserPresence(UserStatus.Offline, null)); + } + private void setPresence(UserStatus status, UserActivity? activity) { if (status == UserStatus.Offline) @@ -188,6 +210,13 @@ namespace osu.Game.Tests.Visual.Online metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); } + private void setLocalUserPresence(UserStatus status, UserActivity? activity) + { + DummyAPIAccess dummyAPI = (DummyAPIAccess)API; + dummyAPI.Status.Value = status; + dummyAPI.Activity.Value = activity; + } + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) From a4174a36447fddeeb13c83fa6724520486271c62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 17:39:34 +0900 Subject: [PATCH 257/620] Add failing test coverage showing offset adjust is not limited correctly --- .../Navigation/TestSceneScreenNavigation.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 58e780cf16..326f21ff13 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -317,6 +317,82 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen()); } + [Test] + public void TestOffsetAdjustDuringPause() + { + Player player = null; + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() }); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + player = Game.ScreenStack.CurrentScreen as Player; + return player?.IsLoaded == true; + }); + + AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); + checkOffset(0); + + AddStep("adjust offset via keyboard", () => InputManager.Key(Key.Minus)); + checkOffset(-1); + + AddStep("pause", () => player.ChildrenOfType().First().Stop()); + AddUntilStep("wait for pause", () => player.ChildrenOfType().First().IsPaused.Value, () => Is.True); + AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus)); + checkOffset(-1); + + void checkOffset(double offset) => AddUntilStep($"offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, + () => Is.EqualTo(offset)); + } + + [Test] + public void TestOffsetAdjustDuringGameplay() + { + Player player = null; + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() }); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + player = Game.ScreenStack.CurrentScreen as Player; + return player?.IsLoaded == true; + }); + + AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); + checkOffset(0); + + AddStep("adjust offset via keyboard", () => InputManager.Key(Key.Minus)); + checkOffset(-1); + + AddStep("seek beyond 10 seconds", () => player.ChildrenOfType().First().Seek(10500)); + AddUntilStep("wait for seek", () => player.ChildrenOfType().First().CurrentTime, () => Is.GreaterThan(10600)); + AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus)); + checkOffset(-1); + + void checkOffset(double offset) => AddUntilStep($"offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, + () => Is.EqualTo(offset)); + } + [Test] public void TestRetryCountIncrements() { From 1d240eb4050d1c195e17cb36c0e511a1e834b6c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 17:23:02 +0900 Subject: [PATCH 258/620] Fix gameplay limitations for adjusting offset not actually being applied --- osu.Game/Screens/Play/Player.cs | 1 + .../PlayerSettings/BeatmapOffsetControl.cs | 46 +++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 228b77b780..513f4854ad 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -322,6 +322,7 @@ namespace osu.Game.Screens.Play } dependencies.CacheAs(DrawableRuleset.FrameStableClock); + dependencies.CacheAs(DrawableRuleset.FrameStableClock); // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index ac224794ea..e988760834 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -274,20 +274,36 @@ namespace osu.Game.Screens.Play.PlayerSettings beatmapOffsetSubscription?.Dispose(); } + protected override void Update() + { + base.Update(); + Current.Disabled = !allowOffsetAdjust; + } + + private bool allowOffsetAdjust + { + get + { + // General limitations to ensure players don't do anything too weird. + // These match stable for now. + if (player is SubmittingPlayer) + { + Debug.Assert(gameplayClock != null); + + // TODO: the blocking conditions should probably display a message. + if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.StartTime > 10000) + return false; + + if (gameplayClock.IsPaused.Value) + return false; + } + + return true; + } + } + public bool OnPressed(KeyBindingPressEvent e) { - // General limitations to ensure players don't do anything too weird. - // These match stable for now. - if (player is SubmittingPlayer) - { - // TODO: the blocking conditions should probably display a message. - if (player?.IsBreakTime.Value == false && gameplayClock?.CurrentTime - gameplayClock?.StartTime > 10000) - return false; - - if (gameplayClock?.IsPaused.Value == true) - return false; - } - // To match stable, this should adjust by 5 ms, or 1 ms when holding alt. // But that is hard to make work with global actions due to the operating mode. // Let's use the more precise as a default for now. @@ -296,11 +312,13 @@ namespace osu.Game.Screens.Play.PlayerSettings switch (e.Action) { case GlobalAction.IncreaseOffset: - Current.Value += amount; + if (!Current.Disabled) + Current.Value += amount; return true; case GlobalAction.DecreaseOffset: - Current.Value -= amount; + if (!Current.Disabled) + Current.Value -= amount; return true; } From cde8e7b82e204010fad79177f9fa3aa3a7f35b84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 18:54:51 +0900 Subject: [PATCH 259/620] Fix idle/hover colour handling weirdness in `OsuHoverContainer` --- .../Graphics/Containers/OsuHoverContainer.cs | 16 +++++++++------- osu.Game/Online/Chat/DrawableLinkCompiler.cs | 16 +--------------- .../Profile/Header/Components/FollowersButton.cs | 10 +++++++--- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs index 3b5e48d23e..e396eb6ec9 100644 --- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs @@ -15,9 +15,11 @@ namespace osu.Game.Graphics.Containers { protected const float FADE_DURATION = 500; - protected Color4 HoverColour; + public Color4? HoverColour { get; set; } + private Color4 fallbackHoverColour; - protected Color4 IdleColour = Color4.White; + public Color4? IdleColour { get; set; } + private Color4 fallbackIdleColour; protected virtual IEnumerable EffectTargets => new[] { Content }; @@ -67,18 +69,18 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader] private void load(OsuColour colours) { - if (HoverColour == default) - HoverColour = colours.Yellow; + fallbackHoverColour = colours.Yellow; + fallbackIdleColour = Color4.White; } protected override void LoadComplete() { base.LoadComplete(); - EffectTargets.ForEach(d => d.FadeColour(IdleColour)); + EffectTargets.ForEach(d => d.FadeColour(IdleColour ?? fallbackIdleColour)); } - private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour, FADE_DURATION, Easing.OutQuint)); + private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour ?? fallbackHoverColour, FADE_DURATION, Easing.OutQuint)); - private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint)); + private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour ?? fallbackIdleColour, FADE_DURATION, Easing.OutQuint)); } } diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs index f640a3dab5..e4baeb4838 100644 --- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs +++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs @@ -14,7 +14,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Online.Chat { @@ -28,18 +27,6 @@ namespace osu.Game.Online.Chat /// public readonly SlimReadOnlyListWrapper Parts; - public new Color4 IdleColour - { - get => base.IdleColour; - set => base.IdleColour = value; - } - - public new Color4 HoverColour - { - get => base.HoverColour; - set => base.HoverColour = value; - } - [Resolved] private OverlayColourProvider? overlayColourProvider { get; set; } @@ -69,8 +56,7 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load(OsuColour colours) { - if (IdleColour == default) - IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; + IdleColour ??= overlayColourProvider?.Light2 ?? colours.Blue; } protected override IEnumerable EffectTargets => Parts; diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index af78d62789..c4425643fd 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -200,16 +201,19 @@ namespace osu.Game.Overlays.Profile.Header.Components case FriendStatus.NotMutual: IdleColour = colour.Green.Opacity(0.7f); - HoverColour = IdleColour.Lighten(0.1f); + HoverColour = IdleColour.Value.Lighten(0.1f); break; case FriendStatus.Mutual: IdleColour = colour.Pink.Opacity(0.7f); - HoverColour = IdleColour.Lighten(0.1f); + HoverColour = IdleColour.Value.Lighten(0.1f); break; + + default: + throw new ArgumentOutOfRangeException(); } - EffectTargets.ForEach(d => d.FadeColour(IsHovered ? HoverColour : IdleColour, FADE_DURATION, Easing.OutQuint)); + EffectTargets.ForEach(d => d.FadeColour(IsHovered ? HoverColour.Value : IdleColour.Value, FADE_DURATION, Easing.OutQuint)); } private enum FriendStatus From 56dfe4a2314853b1e995cef65a3da7529b58cdf6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 18:56:21 +0900 Subject: [PATCH 260/620] Adjust test to work better when running in sequence --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 3cd37baafd..9a54de1459 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -33,17 +33,21 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); - AddStep("add a user", () => + + AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); - }); - AddStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count))); - AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); - AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); + }, 10); + + AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5); + AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); + + AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); + AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); } } } From 996798d2df27003aa03aeb19585763fbe1afd340 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:02:14 +0900 Subject: [PATCH 261/620] Avoid list width changing when spectator count changes --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ad94b23cd7..19d7f2c490 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuColour colours) { - AutoSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] { @@ -153,6 +153,8 @@ namespace osu.Game.Screens.Play.HUD { Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); Header.Colour = HeaderColour.Value; + + Width = Header.DrawWidth; } private partial class SpectatorListEntry : PoolableDrawable From 32906aefde0543dbce565ecfb7f0b674f91cdd2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:05:19 +0900 Subject: [PATCH 262/620] Add gradient on final spectator if more than list capacity are displayed --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 19d7f2c490..7e928e1861 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Specialized; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Game.Configuration; @@ -16,6 +18,7 @@ using osu.Game.Online.Chat; using osu.Game.Users; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -142,6 +145,13 @@ namespace osu.Game.Screens.Play.HUD Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); updateVisibility(); + + for (int i = 0; i < spectatorsFlow.Count; i++) + { + spectatorsFlow[i].Colour = i < max_spectators_displayed - 1 + ? Color4.White + : ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)); + } } private void updateVisibility() From e47244989a230a845b4ea928dcec2a9a6e9faab0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:23:54 +0900 Subject: [PATCH 263/620] Adjust animations a bit Removed autosize duration stuff because it looks weird when the list is shown from scratch where users are already fully populated in it. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 41 ++++++++++++++-------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 7e928e1861..04bd03f153 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -51,8 +51,6 @@ namespace osu.Game.Screens.Play.HUD mainFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, - AutoSizeDuration = 250, - AutoSizeEasing = Easing.OutQuint, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -84,6 +82,8 @@ namespace osu.Game.Screens.Play.HUD Font.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); FinishTransforms(true); + + this.FadeInFromZero(200, Easing.OutQuint); } private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -100,11 +100,7 @@ namespace osu.Game.Screens.Play.HUD if (index >= max_spectators_displayed) break; - spectatorsFlow.Insert(e.NewStartingIndex + i, pool.Get(entry => - { - entry.Current.Value = spectator; - entry.UserPlayingState = UserPlayingState; - })); + addNewSpectatorToList(index, spectator); } break; @@ -120,14 +116,7 @@ namespace osu.Game.Screens.Play.HUD if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - { - var spectator = Spectators[i]; - spectatorsFlow.Insert(i, pool.Get(entry => - { - entry.Current.Value = spectator; - entry.UserPlayingState = UserPlayingState; - })); - } + addNewSpectatorToList(i, Spectators[i]); } break; @@ -154,6 +143,17 @@ namespace osu.Game.Screens.Play.HUD } } + private void addNewSpectatorToList(int i, Spectator spectator) + { + var entry = pool.Get(entry => + { + entry.Current.Value = spectator; + entry.UserPlayingState = UserPlayingState; + }); + + spectatorsFlow.Insert(i, entry); + } + private void updateVisibility() { mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); @@ -203,6 +203,17 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(_ => updateState(), true); } + protected override void PrepareForUse() + { + base.PrepareForUse(); + + username.MoveToX(10) + .Then() + .MoveToX(0, 400, Easing.OutQuint); + + this.FadeInFromZero(400, Easing.OutQuint); + } + private void updateState() { username.Text = Current.Value.Username; From 840072688749f6c24f3aab3926d9eeed22b36861 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 19:33:38 +0900 Subject: [PATCH 264/620] Move bindables to OsuConfigManager & SessionStatics --- osu.Desktop/DiscordRichPresence.cs | 35 +++++++++---------- .../Online/TestSceneNowPlayingCommand.cs | 20 +++++++---- osu.Game/Configuration/SessionStatics.cs | 4 +++ osu.Game/Online/API/APIAccess.cs | 4 --- osu.Game/Online/API/DummyAPIAccess.cs | 7 ---- osu.Game/Online/API/IAPIProvider.cs | 11 ------ osu.Game/Online/Chat/NowPlayingCommand.cs | 14 ++++++-- .../Online/Metadata/OnlineMetadataClient.cs | 21 +++++++---- osu.Game/OsuGame.cs | 8 +++-- osu.Game/Screens/IOsuScreen.cs | 2 +- osu.Game/Screens/OsuScreen.cs | 2 +- 11 files changed, 67 insertions(+), 61 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6c7e7d393f..7dd9250ab6 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -51,12 +51,9 @@ namespace osu.Desktop [Resolved] private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!; - [Resolved] - private OsuConfigManager config { get; set; } = null!; - - private readonly IBindable status = new Bindable(); - private readonly IBindable activity = new Bindable(); - private readonly Bindable privacyMode = new Bindable(); + private IBindable privacyMode = null!; + private IBindable userStatus = null!; + private IBindable userActivity = null!; private readonly RichPresence presence = new RichPresence { @@ -71,8 +68,12 @@ namespace osu.Desktop private IBindable? user; [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config, SessionStatics session) { + privacyMode = config.GetBindable(OsuSetting.DiscordRichPresence); + userStatus = config.GetBindable(OsuSetting.UserOnlineStatus); + userActivity = session.GetBindable(Static.UserOnlineActivity); + client = new DiscordRpcClient(client_id) { // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation @@ -105,15 +106,11 @@ namespace osu.Desktop { base.LoadComplete(); - config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); - user = api.LocalUser.GetBoundCopy(); - status.BindTo(api.Status); - activity.BindTo(api.Activity); ruleset.BindValueChanged(_ => schedulePresenceUpdate()); - status.BindValueChanged(_ => schedulePresenceUpdate()); - activity.BindValueChanged(_ => schedulePresenceUpdate()); + userStatus.BindValueChanged(_ => schedulePresenceUpdate()); + userActivity.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); multiplayerClient.RoomUpdated += onRoomUpdated; @@ -145,13 +142,13 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (!api.IsLoggedIn || status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) + if (!api.IsLoggedIn || userStatus.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) { client.ClearPresence(); return; } - bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; + bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || userStatus.Value == UserStatus.DoNotDisturb; updatePresence(hideIdentifiableInformation); client.SetPresence(presence); @@ -164,12 +161,12 @@ namespace osu.Desktop return; // user activity - if (activity.Value != null) + if (userActivity.Value != null) { - presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); - presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); + presence.State = clampLength(userActivity.Value.GetStatus(hideIdentifiableInformation)); + presence.Details = clampLength(userActivity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); - if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0) + if (userActivity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0) { presence.Buttons = new[] { diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 1e9b0317fb..428554f761 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -8,7 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Online.API; +using osu.Game.Configuration; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; @@ -23,17 +23,23 @@ namespace osu.Game.Tests.Visual.Online [Cached(typeof(IChannelPostTarget))] private PostTarget postTarget { get; set; } - private DummyAPIAccess api => (DummyAPIAccess)API; + private SessionStatics session = null!; public TestSceneNowPlayingCommand() { Add(postTarget = new PostTarget()); } + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(session = new SessionStatics()); + } + [Test] public void TestGenericActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room())); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -43,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestEditActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo())); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.EditingBeatmap(new BeatmapInfo()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -53,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -64,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online [TestCase(false)] public void TestLinkPresence(bool hasOnlineId) { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room())); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { @@ -82,7 +88,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestModPresence() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod() }); diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index c55a597c32..bdfb0217ad 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Configuration { @@ -30,6 +31,7 @@ namespace osu.Game.Configuration SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); SetDefault(Static.LastLocalUserScore, null); SetDefault(Static.LastAppliedOffsetScore, null); + SetDefault(Static.UserOnlineActivity, null); } /// @@ -92,5 +94,7 @@ namespace osu.Game.Configuration /// This is reset when a new challenge is up. /// DailyChallengeIntroPlayed, + + UserOnlineActivity, } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index dcb8a193bc..f7fbacf76c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -60,8 +60,6 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; - public IBindable Status => configStatus; - public IBindable Activity => activity; public INotificationsClient NotificationsClient { get; } @@ -71,8 +69,6 @@ namespace osu.Game.Online.API private BindableList friends { get; } = new BindableList(); - private Bindable activity { get; } = new Bindable(); - protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly Bindable configStatus = new Bindable(); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 3fef2b59cf..48c08afb8c 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -12,7 +12,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Notifications.WebSocket; using osu.Game.Tests; -using osu.Game.Users; namespace osu.Game.Online.API { @@ -28,10 +27,6 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public Bindable Status { get; } = new Bindable(UserStatus.Online); - - public Bindable Activity { get; } = new Bindable(); - public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; @@ -197,8 +192,6 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; - IBindable IAPIProvider.Status => Status; - IBindable IAPIProvider.Activity => Activity; /// /// Skip 2FA requirement for next login. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 9ac7343885..3b6763d736 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -8,7 +8,6 @@ using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Notifications.WebSocket; -using osu.Game.Users; namespace osu.Game.Online.API { @@ -24,16 +23,6 @@ namespace osu.Game.Online.API /// IBindableList Friends { get; } - /// - /// The status for the current user that's broadcast to other players. - /// - IBindable Status { get; } - - /// - /// The activity for the current user that's broadcast to other players. - /// - IBindable Activity { get; } - /// /// The language supplied by this provider to API requests. /// diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 0e6f6f0bf6..db44017a1b 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -33,6 +34,7 @@ namespace osu.Game.Online.Chat private IBindable currentRuleset { get; set; } = null!; private readonly Channel? target; + private IBindable userActivity = null!; /// /// Creates a new to post the currently-playing beatmap to a parenting . @@ -43,6 +45,12 @@ namespace osu.Game.Online.Chat this.target = target; } + [BackgroundDependencyLoader] + private void load(SessionStatics session) + { + userActivity = session.GetBindable(Static.UserOnlineActivity); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -52,7 +60,7 @@ namespace osu.Game.Online.Chat int beatmapOnlineID; string beatmapDisplayTitle; - switch (api.Activity.Value) + switch (userActivity.Value) { case UserActivity.InGame game: verb = "playing"; @@ -92,14 +100,14 @@ namespace osu.Game.Online.Chat string getRulesetPart() { - if (api.Activity.Value is not UserActivity.InGame) return string.Empty; + if (userActivity.Value is not UserActivity.InGame) return string.Empty; return $"<{currentRuleset.Value.Name}>"; } string getModPart() { - if (api.Activity.Value is not UserActivity.InGame) return string.Empty; + if (userActivity.Value is not UserActivity.InGame) return string.Empty; if (selectedMods.Value.Count == 0) { diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 101307636a..01d7a564fa 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -34,6 +34,9 @@ namespace osu.Game.Online.Metadata private readonly string endpoint; + [Resolved] + private IAPIProvider api { get; set; } = null!; + private IHubClientConnector? connector; private Bindable lastQueueId = null!; private IBindable localUser = null!; @@ -48,7 +51,7 @@ namespace osu.Game.Online.Metadata } [BackgroundDependencyLoader] - private void load(IAPIProvider api, OsuConfigManager config) + private void load(OsuConfigManager config, SessionStatics session) { // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. @@ -72,11 +75,10 @@ namespace osu.Game.Online.Metadata IsConnected.BindValueChanged(isConnectedChanged, true); } - lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId); - localUser = api.LocalUser.GetBoundCopy(); - userStatus = api.Status.GetBoundCopy(); - userActivity = api.Activity.GetBoundCopy()!; + lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId); + userStatus = config.GetBindable(OsuSetting.UserOnlineStatus); + userActivity = session.GetBindable(Static.UserOnlineActivity); } protected override void LoadComplete() @@ -240,7 +242,14 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userStates.Clear()); + Schedule(() => + { + bool hadLocalUserState = userStates.TryGetValue(api.LocalUser.Value.OnlineID, out var presence); + userStates.Clear(); + if (hadLocalUserState) + userStates[api.LocalUser.Value.OnlineID] = presence; + }); + Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 859991496d..40d13ae0b7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -211,6 +211,8 @@ namespace osu.Game private Bindable uiScale; + private Bindable configUserActivity; + private Bindable configSkin; private readonly string[] args; @@ -391,6 +393,8 @@ namespace osu.Game Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ShortName; + configUserActivity = SessionStatics.GetBindable(Static.UserOnlineActivity); + configSkin = LocalConfig.GetBindable(OsuSetting.Skin); // Transfer skin from config to realm instance once on startup. @@ -1588,14 +1592,14 @@ namespace osu.Game { backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); - API.Activity.UnbindFrom(currentOsuScreen.Activity); + configUserActivity.UnbindFrom(currentOsuScreen.Activity); } if (newScreen is IOsuScreen newOsuScreen) { backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); - API.Activity.BindTo(newOsuScreen.Activity); + configUserActivity.BindTo(newOsuScreen.Activity); GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 9e474ed0c6..69bde877c7 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens /// /// The current for this screen. /// - IBindable Activity { get; } + Bindable Activity { get; } /// /// The amount of parallax to be applied while this screen is displayed. diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index ab66241a77..f5325b3928 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens /// protected readonly Bindable Activity = new Bindable(); - IBindable IOsuScreen.Activity => Activity; + Bindable IOsuScreen.Activity => Activity; /// /// Whether to disallow changes to game-wise Beatmap/Ruleset bindables for this screen (and all children). From 56b450c4a639b7c73a7e642570cce81fb4d2bcf6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:35:49 +0900 Subject: [PATCH 265/620] Remove setting for right-mouse scroll (make it always applicable) --- osu.Game/Configuration/OsuConfigManager.cs | 3 --- .../Settings/Sections/UserInterface/SongSelectSettings.cs | 6 ------ osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +----- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index d4a75334a9..dea7931ed5 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -170,8 +170,6 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); SetDefault(OsuSetting.ScreenshotCaptureMenuCursor, false); - SetDefault(OsuSetting.SongSelectRightMouseScroll, false); - SetDefault(OsuSetting.Scaling, ScalingMode.Off); SetDefault(OsuSetting.SafeAreaConsiderations, true); SetDefault(OsuSetting.ScalingBackgroundDim, 0.9f, 0.5f, 1f, 0.01f); @@ -401,7 +399,6 @@ namespace osu.Game.Configuration Skin, ScreenshotFormat, ScreenshotCaptureMenuCursor, - SongSelectRightMouseScroll, BeatmapSkins, BeatmapColours, BeatmapHitsounds, diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index 49bd17dfde..cb0d738a2c 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -19,12 +19,6 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { Children = new Drawable[] { - new SettingsCheckbox - { - ClassicDefault = true, - LabelText = UserInterfaceStrings.RightMouseScroll, - Current = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), - }, new SettingsCheckbox { LabelText = UserInterfaceStrings.ShowConvertedBeatmaps, diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index de12b36b17..37876eeca6 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -184,8 +184,6 @@ namespace osu.Game.Screens.Select private readonly Cached itemsCache = new Cached(); private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None; - public Bindable RightClickScrollingEnabled = new Bindable(); - public Bindable RandomAlgorithm = new Bindable(); private readonly List previouslyVisitedRandomSets = new List(); private readonly List randomSelectedBeatmaps = new List(); @@ -210,6 +208,7 @@ namespace osu.Game.Screens.Select setPool, Scroll = new CarouselScrollContainer { + RightMouseScrollbar = true, RelativeSizeAxes = Axes.Both, }, noResultsPlaceholder = new NoResultsPlaceholder() @@ -226,9 +225,6 @@ namespace osu.Game.Screens.Select randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); - config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); - - RightClickScrollingEnabled.BindValueChanged(enabled => Scroll.RightMouseScrollbar = enabled.NewValue, true); detachedBeatmapSets = beatmaps.GetBeatmapSets(cancellationToken); detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); From 1c2621d88e8c86954c949ef538df86c05cc78285 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:42:10 +0900 Subject: [PATCH 266/620] Add support to CarouselV2 for right mouse button scrolling --- osu.Game/Screens/SelectV2/Carousel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 12a86be7b9..84b90c8fe0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -121,6 +121,7 @@ namespace osu.Game.Screens.SelectV2 }, scroll = new CarouselScrollContainer { + RightMouseScrollbar = true, RelativeSizeAxes = Axes.Both, Masking = false, } @@ -390,7 +391,7 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class CarouselScrollContainer : OsuScrollContainer + private partial class CarouselScrollContainer : UserTrackingScrollContainer { public readonly Container Panels; From 48609d44e2f24a3733e114807ce095b6b23335ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 12:30:27 +0100 Subject: [PATCH 267/620] Bump NVika tool to 4.0.0 Code quality CI runs have suddenly started failing out of nowhere: - Passing run: https://github.com/ppy/osu/actions/runs/12806242929/job/35704267944#step:10:1 - Failing run: https://github.com/ppy/osu/actions/runs/12807108792/job/35707131634#step:10:1 In classic github fashion, they began rolling out another runner change wherein `ubuntu-latest` has started meaning `ubuntu-24.04` rather than `ubuntu-22.04`. `ubuntu-24.04` no longer has .NET 6 bundled. Therefore, upgrade NVika to 4.0.0 because that version is compatible with .NET 8. --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c4ba6e5143..6ec071be2f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "nvika": { - "version": "3.0.0", + "version": "4.0.0", "commands": [ "nvika" ] From 65b88ab365df223e07a4b7c56794b75b42d4b338 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 19:38:14 +0900 Subject: [PATCH 268/620] Use MetadataClient for local user status --- .../Visual/Online/TestSceneUserPanel.cs | 38 ++++++++----------- osu.Game/Users/ExtendedUserPanel.cs | 8 +--- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 684e8b7b86..f4fc15da20 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Overlays; @@ -188,33 +187,26 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLocalUserActivity() { - AddStep("idle", () => setLocalUserPresence(UserStatus.Online, null)); - AddStep("watching replay", () => setLocalUserPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); - AddStep("spectating user", () => setLocalUserPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); - AddStep("solo (osu!)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(0))); - AddStep("solo (osu!taiko)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(1))); - AddStep("solo (osu!catch)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(2))); - AddStep("solo (osu!mania)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(3))); - AddStep("choosing", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); - AddStep("editing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); - AddStep("modding beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); - AddStep("testing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); - AddStep("set offline status", () => setLocalUserPresence(UserStatus.Offline, null)); + AddStep("idle", () => setPresence(UserStatus.Online, null, API.LocalUser.Value.OnlineID)); + AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")), API.LocalUser.Value.OnlineID)); + AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3), API.LocalUser.Value.OnlineID)); + AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap(), API.LocalUser.Value.OnlineID)); + AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("set offline status", () => setPresence(UserStatus.Offline, null, API.LocalUser.Value.OnlineID)); } - private void setPresence(UserStatus status, UserActivity? activity) + private void setPresence(UserStatus status, UserActivity? activity, int? userId = null) { if (status == UserStatus.Offline) - metadataClient.UserPresenceUpdated(panel.User.OnlineID, null); + metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, null); else - metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); - } - - private void setLocalUserPresence(UserStatus status, UserActivity? activity) - { - DummyAPIAccess dummyAPI = (DummyAPIAccess)API; - dummyAPI.Status.Value = status; - dummyAPI.Activity.Value = activity; + metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); } private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index eb1115e296..2fc2a97b47 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -94,13 +94,7 @@ namespace osu.Game.Users private void updatePresence() { - UserPresence? presence; - - if (User.Equals(api?.LocalUser.Value)) - presence = new UserPresence { Status = api.Status.Value, Activity = api.Activity.Value }; - else - presence = metadata?.GetPresence(User.OnlineID); - + UserPresence? presence = metadata?.GetPresence(User.OnlineID); UserStatus status = presence?.Status ?? UserStatus.Offline; UserActivity? activity = presence?.Activity; From 94db39317b626a90c6dd60c2c9dee530bdc58fe2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 20:43:22 +0900 Subject: [PATCH 269/620] Add xmldoc --- osu.Game/Configuration/SessionStatics.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index bdfb0217ad..d2069e4027 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -95,6 +95,9 @@ namespace osu.Game.Configuration /// DailyChallengeIntroPlayed, + /// + /// The activity for the current user to broadcast to other players. + /// UserOnlineActivity, } } From a6057a9f54e186557694861f292a132c5c881d0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 20:25:16 +0900 Subject: [PATCH 270/620] Move absolute scroll support local to carousel and allow custom bindings --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 2 +- .../Graphics/Containers/OsuScrollContainer.cs | 77 ++++--------------- .../Containers/UserTrackingScrollContainer.cs | 6 +- .../Input/Bindings/GlobalActionContainer.cs | 4 + .../GlobalActionKeyBindingStrings.cs | 5 ++ osu.Game/Screens/Select/BeatmapCarousel.cs | 61 ++++++++++----- osu.Game/Screens/SelectV2/Carousel.cs | 57 +++++++++++++- 7 files changed, 122 insertions(+), 90 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index f99e0a418a..b13d450c32 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelect private OsuTextFlowContainer stats = null!; private BeatmapCarousel carousel = null!; - private OsuScrollContainer scroll => carousel.ChildrenOfType().Single(); + private OsuScrollContainer scroll => carousel.ChildrenOfType>().Single(); private int beatmapCount; diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index f40c91e27e..43a42eae57 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -26,26 +26,12 @@ namespace osu.Game.Graphics.Containers } } - public partial class OsuScrollContainer : ScrollContainer where T : Drawable + public partial class OsuScrollContainer : ScrollContainer + where T : Drawable { public const float SCROLL_BAR_WIDTH = 10; public const float SCROLL_BAR_PADDING = 3; - /// - /// Allows controlling the scroll bar from any position in the container using the right mouse button. - /// Uses the value of to smoothly scroll to the dragged location. - /// - public bool RightMouseScrollbar; - - /// - /// Controls the rate with which the target position is approached when performing a relative drag. Default is 0.02. - /// - public double DistanceDecayOnRightMouseScrollbar = 0.02; - - private bool rightMouseDragging; - - protected override bool IsDragging => base.IsDragging || rightMouseDragging; - public OsuScrollContainer(Direction scrollDirection = Direction.Vertical) : base(scrollDirection) { @@ -71,50 +57,6 @@ namespace osu.Game.Graphics.Containers ScrollTo(maxPos - DisplayableContent + extraScroll, animated); } - protected override bool OnMouseDown(MouseDownEvent e) - { - if (shouldPerformRightMouseScroll(e)) - { - ScrollFromMouseEvent(e); - return true; - } - - return base.OnMouseDown(e); - } - - protected override void OnDrag(DragEvent e) - { - if (rightMouseDragging) - { - ScrollFromMouseEvent(e); - return; - } - - base.OnDrag(e); - } - - protected override bool OnDragStart(DragStartEvent e) - { - if (shouldPerformRightMouseScroll(e)) - { - rightMouseDragging = true; - return true; - } - - return base.OnDragStart(e); - } - - protected override void OnDragEnd(DragEndEvent e) - { - if (rightMouseDragging) - { - rightMouseDragging = false; - return; - } - - base.OnDragEnd(e); - } - protected override bool OnScroll(ScrollEvent e) { // allow for controlling volume when alt is held. @@ -124,15 +66,22 @@ namespace osu.Game.Graphics.Containers return base.OnScroll(e); } - protected virtual void ScrollFromMouseEvent(MouseEvent e) + #region Absolute scrolling + + /// + /// Controls the rate with which the target position is approached when performing a relative drag. Default is 0.02. + /// + public double DistanceDecayOnAbsoluteScroll = 0.02; + + protected virtual void ScrollToAbsolutePosition(Vector2 screenSpacePosition) { - float fromScrollbarPosition = FromScrollbarPosition(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim]); + float fromScrollbarPosition = FromScrollbarPosition(ToLocalSpace(screenSpacePosition)[ScrollDim]); float scrollbarCentreOffset = FromScrollbarPosition(Scrollbar.DrawHeight) * 0.5f; - ScrollTo(Clamp(fromScrollbarPosition - scrollbarCentreOffset), true, DistanceDecayOnRightMouseScrollbar); + ScrollTo(Clamp(fromScrollbarPosition - scrollbarCentreOffset), true, DistanceDecayOnAbsoluteScroll); } - private bool shouldPerformRightMouseScroll(MouseButtonEvent e) => RightMouseScrollbar && e.Button == MouseButton.Right; + #endregion protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction); diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index 30b9eeb74c..ab17c3f9e3 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Input.Events; +using osuTK; namespace osu.Game.Graphics.Containers { @@ -47,10 +47,10 @@ namespace osu.Game.Graphics.Containers base.ScrollIntoView(target, animated); } - protected override void ScrollFromMouseEvent(MouseEvent e) + protected override void ScrollToAbsolutePosition(Vector2 screenSpacePosition) { UserScrolling = true; - base.ScrollFromMouseEvent(e); + base.ScrollToAbsolutePosition(screenSpacePosition); } public new void ScrollTo(double value, bool animated = true, double? distanceDecay = null) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 2666b24be9..5e509d2035 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -204,6 +204,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed), + new KeyBinding(new[] { InputKey.MouseRight }, GlobalAction.AbsoluteScrollSongList), }; private static IEnumerable audioControlKeyBindings => new[] @@ -490,6 +491,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextBookmark))] EditorSeekToNextBookmark, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))] + AbsoluteScrollSongList } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index f9db0461ce..436a2be648 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -449,6 +449,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorSeekToNextBookmark => new TranslatableString(getKey(@"editor_seek_to_next_bookmark"), @"Seek to next bookmark"); + /// + /// "Absolute scroll song list" + /// + public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 37876eeca6..7e3c26a1ba 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -14,6 +14,7 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -208,7 +209,6 @@ namespace osu.Game.Screens.Select setPool, Scroll = new CarouselScrollContainer { - RightMouseScrollbar = true, RelativeSizeAxes = Axes.Both, }, noResultsPlaceholder = new NoResultsPlaceholder() @@ -1157,10 +1157,8 @@ namespace osu.Game.Screens.Select } } - public partial class CarouselScrollContainer : UserTrackingScrollContainer + public partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { - private bool rightMouseScrollBlocked; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public CarouselScrollContainer() @@ -1172,31 +1170,54 @@ namespace osu.Game.Screens.Select Masking = false; } - protected override bool OnMouseDown(MouseDownEvent e) + #region Absolute scrolling + + private bool absoluteScrolling; + + protected override bool IsDragging => base.IsDragging || absoluteScrolling; + + public bool OnPressed(KeyBindingPressEvent e) { - if (e.Button == MouseButton.Right) + switch (e.Action) { - // we need to block right click absolute scrolling when hovering a carousel item so context menus can display. - // this can be reconsidered when we have an alternative to right click scrolling. - if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) - { - rightMouseScrollBlocked = true; - return false; - } + case GlobalAction.AbsoluteScrollSongList: + // The default binding for absolute scroll is right mouse button. + // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. + if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) + && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + return true; } - rightMouseScrollBlocked = false; - return base.OnMouseDown(e); + return false; } - protected override bool OnDragStart(DragStartEvent e) + public void OnReleased(KeyBindingReleaseEvent e) { - if (rightMouseScrollBlocked) - return false; - - return base.OnDragStart(e); + switch (e.Action) + { + case GlobalAction.AbsoluteScrollSongList: + absoluteScrolling = false; + break; + } } + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (absoluteScrolling) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + return true; + } + + return base.OnMouseMove(e); + } + + #endregion + protected override ScrollbarContainer CreateScrollbar(Direction direction) { return new PaddedScrollbar(); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 84b90c8fe0..c8a54d4cd5 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -11,13 +11,18 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.SelectV2 { @@ -121,7 +126,6 @@ namespace osu.Game.Screens.SelectV2 }, scroll = new CarouselScrollContainer { - RightMouseScrollbar = true, RelativeSizeAxes = Axes.Both, Masking = false, } @@ -391,7 +395,7 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class CarouselScrollContainer : UserTrackingScrollContainer + private partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { public readonly Container Panels; @@ -466,6 +470,55 @@ namespace osu.Game.Screens.SelectV2 foreach (var d in Panels) d.Y = (float)(((ICarouselPanel)d).DrawYPosition + scrollableExtent); } + + #region Absolute scrolling + + private bool absoluteScrolling; + + protected override bool IsDragging => base.IsDragging || absoluteScrolling; + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.AbsoluteScrollSongList: + + // The default binding for absolute scroll is right mouse button. + // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. + if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) + && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + switch (e.Action) + { + case GlobalAction.AbsoluteScrollSongList: + absoluteScrolling = false; + break; + } + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (absoluteScrolling) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + return true; + } + + return base.OnMouseMove(e); + } + + #endregion } private class BoundsCarouselItem : CarouselItem From 81f54507ddb0cbabbd7d02d80838ff160b52f9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 14:29:41 +0100 Subject: [PATCH 271/620] Fix potential index accounting mistake when creating spectator list with spectators already present Noticed by accident, but if the `BindCollectionChanged()` callback fires immediately in `LoadComplete()` when set up and there are spectators present already, then `NewStartingIndex` in the related event is -1: https://github.com/dotnet/runtime/blob/b03f83de362f7168c94daa2f4b192959abefe366/src/libraries/System.ObjectModel/src/System/Collections/Specialized/NotifyCollectionChangedEventArgs.cs#L84-L92 which kinda breaks the math introducing off-by-ones and in result causes 11 items to be displayed together rather than 10. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 04bd03f153..438aa61d9d 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < e.NewItems!.Count; i++) { var spectator = (Spectator)e.NewItems![i]!; - int index = e.NewStartingIndex + i; + int index = Math.Max(e.NewStartingIndex, 0) + i; if (index >= max_spectators_displayed) break; From 1f1e940adaa1d943707cd3191d876d054659c66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:13:16 +0100 Subject: [PATCH 272/620] Restore virtual modifier to fix tests (and mark for posterity) --- osu.Game/Online/Spectator/SpectatorClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ac11dad0f0..91f009b76f 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; @@ -38,7 +39,8 @@ namespace osu.Game.Online.Spectator /// /// The states of all users currently being watched by the local user. /// - public IBindableDictionary WatchedUserStates => watchedUserStates; + [UsedImplicitly] // Marked virtual due to mock use in testing + public virtual IBindableDictionary WatchedUserStates => watchedUserStates; /// /// All users who are currently watching the local user. @@ -58,6 +60,7 @@ namespace osu.Game.Online.Spectator /// /// Called whenever new frames arrive from the server. /// + [UsedImplicitly] // Marked virtual due to mock use in testing public virtual event Action? OnNewFrames; /// From 5c799d733f2543cbb35295cea68333ab4bd4f31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:25:56 +0100 Subject: [PATCH 273/620] Bind to playing state via `GameplayState` instead to fix more tests --- osu.Game/Screens/Play/GameplayState.cs | 11 ++++++++++- osu.Game/Screens/Play/HUD/SpectatorList.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 478acd7229..bfeabcc82e 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -69,6 +69,11 @@ namespace osu.Game.Screens.Play private readonly Bindable lastJudgementResult = new Bindable(); + /// + /// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar). + /// + public IBindable Playing { get; } = new Bindable(); + public GameplayState( IBeatmap beatmap, Ruleset ruleset, @@ -76,7 +81,8 @@ namespace osu.Game.Screens.Play Score? score = null, ScoreProcessor? scoreProcessor = null, HealthProcessor? healthProcessor = null, - Storyboard? storyboard = null) + Storyboard? storyboard = null, + IBindable? localUserPlaying = null) { Beatmap = beatmap; Ruleset = ruleset; @@ -92,6 +98,9 @@ namespace osu.Game.Screens.Play ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor(); HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); Storyboard = storyboard ?? new Storyboard(); + + if (localUserPlaying != null) + Playing.BindTo(localUserPlaying); } /// diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ab4958f0c1..35a2d1eefb 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -242,10 +242,10 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] - private void load(SpectatorClient client, Player player) + private void load(SpectatorClient client, GameplayState gameplayState) { ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(player.PlayingState); + ((IBindable)UserPlayingState).BindTo(gameplayState.Playing); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 228b77b780..a797603e17 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -261,7 +261,7 @@ namespace osu.Game.Screens.Play Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = gameplayMods; - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard, PlayingState)); var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); GameplayClockContainer.Add(new GameplayScrollWheelHandling()); From 1949c01103c4dec239761c4591222044554e5045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:29:07 +0100 Subject: [PATCH 274/620] Fix skin deserialisation test --- .../Archives/modified-argon-20250116.osk | Bin 0 -> 1675 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk new file mode 100644 index 0000000000000000000000000000000000000000..811e91b74916988a54a9ff2b73cafe0e0ff0a490 GIT binary patch literal 1675 zcmWIWW@Zs#U|`^2aIGwA+QJoLveOyo?d2NrfSbdu0sw2tiOMEEe~b! z{36g&5>^(Lr7R|$zqq}8chOFPKW5zaC*uv0%nc6L#Jyj2=-;snQx@4hF@=FAbtIWf z7uaPld803KNcf42wTXYmb^iyyzMQVD$v9PPa-}zjbuGV+h~(A$R~CvzYVI?=if?Ty zSX5xPd*h1#i#3<7J|)r`8)5A%c=hMC&C(b3!i_h-{OlXt+kD5B-O$}{ZurW?(_4Qs z1)%t(Q1NR`8PFGfK+FroAfIIBrRD2o73b%zeijrE@X7m(Z|J6=fK&QM^;d3;T#mN-<+=?)V1{?zSo%jxr`&!0|zR(Le8 z@~CQOrRvO1)tNsl=T&C@3~2s(r*YZ~*7WpePg9>QeQnwFhT&t0NWvtqn5_(`4%D6) zUf#ycz#t&Uz#s;6pl@Pko=2#QbAE0?eqL%`Nio=^Z)e8l-!c$sd;eQosf;_&&O>2x z)y0Dnm(6&4%l58Vb)v=jQDRPtb#CO<|90vZrZw#D)a5P7+Nn|b_J-xh#h>M7-&Q`n zqLy9HVDZ-NZL#Oq?UspMEZEpza%f7$YLlhMT6zTnuZ#cOy7AKIZI_SAgn0LMUis6I zmcXaX=g?}YkdgT9{H|x5{gSrrg*?nWf>(NfuPaS9{kt*s+5BA>Jv)6YWr96a-IwI? zToOEZQ9WYLCd~`UvxI_H+n=*bU^<=3(0_i(7im?KJvk>zOD7(EIrCa`*>HN!IgjuT6|kH(ywD!7<6 z?Y`%+Dy5TeTzo7&(^@KK+&TX~Z{L~_hZBt}%8OVR9$&cPWaN8EjrKxp4_<^E(@|-T4S4m+v%NED|3-6Sk z>k@b>aLbBY6Qs^DKARrPyYhkU?{bxsm)BaU$KNT~W740~qP0MZVZMQ#vq=-vzQem5V`-ETaI@+_vWsbT--K?QaW#i?K>4Xd!x2#e8&|zzM~6X zPrIqwUeNX<Vnlt-C&Rn#q6td_{k2+H#?H?N(@v~2Px$nhITCA(> z&SmU~3;V;mHupo#f7hM6UT14N{a=6b_v5JN|MjQu-E?*B-_!m&vS!j7SAAz*_wLA^ zJFVrW-0!DFpRv#J0y}g}w z`}3NF{>sHqXRckA8hP)afk|RW-c=LM1sk<0uI*9d*mZaAp0t_ueVg*j_oL<9Q`;Xm z7z1;xB@pugaX?ODWm;xxP70!I;quniI(O!L@TMSxD~umwyM#^)Pc90w{e0 From 5fc277aa7f88677ab68291ef592a1fdc9cb8d1be Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Thu, 16 Jan 2025 21:53:56 +0100 Subject: [PATCH 275/620] Seek in replay scaled by replay speed --- osu.Game/Screens/Play/ReplayPlayer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index c1b5397e61..ba572f6014 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Play private readonly bool replayIsFailedScore; + private PlaybackSettings playbackSettings; + protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo); private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); @@ -73,7 +75,7 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; - var playbackSettings = new PlaybackSettings + playbackSettings = new PlaybackSettings { Depth = float.MaxValue, Expanded = { BindTarget = config.GetBindable(OsuSetting.ReplayPlaybackControlsExpanded) } @@ -124,11 +126,11 @@ namespace osu.Game.Screens.Play return true; case GlobalAction.SeekReplayBackward: - SeekInDirection(-5); + SeekInDirection(-5 * (float)playbackSettings.UserPlaybackRate.Value); return true; case GlobalAction.SeekReplayForward: - SeekInDirection(5); + SeekInDirection(5 * (float)playbackSettings.UserPlaybackRate.Value); return true; case GlobalAction.TogglePauseReplay: From daa7921c2d1510db65cd638a29662aef2b0aca91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 12:55:11 +0900 Subject: [PATCH 276/620] Mark `IsTablet` with `new` to avoid inspection Co-authored-by: Susko3 --- osu.Android/OsuGameActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 0b5deef6fb..66c697801b 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -49,7 +49,7 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; - public bool IsTablet { get; private set; } + public new bool IsTablet { get; private set; } private readonly OsuGameAndroid game; From 224f39825f5f452ec6e7341666b2cae6ac700334 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 14:16:38 +0900 Subject: [PATCH 277/620] Fix test potentially false-negative due to realm write delays --- .../Navigation/TestSceneScreenNavigation.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 326f21ff13..521d097fb9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -41,6 +41,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; @@ -351,8 +352,13 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus)); checkOffset(-1); - void checkOffset(double offset) => AddUntilStep($"offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, - () => Is.EqualTo(offset)); + void checkOffset(double offset) + { + AddUntilStep($"control offset is {offset}", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Current.Value, + () => Is.EqualTo(offset)); + AddUntilStep($"database offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, + () => Is.EqualTo(offset)); + } } [Test] @@ -389,8 +395,13 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus)); checkOffset(-1); - void checkOffset(double offset) => AddUntilStep($"offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, - () => Is.EqualTo(offset)); + void checkOffset(double offset) + { + AddUntilStep($"control offset is {offset}", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Current.Value, + () => Is.EqualTo(offset)); + AddUntilStep($"database offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, + () => Is.EqualTo(offset)); + } } [Test] From ae7e4bef86d68dfb6e3db8f406f97c152e314cff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 15:42:19 +0900 Subject: [PATCH 278/620] Fix tests --- .../Visual/Online/TestSceneNowPlayingCommand.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 428554f761..56d03d4c7f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestGenericActivity() { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestEditActivity() { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.EditingBeatmap(new BeatmapInfo()))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.EditingBeatmap(new BeatmapInfo()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayActivity() { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online [TestCase(false)] public void TestLinkPresence(bool hasOnlineId) { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestModPresence() { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod() }); From a51938f4e97c3d09673dc677bc368d17b351dfaf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 15:59:25 +0900 Subject: [PATCH 279/620] Separate the local user state --- osu.Game/Online/Metadata/MetadataClient.cs | 5 ++++ .../Online/Metadata/OnlineMetadataClient.cs | 27 ++++++++++++------- .../Visual/Metadata/TestMetadataClient.cs | 19 ++++++++++--- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 6578f70f74..507f43467c 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -37,6 +37,11 @@ namespace osu.Game.Online.Metadata /// public abstract IBindable IsWatchingUserPresence { get; } + /// + /// The information about the current user. + /// + public abstract UserPresence LocalUserState { get; } + /// /// Dictionary keyed by user ID containing all of the information about currently online users received from the server. /// diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 01d7a564fa..04abca1e9b 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -23,6 +23,9 @@ namespace osu.Game.Online.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); + public override UserPresence LocalUserState => localUserState; + private UserPresence localUserState; + public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); @@ -110,6 +113,7 @@ namespace osu.Game.Online.Metadata userStates.Clear(); friendStates.Clear(); dailyChallengeInfo.Value = null; + localUserState = default; }); return; } @@ -202,9 +206,19 @@ namespace osu.Game.Online.Metadata Schedule(() => { if (presence?.Status != null) - userStates[userId] = presence.Value; + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = presence.Value; + else + userStates[userId] = presence.Value; + } else - userStates.Remove(userId); + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = default; + else + userStates.Remove(userId); + } }); return Task.CompletedTask; @@ -242,14 +256,7 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => - { - bool hadLocalUserState = userStates.TryGetValue(api.LocalUser.Value.OnlineID, out var presence); - userStates.Clear(); - if (hadLocalUserState) - userStates[api.LocalUser.Value.OnlineID] = presence; - }); - + Schedule(() => userStates.Clear()); Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 36f79a5adc..d32d49b55e 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -19,6 +19,9 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); + public override UserPresence LocalUserState => localUserState; + private UserPresence localUserState; + public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); @@ -71,10 +74,20 @@ namespace osu.Game.Tests.Visual.Metadata { if (isWatchingUserPresence.Value) { - if (presence.HasValue) - userStates[userId] = presence.Value; + if (presence?.Status != null) + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = presence.Value; + else + userStates[userId] = presence.Value; + } else - userStates.Remove(userId); + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = default; + else + userStates.Remove(userId); + } } return Task.CompletedTask; From 626be9d7806b44434bb773cb9afe002c0639356b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 16:01:11 +0900 Subject: [PATCH 280/620] Return local user state where appropriate --- osu.Game/Online/Metadata/MetadataClient.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index a72377721a..1b6f96d91b 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -4,8 +4,10 @@ using System; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Users; namespace osu.Game.Online.Metadata @@ -14,6 +16,9 @@ namespace osu.Game.Online.Metadata { public abstract IBindable IsConnected { get; } + [Resolved] + private IAPIProvider api { get; set; } = null!; + #region Beatmap metadata updates public abstract Task GetChangesSince(int queueId); @@ -59,6 +64,9 @@ namespace osu.Game.Online.Metadata /// The user presence, or null if not available or the user's offline. public UserPresence? GetPresence(int userId) { + if (userId == api.LocalUser.Value.OnlineID) + return LocalUserState; + if (FriendStates.TryGetValue(userId, out UserPresence presence)) return presence; From 3bb4b0c2b8a84c5bf3330a84422e6f3c077b346f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 16:25:48 +0900 Subject: [PATCH 281/620] Rename fields from `State` to `Presence` when presence is involved --- osu.Game/Online/FriendPresenceNotifier.cs | 10 +++--- osu.Game/Online/Metadata/MetadataClient.cs | 6 ++-- .../Online/Metadata/OnlineMetadataClient.cs | 32 +++++++++---------- .../Dashboard/CurrentlyOnlineDisplay.cs | 2 +- .../Visual/Metadata/TestMetadataClient.cs | 32 +++++++++---------- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 330e0a908f..dd141b756b 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online private readonly Bindable notifyOnFriendPresenceChange = new BindableBool(); private readonly IBindableList friends = new BindableList(); - private readonly IBindableDictionary friendStates = new BindableDictionary(); + private readonly IBindableDictionary friendPresences = new BindableDictionary(); private readonly HashSet onlineAlertQueue = new HashSet(); private readonly HashSet offlineAlertQueue = new HashSet(); @@ -63,8 +63,8 @@ namespace osu.Game.Online friends.BindTo(api.Friends); friends.BindCollectionChanged(onFriendsChanged, true); - friendStates.BindTo(metadataClient.FriendStates); - friendStates.BindCollectionChanged(onFriendStatesChanged, true); + friendPresences.BindTo(metadataClient.FriendPresences); + friendPresences.BindCollectionChanged(onFriendPresenceChanged, true); } protected override void Update() @@ -85,7 +85,7 @@ namespace osu.Game.Online if (friend.TargetUser is not APIUser user) continue; - if (friendStates.TryGetValue(friend.TargetID, out _)) + if (friendPresences.TryGetValue(friend.TargetID, out _)) markUserOnline(user); } @@ -105,7 +105,7 @@ namespace osu.Game.Online } } - private void onFriendStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + private void onFriendPresenceChanged(object? sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 507f43467c..3c0b47ad3d 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -40,17 +40,17 @@ namespace osu.Game.Online.Metadata /// /// The information about the current user. /// - public abstract UserPresence LocalUserState { get; } + public abstract UserPresence LocalUserPresence { get; } /// /// Dictionary keyed by user ID containing all of the information about currently online users received from the server. /// - public abstract IBindableDictionary UserStates { get; } + public abstract IBindableDictionary UserPresences { get; } /// /// Dictionary keyed by user ID containing all of the information about currently online friends received from the server. /// - public abstract IBindableDictionary FriendStates { get; } + public abstract IBindableDictionary FriendPresences { get; } /// public abstract Task UpdateActivity(UserActivity? activity); diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 04abca1e9b..5aeeb04d11 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -23,14 +23,14 @@ namespace osu.Game.Online.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserState => localUserState; - private UserPresence localUserState; + public override UserPresence LocalUserPresence => localUserPresence; + private UserPresence localUserPresence; - public override IBindableDictionary UserStates => userStates; - private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindableDictionary UserPresences => userPresences; + private readonly BindableDictionary userPresences = new BindableDictionary(); - public override IBindableDictionary FriendStates => friendStates; - private readonly BindableDictionary friendStates = new BindableDictionary(); + public override IBindableDictionary FriendPresences => friendPresences; + private readonly BindableDictionary friendPresences = new BindableDictionary(); public override IBindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); @@ -110,10 +110,10 @@ namespace osu.Game.Online.Metadata Schedule(() => { isWatchingUserPresence.Value = false; - userStates.Clear(); - friendStates.Clear(); + userPresences.Clear(); + friendPresences.Clear(); dailyChallengeInfo.Value = null; - localUserState = default; + localUserPresence = default; }); return; } @@ -208,16 +208,16 @@ namespace osu.Game.Online.Metadata if (presence?.Status != null) { if (userId == api.LocalUser.Value.OnlineID) - localUserState = presence.Value; + localUserPresence = presence.Value; else - userStates[userId] = presence.Value; + userPresences[userId] = presence.Value; } else { if (userId == api.LocalUser.Value.OnlineID) - localUserState = default; + localUserPresence = default; else - userStates.Remove(userId); + userPresences.Remove(userId); } }); @@ -229,9 +229,9 @@ namespace osu.Game.Online.Metadata Schedule(() => { if (presence?.Status != null) - friendStates[userId] = presence.Value; + friendPresences[userId] = presence.Value; else - friendStates.Remove(userId); + friendPresences.Remove(userId); }); return Task.CompletedTask; @@ -256,7 +256,7 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userStates.Clear()); + Schedule(() => userPresences.Clear()); Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 2ca548fdf5..39023c16f6 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - onlineUsers.BindTo(metadataClient.UserStates); + onlineUsers.BindTo(metadataClient.UserPresences); onlineUsers.BindCollectionChanged(onUserUpdated, true); playingUsers.BindTo(spectatorClient.PlayingUsers); diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index d32d49b55e..7b08108194 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -19,14 +19,14 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserState => localUserState; - private UserPresence localUserState; + public override UserPresence LocalUserPresence => localUserPresence; + private UserPresence localUserPresence; - public override IBindableDictionary UserStates => userStates; - private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindableDictionary UserPresences => userPresences; + private readonly BindableDictionary userPresences = new BindableDictionary(); - public override IBindableDictionary FriendStates => friendStates; - private readonly BindableDictionary friendStates = new BindableDictionary(); + public override IBindableDictionary FriendPresences => friendPresences; + private readonly BindableDictionary friendPresences = new BindableDictionary(); public override Bindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); @@ -50,9 +50,9 @@ namespace osu.Game.Tests.Visual.Metadata { if (isWatchingUserPresence.Value) { - userStates.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); + userPresences.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); localUserPresence = localUserPresence with { Activity = activity }; - userStates[api.LocalUser.Value.Id] = localUserPresence; + userPresences[api.LocalUser.Value.Id] = localUserPresence; } return Task.CompletedTask; @@ -62,9 +62,9 @@ namespace osu.Game.Tests.Visual.Metadata { if (isWatchingUserPresence.Value) { - userStates.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); + userPresences.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); localUserPresence = localUserPresence with { Status = status }; - userStates[api.LocalUser.Value.Id] = localUserPresence; + userPresences[api.LocalUser.Value.Id] = localUserPresence; } return Task.CompletedTask; @@ -77,16 +77,16 @@ namespace osu.Game.Tests.Visual.Metadata if (presence?.Status != null) { if (userId == api.LocalUser.Value.OnlineID) - localUserState = presence.Value; + localUserPresence = presence.Value; else - userStates[userId] = presence.Value; + userPresences[userId] = presence.Value; } else { if (userId == api.LocalUser.Value.OnlineID) - localUserState = default; + localUserPresence = default; else - userStates.Remove(userId); + userPresences.Remove(userId); } } @@ -96,9 +96,9 @@ namespace osu.Game.Tests.Visual.Metadata public override Task FriendPresenceUpdated(int userId, UserPresence? presence) { if (presence.HasValue) - friendStates[userId] = presence.Value; + friendPresences[userId] = presence.Value; else - friendStates.Remove(userId); + friendPresences.Remove(userId); return Task.CompletedTask; } From 311f08b962a3ca2d99bc42f82459a231bbf41fa8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 16:29:02 +0900 Subject: [PATCH 282/620] Update `TestMetadataClient` to correctly set local user state in line with changes --- .../Tests/Visual/Metadata/TestMetadataClient.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 7b08108194..d14cbd7743 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -48,11 +48,12 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UpdateActivity(UserActivity? activity) { + localUserPresence = localUserPresence with { Activity = activity }; + if (isWatchingUserPresence.Value) { - userPresences.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); - localUserPresence = localUserPresence with { Activity = activity }; - userPresences[api.LocalUser.Value.Id] = localUserPresence; + if (userPresences.ContainsKey(api.LocalUser.Value.Id)) + userPresences[api.LocalUser.Value.Id] = localUserPresence; } return Task.CompletedTask; @@ -60,11 +61,12 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UpdateStatus(UserStatus? status) { + localUserPresence = localUserPresence with { Status = status }; + if (isWatchingUserPresence.Value) { - userPresences.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); - localUserPresence = localUserPresence with { Status = status }; - userPresences[api.LocalUser.Value.Id] = localUserPresence; + if (userPresences.ContainsKey(api.LocalUser.Value.Id)) + userPresences[api.LocalUser.Value.Id] = localUserPresence; } return Task.CompletedTask; From 41c603b56f0b9d0fce6b2fe03954d88c82644cba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 16:41:02 +0900 Subject: [PATCH 283/620] Fix double-retrieval of user presence from dictionary in online display --- .../Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 39023c16f6..bb4c9d96c8 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Dashboard private const float padding = 10; private readonly IBindableList playingUsers = new BindableList(); - private readonly IBindableDictionary onlineUsers = new BindableDictionary(); + private readonly IBindableDictionary onlineUserPresences = new BindableDictionary(); private readonly Dictionary userPanels = new Dictionary(); private SearchContainer userFlow; @@ -106,8 +106,8 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - onlineUsers.BindTo(metadataClient.UserPresences); - onlineUsers.BindCollectionChanged(onUserUpdated, true); + onlineUserPresences.BindTo(metadataClient.UserPresences); + onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true); playingUsers.BindTo(spectatorClient.PlayingUsers); playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); @@ -120,7 +120,7 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } - private void onUserUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserPresenceUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { @@ -142,8 +142,10 @@ namespace osu.Game.Overlays.Dashboard { userFlow.Add(userPanels[userId] = createUserPanel(user).With(p => { - p.Status.Value = onlineUsers.GetValueOrDefault(userId).Status; - p.Activity.Value = onlineUsers.GetValueOrDefault(userId).Activity; + var presence = onlineUserPresences.GetValueOrDefault(userId); + + p.Status.Value = presence.Status; + p.Activity.Value = presence.Activity; })); }); }); From f59762f0cb4f199e4e00c034807e1084a3237edc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 17:11:40 +0900 Subject: [PATCH 284/620] `Playing` -> `PlayingState` --- osu.Game/Screens/Play/GameplayState.cs | 8 ++++---- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index bfeabcc82e..851e95495f 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play /// /// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar). /// - public IBindable Playing { get; } = new Bindable(); + public IBindable PlayingState { get; } = new Bindable(); public GameplayState( IBeatmap beatmap, @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Play ScoreProcessor? scoreProcessor = null, HealthProcessor? healthProcessor = null, Storyboard? storyboard = null, - IBindable? localUserPlaying = null) + IBindable? localUserPlayingState = null) { Beatmap = beatmap; Ruleset = ruleset; @@ -99,8 +99,8 @@ namespace osu.Game.Screens.Play HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); Storyboard = storyboard ?? new Storyboard(); - if (localUserPlaying != null) - Playing.BindTo(localUserPlaying); + if (localUserPlayingState != null) + PlayingState.BindTo(localUserPlayingState); } /// diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 35a2d1eefb..ffe6bbf571 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -245,7 +245,7 @@ namespace osu.Game.Screens.Play.HUD private void load(SpectatorClient client, GameplayState gameplayState) { ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.Playing); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } } } From c8b38f05d5990c7a97740f6d6523737297d965b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 17:14:06 +0900 Subject: [PATCH 285/620] Add note about the visibility logic because it tripped me up --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ffe6bbf571..7158f69a7a 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -159,6 +159,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { + // We don't want to show spectators when we are watching a replay. mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } From a1c5fad6d45c24318028c9f00b0750ad2fb77b88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 20:02:46 +0900 Subject: [PATCH 286/620] Add curvature to new carousel implementation --- osu.Game/Screens/SelectV2/Carousel.cs | 67 +++++++++++++++------------ 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index c8a54d4cd5..a19c86d90b 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -21,7 +20,6 @@ using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Screens.SelectV2 @@ -117,18 +115,10 @@ namespace osu.Game.Screens.SelectV2 protected Carousel() { - InternalChildren = new Drawable[] + InternalChild = scroll = new CarouselScrollContainer { - new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, - scroll = new CarouselScrollContainer - { - RelativeSizeAxes = Axes.Both, - Masking = false, - } + RelativeSizeAxes = Axes.Both, + Masking = false, }; Items.BindCollectionChanged((_, _) => FilterAsync()); @@ -283,6 +273,11 @@ namespace osu.Game.Screens.SelectV2 /// private float visibleUpperBound => (float)(scroll.Current - BleedTop); + /// + /// Half the height of the visible content. + /// + private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2; + protected override void Update() { base.Update(); @@ -302,13 +297,39 @@ namespace osu.Game.Screens.SelectV2 foreach (var panel in scroll.Panels) { - var carouselPanel = (ICarouselPanel)panel; + var c = (ICarouselPanel)panel; - if (panel.Depth != carouselPanel.DrawYPosition) - scroll.Panels.ChangeChildDepth(panel, (float)carouselPanel.DrawYPosition); + if (panel.Depth != c.DrawYPosition) + scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition); + + Debug.Assert(c.Item != null); + + if (c.DrawYPosition != c.Item.CarouselYPosition) + c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); + + Vector2 posInScroll = scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); + float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); + + panel.X = offsetX(dist, visibleHalfHeight); } } + /// + /// Computes the x-offset of currently visible items. Makes the carousel appear round. + /// + /// + /// Vertical distance from the center of the carousel container + /// ranging from -1 to 1. + /// + /// Half the height of the carousel container. + private static float offsetX(float dist, float halfHeight) + { + // The radius of the circle the carousel moves on. + const float circle_radius = 3; + float discriminant = MathF.Max(0, circle_radius * circle_radius - dist * dist); + return (circle_radius - MathF.Sqrt(discriminant)) * halfHeight; + } + private DisplayRange getDisplayRange() { Debug.Assert(displayedCarouselItems != null); @@ -425,20 +446,6 @@ namespace osu.Game.Screens.SelectV2 } } - protected override void Update() - { - base.Update(); - - foreach (var panel in Panels) - { - var c = (ICarouselPanel)panel; - Debug.Assert(c.Item != null); - - if (c.DrawYPosition != c.Item.CarouselYPosition) - c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); - } - } - public override void Clear(bool disposeChildren) { Panels.Height = 0; From 54f9cb7f6817341d992b7bbda62d5a31db4aae1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 19:02:27 +0900 Subject: [PATCH 287/620] Add overlapping spacing support --- osu.Game/Screens/SelectV2/Carousel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a19c86d90b..42c272401a 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -51,6 +51,11 @@ namespace osu.Game.Screens.SelectV2 /// public float DistanceOffscreenToPreload { get; set; } + /// + /// Vertical space between panel layout. Negative value can be used to create an overlapping effect. + /// + protected float SpacingBetweenPanels { get; set; } = -5; + /// /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. @@ -207,13 +212,12 @@ namespace osu.Game.Screens.SelectV2 private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => { - const float spacing = 10; float yPos = 0; foreach (var item in carouselItems) { item.CarouselYPosition = yPos; - yPos += item.DrawHeight + spacing; + yPos += item.DrawHeight + SpacingBetweenPanels; } }, cancellationToken).ConfigureAwait(false); From 43b54623d9ac8a02125d896cfb59d341b5eccc95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 20:24:41 +0900 Subject: [PATCH 288/620] Add required padding on either side of panels so selection can remain centered --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 42c272401a..a07022b32f 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -212,7 +212,7 @@ namespace osu.Game.Screens.SelectV2 private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => { - float yPos = 0; + float yPos = visibleHalfHeight; foreach (var item in carouselItems) { @@ -398,7 +398,7 @@ namespace osu.Game.Screens.SelectV2 if (displayedCarouselItems.Count > 0) { var lastItem = displayedCarouselItems[^1]; - scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight)); + scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); } else scroll.SetLayoutHeight(0); From b2150739573b3e3f8ca27577b19b724c66722661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 10:26:59 +0100 Subject: [PATCH 289/620] Add completion marker to daily challenge profile counter --- .../TestSceneUserProfileDailyChallenge.cs | 4 + .../Components/DailyChallengeStatsDisplay.cs | 120 +++++++++++++----- 2 files changed, 92 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index 0477d39193..ce62a3255d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -38,6 +38,10 @@ namespace osu.Game.Tests.Visual.Online AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); + AddStep("user played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); + AddStep("user played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); + AddStep("user is local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); + AddStep("user is not local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); AddStep("create", () => { Clear(); diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 3e86b2268f..ad64f7d7ac 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.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.LocalisationExtensions; @@ -8,11 +9,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osuTK; namespace osu.Game.Overlays.Profile.Header.Components { @@ -23,6 +27,11 @@ namespace osu.Game.Overlays.Profile.Header.Components public DailyChallengeTooltipData? TooltipContent { get; private set; } private OsuSpriteText dailyPlayCount = null!; + private Container content = null!; + private CircularContainer completionMark = null!; + + [Resolved] + private IAPIProvider api { get; set; } [Resolved] private OsuColour colours { get; set; } = null!; @@ -34,58 +43,91 @@ namespace osu.Game.Overlays.Profile.Header.Components private void load() { AutoSizeAxes = Axes.Both; - CornerRadius = 5; - Masking = true; InternalChildren = new Drawable[] { - new Box + content = new Container { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, - }, - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding(5f), AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, + CornerRadius = 6, + BorderThickness = 2, + BorderColour = colourProvider.Background4, + Masking = true, Children = new Drawable[] { - new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + new Box { - AutoSizeAxes = Axes.Both, - // can't use this because osu-web does weird stuff with \\n. - // Text = UsersStrings.ShowDailyChallengeTitle., - Text = "Daily\nChallenge", - Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, }, - new Container + new FillFlowContainer { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - CornerRadius = 5f, - Masking = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(3f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, Children = new Drawable[] { - new Box + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, + AutoSizeAxes = Axes.Both, + // can't use this because osu-web does weird stuff with \\n. + // Text = UsersStrings.ShowDailyChallengeTitle., + Text = "Daily\nChallenge", + Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, }, - dailyPlayCount = new OsuSpriteText + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - UseFullGlyphHeight = false, - Colour = colourProvider.Content2, - Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + CornerRadius = 3, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + dailyPlayCount = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + Colour = colourProvider.Content2, + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + }, + } }, } }, } }, + completionMark = new CircularContainer + { + Alpha = 0, + Size = new Vector2(16), + Anchor = Anchor.TopRight, + Origin = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Lime1, + }, + new SpriteIcon + { + Size = new Vector2(8), + Colour = colourProvider.Background6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Check, + } + } + }, }; } @@ -114,6 +156,20 @@ namespace osu.Game.Overlays.Profile.Header.Components dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount)); + bool playedToday = stats.LastUpdate?.Date == DateTimeOffset.UtcNow.Date; + bool userIsOnOwnProfile = stats.UserID == api.LocalUser.Value.Id; + + if (playedToday && userIsOnOwnProfile) + { + completionMark.Alpha = 1; + content.BorderColour = colours.Lime1; + } + else + { + completionMark.Alpha = 0; + content.BorderColour = colourProvider.Background4; + } + TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); Show(); From a67a68c5969e61349a8d5866dd9c946bbf39c823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 10:40:26 +0100 Subject: [PATCH 290/620] Remove unnecessary masking spec It was clipping the daily challenge completion checkmark, and it originates in some veeeeery old code where the profile overlay looked and behaved very differently (0fa02718786a0eefa063cce18e9e5351f509ab59). --- osu.Game/Overlays/Profile/Header/Components/MainDetails.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 4bdd5425c0..10bb69f0f5 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -41,7 +41,6 @@ namespace osu.Game.Overlays.Profile.Header.Components AutoSizeAxes = Axes.Y, AutoSizeDuration = 200, AutoSizeEasing = Easing.OutQuint, - Masking = true, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 15), Children = new Drawable[] From 3c4bfc0a01f8a1474de23078e935ce64f58f2ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 11:16:35 +0100 Subject: [PATCH 291/620] Merge spectator list classes into one skinnable --- .../Legacy/CatchLegacySkinTransformer.cs | 4 +- .../Argon/ManiaArgonSkinTransformer.cs | 4 +- .../Legacy/ManiaLegacySkinTransformer.cs | 4 +- .../Legacy/OsuLegacySkinTransformer.cs | 4 +- .../Archives/modified-argon-20250116.osk | Bin 1675 -> 1670 bytes .../Visual/Gameplay/TestSceneSpectatorList.cs | 55 ++++++++++++------ osu.Game/Screens/Play/HUD/SpectatorList.cs | 17 ++---- osu.Game/Skinning/ArgonSkin.cs | 4 +- osu.Game/Skinning/LegacySkin.cs | 4 +- osu.Game/Skinning/TrianglesSkin.cs | 4 +- 10 files changed, 57 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 978a098990..11649da2f1 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var keyCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (keyCounter != null) { @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy Children = new Drawable[] { new LegacyKeyCounterDisplay(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 48c487e70d..6f010ffe48 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon }) { new ArgonManiaComboCounter(), - new SkinnableSpectatorList + new SpectatorList { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 359f21561f..76af569b95 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }) { new LegacyManiaComboCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), }; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 03e4bb24f1..d39e05b262 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } var combo = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(); @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { new LegacyDefaultComboCounter(), new LegacyKeyCounterDisplay(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; } diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk index 811e91b74916988a54a9ff2b73cafe0e0ff0a490..23322e7373514b80b5a36897959ac92d5b074187 100644 GIT binary patch delta 1068 zcmV+{1k?MA4TcR8P)h>@6aWAK2mr@fFL*ij< zWIMF1>VKaTXiLD|6fF&OD!nLq?btc-qw^k{yCcJ>{Qxd7qqX;jT~DvO9NnN1flqY8 zlz68!rACB}5K-4x*|o9Ov$pf)98^nz)Un=(S?h;Q%rIme zIxL|QcxB8u+AQPkmzgvG9Wq<`pOJChQkc2H941^XM8M`K#B!O1?s&PNM9gjif<{1p z9!9PLm_8sP<1Q9+C09m_7MOh}8S9xOQ;3*ylFSJ4AbyzN-F{h#fPe#0?_>diz`x*y zfSF=S)zg_BHk&i5lA16-u-h{NGxfbPR&=Pi`$eVc=}B=5xRGs@LW;`w6nMbV7$+i& zI-gZ`-Kdv+DsiLJFibcKmsJ~5!#Tiz{8H4Aa^AClN0L0*sdKW$4aO_;NJppwssZo` z<1<7<7%;4o(qYimvdy;o_$3i$na4X*DD8hI?1cg9V|m6o<6l7mqudPfoU&I>dtKQn zXJ2KT+RsGod($E#AecfRV;oK?h_qjGE4G!f1!=*wzygmjS)sO*t}@hSY@z0V3y|8v zp$K%{Qn%y~%n+X{TNQGdy<$7pCj<7W?Ty);=!-MksKip6Z_ri)?dmXs(P+R~#MN8a zE2a&*JWwsOU6@n5*v8V)=2oX1Of#e+j^^>Rz)3vQVy4@7>dL~(Hyjylik5XHSoLW} z!}V6RpTA3<@4qTrcYPgtIkgyndVgK)?Em$uSGv5lfBA`}!)7B^^?tzBotT%q@UCXz z-E!+WM{A8vDLLmL_X5K?glZ)i|AIwzr&+dZdfu_;YgW{1nXcV(_10WXx@dW3t7%!5 zR|K6GJO`aHDt9WG7|>XiIU5esZg;zd9A=etj?!=UzX4E70|XQR000OC0LNJ|lQIQf z2*+75T13(TCX<*2E(ph2F@6aWAK2mnQVFk0@{+t-m2BmoeSH=PJYd@x#ZA@zKb+)4p7 zlP3XA5=DG4S}T!PcZLH101zGk02Tm~90nMZp8*knAd`C0!5Gsv4UKlasH(_>L*ij< zWIJtH)&D*x(3XI^i7gFuYPl$R?btc-qw^k{+atrMeFrWuqqVn&UC*w&99^Rkfsb^; zlz6K&rN)Gf5K-3``L(gq^S0B)98^nu6kHT|(==AXLJNr_B398*XoN2G2H(h@aDbQn@5>l%_&OTrQ%VhwM z&!qGlB{aNj$aQs*#p#*WgLbzz)Hf#4YN*0}wPAz^=q9jr!#^?lIUdVIwY%-&;C;Yx zB2`1nDd;CON*GsquqJ-xIL35CJQE;#_y)#*54TJZ5wIQrNQr4IHeVTpMs0<@USeo^yz484{r`M}#L+m7&j5%b_cX4^N(f%@sY5JNab*xunR{CKRGmIFA z4oj&7UKulvHp_VXWhM(ihs=ildt_X<6lSh5hY42;5wbaouw3M!J6<6uk+AEKpb?ON z`;n^vrVoh7q>IH|$(0eK1!mt##(E;j>_kjCNoIt2nB33rZoaHkK*)ipcX9=xz(3)c zfSF=S)zg_BHk&i5lA18zv77eS9*ecB5YYSBV>)L{Z97w5ZyU8qNWK=9gU!so*{PcO=QvtvV-P+;F@!k8Ff0pc(+5 zFg`;xf&s%SDIEq~E!zx&pxyRMLd-JTTXQJwe@E<@3F>3{o^!^(fFefO8R7+Hucr30 z(o?~|%67FMsoMBvRYpiKg-|9qnoYw; z)E*ASpzD^|C5Lc^_*D6-kc<2k+l4+ku#au8%x*=$JF|^SJhk}>Z8gv?Pa_zO2b{%R zy@kDE+OW$5)iQ&fIkk7&SvuO>>Xd_Nj#SLiJb4m0iDz2ObQ@1yU0C^!Bd1N#vML3u zKF(;kURL}Ct!?mp|3$gF>uW22LjSMH`{P>Y|1X!lvem8q%TFvFHXE_3_XDo(#Jt>v zcQp&|mRrs_T4!`hNjV3(708dbvLE_}bBNTEF#Z{f>W;OVj_I^ruenW*?b~MC^Q>Tx zd86f-t)^vJ-oY?5VN`C_Gzp-wDtOksCJ4ISoxEn5e~z+mcfSBoO9KQ66aWAK2mnQV zFq16>UI;~eFk0@{+t-tm1uh6hd@x#ZA@zKd$pteFMSL(?E0I@sh64Zq5R(Z8Hy=fO rFj~sD spectators = new BindableList(); - private readonly Bindable localUserPlayingState = new Bindable(); - private int counter; [Test] public void TestBasics() { SpectatorList list = null!; - AddStep("create spectator list", () => Child = list = new SpectatorList + Bindable playingState = new Bindable(); + GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); + TestSpectatorClient client = new TestSpectatorClient(); + + AddStep("create spectator list", () => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spectators = { BindTarget = spectators }, - UserPlayingState = { BindTarget = localUserPlayingState } + Children = new Drawable[] + { + client, + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(GameplayState), gameplayState), + (typeof(SpectatorClient), client) + ], + Child = list = new SpectatorList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; }); - AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); + AddStep("start playing", () => playingState.Value = LocalUserPlayingState.Playing); AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); - spectators.Add(new SpectatorUser - { - OnlineID = id, - Username = $"User {id}" - }); + ((ISpectatorClient)client).UserStartedWatching([ + new SpectatorUser + { + OnlineID = id, + Username = $"User {id}" + } + ]); }, 10); - AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5); + AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); - AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); - AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); + AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break); + AddStep("stop playing", () => playingState.Value = LocalUserPlayingState.NotPlaying); } } } diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 7158f69a7a..7b6bf6f55e 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -24,7 +24,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public partial class SpectatorList : CompositeDrawable + public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable { private const int max_spectators_displayed = 10; @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Play.HUD private DrawablePool pool = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, SpectatorClient client, GameplayState gameplayState) { AutoSizeAxes = Axes.Y; @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Play.HUD }; HeaderColour.Value = Header.Colour; + + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } protected override void LoadComplete() @@ -236,17 +239,7 @@ namespace osu.Game.Screens.Play.HUD linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; } } - } - public partial class SkinnableSpectatorList : SpectatorList, ISerialisableDrawable - { public bool UsesFixedAnchor { get; set; } - - [BackgroundDependencyLoader] - private void load(SpectatorClient client, GameplayState gameplayState) - { - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); - } } } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index c3319b738d..bd31ccd5c9 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -112,7 +112,7 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var comboCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(36, -66); @@ -135,7 +135,7 @@ namespace osu.Game.Skinning Origin = Anchor.BottomLeft, Scale = new Vector2(1.3f), }, - new SkinnableSpectatorList + new SpectatorList { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index c607c57fcc..08fa068830 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -367,7 +367,7 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var combo = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(); @@ -389,7 +389,7 @@ namespace osu.Game.Skinning }) { new LegacyDefaultComboCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), }; } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 8853a5c4ac..06fe1c80ee 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -91,7 +91,7 @@ namespace osu.Game.Skinning var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (score != null) { @@ -177,7 +177,7 @@ namespace osu.Game.Skinning new BarHitErrorMeter(), new BarHitErrorMeter(), new TrianglesPerformancePointsCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; From b79e937d2dbf0d9363833c7d725a5ab4c5d9f28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 13:34:16 +0100 Subject: [PATCH 292/620] Fix code quality --- .../Profile/Header/Components/DailyChallengeStatsDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index ad64f7d7ac..a9d982e17f 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private CircularContainer completionMark = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] private OsuColour colours { get; set; } = null!; From ebca2e4b4ffc2bee95016e4fac4063dc5bc78405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 13:33:59 +0100 Subject: [PATCH 293/620] Implement precise movement tool As mentioned in one of the points in https://github.com/ppy/osu/discussions/31263. --- .../Edit/PreciseMovementPopover.cs | 190 ++++++++++++++++++ .../Edit/TransformToolboxGroup.cs | 25 ++- .../UserInterfaceV2/SliderWithTextBoxInput.cs | 5 + .../Input/Bindings/GlobalActionContainer.cs | 6 +- .../GlobalActionKeyBindingStrings.cs | 5 + 5 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs new file mode 100644 index 0000000000..151ca31ac0 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -0,0 +1,190 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Input.Events; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class PreciseMovementPopover : OsuPopover + { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + private readonly Dictionary initialPositions = new Dictionary(); + private RectangleF initialSurroundingQuad; + + private BindableNumber xBindable = null!; + private BindableNumber yBindable = null!; + + private SliderWithTextBoxInput xInput = null!; + private OsuCheckbox relativeCheckbox = null!; + + public PreciseMovementPopover() + { + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 220, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Children = new Drawable[] + { + xInput = new SliderWithTextBoxInput("X:") + { + Current = xBindable = new BindableNumber + { + Precision = 1, + }, + Instantaneous = true, + TabbableContentContainer = this, + }, + new SliderWithTextBoxInput("Y:") + { + Current = yBindable = new BindableNumber + { + Precision = 1, + }, + Instantaneous = true, + TabbableContentContainer = this, + }, + relativeCheckbox = new OsuCheckbox(false) + { + RelativeSizeAxes = Axes.X, + LabelText = "Relative movement", + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => + { + xInput.TakeFocus(); + xInput.SelectAll(); + }); + } + + protected override void PopIn() + { + base.PopIn(); + editorBeatmap.BeginChange(); + initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair(ho, ((IHasPosition)ho).Position))); + initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast()).AABBFloat; + + Debug.Assert(initialPositions.Count > 0); + + if (initialPositions.Count > 1) + { + relativeCheckbox.Current.Value = true; + relativeCheckbox.Current.Disabled = true; + } + + relativeCheckbox.Current.BindValueChanged(_ => relativeChanged(), true); + xBindable.BindValueChanged(_ => applyPosition()); + yBindable.BindValueChanged(_ => applyPosition()); + } + + protected override void PopOut() + { + base.PopOut(); + if (IsLoaded) editorBeatmap.EndChange(); + } + + private void relativeChanged() + { + // reset bindable bounds to something that is guaranteed to be larger than any previous value. + // this prevents crashes that can happen in the middle of changing the bounds, as updating both bound ends at the same is not atomic - + // if the old and new bounds are disjoint, assigning X first can produce a situation where MinValue > MaxValue. + (xBindable.MinValue, xBindable.MaxValue) = (float.MinValue, float.MaxValue); + (yBindable.MinValue, yBindable.MaxValue) = (float.MinValue, float.MaxValue); + + float previousX = xBindable.Value; + float previousY = yBindable.Value; + + if (relativeCheckbox.Current.Value) + { + (xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X); + (yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y); + + xBindable.Default = yBindable.Default = 0; + + if (initialPositions.Count == 1) + { + var initialPosition = initialPositions.Single().Value; + xBindable.Value = previousX - initialPosition.X; + yBindable.Value = previousY - initialPosition.Y; + } + } + else + { + Debug.Assert(initialPositions.Count == 1); + var initialPosition = initialPositions.Single().Value; + + var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size); + + (xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X); + (yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y); + + xBindable.Default = initialPosition.X; + yBindable.Default = initialPosition.Y; + + xBindable.Value = xBindable.Default + previousX; + yBindable.Value = yBindable.Default + previousY; + } + } + + private void applyPosition() + { + editorBeatmap.PerformOnSelection(ho => + { + if (!initialPositions.TryGetValue(ho, out var initialPosition)) + return; + + var pos = new Vector2(xBindable.Value, yBindable.Value); + if (relativeCheckbox.Current.Value) + ((IHasPosition)ho).Position = initialPosition + pos; + else + ((IHasPosition)ho).Position = pos; + }); + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.Select && !e.Repeat) + { + this.HidePopover(); + return true; + } + + return base.OnPressed(e); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index a41412cbe3..440e06598d 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,6 +11,9 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -18,9 +22,12 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { + private readonly BindableList selectedHitObjects = new BindableList(); + private readonly BindableBool canMove = new BindableBool(); private readonly AggregateBindable canRotate = new AggregateBindable((x, y) => x || y); private readonly AggregateBindable canScale = new AggregateBindable((x, y) => x || y); + private EditorToolButton moveButton = null!; private EditorToolButton rotateButton = null!; private EditorToolButton scaleButton = null!; @@ -35,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit } [BackgroundDependencyLoader] - private void load() + private void load(EditorBeatmap editorBeatmap) { Child = new FillFlowContainer { @@ -44,20 +51,27 @@ namespace osu.Game.Rulesets.Osu.Edit Spacing = new Vector2(5), Children = new Drawable[] { + moveButton = new EditorToolButton("Move", + () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, + () => new PreciseMovementPopover()), rotateButton = new EditorToolButton("Rotate", () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, () => new PreciseRotationPopover(RotationHandler, GridToolbox)), scaleButton = new EditorToolButton("Scale", - () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, + () => new SpriteIcon { Icon = FontAwesome.Solid.ExpandArrowsAlt }, () => new PreciseScalePopover(ScaleHandler, GridToolbox)) } }; + + selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects); } protected override void LoadComplete() { base.LoadComplete(); + selectedHitObjects.BindCollectionChanged((_, _) => canMove.Value = selectedHitObjects.Any(ho => ho is not Spinner), true); + canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin); canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin); @@ -67,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. + canMove.BindValueChanged(move => moveButton.Enabled.Value = move.NewValue, true); canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true); canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true); } @@ -77,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Edit switch (e.Action) { + case GlobalAction.EditorToggleMoveControl: + { + moveButton.TriggerClick(); + return true; + } + case GlobalAction.EditorToggleRotateControl: { if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value) diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index 50d8d763e1..c16a6c612d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -32,6 +32,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => slider.Current = value; } + public CompositeDrawable TabbableContentContainer + { + set => textBox.TabbableContentContainer = value; + } + private bool instantaneous; /// diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 5e509d2035..6c130ff309 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -144,6 +144,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(InputKey.None, GlobalAction.EditorToggleMoveControl), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), @@ -493,7 +494,10 @@ namespace osu.Game.Input.Bindings EditorSeekToNextBookmark, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))] - AbsoluteScrollSongList + AbsoluteScrollSongList, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))] + EditorToggleMoveControl, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 436a2be648..5713df57c9 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -454,6 +454,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list"); + /// + /// "Toggle movement control" + /// + public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } From e753e3ee2feea2bac8d698d910fa741695e5af05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Jan 2025 00:25:06 +0900 Subject: [PATCH 294/620] Update framework (except android) --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e1bc971034..bfb6e51f93 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index ece42e87b4..7b0a027d39 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From cbbcf54d742f0b74d3c122d8487254862a662df6 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Sat, 18 Jan 2025 02:41:15 +0000 Subject: [PATCH 295/620] add warning text on acronym conflict --- .../Screens/Editors/TeamEditorScreen.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 250d5acaae..4008f9d140 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -71,6 +71,8 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] private LadderInfo ladderInfo { get; set; } = null!; + private readonly SettingsTextBox acronymTextBox; + public TeamRow(TournamentTeam team, TournamentScreen parent) { Model = team; @@ -112,7 +114,7 @@ namespace osu.Game.Tournament.Screens.Editors Width = 0.2f, Current = Model.FullName }, - new SettingsTextBox + acronymTextBox = new SettingsTextBox { LabelText = "Acronym", Width = 0.2f, @@ -177,6 +179,28 @@ namespace osu.Game.Tournament.Screens.Editors }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Model.Acronym.BindValueChanged(acronym => + { + var matchingTeams = ladderInfo.Teams + .Where(t => t.Acronym.Value == acronym.NewValue && t != Model) + .ToList(); + + if (matchingTeams.Count > 0) + { + acronymTextBox.SetNoticeText( + $"Acronym '{acronym.NewValue}' is already in use by team{(matchingTeams.Count > 1 ? "s" : "")}:\n" + + $"{string.Join(",\n", matchingTeams)}", true); + return; + } + + acronymTextBox.ClearNoticeText(); + }, true); + } + private partial class LastYearPlacementSlider : RoundedSliderBar { public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText; From 72e1b2954c57087d58a9cd5c6fd540c234ca7f66 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Mon, 20 Jan 2025 00:21:10 +0800 Subject: [PATCH 296/620] Don't highlight friends' scores under beatmap's friend score leaderboard --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 6 ++++-- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 32b25a866d..6acf236bf3 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -54,6 +54,7 @@ namespace osu.Game.Online.Leaderboards private readonly int? rank; private readonly bool isOnlineScope; + private readonly bool highlightFriend; private Box background; private Container content; @@ -86,12 +87,13 @@ namespace osu.Game.Online.Leaderboards [Resolved] private ScoreManager scoreManager { get; set; } = null!; - public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true) + public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true, bool highlightFriend = true) { Score = score; this.rank = rank; this.isOnlineScope = isOnlineScope; + this.highlightFriend = highlightFriend; RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -130,7 +132,7 @@ namespace osu.Game.Online.Leaderboards background = new Box { RelativeSizeAxes = Axes.Both, - Colour = isUserFriend ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), + Colour = (highlightFriend && isUserFriend) ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), Alpha = background_alpha, }, }, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 58c14b15b9..57fe22aa59 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -169,12 +169,12 @@ namespace osu.Game.Screens.Select.Leaderboards return scoreRetrievalRequest = newRequest; } - protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope) + protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend) { Action = () => ScoreSelected?.Invoke(model) }; - protected override LeaderboardScore CreateDrawableTopScore(ScoreInfo model) => new LeaderboardScore(model, model.Position, false) + protected override LeaderboardScore CreateDrawableTopScore(ScoreInfo model) => new LeaderboardScore(model, model.Position, false, Scope != BeatmapLeaderboardScope.Friend) { Action = () => ScoreSelected?.Invoke(model) }; From b6ce72b6d92d28c6f95cf28255535a16ad6a1ef0 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Sun, 19 Jan 2025 23:27:44 +0100 Subject: [PATCH 297/620] Remove redundant ToArray() calls in Osu/ManiaHitObjectComposer --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 926a4b2736..9062c32b7b 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -64,11 +64,11 @@ namespace osu.Game.Rulesets.Mania.Edit return; List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); - string[] objectDescriptions = objectDescription.Split(',').ToArray(); + string[] objectDescriptions = objectDescription.Split(','); for (int i = 0; i < objectDescriptions.Length; i++) { - string[] split = objectDescriptions[i].Split('|').ToArray(); + string[] split = objectDescriptions[i].Split('|'); if (split.Length != 2) continue; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index f5e7ff6004..aad3d0c93b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); - string[] splitDescription = objectDescription.Split(',').ToArray(); + string[] splitDescription = objectDescription.Split(','); for (int i = 0; i < splitDescription.Length; i++) { From a6ca9ba9fb0630562425fe37d0445da5f75e9635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 00:51:43 +0100 Subject: [PATCH 298/620] Display up to 2 decimal places in `MetronomeDisplay` --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 5e5b740b62..5325c8640b 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -228,11 +228,13 @@ namespace osu.Game.Screens.Edit.Timing private double effectiveBeatLength; + private double effectiveBpm => 60_000 / effectiveBeatLength; + private TimingControlPoint timingPoint = null!; private bool isSwinging; - private readonly BindableInt interpolatedBpm = new BindableInt(); + private readonly BindableDouble interpolatedBpm = new BindableDouble(); private ScheduledDelegate? latchDelegate; @@ -255,7 +257,17 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString()); + interpolatedBpm.BindValueChanged(_ => updateBpmText()); + } + + private void updateBpmText() + { + double bpm = Math.Round(interpolatedBpm.Value); + + if (Precision.AlmostEquals(bpm, effectiveBpm, 1.0)) + bpm = effectiveBpm; + + bpmText.Text = bpm.ToLocalisableString("0.##"); } protected override void Update() @@ -277,12 +289,11 @@ namespace osu.Game.Screens.Edit.Timing EarlyActivationMilliseconds = timingPoint.BeatLength / 2; - double effectiveBpm = 60000 / effectiveBeatLength; - float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((effectiveBpm - 30) / 480, 0, 1)); weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); - this.TransformBindableTo(interpolatedBpm, (int)Math.Round(effectiveBpm), 600, Easing.OutQuint); + + this.TransformBindableTo(interpolatedBpm, effectiveBpm, 600, Easing.OutQuint); } if (!BeatSyncSource.Clock.IsRunning && isSwinging) From 3532ce1636460d0988fa7d0c3832b25065600cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:07:13 +0100 Subject: [PATCH 299/620] Olibomby insisted on it being like this so i concede --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 5325c8640b..f8236f922a 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -262,10 +262,9 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { - double bpm = Math.Round(interpolatedBpm.Value); - - if (Precision.AlmostEquals(bpm, effectiveBpm, 1.0)) - bpm = effectiveBpm; + double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) + ? effectiveBpm + : Math.Round(interpolatedBpm.Value); bpmText.Text = bpm.ToLocalisableString("0.##"); } From 8f33b4cc6159b4b65fc7df4b757c1d5418eb15ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:14:21 +0100 Subject: [PATCH 300/620] Add comment --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index f8236f922a..8a4f1c01b1 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -262,6 +262,8 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { + // While interpolating between two integer values, showing the decimal places would look a bit odd + // so rounding is applied until we're close to the final value. double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) ? effectiveBpm : Math.Round(interpolatedBpm.Value); From e386c9e373618fea4acb371447db8d2bee637701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:25:22 +0100 Subject: [PATCH 301/620] Apply snapping when pasting hitobjects --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index f7e523db25..195625dcde 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -31,6 +31,9 @@ namespace osu.Game.Screens.Edit.Compose [Resolved] private IGameplaySettings globalGameplaySettings { get; set; } + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } + private Bindable clipboard { get; set; } private HitObjectComposer composer; @@ -150,7 +153,7 @@ namespace osu.Game.Screens.Edit.Compose Debug.Assert(objects.Any()); - double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime); + double timeOffset = beatSnapProvider.SnapTime(clock.CurrentTime) - objects.Min(o => o.StartTime); foreach (var h in objects) h.StartTime += timeOffset; From 45e0d9154e410e0db5aab353c5d67b3e539db015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:38:18 +0100 Subject: [PATCH 302/620] Adjust tests to worked with snapped start time --- osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs index a766b253aa..ce9dbd5fb1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); - AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime); + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(newTime, null)); } [Test] @@ -122,6 +122,8 @@ namespace osu.Game.Tests.Visual.Editing [TestCase(true)] public void TestCopyPaste(bool deselectAfterCopy) { + const int paste_time = 2000; + var addedObject = new HitCircle { StartTime = 1000 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); @@ -130,7 +132,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("copy hitobject", () => Editor.Copy()); - AddStep("move forward in time", () => EditorClock.Seek(2000)); + AddStep("move forward in time", () => EditorClock.Seek(paste_time)); if (deselectAfterCopy) { @@ -144,7 +146,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2); - AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000); + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(paste_time, null)); AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType().First().ChildrenOfType().First().Alpha > 0); AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType().First().ChildrenOfType().First().Alpha > 0); From 525e16ad1d8442a01b81ba501b49204ba9705c77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:00:35 +0900 Subject: [PATCH 303/620] Fix one more new inspection in EAP 2025 --- osu.Game/Skinning/ResourceStoreBackedSkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/ResourceStoreBackedSkin.cs b/osu.Game/Skinning/ResourceStoreBackedSkin.cs index 206c400a88..450794c4a8 100644 --- a/osu.Game/Skinning/ResourceStoreBackedSkin.cs +++ b/osu.Game/Skinning/ResourceStoreBackedSkin.cs @@ -33,7 +33,7 @@ namespace osu.Game.Skinning public ISample? GetSample(ISampleInfo sampleInfo) { - foreach (string? lookup in sampleInfo.LookupNames) + foreach (string lookup in sampleInfo.LookupNames) { ISample? sample = samples.Get(lookup); if (sample != null) From e3195e23160b8655ca542e9372959ca93e8c5fde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:02:31 +0900 Subject: [PATCH 304/620] Adjust new line break warning to hint --- osu.sln.DotSettings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 8f5e642f94..5cac0024b7 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -170,7 +170,7 @@ WARNING HINT WARNING - WARNING + HINT WARNING ERROR WARNING From b5b407fe7ca888ae1a9a8297767646e3bb60b2c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:40:38 +0900 Subject: [PATCH 305/620] Knock some sense into daily challenge profile test scene --- .../TestSceneUserProfileDailyChallenge.cs | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index ce62a3255d..2be9c1ab14 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -20,28 +21,16 @@ namespace osu.Game.Tests.Visual.Online public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene { [Cached] - public readonly Bindable User = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); + private readonly Bindable userProfileData = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - protected override void LoadComplete() + private DailyChallengeStatsDisplay display = null!; + + [SetUpSteps] + public void SetUpSteps() { - base.LoadComplete(); - - DailyChallengeStatsDisplay display = null!; - - AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); - AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); - AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v)); - AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); - AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); - AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); - AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); - AddStep("user played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); - AddStep("user played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); - AddStep("user is local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); - AddStep("user is not local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); AddStep("create", () => { Clear(); @@ -55,16 +44,40 @@ namespace osu.Game.Tests.Visual.Online Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1f), - User = { BindTarget = User }, + User = { BindTarget = userProfileData }, }); }); + + AddStep("set local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); + AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); + AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v)); + AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); + AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); + AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); + AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); + } + + [Test] + public void TestStates() + { + AddStep("played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); + AddStep("played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); + AddStep("change to non-local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); + AddStep("hover", () => InputManager.MoveMouseTo(display)); } private void update(Action change) { - change.Invoke(User.Value!.User.DailyChallengeStatistics); - User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset); + change.Invoke(userProfileData.Value!.User.DailyChallengeStatistics); + userProfileData.Value = new UserProfileData(userProfileData.Value.User, userProfileData.Value.Ruleset); } [Test] From 04ba686be5f3abbe93ddfc7e59395f1a0b2d9f11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:47:47 +0900 Subject: [PATCH 306/620] Add basic animation --- .../Header/Components/DailyChallengeStatsDisplay.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index a9d982e17f..a3dce89ad4 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -161,12 +161,21 @@ namespace osu.Game.Overlays.Profile.Header.Components if (playedToday && userIsOnOwnProfile) { - completionMark.Alpha = 1; + if (completionMark.Alpha > 0.8f) + { + completionMark.ScaleTo(1.2f).ScaleTo(1, 800, Easing.OutElastic); + } + else + { + completionMark.FadeIn(500, Easing.OutExpo); + completionMark.ScaleTo(1.6f).ScaleTo(1, 500, Easing.OutExpo); + } + content.BorderColour = colours.Lime1; } else { - completionMark.Alpha = 0; + completionMark.FadeOut(50); content.BorderColour = colourProvider.Background4; } From a1bcdb091df348f8c0ccad760ef67215def1d7a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:55:13 +0900 Subject: [PATCH 307/620] Adjust code slightly --- .../Screens/Editors/TeamEditorScreen.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 4008f9d140..162379f4aa 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -185,19 +185,18 @@ namespace osu.Game.Tournament.Screens.Editors Model.Acronym.BindValueChanged(acronym => { - var matchingTeams = ladderInfo.Teams - .Where(t => t.Acronym.Value == acronym.NewValue && t != Model) - .ToList(); + var teamsWithSameAcronym = ladderInfo.Teams + .Where(t => t.Acronym.Value == acronym.NewValue && t != Model) + .ToList(); - if (matchingTeams.Count > 0) + if (teamsWithSameAcronym.Count > 0) { acronymTextBox.SetNoticeText( - $"Acronym '{acronym.NewValue}' is already in use by team{(matchingTeams.Count > 1 ? "s" : "")}:\n" - + $"{string.Join(",\n", matchingTeams)}", true); - return; + $"Acronym '{acronym.NewValue}' is already in use by team{(teamsWithSameAcronym.Count > 1 ? "s" : "")}:\n" + + $"{string.Join(",\n", teamsWithSameAcronym)}", true); } - - acronymTextBox.ClearNoticeText(); + else + acronymTextBox.ClearNoticeText(); }, true); } From dcdb8d13a998b049a377b93a2deed8d92e42562c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 16:17:39 +0900 Subject: [PATCH 308/620] Always select text when an editor slider-textbox is focused --- osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs | 6 +----- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 6 +----- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 +----- osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 3 +-- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index 151ca31ac0..f2cb8794b5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -85,11 +85,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - xInput.TakeFocus(); - xInput.SelectAll(); - }); + ScheduleAfterChildren(() => xInput.TakeFocus()); } protected override void PopIn() diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 477d3b4e57..ae8ad2c01b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -96,11 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - angleInput.TakeFocus(); - angleInput.SelectAll(); - }); + ScheduleAfterChildren(() => angleInput.TakeFocus()); angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index e728290289..ac6d9fbb19 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -139,11 +139,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - scaleInput.TakeFocus(); - scaleInput.SelectAll(); - }); + ScheduleAfterChildren(() => scaleInput.TakeFocus()); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); xCheckBox.Current.BindValueChanged(_ => diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index c16a6c612d..2fbe3ae89b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -74,6 +74,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 textBox = new LabelledTextBox { Label = labelText, + SelectAllOnFocus = true, }, slider = new SettingsSlider { @@ -92,8 +93,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true; - public bool SelectAll() => textBox.SelectAll(); - private bool updatingFromTextBox; private void textChanged(ValueChangedEvent change) From 2b5ea4e6e0e859599affbcb5cf9151060679450b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 20 Jan 2025 03:17:01 -0500 Subject: [PATCH 309/620] Fix recent editor textbox regressions --- .../UserInterface/TestSceneFormControls.cs | 2 +- .../Graphics/UserInterfaceV2/FormNumberBox.cs | 20 +++++++++---------- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 3 +-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index b9ff78b49f..118fbca97b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface Current = { Disabled = true }, TabbableContentContainer = this, }, - new FormNumberBox + new FormNumberBox(allowDecimals: true) { Caption = "Number", HintText = "Insert your favourite number", diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs index 61d3b3fc31..b739155a36 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs @@ -1,32 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Globalization; using osu.Framework.Input; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class FormNumberBox : FormTextBox { - public bool AllowDecimals { get; init; } + private readonly bool allowDecimals; - internal override InnerTextBox CreateTextBox() => new InnerNumberBox + public FormNumberBox(bool allowDecimals = false) + { + this.allowDecimals = allowDecimals; + } + + internal override InnerTextBox CreateTextBox() => new InnerNumberBox(allowDecimals) { - AllowDecimals = AllowDecimals, SelectAllOnFocus = true, }; internal partial class InnerNumberBox : InnerTextBox { - public bool AllowDecimals { get; init; } - - public InnerNumberBox() + public InnerNumberBox(bool allowDecimals) { - InputProperties = new TextInputProperties(TextInputType.Number, false); + InputProperties = new TextInputProperties(allowDecimals ? TextInputType.Decimal : TextInputType.Number, false); } - - protected override bool CanAddCharacter(char character) - => char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character)); } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 532423876e..4e43b133c7 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -119,7 +119,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Caption = Caption, TooltipText = HintText, }, - textBox = new FormNumberBox.InnerNumberBox + textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -127,7 +127,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 Width = 0.5f, CommitOnFocusLost = true, SelectAllOnFocus = true, - AllowDecimals = true, OnInputError = () => { flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); From 89586d5ab25cb7108ed71d7c516debf9950f60cf Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Mon, 20 Jan 2025 13:43:45 +0100 Subject: [PATCH 310/620] Fix settings in replay hiding when dragging a slider --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 668c74e0c2..b285b1b799 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -122,7 +122,10 @@ namespace osu.Game.Screens.Play.HUD { float screenMouseX = inputManager.CurrentState.Mouse.Position.X; - Expanded.Value = screenMouseX >= button.ScreenSpaceDrawQuad.TopLeft.X && screenMouseX <= ToScreenSpace(new Vector2(DrawWidth + EXPANDED_WIDTH, 0)).X; + Expanded.Value = + (screenMouseX >= button.ScreenSpaceDrawQuad.TopLeft.X && screenMouseX <= ToScreenSpace(new Vector2(DrawWidth + EXPANDED_WIDTH, 0)).X) + // Stay expanded if the user is dragging a slider. + || inputManager.DraggedDrawable != null; } protected override void OnHoverLost(HoverLostEvent e) From 6b524aba60e2474b9faa281f299fb4ee365fd974 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 14:27:48 +0900 Subject: [PATCH 311/620] Enable sentry caching to avoid sentry writing outside of game directory See https://github.com/ppy/osu/discussions/31412. Probably safe enough. --- osu.Game/OsuGame.cs | 8 ++++++-- osu.Game/Utils/SentryLogger.cs | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 40d13ae0b7..47e301c4e4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -233,8 +233,6 @@ namespace osu.Game forwardGeneralLogsToNotifications(); forwardTabletLogsToNotifications(); - - SentryLogger = new SentryLogger(this); } #region IOverlayManager @@ -320,6 +318,12 @@ namespace osu.Game private readonly List dragDropFiles = new List(); private ScheduledDelegate dragDropImportSchedule; + public override void SetupLogging(Storage gameStorage, Storage cacheStorage) + { + base.SetupLogging(gameStorage, cacheStorage); + SentryLogger = new SentryLogger(this, cacheStorage); + } + public override void SetHost(GameHost host) { base.SetHost(host); diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 8d3e5fb834..ed644bf5cb 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -36,7 +37,7 @@ namespace osu.Game.Utils private readonly OsuGame game; - public SentryLogger(OsuGame game) + public SentryLogger(OsuGame game, Storage? storage = null) { this.game = game; @@ -49,6 +50,7 @@ namespace osu.Game.Utils options.AutoSessionTracking = true; options.IsEnvironmentUser = false; options.IsGlobalModeEnabled = true; + options.CacheDirectoryPath = storage?.GetFullPath(string.Empty); // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}"; }); From 46ff9d1aad2d70616114a6b6075b1bdbe6a8f0f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 14:13:42 +0900 Subject: [PATCH 312/620] Fix beat snap grid being lines not being corectly centered to time This was pointed out as an issue in the osu!taiko editor, but actually affects all rulesets. Has now been fixed everywhere. --- Closes https://github.com/ppy/osu/issues/31548. osu!mania could arguable be consdiered "more correct" with the old display, but I don't think it's a huge deal either way (subjective at best). --- .../Edit/Compose/Components/BeatSnapGrid.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs index 766d5b5601..f1b7951999 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs @@ -185,9 +185,28 @@ namespace osu.Game.Screens.Edit.Compose.Components private void onDirectionChanged(ValueChangedEvent direction) { - Origin = Anchor = direction.NewValue == ScrollingDirection.Up - ? Anchor.TopLeft - : Anchor.BottomLeft; + switch (direction.NewValue) + { + case ScrollingDirection.Up: + Anchor = Anchor.TopLeft; + Origin = Anchor.CentreLeft; + break; + + case ScrollingDirection.Down: + Anchor = Anchor.BottomLeft; + Origin = Anchor.CentreLeft; + break; + + case ScrollingDirection.Left: + Anchor = Anchor.TopLeft; + Origin = Anchor.TopCentre; + break; + + case ScrollingDirection.Right: + Anchor = Anchor.TopRight; + Origin = Anchor.TopCentre; + break; + } bool isHorizontal = direction.NewValue == ScrollingDirection.Left || direction.NewValue == ScrollingDirection.Right; From f13304293603b49b304d6acf66f2941310943064 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 01:14:18 -0500 Subject: [PATCH 313/620] Fix silly mistake --- .../Overlays/Settings/Sections/Maintenance/GeneralSettings.cs | 2 +- .../Sections/Maintenance/SystemFileImportComponent.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index ed3e72adbe..99b25808a1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private SystemFileImportComponent systemFileImport = null!; [BackgroundDependencyLoader] - private void load(OsuGame game, GameHost host, IPerformFromScreenRunner? performer) + private void load(OsuGameBase game, GameHost host, IPerformFromScreenRunner? performer) { Add(systemFileImport = new SystemFileImportComponent(game, host)); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs index 9827872702..ded8c81891 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs @@ -10,12 +10,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public partial class SystemFileImportComponent : Component { - private readonly OsuGame game; + private readonly OsuGameBase game; private readonly GameHost host; private ISystemFileSelector? selector; - public SystemFileImportComponent(OsuGame game, GameHost host) + public SystemFileImportComponent(OsuGameBase game, GameHost host) { this.game = game; this.host = host; From a7c9f84a93fd285c58a914615f40380a454a6884 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 15:14:39 +0900 Subject: [PATCH 314/620] Adjust visuals slightly --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 8a4f1c01b1..f3bd9ff257 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -17,9 +17,10 @@ using osu.Framework.Threading; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -28,7 +29,7 @@ namespace osu.Game.Screens.Edit.Timing { private Container swing = null!; - private OsuSpriteText bpmText = null!; + private OsuTextFlowContainer bpmText = null!; private Drawable weight = null!; private Drawable stick = null!; @@ -213,10 +214,15 @@ namespace osu.Game.Screens.Edit.Timing }, } }, - bpmText = new OsuSpriteText + bpmText = new OsuTextFlowContainer(st => + { + st.Font = OsuFont.Default.With(fixedWidth: true); + st.Spacing = new Vector2(-2.2f, 0); + }) { Name = @"BPM display", Colour = overlayColourProvider.Content1, + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, Y = -3, @@ -262,13 +268,20 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { + int intPart = (int)interpolatedBpm.Value; + + bpmText.Text = intPart.ToLocalisableString(); + // While interpolating between two integer values, showing the decimal places would look a bit odd // so rounding is applied until we're close to the final value. - double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) - ? effectiveBpm - : Math.Round(interpolatedBpm.Value); + int decimalPlaces = FormatUtils.FindPrecision((decimal)effectiveBpm); - bpmText.Text = bpm.ToLocalisableString("0.##"); + if (decimalPlaces > 0) + { + bool reachedFinalNumber = intPart == (int)effectiveBpm; + + bpmText.AddText((effectiveBpm % 1).ToLocalisableString("." + new string('0', decimalPlaces)), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.1f); + } } protected override void Update() @@ -294,7 +307,7 @@ namespace osu.Game.Screens.Edit.Timing weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); - this.TransformBindableTo(interpolatedBpm, effectiveBpm, 600, Easing.OutQuint); + this.TransformBindableTo(interpolatedBpm, effectiveBpm, 300, Easing.OutExpo); } if (!BeatSyncSource.Clock.IsRunning && isSwinging) From 3f51626f07cd76c332518e277000f9931cd4ccee Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 02:20:48 -0500 Subject: [PATCH 315/620] Simplify code immensely Co-authored-by: Dean Herbert --- .../Sections/Maintenance/GeneralSettings.cs | 15 +++--- .../Maintenance/SystemFileImportComponent.cs | 51 ------------------- 2 files changed, 9 insertions(+), 57 deletions(-) delete mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 99b25808a1..47314dcafe 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -17,12 +19,13 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { protected override LocalisableString Header => CommonStrings.General; - private SystemFileImportComponent systemFileImport = null!; + private ISystemFileSelector? selector; [BackgroundDependencyLoader] private void load(OsuGameBase game, GameHost host, IPerformFromScreenRunner? performer) { - Add(systemFileImport = new SystemFileImportComponent(game, host)); + if ((selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray())) != null) + selector.Selected += f => Task.Run(() => game.Import(f.FullName)); AddRange(new Drawable[] { @@ -31,10 +34,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Text = DebugSettingsStrings.ImportFiles, Action = () => { - if (systemFileImport.PresentIfAvailable()) - return; - - performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); + if (selector != null) + selector.Present(); + else + performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); }, }, new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs deleted file mode 100644 index ded8c81891..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs +++ /dev/null @@ -1,51 +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.Linq; -using System.Threading.Tasks; -using osu.Framework.Graphics; -using osu.Framework.Platform; - -namespace osu.Game.Overlays.Settings.Sections.Maintenance -{ - public partial class SystemFileImportComponent : Component - { - private readonly OsuGameBase game; - private readonly GameHost host; - - private ISystemFileSelector? selector; - - public SystemFileImportComponent(OsuGameBase game, GameHost host) - { - this.game = game; - this.host = host; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray()); - - if (selector != null) - selector.Selected += f => Schedule(() => startImport(f.FullName)); - } - - public bool PresentIfAvailable() - { - if (selector == null) - return false; - - selector.Present(); - return true; - } - - private void startImport(string path) - { - Task.Factory.StartNew(async () => - { - await game.Import(path).ConfigureAwait(false); - }, TaskCreationOptions.LongRunning); - } - } -} From 3a37817ab20bc1add6534b4a077f56619ead6dcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 19:01:33 +0900 Subject: [PATCH 316/620] Don't block `Popover` escape handling (just let it work in addition to `GlobalAction.Back`) --- osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index 9b4689958c..7abaca4092 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -14,7 +14,6 @@ using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osuTK; -using osuTK.Input; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -75,14 +74,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 samplePopOut?.Play(); } - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Key == Key.Escape) - return false; // disable the framework-level handling of escape key for conformity (we use GlobalAction.Back). - - return base.OnKeyDown(e); - } - public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) From 9a12f48dcc2ccae6889f35a4add888c4112babd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:55:42 +0900 Subject: [PATCH 317/620] Fix `ComposeBlueprintContainer` handling nudge keys when it can't nudge --- .../Components/ComposeBlueprintContainer.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 5d93c4ea9d..15bbddd97e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -111,25 +111,26 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnKeyDown(KeyDownEvent e) { + // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" + // which has a default of ctrl+shift+arrows. + if (e.ShiftPressed) + return false; + if (e.ControlPressed) { switch (e.Key) { case Key.Left: - nudgeSelection(new Vector2(-1, 0)); - return true; + return nudgeSelection(new Vector2(-1, 0)); case Key.Right: - nudgeSelection(new Vector2(1, 0)); - return true; + return nudgeSelection(new Vector2(1, 0)); case Key.Up: - nudgeSelection(new Vector2(0, -1)); - return true; + return nudgeSelection(new Vector2(0, -1)); case Key.Down: - nudgeSelection(new Vector2(0, 1)); - return true; + return nudgeSelection(new Vector2(0, 1)); } } @@ -151,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). /// /// - private void nudgeSelection(Vector2 delta) + private bool nudgeSelection(Vector2 delta) { if (!nudgeMovementActive) { @@ -162,12 +163,13 @@ namespace osu.Game.Screens.Edit.Compose.Components var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(); if (firstBlueprint == null) - return; + return false; // convert to game space coordinates delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, delta)); + return true; } private void updatePlacementNewCombo() From aeca91cde28d29a82a4d159f7f93f9e2251b4b47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:55:28 +0900 Subject: [PATCH 318/620] Fix main menu osu logo being activated by function keys and escape --- osu.Game/Screens/Menu/ButtonSystem.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 41920605b0..25fa689d4c 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -245,6 +245,15 @@ namespace osu.Game.Screens.Menu if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed) return false; + if (e.Key >= Key.F1 && e.Key <= Key.F35) + return false; + + switch (e.Key) + { + case Key.Escape: + return false; + } + if (triggerInitialOsuLogo()) return true; From b6e7b43b11859046b71c0023e4bfca090cd2f961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:52:18 +0900 Subject: [PATCH 319/620] Remove unnecessary input blocking This was already done by `OverlayContainer`. --- osu.Game/Screens/Play/GameplayMenuOverlay.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 2b961278d5..ffd7845356 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -166,11 +166,6 @@ namespace osu.Game.Screens.Play protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In); - // Don't let mouse down events through the overlay or people can click circles while paused. - protected override bool OnMouseDown(MouseDownEvent e) => true; - - protected override bool OnMouseMove(MouseMoveEvent e) => true; - protected void AddButton(LocalisableString text, Color4 colour, Action? action) { var button = new Button From c8cc36e9af69c551a6149b12ed376fa84f1ac32d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:24:38 +0900 Subject: [PATCH 320/620] Add failing test coverage of random rewind button not working --- .../Navigation/TestSceneScreenNavigation.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 521d097fb9..88b482ab4c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -48,6 +48,7 @@ using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Resources; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -202,6 +203,38 @@ namespace osu.Game.Tests.Visual.Navigation TextBox filterControlTextBox() => songSelect.ChildrenOfType().Single(); } + [Test] + public void TestSongSelectRandomRewindButton() + { + Guid? originalSelection = null; + TestPlaySongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("Add two beatmaps", () => + { + Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8)); + Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8)); + }); + + AddUntilStep("wait for selected", () => + { + originalSelection = Game.Beatmap.Value.BeatmapInfo.ID; + return !Game.Beatmap.IsDefault; + }); + + AddStep("hit random", () => + { + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for selection changed", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.Not.EqualTo(originalSelection)); + + AddStep("hit random rewind", () => InputManager.Click(MouseButton.Right)); + AddUntilStep("wait for selection reverted", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.EqualTo(originalSelection)); + } + [Test] public void TestSongSelectScrollHandling() { From 66be9f2d1b9908001baacf24329bdba585a8ac3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:05:39 +0900 Subject: [PATCH 321/620] Remove right click default for absolute scroll --- osu.Game/Database/RealmAccess.cs | 14 +++++++++++++- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e1b8de89fa..f0f5864e32 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -97,8 +97,9 @@ namespace osu.Game.Database /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. /// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯. + /// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions. /// - private const int schema_version = 46; + private const int schema_version = 47; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1239,6 +1240,17 @@ namespace osu.Game.Database break; } + + case 47: + { + var keyBindings = migration.NewRealm.All(); + + var existingBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.AbsoluteScrollSongList); + if (existingBinding != null && existingBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.MouseRight })) + migration.NewRealm.Remove(existingBinding); + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 6c130ff309..599ca6d6c1 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -205,7 +205,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed), - new KeyBinding(new[] { InputKey.MouseRight }, GlobalAction.AbsoluteScrollSongList), + new KeyBinding(InputKey.None, GlobalAction.AbsoluteScrollSongList), }; private static IEnumerable audioControlKeyBindings => new[] From 6c27e87714ec959d017a2c198b095ea5bfdbb08e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:12:45 +0900 Subject: [PATCH 322/620] Add back explicit right click handling of carousel absolute scrolling --- osu.Game/Screens/Select/BeatmapCarousel.cs | 40 ++++++++++++++++----- osu.Game/Screens/SelectV2/Carousel.cs | 41 ++++++++++++++++------ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 7e3c26a1ba..a807fc6a34 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1181,14 +1181,7 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - // The default binding for absolute scroll is right mouse button. - // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. - if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) - && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) - return false; - - ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); - absoluteScrolling = true; + beginAbsoluteScrolling(e); return true; } @@ -1200,11 +1193,32 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - absoluteScrolling = false; + endAbsoluteScrolling(); break; } } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + // To avoid conflicts with context menus, disallow absolute scroll if it looks like things will fall over. + if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + beginAbsoluteScrolling(e); + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button == MouseButton.Right) + endAbsoluteScrolling(); + base.OnMouseUp(e); + } + protected override bool OnMouseMove(MouseMoveEvent e) { if (absoluteScrolling) @@ -1216,6 +1230,14 @@ namespace osu.Game.Screens.Select return base.OnMouseMove(e); } + private void beginAbsoluteScrolling(UIEvent e) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + } + + private void endAbsoluteScrolling() => absoluteScrolling = false; + #endregion protected override ScrollbarContainer CreateScrollbar(Direction direction) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a07022b32f..ec1bf6b7c0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -493,15 +493,7 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - - // The default binding for absolute scroll is right mouse button. - // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. - if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) - && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) - return false; - - ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); - absoluteScrolling = true; + beginAbsoluteScrolling(e); return true; } @@ -513,11 +505,32 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - absoluteScrolling = false; + endAbsoluteScrolling(); break; } } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + // To avoid conflicts with context menus, disallow absolute scroll if it looks like things will fall over. + if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + beginAbsoluteScrolling(e); + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button == MouseButton.Right) + endAbsoluteScrolling(); + base.OnMouseUp(e); + } + protected override bool OnMouseMove(MouseMoveEvent e) { if (absoluteScrolling) @@ -529,6 +542,14 @@ namespace osu.Game.Screens.SelectV2 return base.OnMouseMove(e); } + private void beginAbsoluteScrolling(UIEvent e) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + } + + private void endAbsoluteScrolling() => absoluteScrolling = false; + #endregion } From 0265a2900050d0c11df6d09a38b970ab4f80b923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 10:02:16 +0100 Subject: [PATCH 323/620] Move bindings to `LoadComplete()` --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 7b6bf6f55e..c784fc298a 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -43,8 +43,14 @@ namespace osu.Game.Screens.Play.HUD private FillFlowContainer spectatorsFlow = null!; private DrawablePool pool = null!; + [Resolved] + private SpectatorClient client { get; set; } = null!; + + [Resolved] + private GameplayState gameplayState { get; set; } = null!; + [BackgroundDependencyLoader] - private void load(OsuColour colours, SpectatorClient client, GameplayState gameplayState) + private void load(OsuColour colours) { AutoSizeAxes = Axes.Y; @@ -73,15 +79,15 @@ namespace osu.Game.Screens.Play.HUD }; HeaderColour.Value = Header.Colour; - - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } protected override void LoadComplete() { base.LoadComplete(); + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); + Spectators.BindCollectionChanged(onSpectatorsChanged, true); UserPlayingState.BindValueChanged(_ => updateVisibility()); From f88102610d5272fccc32b5d0a73782b5d0c2d127 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 18:35:56 +0900 Subject: [PATCH 324/620] Add tooltips explaining multiplayer mod selection buttons --- osu.Game/Localisation/MultiplayerMatchStrings.cs | 15 +++++++++++++++ .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 3 +++ .../Screens/OnlinePlay/FooterButtonFreeStyle.cs | 3 +++ .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 7 +++++-- .../Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/MultiplayerMatchStrings.cs b/osu.Game/Localisation/MultiplayerMatchStrings.cs index 95c7168a09..8c9e76d722 100644 --- a/osu.Game/Localisation/MultiplayerMatchStrings.cs +++ b/osu.Game/Localisation/MultiplayerMatchStrings.cs @@ -24,6 +24,21 @@ namespace osu.Game.Localisation /// public static LocalisableString StartMatchWithCountdown(string humanReadableTime) => new TranslatableString(getKey(@"start_match_width_countdown"), @"Start match in {0}", humanReadableTime); + /// + /// "Choose the mods which all players should play with." + /// + public static LocalisableString RequiredModsButtonTooltip => new TranslatableString(getKey(@"required_mods_button_tooltip"), @"Choose the mods which all players should play with."); + + /// + /// "Each player can choose their preferred mods from a selected list." + /// + public static LocalisableString FreeModsButtonTooltip => new TranslatableString(getKey(@"free_mods_button_tooltip"), @"Each player can choose their preferred mods from a selected list."); + + /// + /// "Each player can choose their preferred difficulty, ruleset and mods." + /// + public static LocalisableString FreestyleButtonTooltip => new TranslatableString(getKey(@"freestyle_button_tooltip"), @"Each player can choose their preferred difficulty, ruleset and mods."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 952b15a873..402f538716 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -95,6 +96,8 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freemods"; + + TooltipText = MultiplayerMatchStrings.FreeModsButtonTooltip; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index cdfb73cee1..0e22b3d3fb 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Select; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -72,6 +73,8 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freestyle"; + + TooltipText = MultiplayerMatchStrings.FreestyleButtonTooltip; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 9df01ead42..f6403c010e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osu.Game.Users; using osu.Game.Utils; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -196,14 +197,16 @@ namespace osu.Game.Screens.OnlinePlay IsValidMod = IsValidMod }; - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() { var baseButtons = base.CreateSongSelectFooterButtons().ToList(); + baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; + freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; - baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] + baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, null), (freeStyleButton, null) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index d1fcf94152..22290f8fed 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() { // Required to create the drawable components. base.CreateSongSelectFooterButtons(); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index dda7b568d2..c20dcb8593 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -415,7 +415,7 @@ namespace osu.Game.Screens.Select /// Creates the buttons to be displayed in the footer. /// /// A set of and an optional which the button opens when pressed. - protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] + protected virtual IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] { (ModsFooterButton = new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom From 6ec718304e4df307d8ae3598de96585ff836a99e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 04:58:27 -0500 Subject: [PATCH 325/620] Revert "Fix triangles judgement mispositioned on a miss" This reverts commit e5713e52392066a1430ebce460d07d8af01ad29f. --- .../UI/DrawableManiaJudgement.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a1dabd66bc..5b87c74bbe 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -36,20 +35,8 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: - this.FadeOutFromOne(800); - break; - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveToY(judgement_y_position); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); + base.PlayAnimation(); break; default: From cc7c549468591c0414ad8425f8e0118b1b91dd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 21 Jan 2025 11:02:28 +0100 Subject: [PATCH 326/620] Add test scene for clipboard snapping --- .../TestSceneEditorClipboardSnapping.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs new file mode 100644 index 0000000000..e32cad12d2 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneEditorClipboardSnapping : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + private const double beat_length = 60_000 / 180.0; // 180 bpm + private const double timing_point_time = 1500; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var controlPointInfo = new ControlPointInfo(); + controlPointInfo.Add(timing_point_time, new TimingControlPoint { BeatLength = beat_length }); + return new TestBeatmap(ruleset, false) + { + ControlPointInfo = controlPointInfo + }; + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(6)] + [TestCase(8)] + [TestCase(12)] + [TestCase(16)] + public void TestPasteSnapping(int divisor) + { + const double paste_time = timing_point_time + 1271; // arbitrary timestamp that doesn't snap to the timing point at any divisor + + var addedObjects = new HitObject[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1200 }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + AddStep("select added objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + AddStep("copy hitobjects", () => Editor.Copy()); + + AddStep($"set beat divisor to 1/{divisor}", () => + { + var beatDivisor = (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor)); + beatDivisor.SetArbitraryDivisor(divisor); + }); + + AddStep("move forward in time", () => EditorClock.Seek(paste_time)); + AddAssert("not at snapped time", () => EditorClock.CurrentTime != EditorBeatmap.SnapTime(EditorClock.CurrentTime, null)); + + AddStep("paste hitobjects", () => Editor.Paste()); + + AddAssert("first object is snapped", () => Precision.AlmostEquals( + EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime).StartTime, + EditorBeatmap.ControlPointInfo.GetClosestSnappedTime(paste_time, divisor) + )); + + AddAssert("duration between pasted objects is same", () => + { + var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime); + var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime); + + return Precision.AlmostEquals(secondObject.StartTime - firstObject.StartTime, addedObjects[1].StartTime - addedObjects[0].StartTime); + }); + } + } +} From b63d94101c1ecc69b68d0e4002b208b5492ab4cf Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 05:45:37 -0500 Subject: [PATCH 327/620] Reapply "Fix triangles judgement mispositioned on a miss" This reverts commit 6ec718304e4df307d8ae3598de96585ff836a99e. --- .../UI/DrawableManiaJudgement.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 5b87c74bbe..a1dabd66bc 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -35,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: + this.FadeOutFromOne(800); + break; + case HitResult.Miss: - base.PlayAnimation(); + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); break; default: From 459847cb80b3e34ca4d4bf35dabd7d1d081b94d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 19:51:13 +0900 Subject: [PATCH 328/620] Perform client side validation that the selected beatmap and ruleset have valid online IDs This is local to playlists, since in multiplayer the validation is already provided by `osu-server-spectator`. --- osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 1 + .../OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs | 7 +++++++ osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 3 +++ osu.Game/Screens/Select/FilterCriteria.cs | 2 ++ 4 files changed, 13 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 22290f8fed..4d34000d3c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -89,6 +89,7 @@ namespace osu.Game.Screens.OnlinePlay // Must be from the same set as the playlist item. criteria.BeatmapSetId = beatmapSetId; + criteria.HasOnlineID = true; // Must be within 30s of the playlist item. criteria.Length.Min = itemLength - 30000; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs index f3d868b0de..912496ba34 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs @@ -21,6 +21,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override bool OnStart() { + // Beatmaps without a valid online ID are filtered away; this is just a final safety. + if (base.Beatmap.Value.BeatmapInfo.OnlineID < 0) + return false; + + if (base.Ruleset.Value.OnlineID < 0) + return false; + Beatmap.Value = base.Beatmap.Value.BeatmapInfo; Ruleset.Value = base.Ruleset.Value; this.Exit(); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 95186e98d8..dc77b0101e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -90,6 +90,9 @@ namespace osu.Game.Screens.Select.Carousel if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria); + if (match && criteria.HasOnlineID == true) + match &= BeatmapInfo.OnlineID >= 0; + if (match && criteria.BeatmapSetId != null) match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 63dbdfbed3..15cb3c5104 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -58,6 +58,8 @@ namespace osu.Game.Screens.Select public bool AllowConvertedBeatmaps; public int? BeatmapSetId; + public bool? HasOnlineID; + private string searchText = string.Empty; /// From 2a5a2738e152e4d23835e0c618873792ed57f148 Mon Sep 17 00:00:00 2001 From: Layendan Date: Tue, 21 Jan 2025 12:45:23 -0700 Subject: [PATCH 329/620] Add context menu to open in browser to rooms --- .../Lounge/Components/DrawableRoom.cs | 35 ++++++++++++++++++- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 16 +++++++++ .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 12 +++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index c39ca347c7..321a1131de 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -12,15 +12,19 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -31,11 +35,17 @@ using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public abstract partial class DrawableRoom : CompositeDrawable + public abstract partial class DrawableRoom : CompositeDrawable, IHasContextMenu { protected const float CORNER_RADIUS = 10; private const float height = 100; + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } = null!; + public readonly Room Room; protected readonly Bindable SelectedItem = new Bindable(); @@ -330,6 +340,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + public MenuItem[] ContextMenuItems + { + get + { + var items = new List(); + + if (Room.RoomID.HasValue) + { + items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + })]); + } + + return items.ToArray(); + } + } + + private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); protected virtual IEnumerable CreateBottomDetails() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 0a55472c2d..2c15e5107a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -59,6 +59,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } = null!; + private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; private Sample? sampleJoin; @@ -167,6 +170,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }) }; + if (Room.RoomID.HasValue) + { + items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + })]); + } + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => @@ -234,6 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Room.PropertyChanged -= onRoomPropertyChanged; } + private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + public partial class PasswordEntryPopover : OsuPopover { private readonly Room room; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..3ba056b18d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -18,6 +18,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -156,10 +157,15 @@ namespace osu.Game.Screens.OnlinePlay.Match { new Drawable[] { - new DrawableMatchRoom(Room, allowEdit) + new OsuContextMenuContainer { - OnEdit = () => settingsOverlay.Show(), - SelectedItem = SelectedItem + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new DrawableMatchRoom(Room, allowEdit) + { + OnEdit = () => settingsOverlay.Show(), + SelectedItem = SelectedItem + } } }, null, From fde2b22bbcd86ed44c83a3c018abd15b57bfddf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:29:50 +0900 Subject: [PATCH 330/620] Add transient flag for notifications which shouldn't linger in history --- .../TestSceneNotificationOverlay.cs | 34 +++++++++++++++++++ osu.Game/Online/FriendPresenceNotifier.cs | 2 ++ .../Overlays/NotificationOverlayToastTray.cs | 11 ++++-- .../Overlays/Notifications/Notification.cs | 8 ++++- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index c584c7dba0..caee5e634e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -83,6 +83,40 @@ namespace osu.Game.Tests.Visual.UserInterface waitForCompletion(); } + [Test] + public void TestNormalDoesForwardToOverlay() + { + SimpleNotification notification = null!; + + AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification + { + Text = @"This shouldn't annoy you too much", + Transient = false, + })); + + AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True); + AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False); + + checkDisplayedCount(1); + } + + [Test] + public void TestTransientDoesNotForwardToOverlay() + { + SimpleNotification notification = null!; + + AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification + { + Text = @"This shouldn't annoy you too much", + Transient = true, + })); + + AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True); + AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False); + + checkDisplayedCount(0); + } + [Test] public void TestForwardWithFlingRight() { diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index dd141b756b..e39e3cf94d 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -169,6 +169,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { + Transient = true, Icon = FontAwesome.Solid.UserPlus, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Green, @@ -204,6 +205,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { + Transient = true, Icon = FontAwesome.Solid.UserMinus, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Red diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index df07b4f138..ddb2e02fb8 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public Action? ForwardNotificationToPermanentStore { get; set; } + public required Action ForwardNotificationToPermanentStore { get; init; } public int UnreadCount => Notifications.Count(n => !n.WasClosed && !n.Read); @@ -142,8 +142,15 @@ namespace osu.Game.Overlays notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => { + if (notification.Transient) + { + notification.IsInToastTray = false; + notification.Close(false); + return; + } + RemoveInternal(notification, false); - ForwardNotificationToPermanentStore?.Invoke(notification); + ForwardNotificationToPermanentStore(notification); notification.FadeIn(300, Easing.OutQuint); }); diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index d48524d8b0..e41aa8b625 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -34,10 +34,16 @@ namespace osu.Game.Overlays.Notifications public abstract LocalisableString Text { get; set; } /// - /// Whether this notification should forcefully display itself. + /// Important notifications display for longer, and announce themselves at an OS level (ie flashing the taskbar). + /// This defaults to true. /// public virtual bool IsImportant => true; + /// + /// Transient notifications only show as a toast, and do not linger in notification history. + /// + public bool Transient { get; init; } + /// /// Run on user activating the notification. Return true to close. /// From 4cf4b8c73de124d99995a1a1ea4d1dab5f0e3e28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:36:12 +0900 Subject: [PATCH 331/620] Switch `IsImportant` to `init` property isntead of `virtual` --- osu.Desktop/Security/ElevatedPrivilegesChecker.cs | 2 -- .../UserInterface/TestSceneNotificationOverlay.cs | 10 ++++++++-- osu.Game/Database/ModelDownloader.cs | 7 ++++--- osu.Game/Overlays/Notifications/Notification.cs | 2 +- .../Overlays/Notifications/ProgressNotification.cs | 4 ++-- osu.Game/Screens/Play/PlayerLoader.cs | 4 ---- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs index 0bed9830df..4b6ebc9b56 100644 --- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs +++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs @@ -30,8 +30,6 @@ namespace osu.Desktop.Security private partial class ElevatedPrivilegesNotification : SimpleNotification { - public override bool IsImportant => true; - public ElevatedPrivilegesNotification() { Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user."; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index caee5e634e..65c8b913d3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -668,12 +668,18 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class BackgroundNotification : SimpleNotification { - public override bool IsImportant => false; + public BackgroundNotification() + { + IsImportant = false; + } } private partial class BackgroundProgressNotification : ProgressNotification { - public override bool IsImportant => false; + public BackgroundProgressNotification() + { + IsImportant = false; + } } } } diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs index dfeec259fe..8e89db4d06 100644 --- a/osu.Game/Database/ModelDownloader.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -131,8 +131,6 @@ namespace osu.Game.Database private partial class DownloadNotification : ProgressNotification { - public override bool IsImportant => false; - protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification { Activated = CompletionClickAction, @@ -141,7 +139,10 @@ namespace osu.Game.Database private partial class SilencedProgressCompletionNotification : ProgressCompletionNotification { - public override bool IsImportant => false; + public SilencedProgressCompletionNotification() + { + IsImportant = false; + } } } } diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index e41aa8b625..ccfd1adb39 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Notifications /// Important notifications display for longer, and announce themselves at an OS level (ie flashing the taskbar). /// This defaults to true. /// - public virtual bool IsImportant => true; + public bool IsImportant { get; init; } = true; /// /// Transient notifications only show as a toast, and do not linger in notification history. diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 2362cb11f6..0b42188252 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -191,8 +191,6 @@ namespace osu.Game.Overlays.Notifications public override bool DisplayOnTop => false; - public override bool IsImportant => false; - private readonly ProgressBar progressBar; private Color4 colourQueued; private Color4 colourActive; @@ -206,6 +204,8 @@ namespace osu.Game.Overlays.Notifications public ProgressNotification() { + IsImportant = false; + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 06086c1004..fc956e15fd 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -663,8 +663,6 @@ namespace osu.Game.Screens.Play private partial class MutedNotification : SimpleNotification { - public override bool IsImportant => true; - public MutedNotification() { Text = NotificationsStrings.GameVolumeTooLow; @@ -716,8 +714,6 @@ namespace osu.Game.Screens.Play private partial class BatteryWarningNotification : SimpleNotification { - public override bool IsImportant => true; - public BatteryWarningNotification() { Text = NotificationsStrings.BatteryLow; From 9e023340b011ae376d8e90823e0c730e33d2920c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:36:48 +0900 Subject: [PATCH 332/620] Mark friend notifications as non-important --- osu.Game/Online/FriendPresenceNotifier.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index e39e3cf94d..75b487384a 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -170,6 +170,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { Transient = true, + IsImportant = false, Icon = FontAwesome.Solid.UserPlus, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Green, @@ -206,6 +207,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { Transient = true, + IsImportant = false, Icon = FontAwesome.Solid.UserMinus, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Red From 910c0022e3638e204ba3a0fc201139fb0a55fd73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 17:03:01 +0900 Subject: [PATCH 333/620] Adjust code style slightly --- .../LocalCachedBeatmapMetadataSource.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 7495805cff..113b16b0db 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -114,14 +114,25 @@ namespace osu.Game.Beatmaps } } } - catch (SqliteException sqliteException) when (sqliteException.SqliteErrorCode == 11 || sqliteException.SqliteErrorCode == 26) // SQLITE_CORRUPT, SQLITE_NOTADB + catch (SqliteException sqliteException) { - // only attempt purge & refetch if there is no other refetch in progress - if (cacheDownloadRequest == null) + // There have been cases where the user's local database is corrupt. + // Let's attempt to identify these cases and re-initialise the local cache. + switch (sqliteException.SqliteErrorCode) { - tryPurgeCache(); - prepareLocalCache(); + case 26: // SQLITE_NOTADB + case 11: // SQLITE_CORRUPT + // only attempt purge & re-download if there is no other refetch in progress + if (cacheDownloadRequest != null) + throw; + + tryPurgeCache(); + prepareLocalCache(); + onlineMetadata = null; + return false; } + + throw; } catch (Exception ex) { From 26ef23c9a9c84e582796b6bbc35d38e1493d42da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 17:04:24 +0900 Subject: [PATCH 334/620] Remove outdated ef related catch-when usage --- osu.Game/Database/RealmAccess.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e1b8de89fa..28033883d1 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -11,7 +11,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Extensions; @@ -413,18 +412,7 @@ namespace osu.Game.Database /// Compact this realm. /// /// - public bool Compact() - { - try - { - return Realm.Compact(getConfiguration()); - } - // Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator). - catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException) - { - return true; - } - } + public bool Compact() => Realm.Compact(getConfiguration()); /// /// Run work on realm with a return value. @@ -720,11 +708,6 @@ namespace osu.Game.Database return Realm.GetInstance(getConfiguration()); } - // Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator). - catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException) - { - return Realm.GetInstance(); - } finally { if (tookSemaphoreLock) From 6ceb348cf6109c4b5acc653ff227b35dbaa198ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 18:24:01 +0900 Subject: [PATCH 335/620] Adjust code again to avoid weird `throw` mishandling --- .../Beatmaps/LocalCachedBeatmapMetadataSource.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 113b16b0db..a1744f74b3 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -113,9 +113,14 @@ namespace osu.Game.Beatmaps return queryCacheVersion2(db, beatmapInfo, out onlineMetadata); } } + + onlineMetadata = null; + return false; } catch (SqliteException sqliteException) { + onlineMetadata = null; + // There have been cases where the user's local database is corrupt. // Let's attempt to identify these cases and re-initialise the local cache. switch (sqliteException.SqliteErrorCode) @@ -124,15 +129,15 @@ namespace osu.Game.Beatmaps case 11: // SQLITE_CORRUPT // only attempt purge & re-download if there is no other refetch in progress if (cacheDownloadRequest != null) - throw; + return false; tryPurgeCache(); prepareLocalCache(); - onlineMetadata = null; return false; } - throw; + logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with unhandled sqlite error {sqliteException}."); + return false; } catch (Exception ex) { @@ -140,9 +145,6 @@ namespace osu.Game.Beatmaps onlineMetadata = null; return false; } - - onlineMetadata = null; - return false; } private void tryPurgeCache() From c94b8bf051871e7e6495a20eacabbb0f26622bc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 18:36:13 +0900 Subject: [PATCH 336/620] Apply NRT to new class --- .../Visual/Editing/TestSceneEditorClipboardSnapping.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs index e32cad12d2..edaba67591 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Utils; @@ -68,14 +66,14 @@ namespace osu.Game.Tests.Visual.Editing AddStep("paste hitobjects", () => Editor.Paste()); AddAssert("first object is snapped", () => Precision.AlmostEquals( - EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime).StartTime, + EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!.StartTime, EditorBeatmap.ControlPointInfo.GetClosestSnappedTime(paste_time, divisor) )); AddAssert("duration between pasted objects is same", () => { - var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime); - var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime); + var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!; + var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime)!; return Precision.AlmostEquals(secondObject.StartTime - firstObject.StartTime, addedObjects[1].StartTime - addedObjects[0].StartTime); }); From 3da220b8f68829b691e4230a957c3ed2fcd77595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 Jan 2025 11:39:32 +0100 Subject: [PATCH 337/620] Fix crash from new combo colour selector when there are no combo colours present Closes https://github.com/ppy/osu/issues/31615. --- .../Edit/Components/TernaryButtons/NewComboTernaryButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs index 1f95d5f239..c6ecee5f45 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { Enabled.Value = SelectedHitObject.Value != null; - if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0) + if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0 || ComboColours.Count <= 1) { BackgroundColour = colourProvider.Background3; icon.Colour = BackgroundColour.Darken(0.5f); From f673d16a1f97d153f74cfbd6e8549886552910cb Mon Sep 17 00:00:00 2001 From: Layendan Date: Wed, 22 Jan 2025 11:42:11 -0700 Subject: [PATCH 338/620] Fix formatting --- .../Lounge/Components/DrawableRoom.cs | 19 ++++++++++------- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 21 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 321a1131de..7fefa0a1a8 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private IAPIProvider api { get; set; } = null!; [Resolved] - private OsuGame? game { get; set; } = null!; + private OsuGame? game { get; set; } public readonly Room Room; @@ -348,13 +348,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (Room.RoomID.HasValue) { - items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - })]); + items.AddRange([ + new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + }) + ]); } return items.ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 2c15e5107a..da04152bd3 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IAPIProvider api { get; set; } = null!; [Resolved] - private OsuGame? game { get; set; } = null!; + private OsuGame? game { get; set; } private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public Popover GetPopover() => new PasswordEntryPopover(Room); - public MenuItem[] ContextMenuItems + public new MenuItem[] ContextMenuItems { get { @@ -172,13 +172,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (Room.RoomID.HasValue) { - items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - })]); + items.AddRange([ + new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + }) + ]); } if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) From 865757621082cb3e2cba36a7f6a5dbd5d71d74a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 18:25:31 +0900 Subject: [PATCH 339/620] Show selection defaults in test scene (and make prettier) --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index b13d450c32..984352b2f5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -16,6 +16,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -117,12 +118,11 @@ namespace osu.Game.Tests.Visual.SongSelect } } }, - stats = new OsuTextFlowContainer(cp => cp.Font = FrameworkFont.Regular.With()) + stats = new OsuTextFlowContainer { + AutoSizeAxes = Axes.Both, Padding = new MarginPadding(10), TextAnchor = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, }, }; }); @@ -258,16 +258,29 @@ namespace osu.Game.Tests.Visual.SongSelect if (carousel.IsNull()) return; - stats.Text = $""" - store - sets: {beatmapSets.Count} - beatmaps: {beatmapCount} - carousel: - sorting: {carousel.IsFiltering} - tracked: {carousel.ItemsTracked} - displayable: {carousel.DisplayableItems} - displayed: {carousel.VisibleItems} - """; + stats.Clear(); + createHeader("beatmap store"); + stats.AddParagraph($""" + sets: {beatmapSets.Count} + beatmaps: {beatmapCount} + """); + createHeader("carousel"); + stats.AddParagraph($""" + sorting: {carousel.IsFiltering} + tracked: {carousel.ItemsTracked} + displayable: {carousel.DisplayableItems} + displayed: {carousel.VisibleItems} + selected: {carousel.CurrentSelection} + """); + + void createHeader(string text) + { + stats.AddParagraph(string.Empty); + stats.AddParagraph(text, cp => + { + cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); + }); + } } } } From 6ac2dbc818ff5d5de1249280095e0804284ce327 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 18:49:12 +0900 Subject: [PATCH 340/620] Reorder carousel methods into logical regions --- osu.Game/Screens/SelectV2/Carousel.cs | 68 +++++++++++++++++---------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index ec1bf6b7c0..190792b19e 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -30,10 +30,7 @@ namespace osu.Game.Screens.SelectV2 /// public abstract partial class Carousel : CompositeDrawable { - /// - /// A collection of filters which should be run each time a is executed. - /// - protected IEnumerable Filters { get; init; } = Enumerable.Empty(); + #region Properties and methods for external usage /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. @@ -82,15 +79,6 @@ namespace osu.Game.Screens.SelectV2 /// public int VisibleItems => scroll.Panels.Count; - /// - /// All items which are to be considered for display in this carousel. - /// Mutating this list will automatically queue a . - /// - /// - /// Note that an may add new items which are displayed but not tracked in this list. - /// - protected readonly BindableList Items = new BindableList(); - /// /// The currently selected model. /// @@ -114,20 +102,31 @@ namespace osu.Game.Screens.SelectV2 } } - private List? displayedCarouselItems; + #endregion - private readonly CarouselScrollContainer scroll; + #region Properties and methods concerning implementations - protected Carousel() - { - InternalChild = scroll = new CarouselScrollContainer - { - RelativeSizeAxes = Axes.Both, - Masking = false, - }; + /// + /// A collection of filters which should be run each time a is executed. + /// + /// + /// Implementations should add all required filters as part of their initialisation. + /// + /// Importantly, each filter is sequentially run in the order provided. + /// Each filter receives the output of the previous filter. + /// + /// A filter may add, mutate or remove items. + /// + protected IEnumerable Filters { get; init; } = Enumerable.Empty(); - Items.BindCollectionChanged((_, _) => FilterAsync()); - } + /// + /// All items which are to be considered for display in this carousel. + /// Mutating this list will automatically queue a . + /// + /// + /// Note that an may add new items which are displayed but not tracked in this list. + /// + protected readonly BindableList Items = new BindableList(); /// /// Queue an asynchronous filter operation. @@ -151,8 +150,29 @@ namespace osu.Game.Screens.SelectV2 /// A representing the model. protected abstract CarouselItem CreateCarouselItemForModel(T model); + #endregion + + #region Initialisation + + private readonly CarouselScrollContainer scroll; + + protected Carousel() + { + InternalChild = scroll = new CarouselScrollContainer + { + RelativeSizeAxes = Axes.Both, + Masking = false, + }; + + Items.BindCollectionChanged((_, _) => FilterAsync()); + } + + #endregion + #region Filtering and display preparation + private List? displayedCarouselItems; + private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); From d5268356277030b4ef36b6fe2623d58193da256c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 04:03:43 +0900 Subject: [PATCH 341/620] Only show loading when doing a user triggered filter --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 93d4c90be0..d9c049bbae 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -14,7 +13,6 @@ using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; using osu.Game.Screens.Select; namespace osu.Game.Screens.SelectV2 @@ -93,14 +91,8 @@ namespace osu.Game.Screens.SelectV2 public void Filter(FilterCriteria criteria) { Criteria = criteria; - FilterAsync().FireAndForget(); - } - - protected override async Task FilterAsync() - { loading.Show(); - await base.FilterAsync().ConfigureAwait(true); - loading.Hide(); + FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide())); } } } From ded1d9f01994e5e54e52f4ee02fd9f02ecad4847 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 15:58:35 +0900 Subject: [PATCH 342/620] `displayedCarouselItems` -> `carouselItems` --- osu.Game/Screens/SelectV2/Carousel.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 190792b19e..c042da167e 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The number of carousel items currently in rotation for display. /// - public int DisplayableItems => displayedCarouselItems?.Count ?? 0; + public int DisplayableItems => carouselItems?.Count ?? 0; /// /// The number of items currently actualised into drawables. @@ -171,7 +171,7 @@ namespace osu.Game.Screens.SelectV2 #region Filtering and display preparation - private List? displayedCarouselItems; + private List? carouselItems; private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 return; log("Items ready for display"); - displayedCarouselItems = items.ToList(); + carouselItems = items.ToList(); displayedRange = null; updateSelection(); @@ -253,9 +253,9 @@ namespace osu.Game.Screens.SelectV2 { currentSelectionCarouselItem = null; - if (displayedCarouselItems == null) return; + if (carouselItems == null) return; - foreach (var item in displayedCarouselItems) + foreach (var item in carouselItems) { bool isSelected = item.Model == currentSelection; @@ -306,7 +306,7 @@ namespace osu.Game.Screens.SelectV2 { base.Update(); - if (displayedCarouselItems == null) + if (carouselItems == null) return; var range = getDisplayRange(); @@ -356,15 +356,15 @@ namespace osu.Game.Screens.SelectV2 private DisplayRange getDisplayRange() { - Debug.Assert(displayedCarouselItems != null); + Debug.Assert(carouselItems != null); // Find index range of all items that should be on-screen carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload; - int firstIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + int firstIndex = carouselItems.BinarySearch(carouselBoundsItem); if (firstIndex < 0) firstIndex = ~firstIndex; carouselBoundsItem.CarouselYPosition = visibleBottomBound + DistanceOffscreenToPreload; - int lastIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + int lastIndex = carouselItems.BinarySearch(carouselBoundsItem); if (lastIndex < 0) lastIndex = ~lastIndex; firstIndex = Math.Max(0, firstIndex - 1); @@ -375,11 +375,11 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplayedRange(DisplayRange range) { - Debug.Assert(displayedCarouselItems != null); + Debug.Assert(carouselItems != null); List toDisplay = range.Last - range.First == 0 ? new List() - : displayedCarouselItems.GetRange(range.First, range.Last - range.First + 1); + : carouselItems.GetRange(range.First, range.Last - range.First + 1); // Iterate over all panels which are already displayed and figure which need to be displayed / removed. foreach (var panel in scroll.Panels) @@ -415,9 +415,9 @@ namespace osu.Game.Screens.SelectV2 // Update the total height of all items (to make the scroll container scrollable through the full height even though // most items are not displayed / loaded). - if (displayedCarouselItems.Count > 0) + if (carouselItems.Count > 0) { - var lastItem = displayedCarouselItems[^1]; + var lastItem = carouselItems[^1]; scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); } else From b4e8a17f0386523e1fb15faf7e13ffd8aa0011c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Jan 2025 09:57:23 +0100 Subject: [PATCH 343/620] Roll back windows build image to 2019 on android build job Per workaround suggested in https://github.com/actions/runner-images/issues/11402#issuecomment-2596473501. Applying this now as my hopes for a swift resolution without changes on our side are slim to none (read thread linked above in full to learn why). --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8645d728e..a88f1320cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: build-only-android: name: Build only (Android) - runs-on: windows-latest + runs-on: windows-2019 timeout-minutes: 60 steps: - name: Checkout From c67c0a7fc02d29a093821d00b95a249e73f01a4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:07:18 +0900 Subject: [PATCH 344/620] Move `Selected` status to drawables Basically, I don't want bindables in `CarouselItem`. It means there needs to be a bind-unbind process on pooling. By moving these to the drawable and just updating every frame, we can simplify things a lot. --- osu.Game/Screens/SelectV2/CarouselItem.cs | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 4636e8a32f..2cb96a3d7f 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; namespace osu.Game.Screens.SelectV2 { @@ -10,9 +9,9 @@ namespace osu.Game.Screens.SelectV2 /// Represents a single display item for display in a . /// This is used to house information related to the attached model that helps with display and tracking. /// - public abstract class CarouselItem : IComparable + public sealed class CarouselItem : IComparable { - public readonly BindableBool Selected = new BindableBool(); + public const float DEFAULT_HEIGHT = 40; /// /// The model this item is representing. @@ -20,16 +19,27 @@ namespace osu.Game.Screens.SelectV2 public readonly object Model; /// - /// The current Y position in the carousel. This is managed by and should not be set manually. + /// The current Y position in the carousel. + /// This is managed by and should not be set manually. /// public double CarouselYPosition { get; set; } /// - /// The height this item will take when displayed. + /// The height this item will take when displayed. Defaults to . /// - public abstract float DrawHeight { get; } + public float DrawHeight { get; set; } = DEFAULT_HEIGHT; - protected CarouselItem(object model) + /// + /// Whether this item should be a valid target for user group selection hotkeys. + /// + public bool IsGroupSelectionTarget { get; set; } + + /// + /// Whether this item is visible or collapsed (hidden). + /// + public bool IsVisible { get; set; } = true; + + public CarouselItem(object model) { Model = model; } From 980f6cf18e0d2177b9b8a63c5afcf803eea48e1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:09:18 +0900 Subject: [PATCH 345/620] Make `CarouselItem` `sealed` and remove `BeatmapCarouselItem` concept Less abstraction is better. As far as I can tell, we don't need a custom model for this. If there's any tracking to be done, it should be done within `BeatmapCarousel`'s implementation (or a filter). --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 10 +- .../SelectV2/BeatmapCarouselFilterSorting.cs | 43 ++++----- .../Screens/SelectV2/BeatmapCarouselItem.cs | 48 ---------- .../Screens/SelectV2/BeatmapCarouselPanel.cs | 95 ++++++++++++------- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 13 +++ 5 files changed, 99 insertions(+), 110 deletions(-) delete mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 984352b2f5..dee61bbcde 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -181,15 +181,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); - AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Item!.Selected.Value))); + AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Selected.Value))); AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -212,11 +212,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index df41aa3e86..dd82bf3495 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -28,37 +28,32 @@ namespace osu.Game.Screens.SelectV2 return items.OrderDescending(Comparer.Create((a, b) => { - int comparison = 0; + int comparison; - if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) + var ab = (BeatmapInfo)a.Model; + var bb = (BeatmapInfo)b.Model; + + switch (criteria.Sort) { - switch (criteria.Sort) - { - case SortMode.Artist: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); - if (comparison == 0) - goto case SortMode.Title; - break; + case SortMode.Artist: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + if (comparison == 0) + goto case SortMode.Title; + break; - case SortMode.Difficulty: - comparison = ab.StarRating.CompareTo(bb.StarRating); - break; + case SortMode.Difficulty: + comparison = ab.StarRating.CompareTo(bb.StarRating); + break; - case SortMode.Title: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); - break; + case SortMode.Title: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + break; - default: - throw new ArgumentOutOfRangeException(); - } + default: + throw new ArgumentOutOfRangeException(); } - if (comparison != 0) return comparison; - - if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) - return aItem.ID.CompareTo(bItem.ID); - - return 0; + return comparison; })); }, cancellationToken).ConfigureAwait(false); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs deleted file mode 100644 index dd7aae3db9..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs +++ /dev/null @@ -1,48 +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.Game.Beatmaps; -using osu.Game.Database; - -namespace osu.Game.Screens.SelectV2 -{ - public class BeatmapCarouselItem : CarouselItem - { - public readonly Guid ID; - - /// - /// Whether this item has a header providing extra information for it. - /// When displaying items which don't have header, we should make sure enough information is included inline. - /// - public bool HasGroupHeader { get; set; } - - /// - /// Whether this item is a group header. - /// Group headers are generally larger in display. Setting this will account for the size difference. - /// - public bool IsGroupHeader { get; set; } - - public override float DrawHeight => IsGroupHeader ? 80 : 40; - - public BeatmapCarouselItem(object model) - : base(model) - { - ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); - } - - public override string? ToString() - { - switch (Model) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return Model.ToString(); - } - } -} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 27023b50be..da3e1b0964 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -21,27 +21,41 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapCarousel carousel { get; set; } = null!; - public CarouselItem? Item - { - get => item; - set - { - item = value; - - selected.UnbindBindings(); - - if (item != null) - selected.BindTo(item.Selected); - } - } - - private readonly BindableBool selected = new BindableBool(); - private CarouselItem? item; + private Box activationFlash = null!; + private Box background = null!; + private OsuSpriteText text = null!; [BackgroundDependencyLoader] private void load() { - selected.BindValueChanged(value => + InternalChildren = new Drawable[] + { + background = new Box + { + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) { @@ -59,6 +73,8 @@ namespace osu.Game.Screens.SelectV2 { base.FreeAfterUse(); Item = null; + Selected.Value = false; + KeyboardSelected.Value = false; } protected override void PrepareForUse() @@ -72,31 +88,44 @@ namespace osu.Game.Screens.SelectV2 Size = new Vector2(500, Item.DrawHeight); Masking = true; - InternalChildren = new Drawable[] - { - new Box - { - Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = Item.ToString() ?? string.Empty, - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; + background.Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5); + text.Text = getTextFor(Item.Model); this.FadeInFromZero(500, Easing.OutQuint); } + private string getTextFor(object item) + { + switch (item) + { + case BeatmapInfo bi: + return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; + + case BeatmapSetInfo si: + return $"{si.Metadata}"; + } + + return "unknown"; + } + protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + if (carousel.CurrentSelection == Item!.Model) + carousel.ActivateSelection(); + else + carousel.CurrentSelection = Item!.Model; return true; } + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + public double DrawYPosition { get; set; } + + public void FlashFromActivation() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } } } diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 117feab621..c592734d8d 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; namespace osu.Game.Screens.SelectV2 @@ -10,6 +11,18 @@ namespace osu.Game.Screens.SelectV2 /// public interface ICarouselPanel { + /// + /// Whether this item has selection. + /// This is managed by and should not be set manually. + /// + BindableBool Selected { get; } + + /// + /// Whether this item has keyboard selection. + /// This is managed by and should not be set manually. + /// + BindableBool KeyboardSelected { get; } + /// /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. /// From ecef5e5d715b5f783ad630040131eb9791fd16fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:10:42 +0900 Subject: [PATCH 346/620] Add set-difficulty tracking in `BeatmapCarouselFilterGrouping` Rather than tracking inside individual items, let's just maintain a single dictionary which is refreshed every time we regenerate filters. --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 6cdd15d301..4f0767048a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -13,6 +13,13 @@ namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + /// + /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. + /// + public IDictionary> SetItems => setItems; + + private readonly Dictionary> setItems = new Dictionary>(); + private readonly Func getCriteria; public BeatmapCarouselFilterGrouping(Func getCriteria) @@ -27,7 +34,10 @@ namespace osu.Game.Screens.SelectV2 if (criteria.SplitOutDifficulties) { foreach (var item in items) - ((BeatmapCarouselItem)item).HasGroupHeader = false; + { + item.IsVisible = true; + item.IsGroupSelectionTarget = true; + } return items; } @@ -44,14 +54,25 @@ namespace osu.Game.Screens.SelectV2 { // Add set header if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b.BeatmapSet!) { IsGroupHeader = true }); + { + newItems.Add(new CarouselItem(b.BeatmapSet!) + { + DrawHeight = 80, + IsGroupSelectionTarget = true + }); + } + + if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) + setItems[b.BeatmapSet!] = related = new HashSet(); + + related.Add(item); } newItems.Add(item); lastItem = item; - var beatmapCarouselItem = (BeatmapCarouselItem)item; - beatmapCarouselItem.HasGroupHeader = true; + item.IsGroupSelectionTarget = false; + item.IsVisible = false; } return newItems; From 2f94456a06dbdc50fcc4d87b4823e1baac27179b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:11:02 +0900 Subject: [PATCH 347/620] Add selection and activation flow --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 49 ++- osu.Game/Screens/SelectV2/Carousel.cs | 347 +++++++++++++++---- 2 files changed, 329 insertions(+), 67 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d9c049bbae..e3bc487154 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 private readonly LoadingLayer loading; + private readonly BeatmapCarouselFilterGrouping grouping; + public BeatmapCarousel() { DebounceDelay = 100; @@ -34,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { new BeatmapCarouselFilterSorting(() => Criteria), - new BeatmapCarouselFilterGrouping(() => Criteria), + grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; AddInternal(carouselPanelPool); @@ -51,7 +53,50 @@ namespace osu.Game.Screens.SelectV2 protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); - protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); + protected override void HandleItemDeselected(object? model) + { + base.HandleItemDeselected(model); + + var deselectedSet = model as BeatmapSetInfo ?? (model as BeatmapInfo)?.BeatmapSet; + + if (grouping.SetItems.TryGetValue(deselectedSet!, out var group)) + { + foreach (var i in group) + i.IsVisible = false; + } + } + + protected override void HandleItemSelected(object? model) + { + base.HandleItemSelected(model); + + // Selecting a set isn't valid – let's re-select the first difficulty. + if (model is BeatmapSetInfo setInfo) + { + CurrentSelection = setInfo.Beatmaps.First(); + return; + } + + var currentSelectionSet = (model as BeatmapInfo)?.BeatmapSet; + + if (currentSelectionSet == null) + return; + + if (grouping.SetItems.TryGetValue(currentSelectionSet, out var group)) + { + foreach (var i in group) + i.IsVisible = true; + } + } + + protected override void HandleItemActivated(CarouselItem item) + { + base.HandleItemActivated(item); + + // TODO: maybe this should be handled by the panel itself? + if (GetMaterialisedDrawableForItem(item) is BeatmapCarouselPanel drawable) + drawable.FlashFromActivation(); + } private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index c042da167e..598a898686 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -28,7 +28,8 @@ namespace osu.Game.Screens.SelectV2 /// A highly efficient vertical list display that is used primarily for the song select screen, /// but flexible enough to be used for other use cases. /// - public abstract partial class Carousel : CompositeDrawable + public abstract partial class Carousel : CompositeDrawable, IKeyBindingHandler + where T : notnull { #region Properties and methods for external usage @@ -80,26 +81,34 @@ namespace osu.Game.Screens.SelectV2 public int VisibleItems => scroll.Panels.Count; /// - /// The currently selected model. + /// The currently selected model. Generally of type T. /// /// - /// Setting this will ensure is set to true only on the matching . - /// Of note, if no matching item exists all items will be deselected while waiting for potential new item which matches. + /// A carousel may create panels for non-T types. + /// To keep things simple, we therefore avoid generic constraints on the current selection. + /// + /// The selection is never reset due to not existing. It can be set to anything. + /// If no matching carousel item exists, there will be no visually selected item while waiting for potential new item which matches. /// - public virtual object? CurrentSelection + public object? CurrentSelection { - get => currentSelection; - set + get => currentSelection.Model; + set => setSelection(value); + } + + /// + /// Activate the current selection, if a selection exists. + /// + public void ActivateSelection() + { + if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - if (currentSelectionCarouselItem != null) - currentSelectionCarouselItem.Selected.Value = false; - - currentSelection = value; - - currentSelectionCarouselItem = null; - currentSelectionYPosition = null; - updateSelection(); + CurrentSelection = currentKeyboardSelection.Model; + return; } + + if (currentSelection.CarouselItem != null) + HandleItemActivated(currentSelection.CarouselItem); } #endregion @@ -144,11 +153,42 @@ namespace osu.Game.Screens.SelectV2 protected abstract Drawable GetDrawableForDisplay(CarouselItem item); /// - /// Create an internal carousel representation for the provided model object. + /// Given a , find a drawable representation if it is currently displayed in the carousel. /// - /// The model. - /// A representing the model. - protected abstract CarouselItem CreateCarouselItemForModel(T model); + /// + /// This will only return a drawable if it is "on-screen". + /// + /// The item to find a related drawable representation. + /// The drawable representation if it exists. + protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => + scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + + /// + /// Called when an item is "selected". + /// + protected virtual void HandleItemSelected(object? model) + { + } + + /// + /// Called when an item is "deselected". + /// + protected virtual void HandleItemDeselected(object? model) + { + } + + /// + /// Called when an item is "activated". + /// + /// + /// An activated item should for instance: + /// - Open or close a folder + /// - Start gameplay on a beatmap difficulty. + /// + /// The carousel item which was activated. + protected virtual void HandleItemActivated(CarouselItem item) + { + } #endregion @@ -197,7 +237,7 @@ namespace osu.Game.Screens.SelectV2 // Copy must be performed on update thread for now (see ConfigureAwait above). // Could potentially be optimised in the future if it becomes an issue. - IEnumerable items = new List(Items.Select(CreateCarouselItemForModel)); + IEnumerable items = new List(Items.Select(m => new CarouselItem(m))); await Task.Run(async () => { @@ -210,7 +250,7 @@ namespace osu.Game.Screens.SelectV2 } log("Updating Y positions"); - await updateYPositions(items, cts.Token).ConfigureAwait(false); + updateYPositions(items, visibleHalfHeight, SpacingBetweenPanels); } catch (OperationCanceledException) { @@ -225,58 +265,231 @@ namespace osu.Game.Screens.SelectV2 carouselItems = items.ToList(); displayedRange = null; - updateSelection(); + // Need to call this to ensure correct post-selection logic is handled on the new items list. + HandleItemSelected(currentSelection.Model); + + refreshAfterSelection(); void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } - private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => + private static void updateYPositions(IEnumerable carouselItems, float offset, float spacing) { - float yPos = visibleHalfHeight; - foreach (var item in carouselItems) + updateItemYPosition(item, ref offset, spacing); + } + + private static void updateItemYPosition(CarouselItem item, ref float offset, float spacing) + { + item.CarouselYPosition = offset; + if (item.IsVisible) + offset += item.DrawHeight + spacing; + } + + #endregion + + #region Input handling + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) { - item.CarouselYPosition = yPos; - yPos += item.DrawHeight + SpacingBetweenPanels; + case GlobalAction.Select: + ActivateSelection(); + return true; + + case GlobalAction.SelectNext: + selectNext(1, isGroupSelection: false); + return true; + + case GlobalAction.SelectNextGroup: + selectNext(1, isGroupSelection: true); + return true; + + case GlobalAction.SelectPrevious: + selectNext(-1, isGroupSelection: false); + return true; + + case GlobalAction.SelectPreviousGroup: + selectNext(-1, isGroupSelection: true); + return true; } - }, cancellationToken).ConfigureAwait(false); + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + /// + /// Select the next valid selection relative to a current selection. + /// This is generally for keyboard based traversal. + /// + /// Positive for downwards, negative for upwards. + /// Whether the selection should traverse groups. Group selection updates the actual selection immediately, while non-group selection will only prepare a future keyboard selection. + /// Whether selection was possible. + private bool selectNext(int direction, bool isGroupSelection) + { + // Ensure sanity + Debug.Assert(direction != 0); + direction = direction > 0 ? 1 : -1; + + if (carouselItems == null || carouselItems.Count == 0) + return false; + + // If the user has a different keyboard selection and requests + // group selection, first transfer the keyboard selection to actual selection. + if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) + { + ActivateSelection(); + return true; + } + + CarouselItem? selectionItem = currentKeyboardSelection.CarouselItem; + int selectionIndex = currentKeyboardSelection.Index ?? -1; + + // To keep things simple, let's first handle the cases where there's no selection yet. + if (selectionItem == null || selectionIndex < 0) + { + // Start by selecting the first item. + selectionItem = carouselItems.First(); + selectionIndex = 0; + + // In the forwards case, immediately attempt selection of this panel. + // If selection fails, continue with standard logic to find the next valid selection. + if (direction > 0 && attemptSelection(selectionItem)) + return true; + + // In the backwards direction we can just allow the selection logic to go ahead and loop around to the last valid. + } + + Debug.Assert(selectionItem != null); + + // As a second special case, if we're group selecting backwards and the current selection isn't + // a group, base this selection operation from the closest previous group. + if (isGroupSelection && direction < 0) + { + while (!carouselItems[selectionIndex].IsGroupSelectionTarget) + selectionIndex--; + } + + CarouselItem? newItem; + + // Iterate over every item back to the current selection, finding the first valid item. + // The fail condition is when we reach the selection after a cyclic loop over every item. + do + { + selectionIndex += direction; + newItem = carouselItems[(selectionIndex + carouselItems.Count) % carouselItems.Count]; + + if (attemptSelection(newItem)) + return true; + } while (newItem != selectionItem); + + return false; + + bool attemptSelection(CarouselItem item) + { + if (!item.IsVisible || (isGroupSelection && !item.IsGroupSelectionTarget)) + return false; + + if (isGroupSelection) + setSelection(item.Model); + else + setKeyboardSelection(item.Model); + + return true; + } + } #endregion #region Selection handling - private object? currentSelection; - private CarouselItem? currentSelectionCarouselItem; - private double? currentSelectionYPosition; + private Selection currentKeyboardSelection = new Selection(); + private Selection currentSelection = new Selection(); - private void updateSelection() + private void setSelection(object? model) { - currentSelectionCarouselItem = null; + if (currentSelection.Model == model) + return; - if (carouselItems == null) return; + var previousSelection = currentSelection; - foreach (var item in carouselItems) + if (previousSelection.Model != null) + HandleItemDeselected(previousSelection.Model); + + currentSelection = currentKeyboardSelection = new Selection(model); + HandleItemSelected(currentSelection.Model); + + // ensure the selection hasn't changed in the handling of selection. + // if it's changed, avoid a second update of selection/scroll. + if (currentSelection.Model != model) + return; + + refreshAfterSelection(); + scrollToSelection(); + } + + private void setKeyboardSelection(object? model) + { + currentKeyboardSelection = new Selection(model); + + refreshAfterSelection(); + scrollToSelection(); + } + + /// + /// Call after a selection of items change to re-attach s to current s. + /// + private void refreshAfterSelection() + { + float yPos = visibleHalfHeight; + + // Invalidate display range as panel positions and visible status may have changed. + // Position transfer won't happen unless we invalidate this. + displayedRange = null; + + // The case where no items are available for display yet. + if (carouselItems == null) { - bool isSelected = item.Model == currentSelection; - - if (isSelected) - { - currentSelectionCarouselItem = item; - - if (currentSelectionYPosition != item.CarouselYPosition) - { - if (currentSelectionYPosition != null) - { - float adjustment = (float)(item.CarouselYPosition - currentSelectionYPosition.Value); - scroll.OffsetScrollPosition(adjustment); - } - - currentSelectionYPosition = item.CarouselYPosition; - } - } - - item.Selected.Value = isSelected; + currentKeyboardSelection = new Selection(); + currentSelection = new Selection(); + return; } + + float spacing = SpacingBetweenPanels; + int count = carouselItems.Count; + + Selection prevKeyboard = currentKeyboardSelection; + + // We are performing two important operations here: + // - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions. + // - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use. + for (int i = 0; i < count; i++) + { + var item = carouselItems[i]; + + updateItemYPosition(item, ref yPos, spacing); + + if (ReferenceEquals(item.Model, currentKeyboardSelection.Model)) + currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i); + + if (ReferenceEquals(item.Model, currentSelection.Model)) + currentSelection = new Selection(item.Model, item, item.CarouselYPosition, i); + } + + // If a keyboard selection is currently made, we want to keep the view stable around the selection. + // That means that we should offset the immediate scroll position by any change in Y position for the selection. + if (prevKeyboard.YPosition != null && currentKeyboardSelection.YPosition != prevKeyboard.YPosition) + scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); + } + + private void scrollToSelection() + { + if (currentKeyboardSelection.CarouselItem != null) + scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); } #endregion @@ -285,7 +498,7 @@ namespace osu.Game.Screens.SelectV2 private DisplayRange? displayedRange; - private readonly CarouselItem carouselBoundsItem = new BoundsCarouselItem(); + private readonly CarouselItem carouselBoundsItem = new CarouselItem(new object()); /// /// The position of the lower visible bound with respect to the current scroll position. @@ -335,6 +548,9 @@ namespace osu.Game.Screens.SelectV2 float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); panel.X = offsetX(dist, visibleHalfHeight); + + c.Selected.Value = c.Item == currentSelection?.CarouselItem; + c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; } } @@ -381,6 +597,8 @@ namespace osu.Game.Screens.SelectV2 ? new List() : carouselItems.GetRange(range.First, range.Last - range.First + 1); + toDisplay.RemoveAll(i => !i.IsVisible); + // Iterate over all panels which are already displayed and figure which need to be displayed / removed. foreach (var panel in scroll.Panels) { @@ -434,6 +652,15 @@ namespace osu.Game.Screens.SelectV2 #region Internal helper classes + /// + /// Bookkeeping for a current selection. + /// + /// The selected model. If null, there's no selection. + /// A related carousel item representation for the model. May be null if selection is not present as an item, or if has not been run yet. + /// The Y position of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet. + /// The index of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet. + private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null); + private record DisplayRange(int First, int Last); /// @@ -573,16 +800,6 @@ namespace osu.Game.Screens.SelectV2 #endregion } - private class BoundsCarouselItem : CarouselItem - { - public override float DrawHeight => 0; - - public BoundsCarouselItem() - : base(new object()) - { - } - } - #endregion } } From 9ab045495d4dcb0d2d5afb52e4f4d69a5ed3074d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:24:04 +0900 Subject: [PATCH 348/620] Tidy up tests in preparation for adding more --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 187 ++++++++++++ .../SongSelect/TestSceneBeatmapCarouselV2.cs | 286 ------------------ .../TestSceneBeatmapCarouselV2Basics.cs | 119 ++++++++ 3 files changed, 306 insertions(+), 286 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs delete mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs new file mode 100644 index 0000000000..eaa29abf01 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -0,0 +1,187 @@ +// 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.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Graphics; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene + { + protected readonly BindableList BeatmapSets = new BindableList(); + + protected BeatmapCarousel Carousel = null!; + + protected OsuScrollContainer Scroll => Carousel.ChildrenOfType>().Single(); + + [Cached(typeof(BeatmapStore))] + private BeatmapStore store; + + private OsuTextFlowContainer stats = null!; + + private int beatmapCount; + + protected BeatmapCarouselV2TestScene() + { + store = new TestBeatmapStore + { + BeatmapSets = { BindTarget = BeatmapSets } + }; + + BeatmapSets.BindCollectionChanged((_, _) => beatmapCount = BeatmapSets.Sum(s => s.Beatmaps.Count)); + + Scheduler.AddDelayed(updateStats, 100, true); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset beatmaps", () => BeatmapSets.Clear()); + + CreateCarousel(); + + SortBy(new FilterCriteria { Sort = SortMode.Title }); + } + + protected void CreateCarousel() + { + AddStep("create components", () => + { + Box topBox; + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 1), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 200), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 200), + }, + Content = new[] + { + new Drawable[] + { + topBox = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + }, + new Drawable[] + { + Carousel = new BeatmapCarousel + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + RelativeSizeAxes = Axes.Y, + }, + }, + new[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + topBox.CreateProxy(), + } + } + }, + stats = new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + TextAnchor = Anchor.CentreLeft, + }, + }; + }); + } + + protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); + + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); + protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); + + /// + /// Add requested beatmap sets count to list. + /// + /// The count of beatmap sets to add. + /// If not null, the number of difficulties per set. If null, randomised difficulty count will be used. + protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => + { + for (int i = 0; i < count; i++) + BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); + }); + + protected void RemoveLastBeatmap() => + AddStep("remove last beatmap", () => + { + if (BeatmapSets.Count == 0) return; + + BeatmapSets.Remove(BeatmapSets.Last()); + }); + + private void updateStats() + { + if (Carousel.IsNull()) + return; + + stats.Clear(); + createHeader("beatmap store"); + stats.AddParagraph($""" + sets: {BeatmapSets.Count} + beatmaps: {beatmapCount} + """); + createHeader("carousel"); + stats.AddParagraph($""" + sorting: {Carousel.IsFiltering} + tracked: {Carousel.ItemsTracked} + displayable: {Carousel.DisplayableItems} + displayed: {Carousel.VisibleItems} + selected: {Carousel.CurrentSelection} + """); + + void createHeader(string text) + { + stats.AddParagraph(string.Empty); + stats.AddParagraph(text, cp => + { + cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); + }); + } + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs deleted file mode 100644 index dee61bbcde..0000000000 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; -using osu.Game.Tests.Beatmaps; -using osu.Game.Tests.Resources; -using osuTK.Graphics; -using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; - -namespace osu.Game.Tests.Visual.SongSelect -{ - [TestFixture] - public partial class TestSceneBeatmapCarouselV2 : OsuManualInputManagerTestScene - { - private readonly BindableList beatmapSets = new BindableList(); - - [Cached(typeof(BeatmapStore))] - private BeatmapStore store; - - private OsuTextFlowContainer stats = null!; - private BeatmapCarousel carousel = null!; - - private OsuScrollContainer scroll => carousel.ChildrenOfType>().Single(); - - private int beatmapCount; - - public TestSceneBeatmapCarouselV2() - { - store = new TestBeatmapStore - { - BeatmapSets = { BindTarget = beatmapSets } - }; - - beatmapSets.BindCollectionChanged((_, _) => - { - beatmapCount = beatmapSets.Sum(s => s.Beatmaps.Count); - }); - - Scheduler.AddDelayed(updateStats, 100, true); - } - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("create components", () => - { - beatmapSets.Clear(); - - Box topBox; - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, 1), - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 200), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 200), - }, - Content = new[] - { - new Drawable[] - { - topBox = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Cyan, - RelativeSizeAxes = Axes.Both, - Alpha = 0.4f, - }, - }, - new Drawable[] - { - carousel = new BeatmapCarousel - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 500, - RelativeSizeAxes = Axes.Y, - }, - }, - new[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Cyan, - RelativeSizeAxes = Axes.Both, - Alpha = 0.4f, - }, - topBox.CreateProxy(), - } - } - }, - stats = new OsuTextFlowContainer - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - TextAnchor = Anchor.CentreLeft, - }, - }; - }); - - AddStep("sort by title", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Title }); - }); - } - - [Test] - public void TestBasic() - { - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddStep("add 1 beatmap", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)))); - - AddStep("remove all beatmaps", () => beatmapSets.Clear()); - } - - [Test] - public void TestSorting() - { - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddStep("sort by difficulty", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }); - }); - - AddStep("sort by artist", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }); - }); - } - - [Test] - public void TestScrollPositionMaintainedOnAddSecondSelected() - { - Quad positionBefore = default; - - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - - AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); - AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Selected.Value))); - - AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); - AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestScrollPositionMaintainedOnAddLastSelected() - { - Quad positionBefore = default; - - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - - AddStep("scroll to last item", () => scroll.ScrollToEnd(false)); - - AddStep("select last beatmap", () => carousel.CurrentSelection = beatmapSets.First()); - - AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); - AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestAddRemoveOneByOne() - { - AddRepeatStep("add beatmaps", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); - - AddRepeatStep("remove beatmaps", () => beatmapSets.RemoveAt(RNG.Next(0, beatmapSets.Count)), 20); - } - - [Test] - [Explicit] - public void TestInsane() - { - const int count = 200000; - - List generated = new List(); - - AddStep($"populate {count} test beatmaps", () => - { - generated.Clear(); - Task.Run(() => - { - for (int j = 0; j < count; j++) - generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }).ConfigureAwait(true); - }); - - AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3)); - AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2)); - AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count)); - - AddStep("add all beatmaps", () => beatmapSets.AddRange(generated)); - } - - private void updateStats() - { - if (carousel.IsNull()) - return; - - stats.Clear(); - createHeader("beatmap store"); - stats.AddParagraph($""" - sets: {beatmapSets.Count} - beatmaps: {beatmapCount} - """); - createHeader("carousel"); - stats.AddParagraph($""" - sorting: {carousel.IsFiltering} - tracked: {carousel.ItemsTracked} - displayable: {carousel.DisplayableItems} - displayed: {carousel.VisibleItems} - selected: {carousel.CurrentSelection} - """); - - void createHeader(string text) - { - stats.AddParagraph(string.Empty); - stats.AddParagraph(text, cp => - { - cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); - }); - } - } - } -} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs new file mode 100644 index 0000000000..8d801930fc --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -0,0 +1,119 @@ +// 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 System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.SongSelect +{ + /// + /// Currently covers adding and removing of items and scrolling. + /// If we add more tests here, these two categories can likely be split out into separate scenes. + /// + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene + { + [Test] + public void TestBasics() + { + AddBeatmaps(1); + AddBeatmaps(10); + RemoveLastBeatmap(); + AddStep("remove all beatmaps", () => BeatmapSets.Clear()); + } + + [Test] + public void TestAddRemoveOneByOne() + { + AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); + AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20); + } + + [Test] + public void TestSorting() + { + AddBeatmaps(10); + SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Sort = SortMode.Artist }); + } + + [Test] + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddBeatmaps(10); + WaitForDrawablePanels(); + + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveLastBeatmap(); + WaitForSorting(); + + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() + { + Quad positionBefore = default; + + AddBeatmaps(10); + WaitForDrawablePanels(); + + AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); + + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.First()); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveLastBeatmap(); + WaitForSorting(); + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + [Explicit] + public void TestPerformanceWithManyBeatmaps() + { + const int count = 200000; + + List generated = new List(); + + AddStep($"populate {count} test beatmaps", () => + { + generated.Clear(); + Task.Run(() => + { + for (int j = 0; j < count; j++) + generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }).ConfigureAwait(true); + }); + + AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3)); + AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2)); + AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count)); + + AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated)); + } + } +} From ffca90779fcc9a781fb6a5c6063c0f1baa927f81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:48:03 +0900 Subject: [PATCH 349/620] Fix sort direction being flipped --- .../Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 10 ++++++---- .../SongSelect/TestSceneBeatmapCarouselV2Basics.cs | 10 +++++----- .../Screens/SelectV2/BeatmapCarouselFilterSorting.cs | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index eaa29abf01..3aa9f60181 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.SongSelect [SetUpSteps] public void SetUpSteps() { - AddStep("reset beatmaps", () => BeatmapSets.Clear()); + RemoveAllBeatmaps(); CreateCarousel(); @@ -146,12 +146,14 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); }); - protected void RemoveLastBeatmap() => - AddStep("remove last beatmap", () => + protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); + + protected void RemoveFirstBeatmap() => + AddStep("remove first beatmap", () => { if (BeatmapSets.Count == 0) return; - BeatmapSets.Remove(BeatmapSets.Last()); + BeatmapSets.Remove(BeatmapSets.First()); }); private void updateStats() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 8d801930fc..748831bf7b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(1); AddBeatmaps(10); - RemoveLastBeatmap(); - AddStep("remove all beatmaps", () => BeatmapSets.Clear()); + RemoveFirstBeatmap(); + RemoveAllBeatmaps(); } [Test] @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - RemoveLastBeatmap(); + RemoveFirstBeatmap(); WaitForSorting(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, @@ -79,13 +79,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.First()); + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); WaitForScrolling(); AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - RemoveLastBeatmap(); + RemoveFirstBeatmap(); WaitForSorting(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index dd82bf3495..0298616aa8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2 { var criteria = getCriteria(); - return items.OrderDescending(Comparer.Create((a, b) => + return items.Order(Comparer.Create((a, b) => { int comparison; From eaea053c7d8824d26ba43821bc4e46bb9ba227a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 17:19:09 +0900 Subject: [PATCH 350/620] Add test coverage of various selection examples Where possible I've tried to match the test and method names of `TestSceneBeatmapCarousel` for easy coverage comparison. --- .../TestSceneBeatmapCarouselV2Selection.cs | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs new file mode 100644 index 0000000000..305774b7d3 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -0,0 +1,216 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.SelectV2; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene + { + /// + /// Keyboard selection via up and down arrows doesn't actually change the selection until + /// the select key is pressed. + /// + [Test] + public void TestKeyboardSelectionKeyRepeat() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + checkNoSelection(); + + select(); + checkNoSelection(); + + AddStep("press down arrow", () => InputManager.PressKey(Key.Down)); + checkSelectionIterating(false); + + AddStep("press up arrow", () => InputManager.PressKey(Key.Up)); + checkSelectionIterating(false); + + AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down)); + checkSelectionIterating(false); + + AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up)); + checkSelectionIterating(false); + + select(); + checkHasSelection(); + } + + /// + /// Keyboard selection via left and right arrows moves between groups, updating the selection + /// immediately. + /// + [Test] + public void TestGroupSelectionKeyRepeat() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + checkNoSelection(); + + AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); + checkSelectionIterating(true); + + AddStep("press left arrow", () => InputManager.PressKey(Key.Left)); + checkSelectionIterating(true); + + AddStep("release right arrow", () => InputManager.ReleaseKey(Key.Right)); + checkSelectionIterating(true); + + AddStep("release left arrow", () => InputManager.ReleaseKey(Key.Left)); + checkSelectionIterating(false); + } + + [Test] + public void TestCarouselRemembersSelection() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + + selectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + checkHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + checkHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + BeatmapCarouselPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestTraversalBeyondStart() + { + const int total_set_count = 200; + + AddBeatmaps(total_set_count); + WaitForDrawablePanels(); + + selectNextGroup(); + waitForSelection(0, 0); + selectPrevGroup(); + waitForSelection(total_set_count - 1, 0); + } + + [Test] + public void TestTraversalBeyondEnd() + { + const int total_set_count = 200; + + AddBeatmaps(total_set_count); + WaitForDrawablePanels(); + + selectPrevGroup(); + waitForSelection(total_set_count - 1, 0); + selectNextGroup(); + waitForSelection(0, 0); + } + + [Test] + public void TestKeyboardSelection() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + selectNextPanel(); + selectNextPanel(); + selectNextPanel(); + selectNextPanel(); + checkNoSelection(); + + select(); + waitForSelection(3, 0); + + selectNextPanel(); + waitForSelection(3, 0); + + select(); + waitForSelection(3, 1); + + selectNextPanel(); + waitForSelection(3, 1); + + select(); + waitForSelection(3, 2); + + selectNextPanel(); + waitForSelection(3, 2); + + select(); + waitForSelection(4, 0); + } + + [Test] + public void TestEmptyTraversal() + { + selectNextPanel(); + checkNoSelection(); + + selectNextGroup(); + checkNoSelection(); + + selectPrevPanel(); + checkNoSelection(); + + selectPrevGroup(); + checkNoSelection(); + } + + private void waitForSelection(int set, int? diff = null) + { + AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => + { + if (diff != null) + return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); + + return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); + }); + } + + private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); + private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); + private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); + private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); + + private void select() => AddStep("select", () => InputManager.Key(Key.Enter)); + + private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); + private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + + private void checkSelectionIterating(bool isIterating) + { + object? selection = null; + + for (int i = 0; i < 3; i++) + { + AddStep("store selection", () => selection = Carousel.CurrentSelection); + if (isIterating) + AddUntilStep("selection changed", () => Carousel.CurrentSelection != selection); + else + AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); + } + } + } +} From 2feab314267ae017cce1334ed8e83ba4fdfc0ec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 22:41:20 +0900 Subject: [PATCH 351/620] Adjust inline commentary based on review feedback --- osu.Game/Screens/SelectV2/Carousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 598a898686..8194ddaaed 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -366,8 +366,8 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(selectionItem != null); - // As a second special case, if we're group selecting backwards and the current selection isn't - // a group, base this selection operation from the closest previous group. + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (isGroupSelection && direction < 0) { while (!carouselItems[selectionIndex].IsGroupSelectionTarget) @@ -423,8 +423,8 @@ namespace osu.Game.Screens.SelectV2 currentSelection = currentKeyboardSelection = new Selection(model); HandleItemSelected(currentSelection.Model); - // ensure the selection hasn't changed in the handling of selection. - // if it's changed, avoid a second update of selection/scroll. + // `HandleItemSelected` can alter `CurrentSelection`, which will recursively call `setSelection()` again. + // if that happens, the rest of this method should be a no-op. if (currentSelection.Model != model) return; From 0716b73d2aa43f6343c700a3b9bb0eb451542f26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 22:44:39 +0900 Subject: [PATCH 352/620] `ActivateSelection` -> `TryActivateSelection` --- osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index da3e1b0964..9219656365 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { if (carousel.CurrentSelection == Item!.Model) - carousel.ActivateSelection(); + carousel.TryActivateSelection(); else carousel.CurrentSelection = Item!.Model; return true; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 8194ddaaed..6899c10451 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -97,9 +97,10 @@ namespace osu.Game.Screens.SelectV2 } /// - /// Activate the current selection, if a selection exists. + /// Activate the current selection, if a selection exists and matches keyboard selection. + /// If keyboard selection does not match selection, this will transfer the selection on first invocation. /// - public void ActivateSelection() + public void TryActivateSelection() { if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { @@ -295,7 +296,7 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.Select: - ActivateSelection(); + TryActivateSelection(); return true; case GlobalAction.SelectNext: @@ -342,7 +343,7 @@ namespace osu.Game.Screens.SelectV2 // group selection, first transfer the keyboard selection to actual selection. if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - ActivateSelection(); + TryActivateSelection(); return true; } From d5369d3508c4ae9227a5f5858536153f947ee600 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 23:53:09 +0900 Subject: [PATCH 353/620] Add regions to `BeatmapCarousel` --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 97 ++++++++++++-------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index e3bc487154..540eedbd92 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -22,8 +22,6 @@ namespace osu.Game.Screens.SelectV2 { private IBindableList detachedBeatmaps = null!; - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); - private readonly LoadingLayer loading; private readonly BeatmapCarouselFilterGrouping grouping; @@ -39,19 +37,60 @@ namespace osu.Game.Screens.SelectV2 grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; - AddInternal(carouselPanelPool); - AddInternal(loading = new LoadingLayer(dimBackground: true)); } [BackgroundDependencyLoader] private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) + { + setupPools(); + setupBeatmaps(beatmapStore, cancellationToken); + } + + #region Beatmap source hookup + + private void setupBeatmaps(BeatmapStore beatmapStore, CancellationToken? cancellationToken) { detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); } - protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + { + // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. + // right now we are managing this locally which is a bit of added overhead. + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); + break; + + case NotifyCollectionChangedAction.Remove: + + foreach (var set in beatmapSetInfos!) + { + foreach (var beatmap in set.Beatmaps) + Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + throw new NotImplementedException(); + + case NotifyCollectionChangedAction.Reset: + Items.Clear(); + break; + } + } + + #endregion + + #region Selection handling protected override void HandleItemDeselected(object? model) { @@ -98,38 +137,9 @@ namespace osu.Game.Screens.SelectV2 drawable.FlashFromActivation(); } - private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) - { - // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. - // right now we are managing this locally which is a bit of added overhead. - IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); - IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + #endregion - switch (changed.Action) - { - case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); - break; - - case NotifyCollectionChangedAction.Remove: - - foreach (var set in beatmapSetInfos!) - { - foreach (var beatmap in set.Beatmaps) - Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); - } - - break; - - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Replace: - throw new NotImplementedException(); - - case NotifyCollectionChangedAction.Reset: - Items.Clear(); - break; - } - } + #region Filtering public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); @@ -139,5 +149,20 @@ namespace osu.Game.Screens.SelectV2 loading.Show(); FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide())); } + + #endregion + + #region Drawable pooling + + private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + + private void setupPools() + { + AddInternal(carouselPanelPool); + } + + protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + + #endregion } } From f4270ab3b994dad45acfc9c735da1a52c323e0ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 23:58:51 +0900 Subject: [PATCH 354/620] Simplify selection handling logic --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 32 +++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 540eedbd92..aca71efe93 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -92,19 +92,6 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling - protected override void HandleItemDeselected(object? model) - { - base.HandleItemDeselected(model); - - var deselectedSet = model as BeatmapSetInfo ?? (model as BeatmapInfo)?.BeatmapSet; - - if (grouping.SetItems.TryGetValue(deselectedSet!, out var group)) - { - foreach (var i in group) - i.IsVisible = false; - } - } - protected override void HandleItemSelected(object? model) { base.HandleItemSelected(model); @@ -116,15 +103,24 @@ namespace osu.Game.Screens.SelectV2 return; } - var currentSelectionSet = (model as BeatmapInfo)?.BeatmapSet; + if (model is BeatmapInfo beatmapInfo) + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + } - if (currentSelectionSet == null) - return; + protected override void HandleItemDeselected(object? model) + { + base.HandleItemDeselected(model); - if (grouping.SetItems.TryGetValue(currentSelectionSet, out var group)) + if (model is BeatmapInfo beatmapInfo) + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, false); + } + + private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) + { + if (grouping.SetItems.TryGetValue(set, out var group)) { foreach (var i in group) - i.IsVisible = true; + i.IsVisible = visible; } } From 13c64b59af7a6e809ffdb2632973f10ef14ff722 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:36:20 -0700 Subject: [PATCH 355/620] Inherit menu items from parent class --- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7fefa0a1a8..7463e05c96 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -340,7 +340,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - public MenuItem[] ContextMenuItems + public virtual MenuItem[] ContextMenuItems { get { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index da04152bd3..700cc09eb6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public Popover GetPopover() => new PasswordEntryPopover(Room); - public new MenuItem[] ContextMenuItems + public override MenuItem[] ContextMenuItems { get { @@ -170,19 +170,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }) }; - if (Room.RoomID.HasValue) - { - items.AddRange([ - new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - }) - ]); - } + items.AddRange(base.ContextMenuItems); if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { From b0a7237fd6c397f2412a4e209af40094788bcc30 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:37:30 -0700 Subject: [PATCH 356/620] Fix formatting --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 700cc09eb6..47630ce1ff 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge /// /// A with lounge-specific interactions such as selection and hover sounds. /// - public partial class DrawableLoungeRoom : DrawableRoom, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler + public partial class DrawableLoungeRoom : DrawableRoom, IFilterable, IHasPopover, IKeyBindingHandler { private const float transition_duration = 60; private const float selection_border_width = 4; @@ -59,9 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private OsuGame? game { get; set; } - private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; private Sample? sampleJoin; From d326f23576176fb02700cb9a3cfc989374f44664 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:39:18 -0700 Subject: [PATCH 357/620] Remove unused method --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 47630ce1ff..f2afbcef71 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -236,8 +236,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Room.PropertyChanged -= onRoomPropertyChanged; } - private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; - public partial class PasswordEntryPopover : OsuPopover { private readonly Room room; From 61a818e4eddc8805a6584095656cf70511e945e5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 23 Jan 2025 21:22:35 -0500 Subject: [PATCH 358/620] Hide Discord RPC error messages away from user attention --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 7dd9250ab6..6afb3e319d 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -82,7 +82,7 @@ namespace osu.Desktop }; client.OnReady += onReady; - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network); try { From 5cc8181bad679ab8f1171531493f47c856c0633b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:29:49 +0900 Subject: [PATCH 359/620] Expose `GameplayStartTime` in `IGameplayClock` --- .../TestSceneClicksPerSecondCalculator.cs | 1 + osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 8 ++++---- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 ++ osu.Game/Screens/Play/IGameplayClock.cs | 5 +++++ .../Play/MasterGameplayClockContainer.cs | 17 ++++++++--------- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index db06329d74..55d57d7a65 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -120,6 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay public double FramesPerSecond => throw new NotImplementedException(); public FrameTimeInfo TimeInfo => throw new NotImplementedException(); public double StartTime => throw new NotImplementedException(); + public double GameplayStartTime => throw new NotImplementedException(); public IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent; diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 92258f3fc9..50111e64a8 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.UI private readonly Bindable waitingOnFrames = new Bindable(); - private readonly double gameplayStartTime; + public double GameplayStartTime { get; } private IGameplayClock? parentGameplayClock; @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.UI framedClock = new FramedClock(manualClock = new ManualClock()); - this.gameplayStartTime = gameplayStartTime; + GameplayStartTime = gameplayStartTime; } [BackgroundDependencyLoader(true)] @@ -257,8 +257,8 @@ namespace osu.Game.Rulesets.UI return; } - if (manualClock.CurrentTime < gameplayStartTime) - manualClock.CurrentTime = proposedTime = Math.Min(gameplayStartTime, proposedTime); + if (manualClock.CurrentTime < GameplayStartTime) + manualClock.CurrentTime = proposedTime = Math.Min(GameplayStartTime, proposedTime); else if (Math.Abs(manualClock.CurrentTime - proposedTime) > sixty_frame_time * 1.2f) { proposedTime = proposedTime > manualClock.CurrentTime diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 255877e0aa..2afdcfaebb 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Play /// public double StartTime { get; protected set; } + public double GameplayStartTime { get; protected set; } + public IAdjustableAudioComponent AdjustmentsFromMods { get; } = new AudioAdjustments(); private readonly BindableBool isPaused = new BindableBool(true); diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index ad28e343ff..bef7362aa9 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -18,6 +18,11 @@ namespace osu.Game.Screens.Play /// double StartTime { get; } + /// + /// The time from which actual gameplay should start. When intro time is skipped, this will be the seeked location. + /// + double GameplayStartTime { get; } + /// /// All adjustments applied to this clock which come from mods. /// diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 3851806788..0b47d8ed85 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -57,8 +57,6 @@ namespace osu.Game.Screens.Play private Track track; - private readonly double skipTargetTime; - [Resolved] private MusicController musicController { get; set; } = null!; @@ -66,16 +64,16 @@ namespace osu.Game.Screens.Play /// Create a new master gameplay clock container. /// /// The beatmap to be used for time and metadata references. - /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. - public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime) + /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) : base(beatmap.Track, applyOffsets: true, requireDecoupling: true) { this.beatmap = beatmap; - this.skipTargetTime = skipTargetTime; track = beatmap.Track; StartTime = findEarliestStartTime(); + GameplayStartTime = gameplayStartTime; } private double findEarliestStartTime() @@ -84,7 +82,7 @@ namespace osu.Game.Screens.Play // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. // start with the originally provided latest time (if before zero). - double time = Math.Min(0, skipTargetTime); + double time = Math.Min(0, GameplayStartTime); // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. @@ -119,10 +117,10 @@ namespace osu.Game.Screens.Play /// public void Skip() { - if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) + if (GameplayClock.CurrentTime > GameplayStartTime - MINIMUM_SKIP_TIME) return; - double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME; + double skipTarget = GameplayStartTime - MINIMUM_SKIP_TIME; if (StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros @@ -187,7 +185,8 @@ namespace osu.Game.Screens.Play } else { - Logger.Log($"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}"); + Logger.Log( + $"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}"); } elapsedValidationTime = null; From fb10996951a1821d06f6ffe5a092763cb1e44bca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:30:02 +0900 Subject: [PATCH 360/620] Consume `GameplayStartTime` for more lenient offset adjustments --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index e988760834..503e9ad15e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -291,7 +291,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Debug.Assert(gameplayClock != null); // TODO: the blocking conditions should probably display a message. - if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.StartTime > 10000) + if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.GameplayStartTime > 10000) return false; if (gameplayClock.IsPaused.Value) From ee78e1b2234bd7c1a94a7be58d48c9b82ce88923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:33:39 +0900 Subject: [PATCH 361/620] Add safeties against attempting to apply previous play while offset adjust is not allowed This should theoretically not be possible, but while we are sharing this control's implementation between gameplay and non-gameplay usages, let's ensure nothing weird can occur. --- .../Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index e988760834..9465624b02 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -245,6 +245,9 @@ namespace osu.Game.Screens.Play.PlayerSettings Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => { + if (Current.Disabled) + return; + Current.Value = lastPlayBeatmapOffset - lastPlayAverage; lastAppliedScore.Value = ReferenceScore.Value; }, @@ -277,6 +280,9 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override void Update() { base.Update(); + + if (useAverageButton != null) + useAverageButton.Enabled.Value = allowOffsetAdjust; Current.Disabled = !allowOffsetAdjust; } From 8f8a6455b4dfde594d78234c1cd3ca346337570f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:34:03 +0900 Subject: [PATCH 362/620] Bypass offset disallowed status when handling realm callbacks Hopefully don't need to overthink this one. --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 9465624b02..c7367ea8c6 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -121,7 +121,11 @@ namespace osu.Game.Screens.Play.PlayerSettings // At the point we reach here, it's not guaranteed that all realm writes have taken place (there may be some in-flight). // We are only aware of writes that originated from our own flow, so if we do see one that's active we can avoid handling the feedback value arriving. if (realmWriteTask == null) + { + Current.Disabled = false; + Current.Disabled = allowOffsetAdjust; Current.Value = val; + } if (realmWriteTask?.IsCompleted == true) { From 05b1002e9d4f7de5d5db4ef28784dd3b8bf57c99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:57:13 +0900 Subject: [PATCH 363/620] Adjust layout and code quality slightly --- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 14 ++++---------- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 11 ++++------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7463e05c96..4402d1cf5c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -349,23 +349,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (Room.RoomID.HasValue) { items.AddRange([ - new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - }) + new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value))), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value))) ]); } return items.ToArray(); + + string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; } } - private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; - protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); protected virtual IEnumerable CreateBottomDetails() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index f2afbcef71..1cabb22e30 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -159,16 +159,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { get { - var items = new List - { - new OsuMenuItem("Create copy", MenuItemType.Standard, () => - { - lounge?.OpenCopy(Room); - }) - }; + var items = new List(); items.AddRange(base.ContextMenuItems); + items.Add(new OsuMenuItemSpacer()); + items.Add(new OsuMenuItem("Create copy", MenuItemType.Standard, () => lounge?.OpenCopy(Room))); + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => From 28a59f4e29bce5a14c8672de6a7ed8b5bb417fcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 16:45:14 +0900 Subject: [PATCH 364/620] Move line to correct location --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index c7367ea8c6..ace001f635 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -123,8 +123,8 @@ namespace osu.Game.Screens.Play.PlayerSettings if (realmWriteTask == null) { Current.Disabled = false; - Current.Disabled = allowOffsetAdjust; Current.Value = val; + Current.Disabled = allowOffsetAdjust; } if (realmWriteTask?.IsCompleted == true) From 721b2dfbbaed488fbc65cd44b91506bc073703eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 17:16:51 +0900 Subject: [PATCH 365/620] Fix average button not correctly becoming disabled where it previously would --- .../PlayerSettings/BeatmapOffsetControl.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index ace001f635..e0b0a1b0ab 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -138,15 +138,15 @@ namespace osu.Game.Screens.Play.PlayerSettings ReferenceScore.BindValueChanged(scoreChanged, true); } + // the last play graph is relative to the offset at the point of the last play, so we need to factor that out for some usages. + private double adjustmentSinceLastPlay => lastPlayBeatmapOffset - Current.Value; + private void currentChanged(ValueChangedEvent offset) { Scheduler.AddOnce(updateOffset); void updateOffset() { - // the last play graph is relative to the offset at the point of the last play, so we need to factor that out. - double adjustmentSinceLastPlay = lastPlayBeatmapOffset - Current.Value; - // Negative is applied here because the play graph is considering a hit offset, not track (as we currently use for clocks). lastPlayGraph?.UpdateOffset(-adjustmentSinceLastPlay); @@ -157,11 +157,6 @@ namespace osu.Game.Screens.Play.PlayerSettings return; } - if (useAverageButton != null) - { - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); - } - realmWriteTask = realm.WriteAsync(r => { var setInfo = r.Find(beatmap.Value.BeatmapSetInfo.ID); @@ -255,7 +250,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Current.Value = lastPlayBeatmapOffset - lastPlayAverage; lastAppliedScore.Value = ReferenceScore.Value; }, - Enabled = { Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2) } }, globalOffsetText = new LinkFlowContainer { @@ -285,9 +279,12 @@ namespace osu.Game.Screens.Play.PlayerSettings { base.Update(); + bool allow = allowOffsetAdjust; + if (useAverageButton != null) - useAverageButton.Enabled.Value = allowOffsetAdjust; - Current.Disabled = !allowOffsetAdjust; + useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); + + Current.Disabled = !allow; } private bool allowOffsetAdjust From 17b1739ae49b549692c61eaddae35682b9e9053b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:00:05 +0900 Subject: [PATCH 366/620] Combine countless update methods all called together into a single method --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 52 +++++++------------ 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index edb44a7666..9915560a95 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -355,11 +355,11 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + updateBeatmap(); + updateSpecifics(); + beginHandlingTrack(); - Scheduler.AddOnce(updateMods); - Scheduler.AddOnce(updateRuleset); - Scheduler.AddOnce(updateUserStyle); } protected bool ExitConfirmed { get; private set; } @@ -448,9 +448,7 @@ namespace osu.Game.Screens.OnlinePlay.Match updateUserMods(); updateBeatmap(); - updateMods(); - updateRuleset(); - updateUserStyle(); + updateSpecifics(); if (!item.AllowedMods.Any()) { @@ -501,43 +499,31 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; } - private void updateMods() + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; var rulesetInstance = GetGameplayRuleset().CreateInstance(); Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); - } - - private void updateRuleset() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; Ruleset.Value = GetGameplayRuleset(); - } - private void updateUserStyle() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; - - if (UserStyleDisplayContainer == null) - return; - - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + if (UserStyleDisplayContainer != null) { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; + } } protected virtual APIMod[] GetGameplayMods() From 92429b2ed9e8f7a658196659656aeb9ec7dcd14d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:34:04 +0900 Subject: [PATCH 367/620] Adjust comments on `ICarouselPanel` to imply external use --- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index c592734d8d..2776fdec6c 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -3,33 +3,33 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; namespace osu.Game.Screens.SelectV2 { /// /// An interface to be attached to any s which are used for display inside a . + /// Importantly, all properties in this interface are managed by and should not be written to elsewhere. /// public interface ICarouselPanel { /// - /// Whether this item has selection. - /// This is managed by and should not be set manually. + /// Whether this item has selection. Should be read from to update the visual state. /// BindableBool Selected { get; } /// - /// Whether this item has keyboard selection. - /// This is managed by and should not be set manually. + /// Whether this item has keyboard selection. Should be read from to update the visual state. /// BindableBool KeyboardSelected { get; } /// - /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. + /// The Y position used internally for positioning in the carousel. /// double DrawYPosition { get; set; } /// - /// The carousel item this drawable is representing. This is managed by and should not be set manually. + /// The carousel item this drawable is representing. Will be set before is called. /// CarouselItem? Item { get; set; } } From 9366bfbf0d317e18884086f5532c5cf12443f904 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:40:48 +0900 Subject: [PATCH 368/620] Move activation drawable flow portion to `ICarouselPanel` --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 9 --------- osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 3 +++ osu.Game/Screens/SelectV2/ICarouselPanel.cs | 5 +++++ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index aca71efe93..630f7b6583 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -124,15 +124,6 @@ namespace osu.Game.Screens.SelectV2 } } - protected override void HandleItemActivated(CarouselItem item) - { - base.HandleItemActivated(item); - - // TODO: maybe this should be handled by the panel itself? - if (GetMaterialisedDrawableForItem(item) is BeatmapCarouselPanel drawable) - drawable.FlashFromActivation(); - } - #endregion #region Filtering diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 9219656365..398ec7bf4c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.SelectV2 public double DrawYPosition { get; set; } - public void FlashFromActivation() + public void Activated() { activationFlash.FadeOutFromOne(500, Easing.OutQuint); } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6899c10451..6ff27c6198 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -109,7 +109,10 @@ namespace osu.Game.Screens.SelectV2 } if (currentSelection.CarouselItem != null) + { + (GetMaterialisedDrawableForItem(currentSelection.CarouselItem) as ICarouselPanel)?.Activated(); HandleItemActivated(currentSelection.CarouselItem); + } } #endregion diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 2776fdec6c..a956bb22a3 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -23,6 +23,11 @@ namespace osu.Game.Screens.SelectV2 /// BindableBool KeyboardSelected { get; } + /// + /// Called when the panel is activated. Should be used to update the panel's visual state. + /// + void Activated(); + /// /// The Y position used internally for positioning in the carousel. /// From 15b6e28ebe888b1a87574891be1a0db3b04093b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 12:16:36 +0100 Subject: [PATCH 369/620] Remove dependence of blueprint containers on `IPositionSnapProvider` --- .../Edit/CatchBlueprintContainer.cs | 29 +++++++ .../Edit/CatchHitObjectComposer.cs | 20 ++--- .../Editor/TestSceneManiaBeatSnapGrid.cs | 6 -- .../Blueprints/HoldNoteSelectionBlueprint.cs | 3 +- .../Edit/ManiaBlueprintContainer.cs | 25 +++++- .../Components/PathControlPointVisualiser.cs | 8 +- .../Edit/OsuBlueprintContainer.cs | 67 ++++++++++++++- .../Edit/OsuHitObjectComposer.cs | 80 ++++++++--------- .../Edit/TaikoBlueprintContainer.cs | 25 +++++- .../SkinEditor/SkinBlueprintContainer.cs | 5 ++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 32 +------ .../Edit/ScrollingHitObjectComposer.cs | 17 ++++ .../Compose/Components/BlueprintContainer.cs | 86 +++---------------- .../Components/ComposeBlueprintContainer.cs | 6 +- .../Components/EditorBlueprintContainer.cs | 29 +++---- .../Compose/Components/Timeline/Timeline.cs | 4 +- .../Timeline/TimelineBlueprintContainer.cs | 17 ++++ 17 files changed, 263 insertions(+), 196 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs index 3979d30616..47035b0227 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs @@ -1,16 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Catch.Edit { public partial class CatchBlueprintContainer : ComposeBlueprintContainer { + public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer; + public CatchBlueprintContainer(CatchHitObjectComposer composer) : base(composer) { @@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit } protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition); + gridSnapResult.ScreenSpacePosition.X = movePosition.X; + var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 7bb5539963..9618eb28a9 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit { public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { - private const float distance_snap_radius = 50; + public const float DISTANCE_SNAP_RADIUS = 50; private CatchDistanceSnapGrid distanceSnapGrid = null!; @@ -135,22 +135,12 @@ namespace osu.Game.Rulesets.Catch.Edit DistanceSnapProvider.HandleToggleViaKey(key); } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition) { - var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult) + return snapResult; - result.ScreenSpacePosition.X = screenSpacePosition.X; - - if (snapType.HasFlag(SnapType.RelativeGrids)) - { - if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && - Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) - { - result = snapResult; - } - } - - return result; + return null; } private PalpableCatchHitObject? getLastSnappableHitObject(double time) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 127beed83e..19ff13e216 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Mania.Tests.Editor { @@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { set => InternalChild = value; } - - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - throw new NotImplementedException(); - } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index 915706c044..ff29154f87 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private EditorBeatmap? editorBeatmap { get; set; } [Resolved] - private IPositionSnapProvider? positionSnapProvider { get; set; } + private ManiaHitObjectComposer? positionSnapProvider { get; set; } private EditBodyPiece body = null!; private EditHoldNoteEndPiece head = null!; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index d0eb8c1e6e..4eb54e6366 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Mania.Edit { public partial class ManiaBlueprintContainer : ComposeBlueprintContainer { - public ManiaBlueprintContainer(HitObjectComposer composer) + public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer; + + public ManiaBlueprintContainer(ManiaHitObjectComposer composer) : base(composer) { } @@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler(); protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.FindSnappedPositionAndTime(movePosition); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f98117c0fa..bac5f0101c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] - private IPositionSnapProvider positionSnapProvider { get; set; } + private OsuHitObjectComposer positionSnapProvider { get; set; } [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } @@ -433,7 +433,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); + SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition) + ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) + ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; @@ -453,7 +455,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); + SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition)); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 54c54fca17..235368e552 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -8,12 +11,15 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuBlueprintContainer : ComposeBlueprintContainer { - public OsuBlueprintContainer(HitObjectComposer composer) + public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer; + + public OsuBlueprintContainer(OsuHitObjectComposer composer) : base(composer) { } @@ -36,5 +42,64 @@ namespace osu.Game.Rulesets.Osu.Edit return base.CreateHitObjectBlueprintFor(hitObject); } + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + for (int i = 0; i < blueprints.Count; i++) + { + if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions)) + return true; + } + + // if no positional snapping could be performed, try unrestricted snapping from the earliest + // item in the selection. + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.TrySnapToDistanceGrid(movePosition) ?? Composer.TrySnapToPositionGrid(movePosition) ?? new SnapResult(movePosition, null); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } + + /// + /// Check for positional snap for given blueprint. + /// + /// The blueprint to check for snapping. + /// Distance travelled since start of dragging action. + /// The snap positions of blueprint before start of dragging action. + /// Whether an object to snap to was found. + private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint blueprint, Vector2 distanceTravelled, Vector2[] originalPositions) + { + var currentPositions = blueprint.ScreenSpaceSnapPoints; + + for (int i = 0; i < originalPositions.Length; i++) + { + Vector2 originalPosition = originalPositions[i]; + var testPosition = originalPosition + distanceTravelled; + + var positionalResult = Composer.TrySnapToNearbyObjects(testPosition); + + if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue; + + var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; + + // attempt to move the objects, and apply any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, delta))) + { + ApplySnapResultTime(positionalResult, blueprint.Item.StartTime); + return true; + } + } + + return false; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index aad3d0c93b..06a74fb631 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -222,56 +223,55 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + [CanBeNull] + public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition) { - if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) - { - // In the case of snapping to nearby objects, a time value is not provided. - // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap - // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is - // BOTH on a valid distance snap ring, and also at the same position as a previous object. - // - // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. - // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over - // the time value if the proposed positions are roughly the same. - if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) - { - (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); - if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) - snapResult.Time = distanceSnappedTime; - } + if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + return null; + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; - } - SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + // In the case of snapping to nearby objects, a time value is not provided. + // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap + // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is + // BOTH on a valid distance snap ring, and also at the same position as a previous object. + // + // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. + // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over + // the time value if the proposed positions are roughly the same. + (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); + if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) + snapResult.Time = distanceSnappedTime; - if (snapType.HasFlag(SnapType.RelativeGrids)) - { - if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) - { - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return snapResult; + } - result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos); - result.Time = time; - } - } + [CanBeNull] + public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition) + { + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) + return null; - if (snapType.HasFlag(SnapType.GlobalGrids)) - { - if (rectangularGridSnapToggle.Value == TernaryState.True) - { - Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield); + } - // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. - // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. - pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + [CanBeNull] + public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition) + { + if (rectangularGridSnapToggle.Value != TernaryState.True) + return null; - result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos); - } - } + Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition)); - return result; + // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. + // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. + pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + return new SnapResult(positionSnapGrid.ToScreenSpace(pos), null, playfield); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 027723c02c..f0c3eec044 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,16 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Taiko.Edit { public partial class TaikoBlueprintContainer : ComposeBlueprintContainer { - public TaikoBlueprintContainer(HitObjectComposer composer) + public new TaikoHitObjectComposer Composer => (TaikoHitObjectComposer)base.Composer; + + public TaikoBlueprintContainer(TaikoHitObjectComposer composer) : base(composer) { } @@ -19,5 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Edit public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => new TaikoSelectionBlueprint(hitObject); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.FindSnappedPositionAndTime(movePosition); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index 3f8d9f80d4..8f831a6f18 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -111,6 +111,11 @@ namespace osu.Game.Overlays.SkinEditor SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray()); } + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + throw new System.NotImplementedException(); + } + /// /// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints). /// diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 15b60114af..b38b0291e8 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -376,7 +376,7 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this); + protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector(); @@ -566,28 +566,6 @@ namespace osu.Game.Rulesets.Edit /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - double? targetTime = null; - - if (snapType.HasFlag(SnapType.GlobalGrids)) - { - if (playfield is ScrollingPlayfield scrollingPlayfield) - { - targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); - - // apply beat snapping - targetTime = BeatSnapProvider.SnapTime(targetTime.Value); - - // convert back to screen space - screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); - } - } - - return new SnapResult(screenSpacePosition, targetTime, playfield); - } - #endregion } @@ -596,7 +574,7 @@ namespace osu.Game.Rulesets.Edit /// Generally used to access certain methods without requiring a generic type for . /// [Cached] - public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider + public abstract partial class HitObjectComposer : CompositeDrawable { public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60; public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 120; @@ -639,11 +617,5 @@ namespace osu.Game.Rulesets.Edit /// The time instant to seek to, in milliseconds. /// The ruleset-specific description of objects to select at the given timestamp. public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { } - - #region IPositionSnapProvider - - public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); - - #endregion } } diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index e7161ce36c..3671724042 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -117,6 +117,23 @@ namespace osu.Game.Rulesets.Edit } } + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) + { + var scrollingPlayfield = PlayfieldAtScreenSpacePosition(screenSpacePosition) as ScrollingPlayfield; + if (scrollingPlayfield == null) + return new SnapResult(screenSpacePosition, null); + + double? targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); + + // apply beat snapping + targetTime = BeatSnapProvider.SnapTime(targetTime.Value); + + // convert back to screen space + screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); + + return new SnapResult(screenSpacePosition, targetTime, scrollingPlayfield); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 4a321f4a81..dc04561242 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -43,9 +43,6 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly Dictionary> blueprintMap = new Dictionary>(); - [Resolved(canBeNull: true)] - private IPositionSnapProvider snapProvider { get; set; } - [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -333,19 +330,19 @@ namespace osu.Game.Screens.Edit.Compose.Components protected void RemoveBlueprintFor(T item) { - if (!blueprintMap.Remove(item, out var blueprint)) + if (!blueprintMap.Remove(item, out var blueprintToRemove)) return; - blueprint.Deselect(); - blueprint.Selected -= OnBlueprintSelected; - blueprint.Deselected -= OnBlueprintDeselected; + blueprintToRemove.Deselect(); + blueprintToRemove.Selected -= OnBlueprintSelected; + blueprintToRemove.Deselected -= OnBlueprintDeselected; - SelectionBlueprints.Remove(blueprint, true); + SelectionBlueprints.Remove(blueprintToRemove, true); - if (movementBlueprints?.Contains(blueprint) == true) + if (movementBlueprints?.Any(m => m.blueprint == blueprintToRemove) == true) finishSelectionMovement(); - OnBlueprintRemoved(blueprint.Item); + OnBlueprintRemoved(blueprintToRemove.Item); } /// @@ -538,8 +535,7 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2[][] movementBlueprintsOriginalPositions; - private SelectionBlueprint[] movementBlueprints; + private (SelectionBlueprint blueprint, Vector2[] originalSnapPositions)[] movementBlueprints; /// /// Whether a blueprint is currently being dragged. @@ -572,8 +568,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item - movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); - movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray(); + movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).Select(b => (b, b.ScreenSpaceSnapPoints)).ToArray(); return true; } @@ -594,68 +589,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - Debug.Assert(movementBlueprintsOriginalPositions != null); - - Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; - - if (snapProvider != null) - { - for (int i = 0; i < movementBlueprints.Length; i++) - { - if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i])) - return true; - } - } - - // if no positional snapping could be performed, try unrestricted snapping from the earliest - // item in the selection. - - // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled; - - // Retrieve a snapped position. - var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); - - if (result == null) - { - return SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), movePosition - movementBlueprints.First().ScreenSpaceSelectionPoint)); - } - - return ApplySnapResult(movementBlueprints, result); + return TryMoveBlueprints(e, movementBlueprints); } - /// - /// Check for positional snap for given blueprint. - /// - /// The blueprint to check for snapping. - /// Distance travelled since start of dragging action. - /// The snap positions of blueprint before start of dragging action. - /// Whether an object to snap to was found. - private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint blueprint, Vector2 distanceTravelled, Vector2[] originalPositions) - { - var currentPositions = blueprint.ScreenSpaceSnapPoints; - - for (int i = 0; i < originalPositions.Length; i++) - { - Vector2 originalPosition = originalPositions[i]; - var testPosition = originalPosition + distanceTravelled; - - var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects); - - if (positionalResult.ScreenSpacePosition == testPosition) continue; - - var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; - - // attempt to move the objects, and abort any time based snapping if we can. - if (SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, delta))) - return true; - } - - return false; - } - - protected virtual bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) => - SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint)); + protected abstract bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints); /// /// Finishes the current movement of selected blueprints. @@ -666,7 +603,6 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - movementBlueprintsOriginalPositions = null; movementBlueprints = null; return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 15bbddd97e..27d6656c69 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A blueprint container generally displayed as an overlay to a ruleset's playfield. /// - public partial class ComposeBlueprintContainer : EditorBlueprintContainer + public abstract partial class ComposeBlueprintContainer : EditorBlueprintContainer { private readonly Container placementBlueprintContainer; @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); - public ComposeBlueprintContainer(HitObjectComposer composer) + protected ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { placementBlueprintContainer = new Container @@ -340,7 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementTimeAndPosition() { - var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); + SnapResult snapResult = new SnapResult(InputManager.CurrentState.Mouse.Position, null); // Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); TODO // if no time was found from positional snapping, we should still quantize to the beat. snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 7b046251e0..f1811dd84f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -17,7 +17,7 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Screens.Edit.Compose.Components { - public partial class EditorBlueprintContainer : BlueprintContainer + public abstract partial class EditorBlueprintContainer : BlueprintContainer { [Resolved] protected EditorClock EditorClock { get; private set; } @@ -73,27 +73,22 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints) => blueprints.OrderBy(b => b.Item.StartTime); - protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) + protected void ApplySnapResultTime(SnapResult result, double referenceTime) { - if (!base.ApplySnapResult(blueprints, result)) - return false; + if (!result.Time.HasValue) + return; - if (result.Time.HasValue) + // Apply the start time at the newly snapped-to position + double offset = result.Time.Value - referenceTime; + + if (offset != 0) { - // Apply the start time at the newly snapped-to position - double offset = result.Time.Value - blueprints.First().Item.StartTime; - - if (offset != 0) + Beatmap.PerformOnSelection(obj => { - Beatmap.PerformOnSelection(obj => - { - obj.StartTime += offset; - Beatmap.Update(obj); - }); - } + obj.StartTime += offset; + Beatmap.Update(obj); + }); } - - return true; } protected override void AddBlueprintFor(HitObject item) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 5f46b3d937..cbf49e62e7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -22,7 +22,7 @@ using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { [Cached] - public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider + public partial class Timeline : ZoomableScrollContainer { private const float timeline_height = 80; @@ -332,7 +332,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return (float)(time / editorClock.TrackLength * Content.DrawWidth); } - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2b5667ff9c..011ff17b30 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -107,6 +107,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return base.OnDragStart(e); } + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = timeline?.FindSnappedPositionAndTime(movePosition) ?? new SnapResult(movePosition, null); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } + private float dragTimeAccumulated; protected override void Update() From a6987f5c95373ac90c8305b39442847f15e42d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 13:49:29 +0100 Subject: [PATCH 370/620] Remove dependence of placement blueprints on `IPositionSnapProvider` --- .../BananaShowerPlacementBlueprint.cs | 8 +++-- .../Blueprints/CatchPlacementBlueprint.cs | 7 +++-- .../Blueprints/FruitPlacementBlueprint.cs | 14 +++++++-- .../JuiceStreamPlacementBlueprint.cs | 13 +++++++-- .../Edit/CatchHitObjectComposer.cs | 1 + .../Blueprints/HoldNotePlacementBlueprint.cs | 6 ++-- .../Blueprints/ManiaPlacementBlueprint.cs | 21 ++++++++++---- .../Edit/Blueprints/NotePlacementBlueprint.cs | 7 +++-- .../Edit/ManiaHitObjectComposer.cs | 1 + .../Edit/Blueprints/GridPlacementBlueprint.cs | 12 ++++---- .../HitCircles/HitCirclePlacementBlueprint.cs | 15 ++++++++-- .../Sliders/SliderPlacementBlueprint.cs | 29 +++++++++++++++---- .../Spinners/SpinnerPlacementBlueprint.cs | 8 +++++ .../Edit/OsuHitObjectComposer.cs | 1 + .../Edit/Blueprints/HitPlacementBlueprint.cs | 10 +++++-- .../Blueprints/TaikoSpanPlacementBlueprint.cs | 16 ++++++---- .../Edit/TaikoHitObjectComposer.cs | 2 ++ .../Edit/HitObjectPlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 14 ++------- .../Components/ComposeBlueprintContainer.cs | 7 +---- 20 files changed, 137 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 6902f78172..85b7624f1b 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Catch.Edit.Blueprints @@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + base.UpdateTimeAndPosition(result); - if (!(result.Time is double time)) return; + if (!(result.Time is double time)) return result; switch (PlacementActive) { @@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints HitObject.StartTime = Math.Min(placementStartTime, placementEndTime); HitObject.EndTime = Math.Max(placementStartTime, placementEndTime); + return result; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index aa862375c5..90b7fa172c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { - public partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint + public abstract partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint where THitObject : CatchHitObject, new() { protected new THitObject HitObject => (THitObject)base.HitObject; @@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints [Resolved] private Playfield playfield { get; set; } = null!; - public CatchPlacementBlueprint() + [Resolved] + protected CatchHitObjectComposer? Composer { get; private set; } + + protected CatchPlacementBlueprint() : base(new THitObject()) { } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs index 72592891fb..83f75771ad 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs @@ -5,6 +5,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Catch.Edit.Blueprints @@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X; + var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + + UpdateTimeAndPosition(result); HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; + return result; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 21cc260462..292175353a 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -83,8 +83,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X; + var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + switch (PlacementActive) { case PlacementState.Waiting: @@ -99,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints break; default: - return; + return result; } // Make sure the up-to-date position is used for outlines. @@ -113,6 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints ApplyDefaultsToHitObject(); scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject); nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject); + return result; } private double positionToTime(float relativeYPosition) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 9618eb28a9..dfe9dc9dd8 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -23,6 +23,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Edit { + [Cached] public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { public const float DISTANCE_SNAP_RADIUS = 50; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 13cfc5f691..094c59da46 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private double originalStartTime; - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = base.UpdateTimeAndPosition(screenSpacePosition, fallbackTime); if (PlacementActive == PlacementState.Active) { @@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (result.Time is double startTime) originalStartTime = HitObject.StartTime = startTime; } + + return result; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index a68bd5d6d6..359a952755 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; @@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { protected new T HitObject => (T)base.HitObject; - private Column column; + [Resolved] + private ManiaHitObjectComposer? composer { get; set; } - public Column Column + private Column? column; + + public Column? Column { get => column; set { + ArgumentNullException.ThrowIfNull(value); + if (value == column) return; @@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); if (result.Playfield is Column col) { @@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (PlacementActive == PlacementState.Waiting) Column = col; } + + return result; } private float getNoteHeight(Column resultPlayfield) => diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 422215db57..a8cccfb067 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -8,6 +8,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints @@ -35,15 +36,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints }; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) { - base.UpdateTimeAndPosition(result); + var result = base.UpdateTimeAndPosition(screenSpacePosition, referenceTime); if (result.Playfield != null) { piece.Width = result.Playfield.DrawWidth; piece.Position = ToLocalSpace(result.ScreenSpacePosition); } + + return result; } protected override bool OnMouseDown(MouseDownEvent e) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 9062c32b7b..bc20456722 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -19,6 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit { + [Cached] public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer { private DrawableManiaEditorRuleset drawableRuleset = null!; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index 163b42bcfd..d3e780df9a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints public partial class GridPlacementBlueprint : PlacementBlueprint { [Resolved] - private HitObjectComposer? hitObjectComposer { get; set; } + private OsuHitObjectComposer? hitObjectComposer { get; set; } private OsuGridToolboxGroup gridToolboxGroup = null!; private Vector2 originalOrigin; @@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints base.OnDragEnd(e); } - public override SnapType SnapType => ~SnapType.GlobalGrids; - - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) { if (State.Value == Visibility.Hidden) - return; + return new SnapResult(screenSpacePosition, referenceTime); + + var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, referenceTime); var pos = ToLocalSpace(result.ScreenSpacePosition); @@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos); } } + + return result; } protected override void PopOut() diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 78a0e36dc2..dad7bd5f0e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles @@ -15,6 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles private readonly HitCirclePiece circlePiece; + [Resolved] + private OsuHitObjectComposer? composer { get; set; } + public HitCirclePlacementBlueprint() : base(new HitCircle()) { @@ -45,10 +50,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) + ?? composer?.TrySnapToPositionGrid(screenSpacePosition) + ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); + return result; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 4f2f6516a8..f5fe00e8b6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public new Slider HitObject => (Slider)base.HitObject; + [Resolved] + private OsuHitObjectComposer? composer { get; set; } + private SliderBodyPiece bodyPiece = null!; private HitCirclePiece headCirclePiece = null!; private HitCirclePiece tailCirclePiece = null!; @@ -40,9 +43,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int currentSegmentLength; private bool usingCustomSegmentType; - [Resolved] - private IPositionSnapProvider? positionSnapProvider { get; set; } - [Resolved] private IDistanceSnapProvider? distanceSnapProvider { get; set; } @@ -106,9 +106,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) + ?? composer?.TrySnapToPositionGrid(screenSpacePosition) + ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); switch (state) { @@ -131,6 +136,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders updateCursor(); break; } + + return result; } protected override bool OnMouseDown(MouseDownEvent e) @@ -375,7 +382,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private Vector2 getCursorPosition() { - var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All); + SnapResult? result = null; + var mousePosition = inputManager.CurrentState.Mouse.Position; + + if (state != SliderPlacementState.ControlPoints) + { + result ??= composer?.TrySnapToNearbyObjects(mousePosition); + result ??= composer?.TrySnapToDistanceGrid(mousePosition); + } + + result ??= composer?.TrySnapToPositionGrid(mousePosition); + return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 17d2dcd75c..6c4847cada 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners @@ -70,5 +71,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners ? Math.Max(HitObject.StartTime, EditorClock.CurrentTime) : Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime)); } + + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) + { + var result = new SnapResult(screenSpacePosition, fallbackTime); + UpdateTimeAndPosition(result); + return result; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 06a74fb631..faed599fa5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -32,6 +32,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Edit { + [Cached] public partial class OsuHitObjectComposer : HitObjectComposer { public OsuHitObjectComposer(Ruleset ruleset) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index 7f45123bd6..b887fac42a 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -16,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints public new Hit HitObject => (Hit)base.HitObject; + [Resolved] + private TaikoHitObjectComposer? composer { get; set; } + public HitPlacementBlueprint() : base(new Hit()) { @@ -40,10 +44,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); piece.Position = ToLocalSpace(result.ScreenSpacePosition); - base.UpdateTimeAndPosition(result); + UpdateTimeAndPosition(result); + return result; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index de3a4d96eb..7263c1ef2c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -26,12 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints private readonly IHasDuration spanPlacementObject; + [Resolved] + private TaikoHitObjectComposer? composer { get; set; } + protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) { - spanPlacementObject = hitObject as IHasDuration; + spanPlacementObject = (hitObject as IHasDuration)!; RelativeSizeAxes = Axes.Both; @@ -79,9 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints EndPlacement(true); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); if (PlacementActive == PlacementState.Active) { @@ -116,6 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints originalPosition = ToLocalSpace(result.ScreenSpacePosition); } } + + return result; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index d97a854ff7..54031f0c9f 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Taiko.Edit { + [Cached] public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer { protected override bool ApplyHorizontalCentering => false; diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 4df2a52743..0bfda94f44 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Edit /// Updates the time and position of this based on the provided snap information. /// /// The snap result information. - public override void UpdateTimeAndPosition(SnapResult result) + public void UpdateTimeAndPosition(SnapResult result) { if (PlacementActive == PlacementState.Waiting) { diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 52b8a5c796..f2d501d1c4 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Edit @@ -75,18 +76,7 @@ namespace osu.Game.Rulesets.Edit PlacementActive = PlacementState.Finished; } - /// - /// Determines which objects to snap to for the snap result in . - /// - public virtual SnapType SnapType => SnapType.All; - - /// - /// Updates the time and position of this based on the provided snap information. - /// - /// The snap result information. - public virtual void UpdateTimeAndPosition(SnapResult result) - { - } + public abstract SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime); public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 27d6656c69..de1f589135 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -340,12 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementTimeAndPosition() { - SnapResult snapResult = new SnapResult(InputManager.CurrentState.Mouse.Position, null); // Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); TODO - - // if no time was found from positional snapping, we should still quantize to the beat. - snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); - - CurrentPlacement.UpdateTimeAndPosition(snapResult); + CurrentPlacement.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, Beatmap.SnapTime(EditorClock.CurrentTime, null)); } #endregion From 32d341a46855d9116aa12ed8f79e1864e3bb6b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 13:49:48 +0100 Subject: [PATCH 371/620] Remove `IPositionSnapProvider` --- .../Rulesets/Edit/IPositionSnapProvider.cs | 23 ------------- osu.Game/Rulesets/Edit/SnapType.cs | 32 ------------------- 2 files changed, 55 deletions(-) delete mode 100644 osu.Game/Rulesets/Edit/IPositionSnapProvider.cs delete mode 100644 osu.Game/Rulesets/Edit/SnapType.cs diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs deleted file mode 100644 index 002a0aafe6..0000000000 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osuTK; - -namespace osu.Game.Rulesets.Edit -{ - /// - /// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap. - /// - [Cached] - public interface IPositionSnapProvider - { - /// - /// Given a position, find a valid time and position snap. - /// - /// The screen-space position to be snapped. - /// The type of snapping to apply. - /// The time and position post-snapping. - SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); - } -} diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs deleted file mode 100644 index cf743f6ace..0000000000 --- a/osu.Game/Rulesets/Edit/SnapType.cs +++ /dev/null @@ -1,32 +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; - -namespace osu.Game.Rulesets.Edit -{ - [Flags] - public enum SnapType - { - None = 0, - - /// - /// Snapping to visible nearby objects. - /// - NearbyObjects = 1 << 0, - - /// - /// Grids which are global to the playfield. - /// - GlobalGrids = 1 << 1, - - /// - /// Grids which are relative to other nearby hit objects. - /// - RelativeGrids = 1 << 2, - - AllGrids = RelativeGrids | GlobalGrids, - - All = NearbyObjects | GlobalGrids | RelativeGrids, - } -} From 269ade178e4513d2873c4caf6e9aacafc9118097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 14:59:12 +0100 Subject: [PATCH 372/620] Fix tests --- .../Editor/CatchPlacementBlueprintTestScene.cs | 9 ++++----- .../Editor/ManiaPlacementBlueprintTestScene.cs | 6 ++---- .../Edit/Blueprints/GridPlacementBlueprint.cs | 6 +++--- .../HitCircles/HitCirclePlacementBlueprint.cs | 2 +- .../Sliders/Components/PathControlPointVisualiser.cs | 2 +- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 +++- .../Overlays/SkinEditor/SkinBlueprintContainer.cs | 7 ++++++- osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs | 2 +- osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs | 11 ++++------- 10 files changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index 0578010c25..a327e6d4c9 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -12,7 +12,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor contentContainer.Playfield.HitObjectContainer.Add(hitObject); } - protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) + protected override void UpdatePlacementTimeAndPosition() { - var result = base.SnapForBlueprint(blueprint); - result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; - return result; + var position = InputManager.CurrentState.Mouse.Position; + double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP; + CurrentBlueprint.UpdateTimeAndPosition(position, time); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 5e633c3161..0f913a6a7d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; @@ -47,12 +46,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor }); } - protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) + protected override void UpdatePlacementTimeAndPosition() { double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); var pos = column.ScreenSpacePositionAtTime(time); - - return new SnapResult(pos, time, column); + CurrentBlueprint.UpdateTimeAndPosition(pos, time); } protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index d3e780df9a..d9edc8dbd4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints base.OnDragEnd(e); } - public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { if (State.Value == Visibility.Hidden) - return new SnapResult(screenSpacePosition, referenceTime); + return new SnapResult(screenSpacePosition, fallbackTime); - var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, referenceTime); + var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); var pos = ToLocalSpace(result.ScreenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index dad7bd5f0e..53784a7f08 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) ?? composer?.TrySnapToPositionGrid(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index bac5f0101c..a3bb0b868a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -433,7 +433,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition) + SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime) ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index f5fe00e8b6..fd72f18b12 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) ?? composer?.TrySnapToPositionGrid(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index faed599fa5..7a93a26e45 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -225,11 +225,13 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition) + public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition, double? fallbackTime = null) { if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return null; + snapResult.Time ??= fallbackTime; + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index 8f831a6f18..df8cb33a71 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -113,7 +113,12 @@ namespace osu.Game.Overlays.SkinEditor protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) { - throw new System.NotImplementedException(); + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + var referenceBlueprint = blueprints.First().blueprint; + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + return SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, movePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); } /// diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 0bfda94f44..3119680272 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Edit /// Updates the time and position of this based on the provided snap information. /// /// The snap result information. - public void UpdateTimeAndPosition(SnapResult result) + protected void UpdateTimeAndPosition(SnapResult result) { if (PlacementActive == PlacementState.Waiting) { diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index aa8aff3adc..baf614d1c8 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); base.Content.Add(new MouseMovementInterceptor { - MouseMoved = updatePlacementTimeAndPosition, + MouseMoved = UpdatePlacementTimeAndPosition, }); } @@ -93,13 +93,10 @@ namespace osu.Game.Tests.Visual if (CurrentBlueprint.PlacementActive == PlacementBlueprint.PlacementState.Finished) ResetPlacement(); - updatePlacementTimeAndPosition(); + UpdatePlacementTimeAndPosition(); } - private void updatePlacementTimeAndPosition() => CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint)); - - protected virtual SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) => - new SnapResult(InputManager.CurrentState.Mouse.Position, null); + protected virtual void UpdatePlacementTimeAndPosition() => CurrentBlueprint.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, 0); public override void Add(Drawable drawable) { @@ -108,7 +105,7 @@ namespace osu.Game.Tests.Visual if (drawable is HitObjectPlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); + UpdatePlacementTimeAndPosition(); } } From 0164a2e4dca86fed1f3ea016eb9b1e4084eebba1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:02:31 +0900 Subject: [PATCH 373/620] Move pool item preparation / cleanup duties to `Carousel` --- osu.Game/Screens/SelectV2/Carousel.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6ff27c6198..648c2d090a 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -540,11 +540,13 @@ namespace osu.Game.Screens.SelectV2 { var c = (ICarouselPanel)panel; + // panel in the process of expiring, ignore it. + if (c.Item == null) + continue; + if (panel.Depth != c.DrawYPosition) scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition); - Debug.Assert(c.Item != null); - if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); @@ -631,7 +633,9 @@ namespace osu.Game.Screens.SelectV2 if (drawable is not ICarouselPanel carouselPanel) throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); + carouselPanel.DrawYPosition = item.CarouselYPosition; carouselPanel.Item = item; + scroll.Add(drawable); } @@ -650,6 +654,12 @@ namespace osu.Game.Screens.SelectV2 { panel.FinishTransforms(); panel.Expire(); + + var carouselPanel = (ICarouselPanel)panel; + + carouselPanel.Item = null; + carouselPanel.Selected.Value = false; + carouselPanel.KeyboardSelected.Value = false; } #endregion From 175eb82ccfed30fed57bbbeea02d687eb0a4794c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:02:47 +0900 Subject: [PATCH 374/620] Split out beatmaps and set panels into two separate classes --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 10 +- .../TestSceneBeatmapCarouselV2Selection.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 20 ++- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- ...eatmapCarouselPanel.cs => BeatmapPanel.cs} | 58 +++------ osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 114 ++++++++++++++++++ 7 files changed, 158 insertions(+), 50 deletions(-) rename osu.Game/Screens/SelectV2/{BeatmapCarouselPanel.cs => BeatmapPanel.cs} (69%) create mode 100644 osu.Game/Screens/SelectV2/BeatmapSetPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 3aa9f60181..4c85cf8fcd 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); - protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 748831bf7b..3a516ea762 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -56,16 +56,16 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForDrawablePanels(); AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -83,11 +83,11 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 305774b7d3..3c42969d8c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - BeatmapCarouselPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 630f7b6583..bb13c7449d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -141,14 +141,28 @@ namespace osu.Game.Screens.SelectV2 #region Drawable pooling - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); + private readonly DrawablePool setPanelPool = new DrawablePool(100); private void setupPools() { - AddInternal(carouselPanelPool); + AddInternal(beatmapPanelPool); + AddInternal(setPanelPool); } - protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + protected override Drawable GetDrawableForDisplay(CarouselItem item) + { + switch (item.Model) + { + case BeatmapInfo: + return beatmapPanelPool.Get(); + + case BeatmapSetInfo: + return setPanelPool.Get(); + } + + throw new InvalidOperationException(); + } #endregion } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 4f0767048a..0658263a8c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.SelectV2 { newItems.Add(new CarouselItem(b.BeatmapSet!) { - DrawHeight = 80, + DrawHeight = BeatmapSetPanel.HEIGHT, IsGroupSelectionTarget = true }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs similarity index 69% rename from osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs rename to osu.Game/Screens/SelectV2/BeatmapPanel.cs index 398ec7bf4c..4a9e406def 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -16,22 +16,25 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel + public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel { [Resolved] private BeatmapCarousel carousel { get; set; } = null!; private Box activationFlash = null!; - private Box background = null!; private OsuSpriteText text = null!; [BackgroundDependencyLoader] private void load() { + Size = new Vector2(500, CarouselItem.DEFAULT_HEIGHT); + Masking = true; + InternalChildren = new Drawable[] { - background = new Box + new Box { + Colour = Color4.Aqua.Darken(5), Alpha = 0.8f, RelativeSizeAxes = Axes.Both, }, @@ -69,63 +72,40 @@ namespace osu.Game.Screens.SelectV2 }); } - protected override void FreeAfterUse() - { - base.FreeAfterUse(); - Item = null; - Selected.Value = false; - KeyboardSelected.Value = false; - } - protected override void PrepareForUse() { base.PrepareForUse(); Debug.Assert(Item != null); + var beatmap = (BeatmapInfo)Item.Model; - DrawYPosition = Item.CarouselYPosition; - - Size = new Vector2(500, Item.DrawHeight); - Masking = true; - - background.Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5); - text.Text = getTextFor(Item.Model); + text.Text = $"Difficulty: {beatmap.DifficultyName} ({beatmap.StarRating:N1}*)"; this.FadeInFromZero(500, Easing.OutQuint); } - private string getTextFor(object item) - { - switch (item) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return "unknown"; - } - protected override bool OnClick(ClickEvent e) { - if (carousel.CurrentSelection == Item!.Model) - carousel.TryActivateSelection(); - else + if (carousel.CurrentSelection != Item!.Model) + { carousel.CurrentSelection = Item!.Model; + return true; + } + + carousel.TryActivateSelection(); return true; } + #region ICarouselPanel + public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } - public void Activated() - { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); - } + public void Activated() => activationFlash.FadeOutFromOne(500, Easing.OutQuint); + + #endregion } } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs new file mode 100644 index 0000000000..0b95f94365 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + private Box activationFlash = null!; + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(500, HEIGHT); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.Yellow.Darken(5), + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + Debug.Assert(Item.IsGroupSelectionTarget); + + var beatmapSetInfo = (BeatmapSetInfo)Item.Model; + + text.Text = $"{beatmapSetInfo.Metadata}"; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From da762384f8450c709c8319ec2a82ba32d29528f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 20:20:18 +0900 Subject: [PATCH 375/620] Fix breakage from reordering co-reliant variable sets (and guard against it) --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 0b47d8ed85..c20d461526 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -72,17 +72,17 @@ namespace osu.Game.Screens.Play track = beatmap.Track; - StartTime = findEarliestStartTime(); GameplayStartTime = gameplayStartTime; + StartTime = findEarliestStartTime(gameplayStartTime, beatmap); } - private double findEarliestStartTime() + private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap beatmap) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. // start with the originally provided latest time (if before zero). - double time = Math.Min(0, GameplayStartTime); + double time = Math.Min(0, gameplayStartTime); // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. From 589035c5348aa16c586c7d28ae04cc598e3410c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 12:34:05 +0100 Subject: [PATCH 376/620] Simplify code --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7a93a26e45..2a7ec79e55 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -230,8 +230,6 @@ namespace osu.Game.Rulesets.Osu.Edit if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return null; - snapResult.Time ??= fallbackTime; - if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; @@ -244,8 +242,9 @@ namespace osu.Game.Rulesets.Osu.Edit // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // the time value if the proposed positions are roughly the same. (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); - if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) - snapResult.Time = distanceSnappedTime; + snapResult.Time = Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1) + ? distanceSnappedTime + : fallbackTime; return snapResult; } From b04144df5489465e59b5305f65cd8b450f84fbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 12:50:46 +0100 Subject: [PATCH 377/620] Fix behavioural change in interaction between grid & distance snap --- .../HitCircles/HitCirclePlacementBlueprint.cs | 9 +++++---- .../Components/PathControlPointVisualiser.cs | 15 ++++++++++----- .../Sliders/SliderPlacementBlueprint.cs | 9 +++++---- .../Edit/OsuBlueprintContainer.cs | 6 +++++- .../Edit/OsuHitObjectComposer.cs | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 53784a7f08..0e1ede4d4c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -52,10 +52,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) - ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) - ?? composer?.TrySnapToPositionGrid(screenSpacePosition) - ?? new SnapResult(screenSpacePosition, fallbackTime); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(screenSpacePosition, fallbackTime); UpdateTimeAndPosition(result); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index a3bb0b868a..189bb005a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -9,6 +9,7 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -48,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] + [CanBeNull] private OsuHitObjectComposer positionSnapProvider { get; set; } [Resolved(CanBeNull = true)] @@ -433,14 +435,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime) - ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) - ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); - Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; + var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newHeadPosition, oldStartTime); + + Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position; hitObject.Position += movementDelta; - hitObject.StartTime = result?.Time ?? hitObject.StartTime; + hitObject.StartTime = result.Time ?? hitObject.StartTime; for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index fd72f18b12..2d38e83b2e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -108,10 +108,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) - ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) - ?? composer?.TrySnapToPositionGrid(screenSpacePosition) - ?? new SnapResult(screenSpacePosition, fallbackTime); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(screenSpacePosition, fallbackTime); UpdateTimeAndPosition(result); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 235368e552..5eff95adec 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -60,7 +60,11 @@ namespace osu.Game.Rulesets.Osu.Edit Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; // Retrieve a snapped position. - var result = Composer.TrySnapToDistanceGrid(movePosition) ?? Composer.TrySnapToPositionGrid(movePosition) ?? new SnapResult(movePosition, null); + var result = Composer.TrySnapToNearbyObjects(movePosition); + result ??= Composer.TrySnapToDistanceGrid(movePosition); + if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(movePosition, null); var referenceBlueprint = blueprints.First().blueprint; bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 2a7ec79e55..194276baf9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition) + public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null) { if (rectangularGridSnapToggle.Value != TernaryState.True) return null; From daec91f61d5410ad2ab879443aea0ce7a757c01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 13:05:38 +0100 Subject: [PATCH 378/620] Refactor further to avoid weird non-virtual common method --- .../Edit/Blueprints/BananaShowerPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/FruitPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 2 +- .../HitCircles/HitCirclePlacementBlueprint.cs | 2 +- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Blueprints/Spinners/SpinnerPlacementBlueprint.cs | 8 -------- .../Edit/Blueprints/HitPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs | 10 ++++++---- 9 files changed, 13 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 85b7624f1b..971c98cafd 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - base.UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (!(result.Time is double time)) return result; diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs index 83f75771ad..96cfbcb046 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints ? distanceSnapResult : gridSnapResult; - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; return result; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 359a952755..423f14b092 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (result.Playfield is Column col) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 0e1ede4d4c..93d79a50ab 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); return result; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 2d38e83b2e..1012578375 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); switch (state) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 6c4847cada..17d2dcd75c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners @@ -71,12 +70,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners ? Math.Max(HitObject.StartTime, EditorClock.CurrentTime) : Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime)); } - - public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) - { - var result = new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); - return result; - } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index b887fac42a..ce2a674e92 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); piece.Position = ToLocalSpace(result.ScreenSpacePosition); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); return result; } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index 7263c1ef2c..3d5c95e1e8 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (PlacementActive == PlacementState.Active) { diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 3119680272..6720540ec2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; +using osuTK; namespace osu.Game.Rulesets.Edit { @@ -87,14 +88,13 @@ namespace osu.Game.Rulesets.Edit } /// - /// Updates the time and position of this based on the provided snap information. + /// Updates the time and position of this . /// - /// The snap result information. - protected void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double time) { if (PlacementActive == PlacementState.Waiting) { - HitObject.StartTime = result.Time ?? EditorClock.CurrentTime; + HitObject.StartTime = time; if (HitObject is IHasComboInformation comboInformation) comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); @@ -129,6 +129,8 @@ namespace osu.Game.Rulesets.Edit for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); } + + return new SnapResult(screenSpacePosition, time); } /// From b0136f98a9f19bd61d3e0519cc200184908b31fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 14:24:16 +0100 Subject: [PATCH 379/620] Fix test failures --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 4c85cf8fcd..281be924a1 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); - protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); From 82c5f37c2cf005e330a5525892246d5a70358174 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 22:45:05 +0900 Subject: [PATCH 380/620] Remove selection animation on set panel --- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 0b95f94365..483869cad2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -56,11 +56,6 @@ namespace osu.Game.Screens.SelectV2 } }; - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); - }); - KeyboardSelected.BindValueChanged(value => { if (value.NewValue) From 55ab3c72f6acce20144a12ae6258138742969fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 15:15:50 +0100 Subject: [PATCH 381/620] Remove unused field --- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 483869cad2..37e8b88f71 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -24,7 +24,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapCarousel carousel { get; set; } = null!; - private Box activationFlash = null!; private OsuSpriteText text = null!; [BackgroundDependencyLoader] @@ -41,13 +40,6 @@ namespace osu.Game.Screens.SelectV2 Alpha = 0.8f, RelativeSizeAxes = Axes.Both, }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, text = new OsuSpriteText { Padding = new MarginPadding(5), From 79df094f17b65c5276d317bc84563d0afbe21e67 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 24 Jan 2025 23:20:04 +0900 Subject: [PATCH 382/620] Add unique samples for friend online/offline notifications --- osu.Game/Online/FriendPresenceNotifier.cs | 4 ++-- .../Notifications/FriendOfflineNotification.cs | 10 ++++++++++ .../Overlays/Notifications/FriendOnlineNotification.cs | 10 ++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Overlays/Notifications/FriendOfflineNotification.cs create mode 100644 osu.Game/Overlays/Notifications/FriendOnlineNotification.cs diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 75b487384a..229ad4f734 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -167,7 +167,7 @@ namespace osu.Game.Online APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; - notifications.Post(new SimpleNotification + notifications.Post(new FriendOnlineNotification { Transient = true, IsImportant = false, @@ -204,7 +204,7 @@ namespace osu.Game.Online return; } - notifications.Post(new SimpleNotification + notifications.Post(new FriendOfflineNotification { Transient = true, IsImportant = false, diff --git a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs new file mode 100644 index 0000000000..147fd4ba6f --- /dev/null +++ b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Notifications +{ + public partial class FriendOfflineNotification : SimpleNotification + { + public override string PopInSampleName => "UI/notification-friend-offline"; + } +} diff --git a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs new file mode 100644 index 0000000000..6a5cf3b517 --- /dev/null +++ b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Notifications +{ + public partial class FriendOnlineNotification : SimpleNotification + { + public override string PopInSampleName => "UI/notification-friend-online"; + } +} From 354126b7f7684a052389fff61715118a6fe3d885 Mon Sep 17 00:00:00 2001 From: ThePooN Date: Fri, 24 Jan 2025 18:14:55 +0100 Subject: [PATCH 383/620] =?UTF-8?q?=F0=9F=94=A7=20Specify=20we're=20not=20?= =?UTF-8?q?using=20non-exempt=20encryption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- osu.iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 70747fc9c8..120e8caecc 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -153,6 +153,8 @@ Editor + ITSAppUsesNonExemptEncryption + LSApplicationCategoryType public.app-category.music-games LSSupportsOpeningDocumentsInPlace From dac7d21302cbd9b7094ba7fc0d5989a9f254d46d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:12:44 -0500 Subject: [PATCH 384/620] Be explicit on nullability in `RequiresPortraitOrientation` Co-authored-by: Dean Herbert --- osu.Game/Screens/Play/Player.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b3274766b2..92c483b24a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,7 +68,16 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; - public override bool RequiresPortraitOrientation => DrawableRuleset?.RequiresPortraitOrientation == true; + public override bool RequiresPortraitOrientation + { + get + { + if (!LoadedBeatmapSuccessfully) + return false; + + return DrawableRuleset!.RequiresPortraitOrientation; + } + } protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; From 8151c3095ddfc6389516054c4ae66ead80f5b605 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:21:20 -0500 Subject: [PATCH 385/620] Revert unnecessary inheritance Everyone is right, too much inheritance and polymorphism backfires very badly. --- .../Skinning/TestSceneColumnHitObjectArea.cs | 10 +++--- .../Mods/ManiaModWithPlayfieldCover.cs | 4 +-- osu.Game.Rulesets.Mania/UI/Column.cs | 6 +++- .../UI/Components/ColumnHitObjectArea.cs | 15 ++++---- .../Components/HitPositionPaddedContainer.cs | 35 ++++++------------- osu.Game.Rulesets.Mania/UI/Stage.cs | 12 ++++--- 6 files changed, 39 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs index d4bbc8acb6..bf67d2d6a9 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(new HitObjectContainer()) + Child = new ColumnHitObjectArea { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Child = new HitObjectContainer(), } }, new ColumnTestContainer(1, ManiaAction.Key2) { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(new HitObjectContainer()) + Child = new ColumnHitObjectArea { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Child = new HitObjectContainer(), } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index b6e6ee7481..1bc16112c5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -5,9 +5,9 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { HitObjectContainer hoc = column.HitObjectContainer; - ColumnHitObjectArea hocParent = (ColumnHitObjectArea)hoc.Parent!; + Container hocParent = (Container)hoc.Parent!; hocParent.Remove(hoc, false); hocParent.Add(CreateCover(hoc).With(c => diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 99d952ef1f..81f4d79281 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -67,7 +67,11 @@ namespace osu.Game.Rulesets.Mania.UI Width = COLUMN_WIDTH; hitPolicy = new OrderedHitPolicy(HitObjectContainer); - HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }; + HitObjectArea = new ColumnHitObjectArea + { + RelativeSizeAxes = Axes.Both, + Child = HitObjectContainer, + }; } [Resolved] diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 2d719ef764..46b6ef86f7 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly Drawable hitTarget; - public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer) + protected override Container Content => content; + + private readonly Container content; + + public ColumnHitObjectArea() { AddRangeInternal(new[] { UnderlayElements = new Container { RelativeSizeAxes = Axes.Both, - Depth = 2, }, hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) { RelativeSizeAxes = Axes.X, - Depth = 1 + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, }, Explosions = new Container { RelativeSizeAxes = Axes.Both, - Depth = -1, } }); } diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index f550e3b241..ae91be1c67 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -4,52 +4,37 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class HitPositionPaddedContainer : SkinReloadableDrawable + public partial class HitPositionPaddedContainer : Container { protected readonly IBindable Direction = new Bindable(); - public HitPositionPaddedContainer(Drawable child) - { - InternalChild = child; - } - - internal void Add(Drawable drawable) - { - base.AddInternal(drawable); - } - - internal void Remove(Drawable drawable, bool disposeImmediately = true) - { - base.RemoveInternal(drawable, disposeImmediately); - } + [Resolved] + private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { Direction.BindTo(scrollingInfo.Direction); - Direction.BindValueChanged(onDirectionChanged, true); - } + Direction.BindValueChanged(onDirectionChanged); + + skin.SourceChanged += onSkinChanged; - protected override void SkinChanged(ISkinSource skin) - { - base.SkinChanged(skin); UpdateHitPosition(); } - private void onDirectionChanged(ValueChangedEvent direction) - { - UpdateHitPosition(); - } + private void onSkinChanged() => UpdateHitPosition(); + private void onDirectionChanged(ValueChangedEvent direction) => UpdateHitPosition(); protected virtual void UpdateHitPosition() { - float hitPosition = CurrentSkin.GetConfig( + float hitPosition = skin.GetConfig( new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value ?? Stage.HIT_TARGET_POSITION; diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 2d73e7bcbe..fb9671c14d 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI Width = 1366, // Bar lines should only be masked on the vertical axis BypassAutoSizeAxes = Axes.Both, Masking = true, - Child = barLineContainer = new HitPositionPaddedContainer(HitObjectContainer) + Child = barLineContainer = new HitPositionPaddedContainer { Name = "Bar lines", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, + Child = HitObjectContainer, } }, columnFlow = new ColumnFlow(definition) @@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Both }, - new HitPositionPaddedContainer(judgements = new JudgementContainer - { - RelativeSizeAxes = Axes.Both, - }) + new HitPositionPaddedContainer { RelativeSizeAxes = Axes.Both, + Child = judgements = new JudgementContainer + { + RelativeSizeAxes = Axes.Both, + }, }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } From ffc37cece0483c9bcdea0962abc8bfbe1dd9b0f1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:50:54 -0500 Subject: [PATCH 386/620] Avoid extra unnecessary DI Co-authored-by: Dean Herbert --- .../UI/DrawableManiaRuleset.cs | 2 +- .../UI/ManiaPlayfieldAdjustmentContainer.cs | 11 ++++------ .../Edit/DrawableEditorRulesetWrapper.cs | 22 +++++++++---------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 - 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index a186d9aa7d..e33cf092c3 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI /// The scroll time. public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(this); protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index b0203643b0..feb75b9f1e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; @@ -16,8 +15,11 @@ namespace osu.Game.Rulesets.Mania.UI private readonly DrawSizePreservingFillContainer scalingContainer; - public ManiaPlayfieldAdjustmentContainer() + private readonly DrawableManiaRuleset drawableManiaRuleset; + + public ManiaPlayfieldAdjustmentContainer(DrawableManiaRuleset drawableManiaRuleset) { + this.drawableManiaRuleset = drawableManiaRuleset; InternalChild = scalingContainer = new DrawSizePreservingFillContainer { Anchor = Anchor.Centre, @@ -30,9 +32,6 @@ namespace osu.Game.Rulesets.Mania.UI }; } - [Resolved] - private DrawableRuleset drawableRuleset { get; set; } = null!; - protected override void Update() { base.Update(); @@ -40,8 +39,6 @@ namespace osu.Game.Rulesets.Mania.UI float aspectRatio = DrawWidth / DrawHeight; bool isPortrait = aspectRatio < 1f; - var drawableManiaRuleset = (DrawableManiaRuleset)drawableRuleset; - if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { // Scale playfield up by 25% to become playable on mobile devices, diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 573eb8c42f..174b278d89 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -19,16 +19,16 @@ namespace osu.Game.Rulesets.Edit internal partial class DrawableEditorRulesetWrapper : CompositeDrawable where TObject : HitObject { - public Playfield Playfield => DrawableRuleset.Playfield; + public Playfield Playfield => drawableRuleset.Playfield; - public readonly DrawableRuleset DrawableRuleset; + private readonly DrawableRuleset drawableRuleset; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { - DrawableRuleset = drawableRuleset; + this.drawableRuleset = drawableRuleset; RelativeSizeAxes = Axes.Both; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - DrawableRuleset.FrameStablePlayback = false; + drawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } @@ -67,27 +67,27 @@ namespace osu.Game.Rulesets.Edit private void regenerateAutoplay() { - var autoplayMod = DrawableRuleset.Mods.OfType().Single(); - DrawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(DrawableRuleset.Beatmap, DrawableRuleset.Mods)); + var autoplayMod = drawableRuleset.Mods.OfType().Single(); + drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods)); } private void addHitObject(HitObject hitObject) { - DrawableRuleset.AddHitObject((TObject)hitObject); - DrawableRuleset.Playfield.PostProcess(); + drawableRuleset.AddHitObject((TObject)hitObject); + drawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - DrawableRuleset.RemoveHitObject((TObject)hitObject); - DrawableRuleset.Playfield.PostProcess(); + drawableRuleset.RemoveHitObject((TObject)hitObject); + drawableRuleset.Playfield.PostProcess(); } public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; - public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => DrawableRuleset.CreatePlayfieldAdjustmentContainer(); + public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => drawableRuleset.CreatePlayfieldAdjustmentContainer(); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 8882d55b42..15b60114af 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -133,7 +133,6 @@ namespace osu.Game.Rulesets.Edit if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset) dependencies.CacheAs(scrollingRuleset.ScrollingInfo); - dependencies.CacheAs(drawableRulesetWrapper.DrawableRuleset); dependencies.CacheAs(Playfield); InternalChildren = new[] From bb7daae08063fb06e16934b7542a14b65a1f189d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 19:08:01 -0500 Subject: [PATCH 387/620] Simplify orientation locking code magnificently --- osu.Game/Mobile/OrientationManager.cs | 30 ++++++++++----------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs index 0f9b56d434..964b40e2af 100644 --- a/osu.Game/Mobile/OrientationManager.cs +++ b/osu.Game/Mobile/OrientationManager.cs @@ -50,30 +50,22 @@ namespace osu.Game.Mobile bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; - if (lockCurrentOrientation) + if (IsTablet) { - if (!IsTablet && lockToPortraitOnPhone && !IsCurrentOrientationPortrait) - SetAllowedOrientations(GameOrientation.Portrait); - else if (!IsTablet && !lockToPortraitOnPhone && IsCurrentOrientationPortrait) - SetAllowedOrientations(GameOrientation.Landscape); - else - { - // if the orientation is already portrait/landscape according to the game's specifications, - // then use Locked instead of Portrait/Landscape to handle the case where the device is - // in landscape-left or reverse-portrait. + if (lockCurrentOrientation) SetAllowedOrientations(GameOrientation.Locked); - } - - return; + else + SetAllowedOrientations(null); } - - if (!IsTablet && lockToPortraitOnPhone) + else { - SetAllowedOrientations(GameOrientation.Portrait); - return; + if (lockToPortraitOnPhone) + SetAllowedOrientations(GameOrientation.Portrait); + else if (lockCurrentOrientation) + SetAllowedOrientations(GameOrientation.Locked); + else + SetAllowedOrientations(null); } - - SetAllowedOrientations(null); } /// From c18128e97419ea1f7c9a4086f1b19de8f9c6022e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:01:12 -0500 Subject: [PATCH 388/620] Remove `OrientationManager` and the entire mobile namespace --- osu.Android/AndroidOrientationManager.cs | 39 ------------ osu.Android/OsuGameAndroid.cs | 32 +++++++++- osu.Game/Mobile/GameOrientation.cs | 34 ----------- osu.Game/Mobile/OrientationManager.cs | 77 ------------------------ osu.Game/OsuGame.cs | 63 ++++++++----------- osu.Game/Utils/MobileUtils.cs | 49 +++++++++++++++ osu.iOS/IOSOrientationManager.cs | 41 ------------- osu.iOS/OsuGameIOS.cs | 33 +++++++++- 8 files changed, 138 insertions(+), 230 deletions(-) delete mode 100644 osu.Android/AndroidOrientationManager.cs delete mode 100644 osu.Game/Mobile/GameOrientation.cs delete mode 100644 osu.Game/Mobile/OrientationManager.cs create mode 100644 osu.Game/Utils/MobileUtils.cs delete mode 100644 osu.iOS/IOSOrientationManager.cs diff --git a/osu.Android/AndroidOrientationManager.cs b/osu.Android/AndroidOrientationManager.cs deleted file mode 100644 index 76d2fc24cb..0000000000 --- a/osu.Android/AndroidOrientationManager.cs +++ /dev/null @@ -1,39 +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 Android.Content.PM; -using Android.Content.Res; -using osu.Framework.Allocation; -using osu.Game.Mobile; - -namespace osu.Android -{ - public partial class AndroidOrientationManager : OrientationManager - { - [Resolved] - private OsuGameActivity gameActivity { get; set; } = null!; - - protected override bool IsCurrentOrientationPortrait => gameActivity.Resources!.Configuration!.Orientation == Orientation.Portrait; - protected override bool IsTablet => gameActivity.IsTablet; - - protected override void SetAllowedOrientations(GameOrientation? orientation) - => gameActivity.RequestedOrientation = orientation == null ? gameActivity.DefaultOrientation : toScreenOrientation(orientation.Value); - - private static ScreenOrientation toScreenOrientation(GameOrientation orientation) - { - if (orientation == GameOrientation.Locked) - return ScreenOrientation.Locked; - - if (orientation == GameOrientation.Portrait) - return ScreenOrientation.Portrait; - - if (orientation == GameOrientation.Landscape) - return ScreenOrientation.Landscape; - - if (orientation == GameOrientation.FullPortrait) - return ScreenOrientation.SensorPortrait; - - return ScreenOrientation.SensorLandscape; - } - } -} diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 4143c8cae6..0f2451f0a0 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -3,11 +3,13 @@ using System; using Android.App; +using Android.Content.PM; using Microsoft.Maui.Devices; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Game; +using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; @@ -71,7 +73,35 @@ namespace osu.Android protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new AndroidOrientationManager(), Add); + UserPlayingState.BindValueChanged(_ => updateOrientation()); + } + + protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen) + { + base.ScreenChanged(current, newScreen); + + if (newScreen != null) + updateOrientation(); + } + + private void updateOrientation() + { + var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet); + + switch (orientation) + { + case MobileUtils.Orientation.Locked: + gameActivity.RequestedOrientation = ScreenOrientation.Locked; + break; + + case MobileUtils.Orientation.Portrait: + gameActivity.RequestedOrientation = ScreenOrientation.Portrait; + break; + + case MobileUtils.Orientation.Default: + gameActivity.RequestedOrientation = gameActivity.DefaultOrientation; + break; + } } public override void SetHost(GameHost host) diff --git a/osu.Game/Mobile/GameOrientation.cs b/osu.Game/Mobile/GameOrientation.cs deleted file mode 100644 index 0022c8fefb..0000000000 --- a/osu.Game/Mobile/GameOrientation.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Mobile -{ - public enum GameOrientation - { - /// - /// Lock the game orientation. - /// - Locked, - - /// - /// Display the game in regular portrait orientation. - /// - Portrait, - - /// - /// Display the game in landscape-right orientation. - /// - Landscape, - - /// - /// Display the game in landscape-right/landscape-left orientations. - /// - FullLandscape, - - /// - /// Display the game in portrait/portrait-upside-down orientations. - /// This is exclusive to tablet mobile devices. - /// - FullPortrait, - } -} diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs deleted file mode 100644 index 964b40e2af..0000000000 --- a/osu.Game/Mobile/OrientationManager.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Screens.Play; - -namespace osu.Game.Mobile -{ - /// - /// A that manages the device orientations a game can display in. - /// - public abstract partial class OrientationManager : Component - { - /// - /// Whether the current orientation of the game is portrait. - /// - protected abstract bool IsCurrentOrientationPortrait { get; } - - /// - /// Whether the mobile device is considered a tablet. - /// - protected abstract bool IsTablet { get; } - - [Resolved] - private OsuGame game { get; set; } = null!; - - [Resolved] - private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; - - private IBindable requiresPortraitOrientation = null!; - private IBindable localUserPlaying = null!; - - protected override void LoadComplete() - { - base.LoadComplete(); - - requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); - requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); - - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(_ => updateOrientations()); - - updateOrientations(); - } - - private void updateOrientations() - { - bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; - - if (IsTablet) - { - if (lockCurrentOrientation) - SetAllowedOrientations(GameOrientation.Locked); - else - SetAllowedOrientations(null); - } - else - { - if (lockToPortraitOnPhone) - SetAllowedOrientations(GameOrientation.Portrait); - else if (lockCurrentOrientation) - SetAllowedOrientations(GameOrientation.Locked); - else - SetAllowedOrientations(null); - } - } - - /// - /// Sets the allowed orientations the device can rotate to. - /// - /// The allowed orientations, or null to return back to default. - protected abstract void SetAllowedOrientations(GameOrientation? orientation); - } -} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index cc6613da89..89aba818a3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -173,25 +173,14 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); - /// - /// On mobile phones, this specifies whether the device should be set and locked to portrait orientation. - /// Tablet devices are unaffected by this property. - /// - /// - /// Implementations can be viewed in mobile projects. - /// - public IBindable RequiresPortraitOrientation => requiresPortraitOrientation; - - private readonly Bindable requiresPortraitOrientation = new BindableBool(); - /// /// Whether the back button is currently displayed. /// private readonly IBindable backButtonVisibility = new Bindable(); - IBindable ILocalUserPlayInfo.PlayingState => playingState; + IBindable ILocalUserPlayInfo.PlayingState => UserPlayingState; - private readonly Bindable playingState = new Bindable(); + protected readonly Bindable UserPlayingState = new Bindable(); protected OsuScreenStack ScreenStack; @@ -319,7 +308,7 @@ namespace osu.Game protected override UserInputManager CreateUserInputManager() { var userInputManager = base.CreateUserInputManager(); - (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(playingState); + (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(UserPlayingState); return userInputManager; } @@ -414,7 +403,7 @@ namespace osu.Game // Transfer any runtime changes back to configuration file. SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); - playingState.BindValueChanged(p => + UserPlayingState.BindValueChanged(p => { BeatmapManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; SkinManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; @@ -1555,7 +1544,7 @@ namespace osu.Game GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false; } - private void screenChanged(IScreen current, IScreen newScreen) + protected virtual void ScreenChanged([CanBeNull] IOsuScreen current, [CanBeNull] IOsuScreen newScreen) { SentrySdk.ConfigureScope(scope => { @@ -1571,10 +1560,10 @@ namespace osu.Game switch (current) { case Player player: - player.PlayingState.UnbindFrom(playingState); + player.PlayingState.UnbindFrom(UserPlayingState); // reset for sanity. - playingState.Value = LocalUserPlayingState.NotPlaying; + UserPlayingState.Value = LocalUserPlayingState.NotPlaying; break; } @@ -1591,7 +1580,7 @@ namespace osu.Game break; case Player player: - player.PlayingState.BindTo(playingState); + player.PlayingState.BindTo(UserPlayingState); break; default: @@ -1599,32 +1588,32 @@ namespace osu.Game break; } - if (current is IOsuScreen currentOsuScreen) + if (current != null) { - backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); - OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); - configUserActivity.UnbindFrom(currentOsuScreen.Activity); + backButtonVisibility.UnbindFrom(current.BackButtonVisibility); + OverlayActivationMode.UnbindFrom(current.OverlayActivationMode); + configUserActivity.UnbindFrom(current.Activity); } - if (newScreen is IOsuScreen newOsuScreen) + // Bind to new screen. + if (newScreen != null) { - backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); - OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); - configUserActivity.BindTo(newOsuScreen.Activity); + backButtonVisibility.BindTo(newScreen.BackButtonVisibility); + OverlayActivationMode.BindTo(newScreen.OverlayActivationMode); + configUserActivity.BindTo(newScreen.Activity); - GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; + // Handle various configuration updates based on new screen settings. + GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newScreen.HideMenuCursorOnNonMouseInput; - requiresPortraitOrientation.Value = newOsuScreen.RequiresPortraitOrientation; - - if (newOsuScreen.HideOverlaysOnEnter) + if (newScreen.HideOverlaysOnEnter) CloseAllOverlays(); else Toolbar.Show(); - if (newOsuScreen.ShowFooter) + if (newScreen.ShowFooter) { BackButton.Hide(); - ScreenFooter.SetButtons(newOsuScreen.CreateFooterButtons()); + ScreenFooter.SetButtons(newScreen.CreateFooterButtons()); ScreenFooter.Show(); } else @@ -1632,16 +1621,16 @@ namespace osu.Game ScreenFooter.SetButtons(Array.Empty()); ScreenFooter.Hide(); } - } - skinEditor.SetTarget((OsuScreen)newScreen); + skinEditor.SetTarget((OsuScreen)newScreen); + } } - private void screenPushed(IScreen lastScreen, IScreen newScreen) => screenChanged(lastScreen, newScreen); + private void screenPushed(IScreen lastScreen, IScreen newScreen) => ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen); private void screenExited(IScreen lastScreen, IScreen newScreen) { - screenChanged(lastScreen, newScreen); + ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen); if (newScreen == null) Exit(); diff --git a/osu.Game/Utils/MobileUtils.cs b/osu.Game/Utils/MobileUtils.cs new file mode 100644 index 0000000000..6e59efb71c --- /dev/null +++ b/osu.Game/Utils/MobileUtils.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Screens; +using osu.Game.Screens.Play; + +namespace osu.Game.Utils +{ + public static class MobileUtils + { + /// + /// Determines the correct state which a mobile device should be put into for the given information. + /// + /// Information about whether the user is currently playing. + /// The current screen which the user is at. + /// Whether the user is playing on a mobile tablet device instead of a phone. + public static Orientation GetOrientation(ILocalUserPlayInfo userPlayInfo, IOsuScreen currentScreen, bool isTablet) + { + bool lockCurrentOrientation = userPlayInfo.PlayingState.Value == LocalUserPlayingState.Playing; + bool lockToPortraitOnPhone = currentScreen.RequiresPortraitOrientation; + + if (lockToPortraitOnPhone && !isTablet) + return Orientation.Portrait; + + if (lockCurrentOrientation) + return Orientation.Locked; + + return Orientation.Default; + } + + public enum Orientation + { + /// + /// Lock the game orientation. + /// + Locked, + + /// + /// Lock the game to portrait orientation (does not include upside-down portrait). + /// + Portrait, + + /// + /// Use the application's default settings. + /// + Default, + } + } +} diff --git a/osu.iOS/IOSOrientationManager.cs b/osu.iOS/IOSOrientationManager.cs deleted file mode 100644 index 6d5bb990c2..0000000000 --- a/osu.iOS/IOSOrientationManager.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Mobile; -using UIKit; - -namespace osu.iOS -{ - public partial class IOSOrientationManager : OrientationManager - { - private readonly AppDelegate appDelegate; - - protected override bool IsCurrentOrientationPortrait => appDelegate.CurrentOrientation.IsPortrait(); - protected override bool IsTablet => UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; - - public IOSOrientationManager(AppDelegate appDelegate) - { - this.appDelegate = appDelegate; - } - - protected override void SetAllowedOrientations(GameOrientation? orientation) - => appDelegate.Orientations = orientation == null ? null : toUIInterfaceOrientationMask(orientation.Value); - - private UIInterfaceOrientationMask toUIInterfaceOrientationMask(GameOrientation orientation) - { - if (orientation == GameOrientation.Locked) - return (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); - - if (orientation == GameOrientation.Portrait) - return UIInterfaceOrientationMask.Portrait; - - if (orientation == GameOrientation.Landscape) - return UIInterfaceOrientationMask.LandscapeRight; - - if (orientation == GameOrientation.FullPortrait) - return UIInterfaceOrientationMask.Portrait | UIInterfaceOrientationMask.PortraitUpsideDown; - - return UIInterfaceOrientationMask.Landscape; - } - } -} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index ed47a1e8b8..a5a42c1e66 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -8,8 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.iOS; using osu.Framework.Platform; using osu.Game; +using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using UIKit; namespace osu.iOS { @@ -28,7 +30,36 @@ namespace osu.iOS protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new IOSOrientationManager(appDelegate), Add); + UserPlayingState.BindValueChanged(_ => updateOrientation()); + } + + protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen) + { + base.ScreenChanged(current, newScreen); + + if (newScreen != null) + updateOrientation(); + } + + private void updateOrientation() + { + bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; + var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad); + + switch (orientation) + { + case MobileUtils.Orientation.Locked: + appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); + break; + + case MobileUtils.Orientation.Portrait: + appDelegate.Orientations = UIInterfaceOrientationMask.Portrait; + break; + + case MobileUtils.Orientation.Default: + appDelegate.Orientations = null; + break; + } } protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 4d7b0710275f2e41317d988f516322cc2c06c45f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 23:58:56 -0500 Subject: [PATCH 389/620] Specifiy second-factor authentication code text box with `Code` type --- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 3022233e9c..506cb70d09 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Graphics; @@ -62,6 +63,7 @@ namespace osu.Game.Overlays.Login }, codeTextBox = new OsuTextBox { + InputProperties = new TextInputProperties(TextInputType.Code), PlaceholderText = "Enter code", RelativeSizeAxes = Axes.X, TabbableContentContainer = this, From 836a9e5c2518dab2d130e6148c17568f02bcd819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 09:40:20 +0100 Subject: [PATCH 390/620] Remove explicit beatmap set from list of bundled beatmap sets --- osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 3aa34a5580..61aa9ef921 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -345,7 +345,6 @@ namespace osu.Game.Beatmaps.Drawables "1971951 James Landino - Shiba Paradise.osz", "1972518 Toromaru - Sleight of Hand.osz", "1982302 KINEMA106 - INVITE.osz", - "1983475 KNOWER - The Government Knows.osz", "2010165 Junk - Yellow Smile (bms edit).osz", "2022737 Andora - Euphoria (feat. WaMi).osz", "2025023 tephe - Genjitsu Escape.osz", From e24af4b341d36e13c1b897a43a5d7d2d13fd94c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 09:40:53 +0100 Subject: [PATCH 391/620] Add inline comments for sets that are not marked FA but should be --- osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 61aa9ef921..16e143f9dc 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -292,7 +292,7 @@ namespace osu.Game.Beatmaps.Drawables "1407228 II-L - VANGUARD-1.osz", "1422686 II-L - VANGUARD-2.osz", "1429217 Street - Phi.osz", - "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", + "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/157 "1447478 Cres. - End Time.osz", "1449942 m108 - Crescent Sakura.osz", "1463778 MuryokuP - A tree without a branch.osz", @@ -336,8 +336,8 @@ namespace osu.Game.Beatmaps.Drawables "1854710 Blaster & Extra Terra - Spacecraft (Cut Ver.).osz", "1859322 Hino Isuka - Delightness Brightness.osz", "1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz", - "1884578 Neko Hacker - People People feat. Nanahira.osz", - "1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", + "1884578 Neko Hacker - People People feat. Nanahira.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/266 + "1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/108 "1905582 KINEMA106 - Fly Away (Cut Ver.).osz", "1934686 ARForest - Rainbow Magic!!.osz", "1963076 METAROOM - S.N.U.F.F.Y.osz", From 01ae1a58f12d2268c3aa12cda499824cad0e184e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 10:25:22 +0100 Subject: [PATCH 392/620] Catch and display user-friendly errors regarding corrupted audio files Addresses lack of user feedback as indicated by https://github.com/ppy/osu/issues/31693. --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 408292c2d0..2eda232b9f 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; @@ -97,7 +98,17 @@ namespace osu.Game.Screens.Edit.Setup if (!source.Exists) return false; - var tagSource = TagLib.File.Create(source.FullName); + TagLib.File? tagSource; + + try + { + tagSource = TagLib.File.Create(source.FullName); + } + catch (Exception e) + { + Logger.Error(e, "The selected audio track appears to be corrupted. Please select another one."); + return false; + } changeResource(source, applyToAllDifficulties, @"audio", metadata => metadata.AudioFile, From be9c96c041b4dc9179b00a1ec13e3eaf0f7b414f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 10:25:53 +0100 Subject: [PATCH 393/620] Fix infinite loop when switching audio tracks fails on an existing beatmap Bit ugly, but appears to work in practice... --- .../Screens/Edit/Setup/ResourcesSection.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 2eda232b9f..cab6eddaa4 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -203,16 +203,40 @@ namespace osu.Game.Screens.Edit.Setup editor?.Save(); } + // to avoid scaring users, both background & audio choosers use fake `FileInfo`s with user-friendly filenames + // when displaying an imported beatmap rather than the actual SHA-named file in storage. + // however, that means that when a background or audio file is chosen that is broken or doesn't exist on disk when switching away from the fake files, + // the rollback could enter an infinite loop, because the fake `FileInfo`s *also* don't exist on disk - at least not in the fake location they indicate. + // to circumvent this issue, just allow rollback to proceed always without actually running any of the change logic to ensure visual consistency. + // note that this means that `Change{BackgroundImage,AudioTrack}()` are required to not have made any modifications to the beatmap files + // (or at least cleaned them up properly themselves) if they return `false`. + private bool rollingBackBackgroundChange; + private bool rollingBackAudioChange; + private void backgroundChanged(ValueChangedEvent file) { + if (rollingBackBackgroundChange) + return; + if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue, backgroundChooser.ApplyToAllDifficulties.Value)) + { + rollingBackBackgroundChange = true; backgroundChooser.Current.Value = file.OldValue; + rollingBackBackgroundChange = false; + } } private void audioTrackChanged(ValueChangedEvent file) { + if (rollingBackAudioChange) + return; + if (file.NewValue == null || !ChangeAudioTrack(file.NewValue, audioTrackChooser.ApplyToAllDifficulties.Value)) + { + rollingBackAudioChange = true; audioTrackChooser.Current.Value = file.OldValue; + rollingBackAudioChange = false; + } } } } From ca979d35423265017435e4cd44b3c3e5c3a92630 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Jan 2025 18:32:12 +0900 Subject: [PATCH 394/620] Adjust xmldocs --- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 8142873fd5..499e84ce80 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -38,13 +38,13 @@ namespace osu.Game.Online.Multiplayer public MatchUserState? MatchState { get; set; } /// - /// Any ruleset applicable only to the local user. + /// If not-null, a local override for this user's ruleset selection. /// [Key(5)] public int? RulesetId; /// - /// Any beatmap applicable only to the local user. + /// If not-null, a local override for this user's beatmap selection. /// [Key(6)] public int? BeatmapId; From fc73037d9f0373f8914e389efc1202900580195f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Jan 2025 18:45:52 +0900 Subject: [PATCH 395/620] Add pill displaying current freestyle status --- .../Lounge/Components/DrawableRoom.cs | 5 ++ .../Lounge/Components/FreeStyleStatusPill.cs | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index c39ca347c7..7bc0b612f1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -169,6 +169,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, + new FreeStyleStatusPill(Room) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, endDateInfo = new EndDateInfo(Room) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs new file mode 100644 index 0000000000..1f3149d788 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Online.Rooms; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class FreeStyleStatusPill : OnlinePlayPill + { + private readonly Room room; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); + + public FreeStyleStatusPill(Room room) + { + this.room = room; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Pill.Background.Alpha = 1; + Pill.Background.Colour = colours.Yellow; + + TextFlow.Text = "Freestyle"; + TextFlow.Colour = Color4.Black; + + room.PropertyChanged += onRoomPropertyChanged; + updateFreeStyleStatus(); + } + + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(Room.CurrentPlaylistItem): + case nameof(Room.Playlist): + updateFreeStyleStatus(); + break; + } + } + + private void updateFreeStyleStatus() + { + PlaylistItem? currentItem = room.Playlist.GetCurrentItem() ?? room.CurrentPlaylistItem; + Alpha = currentItem?.FreeStyle == true ? 1 : 0; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + room.PropertyChanged -= onRoomPropertyChanged; + } + } +} From bb8f58f6d6db344499f50e64f1463cc8ca84e35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 12:28:53 +0100 Subject: [PATCH 396/620] Work around rare sharpcompress failure to extract certain archives Closes https://github.com/ppy/osu/issues/31667. See https://github.com/ppy/osu/issues/31667#issuecomment-2615483900 for explanation. For whatever it's worth, I see rejecting this change and telling upstream to fix it as an equally agreeable outcome, but after I spent an hour+ tracking this down, writing this diff was nothing in comparison. --- osu.Game/IO/Archives/ZipArchiveReader.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 6bb2a314e7..8b9ecc7462 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Text; using Microsoft.Toolkit.HighPerformance; +using osu.Framework.Extensions; using osu.Framework.IO.Stores; using SharpCompress.Archives.Zip; using SharpCompress.Common; @@ -54,12 +55,22 @@ namespace osu.Game.IO.Archives if (entry == null) return null; - var owner = MemoryAllocator.Default.Allocate((int)entry.Size); - using (Stream s = entry.OpenEntryStream()) - s.ReadExactly(owner.Memory.Span); + { + if (entry.Size > 0) + { + var owner = MemoryAllocator.Default.Allocate((int)entry.Size); + s.ReadExactly(owner.Memory.Span); + return new MemoryOwnerMemoryStream(owner); + } - return new MemoryOwnerMemoryStream(owner); + // due to a sharpcompress bug (https://github.com/adamhathcock/sharpcompress/issues/88), + // in rare instances the `ZipArchiveEntry` will not contain a correct `Size` but instead report 0. + // this would lead to the block above reading nothing, and the game basically seeing an archive full of empty files. + // since the bug is years old now, and this is a rather rare situation anyways (reported once in years), + // work around this locally by falling back to reading as many bytes as possible and using a standard non-pooled memory stream. + return new MemoryStream(s.ReadAllRemainingBytesToArray()); + } } public override void Dispose() From 1aa1137b09cc649b1e99d2f0eb18b846feb249ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:22:51 +0900 Subject: [PATCH 397/620] Remove "Accuracy" and "Stack Leniency" from osu!catch editor setup --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 3 +- .../Edit/Setup/CatchDifficultySection.cs | 125 ++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 5bd7a0ff00..d253b9893f 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Catch.Edit; +using osu.Game.Rulesets.Catch.Edit.Setup; using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; @@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch public override IEnumerable CreateEditorSetupSections() => [ new MetadataSection(), - new DifficultySection(), + new CatchDifficultySection(), new FillFlowContainer { AutoSizeAxes = Axes.Y, diff --git a/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs b/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs new file mode 100644 index 0000000000..6ae60c4d24 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Rulesets.Catch.Edit.Setup +{ + public partial class CatchDifficultySection : SetupSection + { + private FormSliderBar circleSizeSlider { get; set; } = null!; + private FormSliderBar healthDrainSlider { get; set; } = null!; + private FormSliderBar approachRateSlider { get; set; } = null!; + private FormSliderBar baseVelocitySlider { get; set; } = null!; + private FormSliderBar tickRateSlider { get; set; } = null!; + + public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + circleSizeSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsCs, + HintText = EditorSetupStrings.CircleSizeDescription, + Current = new BindableFloat(Beatmap.Difficulty.CircleSize) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + healthDrainSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.DrainRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + approachRateSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsAr, + HintText = EditorSetupStrings.ApproachRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + baseVelocitySlider = new FormSliderBar + { + Caption = EditorSetupStrings.BaseVelocity, + HintText = EditorSetupStrings.BaseVelocityDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) + { + Default = 1.4, + MinValue = 0.4, + MaxValue = 3.6, + Precision = 0.01f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + tickRateSlider = new FormSliderBar + { + Caption = EditorSetupStrings.TickRate, + HintText = EditorSetupStrings.TickRateDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) + { + Default = 1, + MinValue = 1, + MaxValue = 4, + Precision = 1, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + }; + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + } + + private void updateValues() + { + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; + Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; + + Beatmap.UpdateAllHitObjects(); + Beatmap.SaveState(); + } + } +} From 017d38af3d0f8af13155cf049e27c5371fd6f3bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:29:17 +0900 Subject: [PATCH 398/620] Change friend online notifications' icon and colours The previous choices made it seem like potentially destructive actions were being performed. I've gone with neutral colours and more suiting icons to attempt to avoid this. --- Addresses concerns in https://github.com/ppy/osu/discussions/31621#discussioncomment-11948377. I chose this design even though it wasn't the #1 most popular because I personally feel that using green/red doesn't work great for these. --- osu.Game/Online/FriendPresenceNotifier.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 75b487384a..bc2bf344b0 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -171,9 +171,9 @@ namespace osu.Game.Online { Transient = true, IsImportant = false, - Icon = FontAwesome.Solid.UserPlus, + Icon = FontAwesome.Solid.User, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Green, + IconColour = colours.GrayD, Activated = () => { if (singleUser != null) @@ -208,9 +208,9 @@ namespace osu.Game.Online { Transient = true, IsImportant = false, - Icon = FontAwesome.Solid.UserMinus, + Icon = FontAwesome.Solid.UserSlash, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Red + IconColour = colours.Gray3 }); offlineAlertQueue.Clear(); From a3a08832b41fd9a46c50142eb0f05b0720a20f78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:31:51 +0900 Subject: [PATCH 399/620] Add keywords to make lighten-during-breaks setting discoverable to stable users See https://github.com/ppy/osu/discussions/31671. --- .../Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs index 048351b4cb..830ccec279 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs @@ -35,7 +35,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = GameplaySettingsStrings.LightenDuringBreaks, - Current = config.GetBindable(OsuSetting.LightenDuringBreaks) + Current = config.GetBindable(OsuSetting.LightenDuringBreaks), + Keywords = new[] { "dim", "level" } }, new SettingsCheckbox { From 6c4b4166ac21324abf6b467c87a166c032cb5933 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:09:42 +0900 Subject: [PATCH 400/620] Add fail cases to unstable rate incremental testing --- osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 03dc91b5d4..18ac5b4964 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -36,6 +36,10 @@ namespace osu.Game.Tests.NonVisual.Ranking .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) .ToList(); + // Add some red herrings + events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null)); + events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null)); + HitEventExtensions.UnstableRateCalculationResult result = null; for (int i = 0; i < events.Count; i++) @@ -57,6 +61,10 @@ namespace osu.Game.Tests.NonVisual.Ranking .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) .ToList(); + // Add some red herrings + events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null)); + events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null)); + HitEventExtensions.UnstableRateCalculationResult result = null; for (int i = 0; i < events.Count; i++) From d8ec3b77e4b29ff95f90873bf0ae83fb4041c460 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:06:13 +0900 Subject: [PATCH 401/620] Fix incremental unstable rate calculation not matching expectations The `EventCount` variable wasn't factoring in that some results do not affect unstable rate. It would therefore become more incorrect as the play continued. Closes https://github.com/ppy/osu/issues/31712. --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 269342460f..fed0c3b51b 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -28,11 +28,12 @@ namespace osu.Game.Rulesets.Scoring result ??= new UnstableRateCalculationResult(); // Handle rewinding in the simplest way possible. - if (hitEvents.Count < result.EventCount + 1) + if (hitEvents.Count < result.LastProcessedIndex + 1) result = new UnstableRateCalculationResult(); - for (int i = result.EventCount; i < hitEvents.Count; i++) + for (int i = result.LastProcessedIndex + 1; i < hitEvents.Count; i++) { + result.LastProcessedIndex = i; HitEvent e = hitEvents[i]; if (!AffectsUnstableRate(e)) @@ -84,6 +85,11 @@ namespace osu.Game.Rulesets.Scoring /// public class UnstableRateCalculationResult { + /// + /// The last result index processed. For internal incremental calculation use. + /// + public int LastProcessedIndex = -1; + /// /// Total events processed. For internal incremental calculation use. /// From fd1d90cbd93ffc0cc9be5c3d18035e78613e0d06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 11:55:35 +0900 Subject: [PATCH 402/620] Update framework Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 7ae16b8b70..d2682fc024 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 7b0a027d39..309a9dcc87 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4c83ef83eeb9b372fdbc31a624a6688f0428dca2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:34:03 +0900 Subject: [PATCH 403/620] 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 bfb6e51f93..bc4c42484d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From bf40f071eb0d17fa54957ac9c3436afe12749506 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:40:52 +0900 Subject: [PATCH 404/620] Code quality pass --- osu.Game/Online/FriendPresenceNotifier.cs | 89 +++++++++++-------- .../FriendOfflineNotification.cs | 10 --- .../Notifications/FriendOnlineNotification.cs | 10 --- 3 files changed, 52 insertions(+), 57 deletions(-) delete mode 100644 osu.Game/Overlays/Notifications/FriendOfflineNotification.cs delete mode 100644 osu.Game/Overlays/Notifications/FriendOnlineNotification.cs diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 229ad4f734..70d532dfeb 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -31,15 +31,6 @@ namespace osu.Game.Online [Resolved] private MetadataClient metadataClient { get; set; } = null!; - [Resolved] - private ChannelManager channelManager { get; set; } = null!; - - [Resolved] - private ChatOverlay chatOverlay { get; set; } = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -165,26 +156,7 @@ namespace osu.Game.Online return; } - APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; - - notifications.Post(new FriendOnlineNotification - { - Transient = true, - IsImportant = false, - Icon = FontAwesome.Solid.UserPlus, - Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Green, - Activated = () => - { - if (singleUser != null) - { - channelManager.OpenPrivateChannel(singleUser); - chatOverlay.Show(); - } - - return true; - } - }); + notifications.Post(new FriendOnlineNotification(onlineAlertQueue)); onlineAlertQueue.Clear(); lastOnlineAlertTime = null; @@ -204,17 +176,60 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOfflineNotification - { - Transient = true, - IsImportant = false, - Icon = FontAwesome.Solid.UserMinus, - Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Red - }); + notifications.Post(new FriendOfflineNotification(offlineAlertQueue)); offlineAlertQueue.Clear(); lastOfflineAlertTime = null; } + + public partial class FriendOnlineNotification : SimpleNotification + { + private readonly ICollection users; + + public FriendOnlineNotification(ICollection users) + { + this.users = users; + Transient = true; + IsImportant = false; + Icon = FontAwesome.Solid.User; + Text = $"Online: {string.Join(@", ", users.Select(u => u.Username))}"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, ChannelManager channelManager, ChatOverlay chatOverlay) + { + IconColour = colours.GrayD; + Activated = () => + { + APIUser? singleUser = users.Count == 1 ? users.Single() : null; + + if (singleUser != null) + { + channelManager.OpenPrivateChannel(singleUser); + chatOverlay.Show(); + } + + return true; + }; + } + + public override string PopInSampleName => "UI/notification-friend-online"; + } + + private partial class FriendOfflineNotification : SimpleNotification + { + public FriendOfflineNotification(ICollection users) + { + Transient = true; + IsImportant = false; + Icon = FontAwesome.Solid.UserSlash; + Text = $"Offline: {string.Join(@", ", users.Select(u => u.Username))}"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) => IconColour = colours.Gray3; + + public override string PopInSampleName => "UI/notification-friend-offline"; + } } } diff --git a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs deleted file mode 100644 index 147fd4ba6f..0000000000 --- a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays.Notifications -{ - public partial class FriendOfflineNotification : SimpleNotification - { - public override string PopInSampleName => "UI/notification-friend-offline"; - } -} diff --git a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs deleted file mode 100644 index 6a5cf3b517..0000000000 --- a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays.Notifications -{ - public partial class FriendOnlineNotification : SimpleNotification - { - public override string PopInSampleName => "UI/notification-friend-online"; - } -} From e8d20fb4020083d85a31a16492ae3c92d2b6382d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 18:16:04 +0900 Subject: [PATCH 405/620] Fix skin `SourceChanged` event never being unbound --- .../UI/Components/HitPositionPaddedContainer.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index ae91be1c67..72daf4b21d 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; @@ -22,15 +23,12 @@ namespace osu.Game.Rulesets.Mania.UI.Components private void load(IScrollingInfo scrollingInfo) { Direction.BindTo(scrollingInfo.Direction); - Direction.BindValueChanged(onDirectionChanged); + Direction.BindValueChanged(_ => UpdateHitPosition(), true); skin.SourceChanged += onSkinChanged; - - UpdateHitPosition(); } private void onSkinChanged() => UpdateHitPosition(); - private void onDirectionChanged(ValueChangedEvent direction) => UpdateHitPosition(); protected virtual void UpdateHitPosition() { @@ -42,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components ? new MarginPadding { Top = hitPosition } : new MarginPadding { Bottom = hitPosition }; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skin.IsNotNull()) + skin.SourceChanged -= onSkinChanged; + } } } From d3f9804ef1de2ee9e9f75df9321183bb9439da8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 18:45:02 +0900 Subject: [PATCH 406/620] Combine more methods to simplify flow --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 9915560a95..3e0d94e992 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -277,7 +277,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay); @@ -346,7 +346,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnSuspending(ScreenTransitionEvent e) { // Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state. - updateBeatmap(); + updateSpecifics(); onLeaving(); base.OnSuspending(e); @@ -356,7 +356,6 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.OnResuming(e); - updateBeatmap(); updateSpecifics(); beginHandlingTrack(); @@ -446,8 +445,6 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - updateUserMods(); - updateBeatmap(); updateSpecifics(); if (!item.AllowedMods.Any()) @@ -471,42 +468,26 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleSection?.Hide(); } - private void updateUserMods() + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + // Remove any user mods that are no longer allowed. - Ruleset rulesetInstance = GetGameplayRuleset().CreateInstance(); Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); - - if (newUserMods.SequenceEqual(UserMods.Value)) - return; - - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); - } - - private void updateBeatmap() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; + if (!newUserMods.SequenceEqual(UserMods.Value)) + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; - } - private void updateSpecifics() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; - - var rulesetInstance = GetGameplayRuleset().CreateInstance(); Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); - Ruleset.Value = GetGameplayRuleset(); if (UserStyleDisplayContainer != null) From 05200e897057c06dc7a4e9ad0cedfbccaf6c9738 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:05:28 +0900 Subject: [PATCH 407/620] Add missing `partial` --- .../Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs index 1f3149d788..1c0135fb89 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class FreeStyleStatusPill : OnlinePlayPill + public partial class FreeStyleStatusPill : OnlinePlayPill { private readonly Room room; From c70ff1108527a58903067eaf39cfa5a7d778b486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:06:14 +0900 Subject: [PATCH 408/620] Remove new bindables from `RoomSubScreen` --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 23 +++---------------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 11 +-------- .../Playlists/PlaylistsRoomSubScreen.cs | 16 +++++++++---- 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 3e0d94e992..d9e22efec5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -69,18 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); - /// - /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), - /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. - /// - public readonly Bindable UserBeatmap = new Bindable(); - - /// - /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), - /// a non-null value indicates a local ruleset selection. - /// - public readonly Bindable UserRuleset = new Bindable(); - [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -273,8 +261,6 @@ namespace osu.Game.Screens.OnlinePlay.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -507,14 +493,11 @@ namespace osu.Game.Screens.OnlinePlay.Match } } - protected virtual APIMod[] GetGameplayMods() - => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); - protected virtual RulesetInfo GetGameplayRuleset() - => Rulesets.GetRuleset(UserRuleset.Value?.OnlineID ?? SelectedItem.Value!.RulesetID)!; + protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!; - protected virtual IBeatmapInfo GetGameplayBeatmap() - => UserBeatmap.Value ?? SelectedItem.Value!.Beatmap; + protected virtual IBeatmapInfo GetGameplayBeatmap() => SelectedItem.Value!.Beatmap; protected abstract void OpenStyleSelection(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b5fe8bf631..7f946a6997 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -388,7 +388,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - updateCurrentItem(); + SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); addItemButton.Alpha = localUserCanAddItem ? 1 : 0; @@ -400,15 +400,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private bool localUserCanAddItem => client.IsHost || Room.QueueMode != QueueMode.HostOnly; - private void updateCurrentItem() - { - Debug.Assert(client.Room != null); - - SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); - UserBeatmap.Value = client.LocalUser?.BeatmapId == null ? null : UserBeatmap.Value; - UserRuleset.Value = client.LocalUser?.RulesetId == null ? null : UserRuleset.Value; - } - private void handleRoomLost() => Schedule(() => { Logger.Log($"{this} exiting due to loss of room or connection"); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index d1b90b18e7..2c74767f42 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -11,11 +11,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -46,6 +48,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; + private readonly Bindable userBeatmap = new Bindable(); + private readonly Bindable userRuleset = new Bindable(); + public PlaylistsRoomSubScreen(Room room) : base(room, false) // Editing is temporarily not allowed. { @@ -78,10 +83,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void onSelectedItemChanged(ValueChangedEvent item) { // Simplest for now. - UserBeatmap.Value = null; - UserRuleset.Value = null; + userBeatmap.Value = null; + userRuleset.Value = null; } + protected override IBeatmapInfo GetGameplayBeatmap() => userBeatmap.Value ?? base.GetGameplayBeatmap(); + protected override RulesetInfo GetGameplayRuleset() => userRuleset.Value ?? base.GetGameplayRuleset(); + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -313,8 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists this.Push(new PlaylistsRoomStyleSelect(Room, item) { - Beatmap = { BindTarget = UserBeatmap }, - Ruleset = { BindTarget = UserRuleset } + Beatmap = { BindTarget = userBeatmap }, + Ruleset = { BindTarget = userRuleset } }); } From facc9a4dc3d2e0d8b3741cddd0536e7775817d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:15:28 +0900 Subject: [PATCH 409/620] Fix reference hashsets getting emptied before used --- osu.Game/Online/FriendPresenceNotifier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 70d532dfeb..a73c705d76 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -156,7 +156,7 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOnlineNotification(onlineAlertQueue)); + notifications.Post(new FriendOnlineNotification(onlineAlertQueue.ToArray())); onlineAlertQueue.Clear(); lastOnlineAlertTime = null; @@ -176,7 +176,7 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOfflineNotification(offlineAlertQueue)); + notifications.Post(new FriendOfflineNotification(offlineAlertQueue.ToArray())); offlineAlertQueue.Clear(); lastOfflineAlertTime = null; From 07bff222008fb729e9a17824dd0e17a206df1c88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:30:55 +0900 Subject: [PATCH 410/620] Fix delay before difficulty panel displays fully --- .../Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 6 ++++-- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 13a282dd52..249cad8ca3 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { new Drawable[] { - new DrawableRoomPlaylistItem(playlistItem) + new DrawableRoomPlaylistItem(playlistItem, true) { RelativeSizeAxes = Axes.X, AllowReordering = false, diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 7a773bb116..1e1e79d256 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay public bool IsSelectedItem => SelectedItem.Value?.ID == Item.ID; - private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; + private readonly DelayedLoadWrapper onScreenLoader; private readonly IBindable valid = new Bindable(); private IBeatmapInfo? beatmap; @@ -120,9 +120,11 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] private ManageCollectionsDialog? manageCollectionsDialog { get; set; } - public DrawableRoomPlaylistItem(PlaylistItem item) + public DrawableRoomPlaylistItem(PlaylistItem item, bool loadImmediately = false) : base(item) { + onScreenLoader = new DelayedLoadWrapper(Empty, timeBeforeLoad: loadImmediately ? 0 : 500) { RelativeSizeAxes = Axes.Both }; + Item = item; valid.BindTo(item.Valid); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index d9e22efec5..8f286c0f16 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -484,7 +484,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (gameplayItem.Equals(currentItem)) return; - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, AllowEditing = true, From a6814d1a8a5c86fae6eb0a587c13c2196b523434 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:48:04 +0900 Subject: [PATCH 411/620] Make multiplayer change room settings more obvious as to what it does "Edit" felt really weird. --- osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 0c993f4abf..0eb8cc3706 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -49,8 +49,10 @@ namespace osu.Game.Screens.OnlinePlay.Match ButtonsContainer.Add(editButton = new PurpleRoundedButton { RelativeSizeAxes = Axes.Y, - Size = new Vector2(100, 1), - Text = CommonStrings.ButtonsEdit, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(120, 0.7f), + Text = "Change settings", Action = () => OnEdit?.Invoke() }); } From e8d0d2a1d9ebaa21bd408a8976902b40827e6cc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:56:36 +0900 Subject: [PATCH 412/620] Combine more methods to simplify flow futher --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 81 +++++++++---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 3 - 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 8f286c0f16..428f0e9ed8 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -259,8 +259,8 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + SelectedItem.BindValueChanged(_ => updateSpecifics()); + UserMods.BindValueChanged(_ => updateSpecifics()); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -426,35 +426,7 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - protected void OnSelectedItemChanged() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) - return; - - updateSpecifics(); - - if (!item.AllowedMods.Any()) - { - UserModsSection?.Hide(); - UserModsSelectOverlay.Hide(); - UserModsSelectOverlay.IsValidMod = _ => false; - } - else - { - UserModsSection?.Show(); - - var rulesetInstance = GetGameplayRuleset().CreateInstance(); - var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } - - if (item.FreeStyle) - UserStyleSection?.Show(); - else - UserStyleSection?.Hide(); - } - - private void updateSpecifics() + private void updateSpecifics() => Scheduler.AddOnce(() => { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; @@ -476,22 +448,41 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - if (UserStyleDisplayContainer != null) + if (!item.AllowedMods.Any()) { - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; + UserModsSection?.Hide(); + UserModsSelectOverlay.Hide(); + UserModsSelectOverlay.IsValidMod = _ => false; } - } + else + { + UserModsSection?.Show(); + UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + + if (item.FreeStyle) + { + UserStyleSection?.Show(); + + if (UserStyleDisplayContainer != null) + { + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) + { + AllowReordering = false, + AllowEditing = item.FreeStyle, + RequestEdit = _ => OpenStyleSelection() + }; + } + } + else + UserStyleSection?.Hide(); + }); protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7f946a6997..f882fb7f89 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -392,9 +392,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer addItemButton.Alpha = localUserCanAddItem ? 1 : 0; - // Forcefully update the selected item so that the user state is applied. - Scheduler.AddOnce(OnSelectedItemChanged); - Activity.Value = new UserActivity.InLobby(Room); } From bc930e8fd32eab12f1bcdf6e57236433ad7ebe40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 20:02:01 +0900 Subject: [PATCH 413/620] Minimal clean-up to get things bearable I plan to do a full refactor of `RoomSubScreen` at first opportunity. --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 428f0e9ed8..c9c9c3eca7 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -49,18 +50,18 @@ namespace osu.Game.Screens.OnlinePlay.Match /// A container that provides controls for selection of user mods. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserModsSection; + protected Drawable UserModsSection = null!; /// /// A container that provides controls for selection of the user style. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserStyleSection; + protected Drawable UserStyleSection = null!; /// /// A container that will display the user's style. /// - protected Container? UserStyleDisplayContainer; + protected Container UserStyleDisplayContainer = null!; private Sample? sampleStart; @@ -448,40 +449,44 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - if (!item.AllowedMods.Any()) + bool freeMod = item.AllowedMods.Any(); + bool freeStyle = item.FreeStyle; + + // For now, the game can never be in a state where freemod and freestyle are on at the same time. + // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. + Debug.Assert(!freeMod || !freeStyle); + + if (freeMod) { - UserModsSection?.Hide(); + UserModsSection.Show(); + UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + else + { + UserModsSection.Hide(); UserModsSelectOverlay.Hide(); UserModsSelectOverlay.IsValidMod = _ => false; } - else - { - UserModsSection?.Show(); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } - if (item.FreeStyle) + if (freeStyle) { - UserStyleSection?.Show(); + UserStyleSection.Show(); - if (UserStyleDisplayContainer != null) + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) - { - AllowReordering = false, - AllowEditing = item.FreeStyle, - RequestEdit = _ => OpenStyleSelection() - }; - } + AllowReordering = false, + AllowEditing = freeStyle, + RequestEdit = _ => OpenStyleSelection() + }; } else - UserStyleSection?.Hide(); + UserStyleSection.Hide(); }); protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); From ca7a36d3d6739d8aee75d937cd7544ea7a071983 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Jan 2025 23:32:44 +0900 Subject: [PATCH 414/620] Remove unused usings --- osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 0eb8cc3706..08bcf32edf 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps.Drawables; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Resources.Localisation.Web; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; From bad2959d5ba6f74d3ab76d32a6110ebccedde922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 08:54:42 +0100 Subject: [PATCH 415/620] Change mirror mod direction setting tooltip to hopefully be less confusing See https://github.com/ppy/osu/issues/29720, https://discord.com/channels/188630481301012481/188630652340404224/1334294048541904906. This removes the tooltip due to being zero or negative information, and also changes the description of the setting to not contain the word "mirror", which will hopefully quash the "this is where I would place a mirror to my screen to achieve what I want" interpretation which seems to be a minority interpretation. The first time this was complained about I figured this was probably a one guy issue, but now it's happened twice, and I never want to see this conversation again. --- osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs index 6d01808fb5..4af88caee4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Flip objects on the chosen axes."; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; - [SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")] + [SettingSource("Flipped axes")] public Bindable Reflection { get; } = new Bindable(); public void ApplyToHitObject(HitObject hitObject) From ec99fc114103f5fd2bc696bcf1bd75ad3bd37241 Mon Sep 17 00:00:00 2001 From: Marvin Helstein Date: Thu, 30 Jan 2025 10:15:16 +0200 Subject: [PATCH 416/620] Move `ApplySelectionOrder` override from `EditorBlueprintContainer` to `ComposeBlueprintContainer` --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 5 +++++ .../Edit/Compose/Components/EditorBlueprintContainer.cs | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index de1f589135..e82f6395d0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using Humanizer; @@ -52,6 +53,10 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); + protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => + base.ApplySelectionOrder(blueprints) + .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); + protected ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index f1811dd84f..e67644baaa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -126,10 +125,6 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => - base.ApplySelectionOrder(blueprints) - .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); - protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; protected override SelectionHandler CreateSelectionHandler() => new EditorSelectionHandler(); From 31c4461fbb1167d3a1c93910b0f5c4263ab348dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Oct 2024 11:04:39 +0200 Subject: [PATCH 417/620] Abstract out `WizardOverlay` for multi-step wizard type screens To be used in the editor, for the beatmap submission wizard. I've recently been on record for hating "abstract" as a rationale to do anything, but seeing this commit ~3 months after I originally made it, it still feels okay to do for me in this particular case. I think the abstraction is loose enough, makes sense from a code reuse and UX consistency standpoint, and doesn't seem to leak any particular implementation details. That said, it is both a huge diffstat and also potentially controversial, which is why I'm PRing first separately. --- .../Overlays/FirstRunSetup/ScreenBeatmaps.cs | 2 +- .../Overlays/FirstRunSetup/ScreenBehaviour.cs | 2 +- .../FirstRunSetup/ScreenImportFromStable.cs | 2 +- .../Overlays/FirstRunSetup/ScreenUIScale.cs | 2 +- .../Overlays/FirstRunSetup/ScreenWelcome.cs | 2 +- osu.Game/Overlays/FirstRunSetupOverlay.cs | 268 +--------------- osu.Game/Overlays/WizardOverlay.cs | 288 ++++++++++++++++++ ...FirstRunSetupScreen.cs => WizardScreen.cs} | 4 +- 8 files changed, 305 insertions(+), 265 deletions(-) create mode 100644 osu.Game/Overlays/WizardOverlay.cs rename osu.Game/Overlays/{FirstRunSetup/FirstRunSetupScreen.cs => WizardScreen.cs} (96%) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index da60951ab6..392b170ad2 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -21,7 +21,7 @@ using Realms; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))] - public partial class ScreenBeatmaps : FirstRunSetupScreen + public partial class ScreenBeatmaps : WizardScreen { private ProgressRoundedButton downloadBundledButton = null!; private ProgressRoundedButton downloadTutorialButton = null!; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs index d31ce7ea18..a583ba5f6b 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs @@ -20,7 +20,7 @@ using osu.Game.Overlays.Settings.Sections; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))] - public partial class ScreenBehaviour : FirstRunSetupScreen + public partial class ScreenBehaviour : WizardScreen { private SearchContainer searchContainer; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 5eb38b6e11..5bdcd8e850 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -31,7 +31,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))] - public partial class ScreenImportFromStable : FirstRunSetupScreen + public partial class ScreenImportFromStable : WizardScreen { private static readonly Vector2 button_size = new Vector2(400, 50); diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index d0eefa55c5..fc64408775 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -32,7 +32,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))] - public partial class ScreenUIScale : FirstRunSetupScreen + public partial class ScreenUIScale : WizardScreen { [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs index 68c6c78986..93cf555bc9 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs @@ -23,7 +23,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.WelcomeTitle))] - public partial class ScreenWelcome : FirstRunSetupScreen + public partial class ScreenWelcome : WizardScreen { [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 1a302cf51d..c2e89f32f1 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -1,38 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Framework.Screens; -using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays.FirstRunSetup; -using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Screens; -using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; namespace osu.Game.Overlays { [Cached] - public partial class FirstRunSetupOverlay : ShearedOverlayContainer + public partial class FirstRunSetupOverlay : WizardOverlay { [Resolved] private IPerformFromScreenRunner performer { get; set; } = null!; @@ -43,28 +27,8 @@ namespace osu.Game.Overlays [Resolved] private OsuConfigManager config { get; set; } = null!; - private ScreenStack? stack; - - public ShearedButton? NextButton => DisplayedFooterContent?.NextButton; - private readonly Bindable showFirstRunSetup = new Bindable(); - private int? currentStepIndex; - - /// - /// The currently displayed screen, if any. - /// - public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen; - - private readonly List steps = new List(); - - private Container screenContent = null!; - - private Container content = null!; - - private LoadingSpinner loading = null!; - private ScheduledDelegate? loadingShowDelegate; - public FirstRunSetupOverlay() : base(OverlayColourScheme.Purple) { @@ -73,67 +37,15 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuColour colours, LegacyImportManager? legacyImportManager) { - steps.Add(typeof(ScreenWelcome)); - steps.Add(typeof(ScreenUIScale)); - steps.Add(typeof(ScreenBeatmaps)); + AddStep(); + AddStep(); + AddStep(); if (legacyImportManager?.SupportsImportFromStable == true) - steps.Add(typeof(ScreenImportFromStable)); - steps.Add(typeof(ScreenBehaviour)); + AddStep(); + AddStep(); Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle; Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription; - - MainAreaContent.AddRange(new Drawable[] - { - content = new PopoverContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = 20 }, - Child = new GridContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(minSize: 640, maxSize: 800), - new Dimension(), - }, - Content = new[] - { - new[] - { - Empty(), - new InputBlockingContainer - { - Masking = true, - CornerRadius = 14, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background6, - }, - loading = new LoadingSpinner(), - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Vertical = 20 }, - Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, }, - }, - }, - }, - Empty(), - }, - } - } - }, - }); } protected override void LoadComplete() @@ -145,55 +57,6 @@ namespace osu.Game.Overlays if (showFirstRunSetup.Value) Show(); } - [Resolved] - private ScreenFooter footer { get; set; } = null!; - - public new FirstRunSetupFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FirstRunSetupFooterContent; - - public override VisibilityContainer CreateFooterContent() - { - var footerContent = new FirstRunSetupFooterContent - { - ShowNextStep = showNextStep, - }; - - footerContent.OnLoadComplete += _ => updateButtons(); - return footerContent; - } - - public override bool OnBackButton() - { - if (currentStepIndex == 0) - return false; - - Debug.Assert(stack != null); - - stack.CurrentScreen.Exit(); - currentStepIndex--; - - updateButtons(); - return true; - } - - public override bool OnPressed(KeyBindingPressEvent e) - { - if (!e.Repeat) - { - switch (e.Action) - { - case GlobalAction.Select: - DisplayedFooterContent?.NextButton.TriggerClick(); - return true; - - case GlobalAction.Back: - footer.BackButton.TriggerClick(); - return false; - } - } - - return base.OnPressed(e); - } - public override void Show() { // if we are valid for display, only do so after reaching the main menu. @@ -207,24 +70,11 @@ namespace osu.Game.Overlays }, new[] { typeof(MainMenu) }); } - protected override void PopIn() - { - base.PopIn(); - - content.ScaleTo(0.99f) - .ScaleTo(1, 400, Easing.OutQuint); - - if (currentStepIndex == null) - showFirstStep(); - } - protected override void PopOut() { base.PopOut(); - content.ScaleTo(0.99f, 400, Easing.OutQuint); - - if (currentStepIndex != null) + if (CurrentStepIndex != null) { notificationOverlay.Post(new SimpleNotification { @@ -237,112 +87,14 @@ namespace osu.Game.Overlays }, }); } - else - { - stack?.FadeOut(100) - .Expire(); - } } - private void showFirstStep() + protected override void ShowNextStep() { - Debug.Assert(currentStepIndex == null); + base.ShowNextStep(); - screenContent.Child = stack = new ScreenStack - { - RelativeSizeAxes = Axes.Both, - }; - - currentStepIndex = -1; - showNextStep(); - } - - private void showNextStep() - { - Debug.Assert(currentStepIndex != null); - Debug.Assert(stack != null); - - currentStepIndex++; - - if (currentStepIndex < steps.Count) - { - var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value])!; - - loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200); - nextScreen.OnLoadComplete += _ => - { - loadingShowDelegate?.Cancel(); - loading.Hide(); - }; - - stack.Push(nextScreen); - } - else - { + if (CurrentStepIndex == null) showFirstRunSetup.Value = false; - currentStepIndex = null; - Hide(); - } - - updateButtons(); - } - - private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps); - - public partial class FirstRunSetupFooterContent : VisibilityContainer - { - public ShearedButton NextButton { get; private set; } = null!; - - public Action? ShowNextStep; - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - RelativeSizeAxes = Axes.Both; - - InternalChild = NextButton = new ShearedButton(0) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 12f }, - RelativeSizeAxes = Axes.X, - Width = 1, - Text = FirstRunSetupOverlayStrings.GetStarted, - DarkerColour = colourProvider.Colour2, - LighterColour = colourProvider.Colour1, - Action = () => ShowNextStep?.Invoke(), - }; - } - - public void UpdateButtons(int? currentStep, IReadOnlyList steps) - { - NextButton.Enabled.Value = currentStep != null; - - if (currentStep == null) - return; - - bool isFirstStep = currentStep == 0; - bool isLastStep = currentStep == steps.Count - 1; - - if (isFirstStep) - NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; - else - { - NextButton.Text = isLastStep - ? CommonStrings.Finish - : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})"); - } - } - - protected override void PopIn() - { - this.FadeIn(); - } - - protected override void PopOut() - { - this.Delay(400).FadeOut(); - } } } } diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs new file mode 100644 index 0000000000..38701efc96 --- /dev/null +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -0,0 +1,288 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Framework.Screens; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osu.Game.Overlays.Mods; +using osu.Game.Screens.Footer; + +namespace osu.Game.Overlays +{ + public partial class WizardOverlay : ShearedOverlayContainer + { + private ScreenStack? stack; + + public ShearedButton? NextButton => DisplayedFooterContent?.NextButton; + + protected int? CurrentStepIndex { get; private set; } + + /// + /// The currently displayed screen, if any. + /// + public WizardScreen? CurrentScreen => (WizardScreen?)stack?.CurrentScreen; + + private readonly List steps = new List(); + + private Container screenContent = null!; + + private Container content = null!; + + private LoadingSpinner loading = null!; + private ScheduledDelegate? loadingShowDelegate; + + protected WizardOverlay(OverlayColourScheme scheme) + : base(scheme) + { + } + + [BackgroundDependencyLoader] + private void load() + { + MainAreaContent.AddRange(new Drawable[] + { + content = new PopoverContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = 20 }, + Child = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(minSize: 640, maxSize: 800), + new Dimension(), + }, + Content = new[] + { + new[] + { + Empty(), + new InputBlockingContainer + { + Masking = true, + CornerRadius = 14, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background6, + }, + loading = new LoadingSpinner(), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = 20 }, + Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, }, + }, + }, + }, + Empty(), + }, + } + } + }, + }); + } + + [Resolved] + private ScreenFooter footer { get; set; } = null!; + + public new WizardFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as WizardFooterContent; + + public override VisibilityContainer CreateFooterContent() + { + var footerContent = new WizardFooterContent + { + ShowNextStep = ShowNextStep, + }; + + footerContent.OnLoadComplete += _ => updateButtons(); + return footerContent; + } + + public override bool OnBackButton() + { + if (CurrentStepIndex == 0) + return false; + + Debug.Assert(stack != null); + + stack.CurrentScreen.Exit(); + CurrentStepIndex--; + + updateButtons(); + return true; + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + if (!e.Repeat) + { + switch (e.Action) + { + case GlobalAction.Select: + DisplayedFooterContent?.NextButton.TriggerClick(); + return true; + + case GlobalAction.Back: + footer.BackButton.TriggerClick(); + return false; + } + } + + return base.OnPressed(e); + } + + protected override void PopIn() + { + base.PopIn(); + + content.ScaleTo(0.99f) + .ScaleTo(1, 400, Easing.OutQuint); + + if (CurrentStepIndex == null) + showFirstStep(); + } + + protected override void PopOut() + { + base.PopOut(); + + content.ScaleTo(0.99f, 400, Easing.OutQuint); + + if (CurrentStepIndex == null) + { + stack?.FadeOut(100) + .Expire(); + } + } + + protected void AddStep() + where T : WizardScreen + { + steps.Add(typeof(T)); + } + + private void showFirstStep() + { + Debug.Assert(CurrentStepIndex == null); + + screenContent.Child = stack = new ScreenStack + { + RelativeSizeAxes = Axes.Both, + }; + + CurrentStepIndex = -1; + ShowNextStep(); + } + + protected virtual void ShowNextStep() + { + Debug.Assert(CurrentStepIndex != null); + Debug.Assert(stack != null); + + CurrentStepIndex++; + + if (CurrentStepIndex < steps.Count) + { + var nextScreen = (Screen)Activator.CreateInstance(steps[CurrentStepIndex.Value])!; + + loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200); + nextScreen.OnLoadComplete += _ => + { + loadingShowDelegate?.Cancel(); + loading.Hide(); + }; + + stack.Push(nextScreen); + } + else + { + CurrentStepIndex = null; + Hide(); + } + + updateButtons(); + } + + private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps); + + public partial class WizardFooterContent : VisibilityContainer + { + public ShearedButton NextButton { get; private set; } = null!; + + public Action? ShowNextStep; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + InternalChild = NextButton = new ShearedButton(0) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 12f }, + RelativeSizeAxes = Axes.X, + Width = 1, + Text = FirstRunSetupOverlayStrings.GetStarted, + DarkerColour = colourProvider.Colour2, + LighterColour = colourProvider.Colour1, + Action = () => ShowNextStep?.Invoke(), + }; + } + + public void UpdateButtons(int? currentStep, IReadOnlyList steps) + { + NextButton.Enabled.Value = currentStep != null; + + if (currentStep == null) + return; + + bool isFirstStep = currentStep == 0; + bool isLastStep = currentStep == steps.Count - 1; + + if (isFirstStep) + NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + else + { + NextButton.Text = isLastStep + ? CommonStrings.Finish + : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})"); + } + } + + protected override void PopIn() + { + this.FadeIn(); + } + + protected override void PopOut() + { + this.Delay(400).FadeOut(); + } + } + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs b/osu.Game/Overlays/WizardScreen.cs similarity index 96% rename from osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs rename to osu.Game/Overlays/WizardScreen.cs index 76921718f2..7f3b1fe7f4 100644 --- a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs +++ b/osu.Game/Overlays/WizardScreen.cs @@ -13,9 +13,9 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; -namespace osu.Game.Overlays.FirstRunSetup +namespace osu.Game.Overlays { - public abstract partial class FirstRunSetupScreen : Screen + public abstract partial class WizardScreen : Screen { private const float offset = 100; From 749704344c5fbb0d46b153d98e60798e331a3965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 13:11:05 +0100 Subject: [PATCH 418/620] Move implicit slider path segment handling logic to Bezier converter The logic in `LegacyBeatmapEncoder` that was supposed to handle the lazer-exclusive feature of supporting multiple slider segment types in a single slider was interfering rather badly with the Bezier converter. Generally it was a bit difficult to follow, too. The nice thing about `BezierConverter` is that it is *guaranteed* to only output Bezier control points. In light of this, the same double-up- -the-control-point logic that was supposed to make multiple slider segment types backwards-compatible with stable can be placed in the Bezier conversion logic, and be *much* more understandable, too. --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 59 +++++-------------- osu.Game/Rulesets/Objects/BezierConverter.cs | 4 ++ 2 files changed, 19 insertions(+), 44 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 6c855e1346..07e88ab956 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -447,60 +447,31 @@ namespace osu.Game.Beatmaps.Formats private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) { - PathType? lastType = null; - for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) { PathControlPoint point = pathData.Path.ControlPoints[i]; + // Note that lazer's encoding format supports specifying multiple curve types for a slider path, which is not supported by stable. + // Backwards compatibility with stable is handled by `LegacyBeatmapExporter` and `BezierConverter.ConvertToModernBezier()`. if (point.Type != null) { - // We've reached a new (explicit) segment! - - // Explicit segments have a new format in which the type is injected into the middle of the control point string. - // To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point. - // One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments - bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1; - - // Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable. - // Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder. - if (i > 1) + switch (point.Type?.Type) { - // We need to use the absolute control point position to determine equality, otherwise floating point issues may arise. - Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position; - Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position; + case SplineType.BSpline: + writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|"); + break; - if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y) - needsExplicitSegment = true; - } + case SplineType.Catmull: + writer.Write("C|"); + break; - if (needsExplicitSegment) - { - switch (point.Type?.Type) - { - case SplineType.BSpline: - writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|"); - break; + case SplineType.PerfectCurve: + writer.Write("P|"); + break; - case SplineType.Catmull: - writer.Write("C|"); - break; - - case SplineType.PerfectCurve: - writer.Write("P|"); - break; - - case SplineType.Linear: - writer.Write("L|"); - break; - } - - lastType = point.Type; - } - else - { - // New segment with the same type - duplicate the control point - writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}|")); + case SplineType.Linear: + writer.Write("L|"); + break; } } diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 638975630e..384c686167 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -147,6 +148,7 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -158,6 +160,7 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < circleResult.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(circleResult[j])); result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null)); } @@ -170,6 +173,7 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < bSplineResult.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(bSplineResult[j])); result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null)); } From 64b67252a2edc1b762c4f4cca311738effe2df68 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 30 Jan 2025 08:22:28 -0500 Subject: [PATCH 419/620] Enable NRT on `Column` --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 2 +- osu.Game.Rulesets.Mania/UI/Column.cs | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 36ccf68d76..e8c993a91b 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania { - [Cached] // Used for touch input, see ColumnTouchInputArea. + [Cached] // Used for touch input, see Column.OnTouchDown/OnTouchUp. public partial class ManiaInputManager : RulesetInputManager { public ManiaInputManager(RulesetInfo ruleset, int variant) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 81f4d79281..5425965897 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using 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.Pooling; @@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; - private DrawablePool hitExplosionPool; + private DrawablePool hitExplosionPool = null!; private readonly OrderedHitPolicy hitPolicy; public Container UnderlayElements => HitObjectArea.UnderlayElements; - private GameplaySampleTriggerSource sampleTriggerSource; + private GameplaySampleTriggerSource sampleTriggerSource = null!; /// /// Whether this is a special (ie. scratch) column. @@ -75,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.UI } [Resolved] - private ISkinSource skin { get; set; } + private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] private void load(GameHost host) @@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); - if (skin != null) + if (skin.IsNotNull()) skin.SourceChanged -= onSourceChanged; } @@ -187,14 +186,14 @@ namespace osu.Game.Rulesets.Mania.UI #region Touch Input - [Resolved(canBeNull: true)] - private ManiaInputManager maniaInputManager { get; set; } + [Resolved] + private ManiaInputManager? maniaInputManager { get; set; } private int touchActivationCount; protected override bool OnTouchDown(TouchDownEvent e) { - maniaInputManager.KeyBindingContainer.TriggerPressed(Action.Value); + maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); touchActivationCount++; return true; } @@ -204,7 +203,7 @@ namespace osu.Game.Rulesets.Mania.UI touchActivationCount--; if (touchActivationCount == 0) - maniaInputManager.KeyBindingContainer.TriggerReleased(Action.Value); + maniaInputManager?.KeyBindingContainer.TriggerReleased(Action.Value); } #endregion From 261a7e537b0451f34725c376af345ff8fdd131f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 14:42:44 +0100 Subject: [PATCH 420/620] Fix distance snap time part ceasing to work when grid snap is also active As pointed out in https://github.com/ppy/osu/pull/31655#discussion_r1935536934. --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 194276baf9..e08968e1aa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Edit pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - return new SnapResult(positionSnapGrid.ToScreenSpace(pos), null, playfield); + return new SnapResult(positionSnapGrid.ToScreenSpace(pos), fallbackTime, playfield); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) From b4f63da048e16c9f0fd0d339ea13f33637dade9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 15:23:22 +0100 Subject: [PATCH 421/620] Move control point double-up logic to `LegacyBeatmapExporter` Done for two reasons: - During review it was requested for the logic to be moved out of `BezierConverter` as `BezierConverter` was intended to produce "lazer style" sliders with per-control-point curve types, as a future usability / code layering concern. - It is also relevant for encode-decode stability. With how the logic was structured between the Bezier converter and the legacy beatmap encoder, the encoder would leave behind per-control-point Bezier curve specs that stable ignored, but subsequent encodes and decodes in lazer would end up multiplying the doubled-up control points ad nauseam. Instead, it is sufficient to only specify the curve type for the head control point as Bezier, not specify any further curve types later on, and instead just keep the double-up-control-point for new implicit segment logic which is enough to make stable cooperate (and also as close to outputting the slider exactly as stable would have produced it as we've ever been) --- osu.Game/Database/LegacyBeatmapExporter.cs | 32 ++++++++++++++------ osu.Game/Rulesets/Objects/BezierConverter.cs | 4 --- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 24e752da31..9bb90ab461 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -120,18 +120,30 @@ namespace osu.Game.Database if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1 && hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue; - var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); - - // Truncate control points to integer positions - foreach (var pathControlPoint in newControlPoints) - { - pathControlPoint.Position = new Vector2( - (float)Math.Floor(pathControlPoint.Position.X), - (float)Math.Floor(pathControlPoint.Position.Y)); - } + var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); hasPath.Path.ControlPoints.Clear(); - hasPath.Path.ControlPoints.AddRange(newControlPoints); + + for (int i = 0; i < convertedToBezier.Count; i++) + { + var convertedPoint = convertedToBezier[i]; + + // Truncate control points to integer positions + var position = new Vector2( + (float)Math.Floor(convertedPoint.Position.X), + (float)Math.Floor(convertedPoint.Position.Y)); + + // stable only supports a single curve type specification per slider. + // we exploit the fact that the converted-to-Bézier path only has Bézier segments, + // and thus we specify the Bézier curve type once ever at the start of the slider. + hasPath.Path.ControlPoints.Add(new PathControlPoint(position, i == 0 ? PathType.BEZIER : null)); + + // however, the Bézier path as output by the converter has multiple segments. + // `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable. + // instead, stable expects control points that start a segment to be present in the path twice in succession. + if (convertedPoint.Type == PathType.BEZIER) + hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); + } } // Encode to legacy format diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 384c686167..638975630e 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -136,7 +136,6 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -148,7 +147,6 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -160,7 +158,6 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < circleResult.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(circleResult[j])); result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null)); } @@ -173,7 +170,6 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < bSplineResult.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(bSplineResult[j])); result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null)); } From 4a164b7b149ff7c8f78d15f904fcb61673ac9ff8 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:17:50 +0100 Subject: [PATCH 422/620] Add legacy taiko swell --- .../Objects/Drawables/DrawableSwell.cs | 113 ++------------ .../Objects/ISkinnableSwell.cs | 22 +++ .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Skinning/Default/DefaultSwell.cs | 142 ++++++++++++++++++ .../Skinning/Legacy/LegacySwell.cs | 136 +++++++++++++++++ .../Skinning/Legacy/LegacySwellCirclePiece.cs | 23 +++ .../Legacy/TaikoLegacySkinTransformer.cs | 10 +- .../TaikoSkinComponents.cs | 1 + 8 files changed, 348 insertions(+), 101 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 28617b35f6..cba044959c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -6,14 +6,9 @@ using System; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -25,11 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public partial class DrawableSwell : DrawableTaikoHitObject { - private const float target_ring_thick_border = 1.4f; - private const float target_ring_thin_border = 1f; - private const float target_ring_scale = 5f; - private const float inner_ring_alpha = 0.65f; - /// /// Offset away from the start time of the swell at which the ring starts appearing. /// @@ -37,10 +27,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; + private readonly SkinnableDrawable spinnerBody; + private readonly Container ticks; - private readonly Container bodyContainer; - private readonly CircularContainer targetRing; - private readonly CircularContainer expandingRing; private double? lastPressHandleTime; @@ -61,82 +50,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(bodyContainer = new Container + Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell()) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Depth = 1, - Children = new Drawable[] - { - expandingRing = new CircularContainer - { - Name = "Expanding ring", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = inner_ring_alpha, - } - } - }, - targetRing = new CircularContainer - { - Name = "Target ring (thick border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thick_border, - Blending = BlendingParameters.Additive, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - new CircularContainer - { - Name = "Target ring (thin border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thin_border, - BorderColour = Color4.White, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } - } - } - } }); AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellCirclePiece), _ => new SwellCirclePiece { // to allow for rotation transform @@ -208,16 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - float completion = (float)numHits / HitObject.RequiredHits; - - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); - - MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); - - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + (spinnerBody.Drawable as ISkinnableSwell)?.OnUserInput(this, numHits, MainPiece); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -252,24 +167,24 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - using (BeginDelayedSequence(-ring_appear_offset)) - targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint); + (spinnerBody.Drawable as ISkinnableSwell)?.ApplyPassiveTransforms(this, MainPiece); } protected override void UpdateHitStateTransforms(ArmedState state) { - const double transition_duration = 300; - switch (state) { case ArmedState.Idle: - expandingRing.FadeTo(0); + HandleUserInput = true; break; case ArmedState.Miss: case ArmedState.Hit: - this.FadeOut(transition_duration, Easing.Out); - bodyContainer.ScaleTo(1.4f, transition_duration); + // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. + LifetimeEnd = Time.Current + 1200; + HandleUserInput = false; + + (spinnerBody.Drawable as ISkinnableSwell)?.OnHitObjectEnd(state, MainPiece); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs new file mode 100644 index 0000000000..18feff5bb9 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public interface ISkinnableSwell + { + void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + + void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece); + + /// + /// Applies passive transforms on HitObject start. Gets called every time DrawableTaikoHitobject + /// changes state. This happens on creation, and when the object is completed (as in hit or missed). + /// + void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index bfc9e8648d..cfd30dd628 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.Swell: + case TaikoSkinComponents.SwellCirclePiece: return new ArgonSwellCirclePiece(); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs new file mode 100644 index 0000000000..e525e9873d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Default +{ + public partial class DefaultSwell : Container, ISkinnableSwell + { + private const float target_ring_thick_border = 1.4f; + private const float target_ring_thin_border = 1f; + private const float target_ring_scale = 5f; + private const float inner_ring_alpha = 0.65f; + + private readonly Container bodyContainer; + private readonly CircularContainer targetRing; + private readonly CircularContainer expandingRing; + + public DefaultSwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + + Content.Add(bodyContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = 1, + Children = new Drawable[] + { + expandingRing = new CircularContainer + { + Name = "Expanding ring", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = inner_ring_alpha, + } + } + }, + targetRing = new CircularContainer + { + Name = "Target ring (thick border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thick_border, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new CircularContainer + { + Name = "Target ring (thin border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thin_border, + BorderColour = Color4.White, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + } + } + } + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + + public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + { + float completion = (float)numHits / swell.HitObject.RequiredHits; + + mainPiece.Drawable.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + + expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + + expandingRing + .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) + .Then() + .FadeTo(completion / 8, 2000, Easing.OutQuint); + } + + public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + mainPiece.FadeOut(transition_duration, Easing.OutQuad); + } + + public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + { + if (swell.IsHit == false) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs new file mode 100644 index 0000000000..240ec71f94 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -0,0 +1,136 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Skinning; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Framework.Audio.Sample; +using osu.Game.Audio; +using osuTK; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + public partial class LegacySwell : Container, ISkinnableSwell + { + private Container bodyContainer = null!; + private Sprite spinnerCircle = null!; + private Sprite shrinkingRing = null!; + private Sprite clearAnimation = null!; + private ISample? clearSample; + private LegacySpriteText remainingHitsCountdown = null!; + + private bool samplePlayed; + + public LegacySwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, SkinManager skinManager) + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), + + Children = new Drawable[] + { + bodyContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] + { + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + shrinkingRing = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = Vector2.One, + }, + remainingHitsCountdown = new LegacySpriteText(LegacyFont.Combo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 165f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + // File extension is included here because of a GetTexture limitation, see #21543 + Texture = skin.GetTexture("spinner-osu.png"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, -165f), + Scale = new Vector2(0.3f), + Alpha = 0, + }, + } + }; + + clearSample = skin.GetSample(new SampleInfo("spinner-osu")); + } + + public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + { + remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; + spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); + } + + public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + { + const double clear_transition_duration = 300; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample?.Play(); + samplePlayed = true; + } + + clearAnimation + .FadeIn(clear_transition_duration, Easing.InQuad) + .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) + .Delay(700).FadeOut(200, Easing.OutQuad); + } + } + + public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + { + if (swell.IsHit == false) + { + remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits}"; + samplePlayed = false; + } + + const double body_transition_duration = 100; + + mainPiece.FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); + shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs new file mode 100644 index 0000000000..40501d1d40 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + internal partial class LegacySwellCirclePiece : Sprite + { + [BackgroundDependencyLoader] + private void load(ISkinSource skin, SkinManager skinManager) + { + Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 5bdb824f1c..243d975216 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -66,7 +66,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return this.GetAnimation("sliderscorepoint", false, false); case TaikoSkinComponents.Swell: - // todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601). + if (GetTexture("spinner-circle") != null) + return new LegacySwell(); + + return null; + + case TaikoSkinComponents.SwellCirclePiece: + if (GetTexture("spinner-circle") != null) + return new LegacySwellCirclePiece(); + return null; case TaikoSkinComponents.HitTarget: diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 28133ffcb2..aa7e4686d8 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, Swell, + SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From fe84e6e5f53d5a3264b1fcbe68fb698b7c039f48 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:19:06 +0100 Subject: [PATCH 423/620] Adjust existing test to accommodate swell size --- osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs index c130b5f366..286b16aa34 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Scale = new osuTK.Vector2(0.5f), })); } From 988450a2c4f8244d1ef1bc572d711ee781eaaa09 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:23:48 +0100 Subject: [PATCH 424/620] Add test for expire delay Delaying the expiry of the drawable hitobject can potentially be dangerous and gameplay-altering when user inputs are accidentally handled. This is why I found a test necessary. --- .../TestSceneDrawableSwellExpireDelay.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs new file mode 100644 index 0000000000..ad78ed3b20 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs @@ -0,0 +1,41 @@ +// 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 NUnit.Framework; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Tests.Judgements; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + public partial class TestSceneDrawableSwellExpireDelay : JudgementTest + { + [Test] + public void TestExpireDelay() + { + const double swell_start = 1000; + const double swell_duration = 1000; + + Swell swell = new Swell + { + StartTime = swell_start, + Duration = swell_duration, + }; + + Hit hit = new Hit { StartTime = swell_start + swell_duration + 50 }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2100, TaikoAction.LeftCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertResult(0, HitResult.Ok); + } + } +} From e2196e8b9b97f447863b61124f7bd3454a505e60 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Tue, 13 Dec 2022 19:32:05 +0100 Subject: [PATCH 425/620] Rename methods and skin component + add comments --- .../Objects/Drawables/DrawableSwell.cs | 13 ++++++++----- osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs | 10 +++------- .../Skinning/Default/DefaultSwell.cs | 6 +++--- .../Skinning/Legacy/LegacySwell.cs | 6 +++--- .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 2 +- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index cba044959c..54a609f7d3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell()) + Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), _ => new DefaultSwell()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (spinnerBody.Drawable as ISkinnableSwell)?.OnUserInput(this, numHits, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits, MainPiece); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (spinnerBody.Drawable as ISkinnableSwell)?.ApplyPassiveTransforms(this, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellStart(this, MainPiece); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -175,16 +175,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { case ArmedState.Idle: + // Only for rewind support. Reallows user inputs if swell is rewound from being hit/missed to being idle. HandleUserInput = true; break; case ArmedState.Miss: case ArmedState.Hit: + const int clear_animation_duration = 1200; + // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. - LifetimeEnd = Time.Current + 1200; + LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (spinnerBody.Drawable as ISkinnableSwell)?.OnHitObjectEnd(state, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state, MainPiece); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs index 18feff5bb9..3cdb3566fb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -9,14 +9,10 @@ namespace osu.Game.Rulesets.Taiko.Objects { public interface ISkinnableSwell { - void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); - void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece); + void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece); - /// - /// Applies passive transforms on HitObject start. Gets called every time DrawableTaikoHitobject - /// changes state. This happens on creation, and when the object is completed (as in hit or missed). - /// - void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index e525e9873d..cec07d8769 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) { float completion = (float)numHits / swell.HitObject.RequiredHits; @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) { const double transition_duration = 300; @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default mainPiece.FadeOut(transition_duration, Easing.OutQuad); } - public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) { if (swell.IsHit == false) expandingRing.FadeTo(0); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 240ec71f94..fdddea2df5 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -91,13 +91,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) { remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) { const double clear_transition_duration = 300; @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) { if (swell.IsHit == false) { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 243d975216..b9ebed6b80 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); - case TaikoSkinComponents.Swell: + case TaikoSkinComponents.SwellBody: if (GetTexture("spinner-circle") != null) return new LegacySwell(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index aa7e4686d8..0145fb6482 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - Swell, + SwellBody, SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, From cf2d0e6911539a23f9f9ae41160b06b1bb52e91f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Jan 2025 16:22:37 +0900 Subject: [PATCH 426/620] Fix results screen sounds persisting after exit --- osu.Game/Screens/Ranking/ResultsScreen.cs | 107 ++++++++++++---------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 5e91171051..95dbfb2712 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -64,6 +64,7 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; + private AudioContainer audioContainer = null!; private bool lastFetchCompleted; @@ -100,76 +101,80 @@ namespace osu.Game.Screens.Ranking popInSample = audio.Samples.Get(@"UI/overlay-pop-in"); - InternalChild = new PopoverContainer + InternalChild = audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both, - Child = new GridContainer + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Content = new[] + Child = new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + Content = new[] { - VerticalScrollContent = new VerticalScrollContainer + new Drawable[] { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new Container + VerticalScrollContent = new VerticalScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + StatisticsPanel = createStatisticsPanel().With(panel => + { + panel.RelativeSizeAxes = Axes.Both; + panel.Score.BindTarget = SelectedScore; + }), + ScorePanelList = new ScorePanelList + { + RelativeSizeAxes = Axes.Both, + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = () => StatisticsPanel.ToggleVisibility() + }, + detachedPanelContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + } + } + }, + }, + new[] + { + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, Children = new Drawable[] { - StatisticsPanel = createStatisticsPanel().With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), - ScorePanelList = new ScorePanelList + new Box { RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => StatisticsPanel.ToggleVisibility() + Colour = Color4Extensions.FromHex("#333") }, - detachedPanelContainer = new Container + buttons = new FillFlowContainer { - RelativeSizeAxes = Axes.Both + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal }, } } - }, - }, - new[] - { - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") - }, - buttons = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal - }, - } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) } } }; @@ -330,6 +335,8 @@ namespace osu.Game.Screens.Ranking if (!skipExitTransition) this.FadeOut(100); + + audioContainer.Volume.Value = 0; return false; } From 20280cd1959d0ceecff45f1e11a7aff3cedd5768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 09:01:42 +0100 Subject: [PATCH 427/620] Do not double up first control point of path --- osu.Game/Database/LegacyBeatmapExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 9bb90ab461..8f94fc9e63 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -141,7 +141,7 @@ namespace osu.Game.Database // however, the Bézier path as output by the converter has multiple segments. // `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable. // instead, stable expects control points that start a segment to be present in the path twice in succession. - if (convertedPoint.Type == PathType.BEZIER) + if (convertedPoint.Type == PathType.BEZIER && i > 0) hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); } } From 8718483c702e7a69a2314d9fd515297615cb6920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 15:18:59 +0100 Subject: [PATCH 428/620] Avoid moving already placed objects temporally when "limit distance snap to current time" is active --- .../HitCircles/HitCirclePlacementBlueprint.cs | 16 +++++++++++++++- .../Components/PathControlPointVisualiser.cs | 11 ++++++++++- .../Sliders/SliderPlacementBlueprint.cs | 12 ++++++++++-- .../Edit/OsuBlueprintContainer.cs | 15 +++++++++++++-- .../Edit/OsuHitObjectComposer.cs | 4 ++-- .../Editing/TestSceneDistanceSnapGrid.cs | 2 +- .../Components/CircularDistanceSnapGrid.cs | 9 +++------ .../Compose/Components/DistanceSnapGrid.cs | 19 +++++-------------- 8 files changed, 59 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 93d79a50ab..61ed30259a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osuTK; using osuTK.Input; @@ -20,12 +23,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles [Resolved] private OsuHitObjectComposer? composer { get; set; } + [Resolved] + private EditorClock? editorClock { get; set; } + + private Bindable limitedDistanceSnap { get; set; } = null!; + public HitCirclePlacementBlueprint() : base(new HitCircle()) { InternalChild = circlePiece = new HitCirclePiece(); } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -53,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); - result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null); if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 189bb005a7..b9938209ae 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -21,6 +21,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -55,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } + private Bindable limitedDistanceSnap { get; set; } = null!; + public PathControlPointVisualiser(T hitObject, bool allowSelection) { this.hitObject = hitObject; @@ -69,6 +72,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components }; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -437,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); - result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null); if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(newHeadPosition, oldStartTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 1012578375..21817045c4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -49,6 +51,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved] private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; } + [Resolved] + private EditorClock? editorClock { get; set; } + + private Bindable limitedDistanceSnap { get; set; } = null!; + private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; @@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { InternalChildren = new Drawable[] { @@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders }; state = SliderPlacementState.Initial; + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); } protected override void LoadComplete() @@ -109,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); - result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null); if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 5eff95adec..9d82046c23 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -3,7 +3,10 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -17,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuBlueprintContainer : ComposeBlueprintContainer { + private Bindable limitedDistanceSnap { get; set; } = null!; + public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer; public OsuBlueprintContainer(OsuHitObjectComposer composer) @@ -24,6 +29,12 @@ namespace osu.Game.Rulesets.Osu.Edit { } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler(); public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject) @@ -58,15 +69,15 @@ namespace osu.Game.Rulesets.Osu.Edit // The final movement position, relative to movementBlueprintOriginalPosition. Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + var referenceBlueprint = blueprints.First().blueprint; // Retrieve a snapped position. var result = Composer.TrySnapToNearbyObjects(movePosition); - result ??= Composer.TrySnapToDistanceGrid(movePosition); + result ??= Composer.TrySnapToDistanceGrid(movePosition, limitedDistanceSnap.Value ? referenceBlueprint.Item.StartTime : null); if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(movePosition, null); - var referenceBlueprint = blueprints.First().blueprint; bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); if (moved) ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e08968e1aa..563d0b1e3e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -250,13 +250,13 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition) + public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null) { if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return null; var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime); return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index c1a788cd22..818862d958 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Editing } } - public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition) + public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition, double? fixedTime = null) => (Vector2.Zero, 0); } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index bd750dac76..e84c2ebc35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -16,9 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { - [Resolved] - private EditorClock editorClock { get; set; } = null!; - protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) : base(referenceObject, startPosition, startTime, endTime) { @@ -76,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - public override (Vector2 position, double time) GetSnappedPosition(Vector2 position) + public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null) { if (MaxIntervals == 0) return (StartPosition, StartTime); @@ -100,8 +97,8 @@ namespace osu.Game.Screens.Edit.Compose.Components if (travelLength < DistanceBetweenTicks) travelLength = DistanceBetweenTicks; - float snappedDistance = LimitedDistanceSnap.Value - ? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime()) + float snappedDistance = fixedTime != null + ? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime()) // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // to allow for snapping at a non-multiplied ratio. : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End); diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 7003d632ca..aaf58e0f7a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -10,7 +10,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -61,18 +60,6 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private BindableBeatDivisor beatDivisor { get; set; } - /// - /// When enabled, distance snap should only snap to the current time (as per the editor clock). - /// This is to emulate stable behaviour. - /// - protected Bindable LimitedDistanceSnap { get; private set; } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - LimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); - } - private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); protected readonly HitObject ReferenceObject; @@ -143,8 +130,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Snaps a position to this grid. /// /// The original position in coordinate space local to this . + /// + /// Whether the snap operation should be temporally constrained to a particular time instant, + /// thus fixing the possible positions to a set distance from the . + /// /// A tuple containing the snapped position in coordinate space local to this and the respective time value. - public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position); + public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null); /// /// Retrieves the applicable colour for a beat index. From 4fd8a4dc5a6f0453767175aa706ea331bbfca7c6 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 16:55:39 +0800 Subject: [PATCH 429/620] Merge taiko swell components Per , taking a variation of the "Make all swell main pieces implement ISkinnableSwellPart" path. Should clean the interface up enough for further refactors. --- .../Objects/Drawables/DrawableSwell.cs | 25 ++++-------- .../Objects/ISkinnableSwell.cs | 7 ++-- .../Skinning/Argon/ArgonSwell.cs | 20 ++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 4 +- .../Skinning/Default/DefaultSwell.cs | 26 +++++++++---- ...wellSymbolPiece.cs => SwellCirclePiece.cs} | 0 .../Skinning/Legacy/LegacySwell.cs | 38 +++++++++++-------- .../Legacy/TaikoLegacySkinTransformer.cs | 6 --- .../TaikoSkinComponents.cs | 1 - 9 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs rename osu.Game.Rulesets.Taiko/Skinning/Default/{SwellSymbolPiece.cs => SwellCirclePiece.cs} (100%) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 54a609f7d3..18d76d02a1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; - private readonly SkinnableDrawable spinnerBody; - private readonly Container ticks; private double? lastPressHandleTime; @@ -50,24 +48,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), _ => new DefaultSwell()) + AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); + } + + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), + _ => new DefaultSwell { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, }); - AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellCirclePiece), - _ => new SwellCirclePiece - { - // to allow for rotation transform - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - protected override void RecreatePieces() { base.RecreatePieces(); @@ -132,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -167,7 +158,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellStart(this, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellStart(this); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -187,7 +178,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs index 3cdb3566fb..9bd169acd7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -3,16 +3,15 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects { public interface ISkinnableSwell { - void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits); - void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece); + void AnimateSwellCompletion(ArmedState state); - void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + void AnimateSwellStart(DrawableTaikoHitObject swell); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs new file mode 100644 index 0000000000..65cd936e38 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Taiko.Skinning.Default; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonSwell : DefaultSwell + { + protected override Drawable CreateCentreCircle() + { + return new ArgonSwellCirclePiece() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index cfd30dd628..b588a22d12 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.SwellCirclePiece: - return new ArgonSwellCirclePiece(); + case TaikoSkinComponents.SwellBody: + return new ArgonSwell(); } break; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index cec07d8769..bdb444db90 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -26,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; + private readonly Drawable centreCircle; public DefaultSwell() { @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default Content.Add(bodyContainer = new Container { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Depth = 1, Children = new Drawable[] @@ -94,11 +96,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default } } } - } + }, + centreCircle = CreateCentreCircle(), } }); } + protected virtual Drawable CreateCentreCircle() + { + return new SwellCirclePiece() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -106,11 +118,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) { float completion = (float)numHits / swell.HitObject.RequiredHits; - mainPiece.Drawable.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + centreCircle.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); @@ -120,16 +132,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state) { const double transition_duration = 300; bodyContainer.FadeOut(transition_duration, Easing.OutQuad); bodyContainer.ScaleTo(1.4f, transition_duration); - mainPiece.FadeOut(transition_duration, Easing.OutQuad); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); } - public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell) { if (swell.IsHit == false) expandingRing.FadeTo(0); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index fdddea2df5..e487c5e051 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public partial class LegacySwell : Container, ISkinnableSwell { private Container bodyContainer = null!; + private Sprite warning = null!; private Sprite spinnerCircle = null!; private Sprite shrinkingRing = null!; private Sprite clearAnimation = null!; @@ -40,14 +41,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), Children = new Drawable[] { + warning = new Sprite + { + Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, bodyContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), Alpha = 0, Children = new Drawable[] @@ -73,31 +81,31 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Position = new Vector2(0f, 165f), Scale = Vector2.One, }, + clearAnimation = new Sprite + { + // File extension is included here because of a GetTexture limitation, see #21543 + Texture = skin.GetTexture("spinner-osu.png"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, -165f), + Scale = new Vector2(0.3f), + Alpha = 0, + }, } }, - clearAnimation = new Sprite - { - // File extension is included here because of a GetTexture limitation, see #21543 - Texture = skin.GetTexture("spinner-osu.png"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, -165f), - Scale = new Vector2(0.3f), - Alpha = 0, - }, } }; clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) { remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state) { const double clear_transition_duration = 300; @@ -118,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell) { if (swell.IsHit == false) { @@ -128,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double body_transition_duration = 100; - mainPiece.FadeOut(body_transition_duration); + warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index b9ebed6b80..8fa4551fd4 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -71,12 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return null; - case TaikoSkinComponents.SwellCirclePiece: - if (GetTexture("spinner-circle") != null) - return new LegacySwellCirclePiece(); - - return null; - case TaikoSkinComponents.HitTarget: if (GetTexture("taikobigcircle") != null) return new TaikoLegacyHitTarget(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 0145fb6482..05c6316a05 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, SwellBody, - SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From 2a5540b39251c19f46a2965f0226f45d7a085f3e Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 17:51:35 +0800 Subject: [PATCH 430/620] remove ISkinnableSwell This commit removes ISkinnableSwell for taiko swell animations. In place of it, an event named UpdateHitProgress is added to DrawableSwell, and the skin swells are converted to listen to said event and ApplyCustomUpdateState, like how spinner skinning is implemented for std. --- .../Objects/Drawables/DrawableSwell.cs | 6 +- .../Objects/ISkinnableSwell.cs | 17 ----- .../Skinning/Default/DefaultSwell.cs | 70 +++++++++++------ .../Skinning/Legacy/LegacySwell.cs | 75 ++++++++++++------- 4 files changed, 101 insertions(+), 67 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 18d76d02a1..e0276db911 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; + public event Action UpdateHitProgress; + public DrawableSwell() : this(null) { @@ -123,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits); + UpdateHitProgress?.Invoke(numHits, HitObject.RequiredHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -158,7 +160,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellStart(this); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -178,7 +179,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs deleted file mode 100644 index 9bd169acd7..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables; - -namespace osu.Game.Rulesets.Taiko.Objects -{ - public interface ISkinnableSwell - { - void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits); - - void AnimateSwellCompletion(ArmedState state); - - void AnimateSwellStart(DrawableTaikoHitObject swell); - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index bdb444db90..852116cbfe 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,13 +16,15 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default { - public partial class DefaultSwell : Container, ISkinnableSwell + public partial class DefaultSwell : Container { private const float target_ring_thick_border = 1.4f; private const float target_ring_thin_border = 1f; private const float target_ring_scale = 5f; private const float inner_ring_alpha = 0.65f; + private DrawableSwell drawableSwell = null!; + private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; @@ -102,6 +105,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }); } + [BackgroundDependencyLoader] + private void load(DrawableHitObject hitObject, OsuColour colours) + { + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; + + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + protected virtual Drawable CreateCentreCircle() { return new SwellCirclePiece() @@ -111,18 +125,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void animateSwellProgress(int numHits, int requiredHits) { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } + float completion = (float)numHits / requiredHits; - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) - { - float completion = (float)numHits / swell.HitObject.RequiredHits; - - centreCircle.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + centreCircle.RotateTo((float)(completion * drawableSwell.HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); @@ -132,23 +139,42 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - const double transition_duration = 300; + if (!(drawableHitObject is DrawableSwell drawableSwell)) + return; - bodyContainer.FadeOut(transition_duration, Easing.OutQuad); - bodyContainer.ScaleTo(1.4f, transition_duration); - centreCircle.FadeOut(transition_duration, Easing.OutQuad); + Swell swell = drawableSwell.HitObject; + + using (BeginAbsoluteSequence(swell.StartTime)) + { + if (state == ArmedState.Idle) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); + } } - public void AnimateSwellStart(DrawableTaikoHitObject swell) + protected override void Dispose(bool isDisposing) { - if (swell.IsHit == false) - expandingRing.FadeTo(0); + base.Dispose(isDisposing); - const double ring_appear_offset = 100; - - targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + if (drawableSwell.IsNotNull()) + { + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index e487c5e051..60a0b1d951 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -12,11 +12,14 @@ using osu.Framework.Audio.Sample; using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; +using osu.Framework.Extensions.ObjectExtensions; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { - public partial class LegacySwell : Container, ISkinnableSwell + public partial class LegacySwell : Container { + private DrawableSwell drawableSwell = null!; + private Container bodyContainer = null!; private Sprite warning = null!; private Sprite spinnerCircle = null!; @@ -35,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(ISkinSource skin, SkinManager skinManager) + private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { Child = new Container { @@ -96,49 +99,71 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } }; + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) + private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; + remainingHitsCountdown.Text = $"{requiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - const double clear_transition_duration = 300; + if (!(drawableHitObject is DrawableSwell drawableSwell)) + return; - bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + Swell swell = drawableSwell.HitObject; - if (state == ArmedState.Hit) + using (BeginAbsoluteSequence(swell.StartTime)) { - if (!samplePlayed) + if (state == ArmedState.Idle) { - clearSample?.Play(); - samplePlayed = true; + remainingHitsCountdown.Text = $"{swell.RequiredHits}"; + samplePlayed = false; } - clearAnimation - .FadeIn(clear_transition_duration, Easing.InQuad) - .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) - .Delay(700).FadeOut(200, Easing.OutQuad); + const double body_transition_duration = 100; + + warning.FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); + shrinkingRing.ResizeTo(0.1f, swell.Duration); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double clear_transition_duration = 300; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample?.Play(); + samplePlayed = true; + } + + clearAnimation + .FadeIn(clear_transition_duration, Easing.InQuad) + .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) + .Delay(700).FadeOut(200, Easing.OutQuad); + } } } - public void AnimateSwellStart(DrawableTaikoHitObject swell) + protected override void Dispose(bool isDisposing) { - if (swell.IsHit == false) + base.Dispose(isDisposing); + + if (drawableSwell.IsNotNull()) { - remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits}"; - samplePlayed = false; + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; } - - const double body_transition_duration = 100; - - warning.FadeOut(body_transition_duration); - bodyContainer.FadeIn(body_transition_duration); - shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); } } } From ad2b469b143d74da7843a42563fe3e170a53d35c Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 18:52:19 +0800 Subject: [PATCH 431/620] remove spinner-osu.png workaround https://github.com/ppy/osu/issues/22084 --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 60a0b1d951..405b0b7692 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -86,8 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy }, clearAnimation = new Sprite { - // File extension is included here because of a GetTexture limitation, see #21543 - Texture = skin.GetTexture("spinner-osu.png"), + Texture = skin.GetTexture("spinner-osu"), Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(0f, -165f), From c3981f1097f1d7d3a29422261ad39d43819cf1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 12:05:30 +0100 Subject: [PATCH 432/620] Do not reset online info on beatmap save --- .../Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs | 6 +++++- osu.Game/Beatmaps/BeatmapManager.cs | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs index 7f9a69833c..636b3f54d8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Tests.Resources; @@ -25,13 +26,16 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestLocallyModifyingOnlineBeatmap() { + string initialHash = string.Empty; AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0)); + AddStep("store hash for later", () => initialHash = EditorBeatmap.BeatmapInfo.MD5Hash); AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0)); SaveEditor(); ReloadEditorToSameBeatmap(); - AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1)); + AddAssert("beatmap marked as locally modified", () => EditorBeatmap.BeatmapInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified)); + AddAssert("beatmap hash changed", () => EditorBeatmap.BeatmapInfo.MD5Hash, () => Is.Not.EqualTo(initialHash)); } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index aa67d3c548..1e66b28b15 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -475,11 +475,8 @@ namespace osu.Game.Beatmaps beatmapContent.BeatmapInfo = beatmapInfo; // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this. - // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file, - // which influences the beatmap checksums. beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; - beatmapInfo.ResetOnlineInfo(); Realm.Write(r => { From 7ef861670379b42ce17ba648c5e5d016fa4a995e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 12:22:05 +0100 Subject: [PATCH 433/620] Fix broken user-facing messaging when beatmap hash mismatch is detected --- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 24c5b2c3d4..0a230ea00b 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Play Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important); break; - case @"invalid beatmap_hash": + case @"invalid or missing beatmap_hash": Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important); break; From ac17b4065f06571cc3bf30cc7536e4746a78e9d3 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 19:55:29 +0800 Subject: [PATCH 434/620] change legacy spinner animations to match stable Also removed a few fallbacks pointed out in code review that I don't understand. --- .../Skinning/Legacy/LegacySwell.cs | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 405b0b7692..9ed21b1bb0 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -13,6 +13,7 @@ using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; +using System; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { @@ -23,10 +24,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Container bodyContainer = null!; private Sprite warning = null!; private Sprite spinnerCircle = null!; - private Sprite shrinkingRing = null!; + private Sprite approachCircle = null!; private Sprite clearAnimation = null!; private ISample? clearSample; - private LegacySpriteText remainingHitsCountdown = null!; + private LegacySpriteText remainingHitsText = null!; private bool samplePlayed; @@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { + var spinnerCircleProvider = skin.FindProvider(s => s.GetTexture("spinner-circle") != null); + Child = new Container { Anchor = Anchor.Centre, @@ -49,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { warning = new Sprite { - Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"), + Texture = skin.GetTexture("spinner-warning"), Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), @@ -70,14 +73,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(0.8f), }, - shrinkingRing = new Sprite + approachCircle = new Sprite { - Texture = skin.GetTexture("spinner-approachcircle") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-approachcircle"), + Texture = skin.GetTexture("spinner-approachcircle"), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = Vector2.One, + Scale = new Vector2(1.86f * 0.8f), }, - remainingHitsCountdown = new LegacySpriteText(LegacyFont.Combo) + remainingHitsText = new LegacySpriteText(LegacyFont.Combo) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -106,8 +109,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsCountdown.Text = $"{requiredHits - numHits}"; - spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); + remainingHitsText.Text = $"{requiredHits - numHits}"; + remainingHitsText.ScaleTo(1.6f - 0.6f * ((float)numHits / requiredHits), 60, Easing.OutQuad); + + spinnerCircle.ClearTransforms(); + spinnerCircle + .RotateTo(180f * numHits, 1000, Easing.OutQuint) + .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) + .ScaleTo(0.8f, 400, Easing.OutQuad); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -121,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { if (state == ArmedState.Idle) { - remainingHitsCountdown.Text = $"{swell.RequiredHits}"; + remainingHitsText.Text = $"{swell.RequiredHits}"; samplePlayed = false; } @@ -129,14 +138,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); - shrinkingRing.ResizeTo(0.1f, swell.Duration); + approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); } using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) { const double clear_transition_duration = 300; + const double clear_fade_in = 120; - bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + bodyContainer + .FadeOut(clear_transition_duration, Easing.OutQuad) + .ScaleTo(1.05f, clear_transition_duration, Easing.OutQuad); if (state == ArmedState.Hit) { @@ -147,9 +159,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .FadeIn(clear_transition_duration, Easing.InQuad) - .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) - .Delay(700).FadeOut(200, Easing.OutQuad); + .FadeIn(clear_fade_in) + .MoveTo(new Vector2(320, 240)) + .ScaleTo(0.4f) + .MoveTo(new Vector2(320, 150), clear_fade_in * 2, Easing.OutQuad) + .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .Delay(clear_fade_in * 3) + .FadeOut(clear_fade_in * 2.5); } } } From a62a84a30f7e92b9a855dfba7ddeb5c42a2bb442 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 20:48:29 +0800 Subject: [PATCH 435/620] fix code style --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 6 ------ osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs | 2 +- osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs | 6 +++--- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index e0276db911..363a6bf8e1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -156,12 +156,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override void UpdateStartTimeStateTransforms() - { - base.UpdateStartTimeStateTransforms(); - - } - protected override void UpdateHitStateTransforms(ArmedState state) { switch (state) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs index 65cd936e38..3b3684d219 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { protected override Drawable CreateCentreCircle() { - return new ArgonSwellCirclePiece() + return new ArgonSwellCirclePiece { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index 852116cbfe..a588f866c6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Depth = 1, - Children = new Drawable[] + Children = new[] { expandingRing = new CircularContainer { @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default protected virtual Drawable CreateCentreCircle() { - return new SwellCirclePiece() + return new SwellCirclePiece { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSwell drawableSwell)) + if (!(drawableHitObject is DrawableSwell)) return; Swell swell = drawableSwell.HitObject; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 9ed21b1bb0..43b2d5c435 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSwell drawableSwell)) + if (!(drawableHitObject is DrawableSwell)) return; Swell swell = drawableSwell.HitObject; From e794389fe83644323a563a343338e282783b53b1 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Sat, 1 Feb 2025 13:34:52 +0800 Subject: [PATCH 436/620] further adjust swell behavior The outstanding visual issues of the clear animation is fixed. The HandleUserInput state management is removed as it no longer seems necessary. --- .../Objects/Drawables/DrawableSwell.cs | 14 +-- .../Skinning/Legacy/LegacySwell.cs | 109 +++++++++--------- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 363a6bf8e1..d75fdbc40a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -158,21 +158,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void UpdateHitStateTransforms(ArmedState state) { + base.UpdateHitStateTransforms(state); + switch (state) { case ArmedState.Idle: - // Only for rewind support. Reallows user inputs if swell is rewound from being hit/missed to being idle. - HandleUserInput = true; break; case ArmedState.Miss: + this.Delay(300).FadeOut(); + break; + case ArmedState.Hit: - const int clear_animation_duration = 1200; - - // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. - LifetimeEnd = Time.Current + clear_animation_duration; - HandleUserInput = false; - + this.Delay(660).FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 43b2d5c435..0eb80d333f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -41,64 +41,63 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { - var spinnerCircleProvider = skin.FindProvider(s => s.GetTexture("spinner-circle") != null); - - Child = new Container + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - - Children = new Drawable[] + warning = new Sprite { - warning = new Sprite - { - Texture = skin.GetTexture("spinner-warning"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), - }, - bodyContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), - Alpha = 0, + Texture = skin.GetTexture("spinner-warning"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), - Children = new Drawable[] + Children = new Drawable[] + { + bodyContainer = new Container { - spinnerCircle = new Sprite + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] { - Texture = skin.GetTexture("spinner-circle"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.8f), - }, - approachCircle = new Sprite - { - Texture = skin.GetTexture("spinner-approachcircle"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.86f * 0.8f), - }, - remainingHitsText = new LegacySpriteText(LegacyFont.Combo) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, 165f), - Scale = Vector2.One, - }, - clearAnimation = new Sprite - { - Texture = skin.GetTexture("spinner-osu"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, -165f), - Scale = new Vector2(0.3f), - Alpha = 0, - }, - } + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + approachCircle = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.86f * 0.8f), + }, + remainingHitsText = new LegacySpriteText(LegacyFont.Combo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 165f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + Texture = skin.GetTexture("spinner-osu"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + }, }, - } + }, }; drawableSwell = (DrawableSwell)hitObject; @@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { remainingHitsText.Text = $"{requiredHits - numHits}"; - remainingHitsText.ScaleTo(1.6f - 0.6f * ((float)numHits / requiredHits), 60, Easing.OutQuad); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)numHits / requiredHits)), 60, Easing.OutQuad); spinnerCircle.ClearTransforms(); spinnerCircle @@ -160,9 +159,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearAnimation .FadeIn(clear_fade_in) - .MoveTo(new Vector2(320, 240)) + .MoveTo(new Vector2(0, 0)) .ScaleTo(0.4f) - .MoveTo(new Vector2(320, 150), clear_fade_in * 2, Easing.OutQuad) + .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.OutQuad) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); From cc3bb590c97b1d818229d06d614003c20370163c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 14:48:13 +0900 Subject: [PATCH 437/620] Remove pointless comment --- osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a1dabd66bc..75f56bffa4 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -59,8 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI this.Delay(50) .ScaleTo(0.75f, 250) .FadeOut(200); - - // osu!mania uses a custom fade length, so the base call is intentionally omitted. break; } } From 3cde11ab773f705e4132d7f837150e1b1232c11b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:28:39 +0900 Subject: [PATCH 438/620] Re-enable masking by default --- .../Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs | 7 +++++++ osu.Game/Screens/SelectV2/Carousel.cs | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 3a516ea762..0e72ee4f8c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -32,6 +32,13 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); } + [Test] + public void TestOffScreenLoading() + { + AddStep("disable masking", () => Scroll.Masking = false); + AddStep("enable masking", () => Scroll.Masking = true); + } + [Test] public void TestAddRemoveOneByOne() { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 648c2d090a..811bb120e1 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -205,7 +205,6 @@ namespace osu.Game.Screens.SelectV2 InternalChild = scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, - Masking = false, }; Items.BindCollectionChanged((_, _) => FilterAsync()); From d5dc55149d93cd534e3106a5997be2262d18be17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:29:14 +0900 Subject: [PATCH 439/620] Add initial difficulty grouping support --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 59 ++++++--- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 36 +++++- osu.Game/Screens/SelectV2/GroupPanel.cs | 113 ++++++++++++++++++ 3 files changed, 191 insertions(+), 17 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/GroupPanel.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index bb13c7449d..9a87fba140 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -92,34 +92,56 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling + private GroupDefinition? lastSelectedGroup; + private BeatmapInfo? lastSelectedBeatmap; + protected override void HandleItemSelected(object? model) { base.HandleItemSelected(model); - // Selecting a set isn't valid – let's re-select the first difficulty. - if (model is BeatmapSetInfo setInfo) + switch (model) { - CurrentSelection = setInfo.Beatmaps.First(); - return; - } + case GroupDefinition group: + if (lastSelectedGroup != null) + setVisibilityOfGroupItems(lastSelectedGroup, false); + lastSelectedGroup = group; - if (model is BeatmapInfo beatmapInfo) - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + setVisibilityOfGroupItems(group, true); + + // In stable, you can kinda select a group (expand without changing selection) + // For simplicity, let's not do that for now and handle similar to a beatmap set header. + CurrentSelection = grouping.GroupItems[group].First().Model; + return; + + case BeatmapSetInfo setInfo: + // Selecting a set isn't valid – let's re-select the first difficulty. + CurrentSelection = setInfo.Beatmaps.First(); + return; + + case BeatmapInfo beatmapInfo: + if (lastSelectedBeatmap != null) + setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + lastSelectedBeatmap = beatmapInfo; + + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + break; + } } - protected override void HandleItemDeselected(object? model) + private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) { - base.HandleItemDeselected(model); - - if (model is BeatmapInfo beatmapInfo) - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, false); + if (grouping.GroupItems.TryGetValue(group, out var items)) + { + foreach (var i in items) + i.IsVisible = visible; + } } private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) { - if (grouping.SetItems.TryGetValue(set, out var group)) + if (grouping.SetItems.TryGetValue(set, out var items)) { - foreach (var i in group) + foreach (var i in items) i.IsVisible = visible; } } @@ -143,9 +165,11 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); private readonly DrawablePool setPanelPool = new DrawablePool(100); + private readonly DrawablePool groupPanelPool = new DrawablePool(100); private void setupPools() { + AddInternal(groupPanelPool); AddInternal(beatmapPanelPool); AddInternal(setPanelPool); } @@ -154,7 +178,12 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { + case GroupDefinition: + return groupPanelPool.Get(); + case BeatmapInfo: + // TODO: if beatmap is a group selection target, it needs to be a different drawable + // with more information attached. return beatmapPanelPool.Get(); case BeatmapSetInfo: @@ -166,4 +195,6 @@ namespace osu.Game.Screens.SelectV2 #endregion } + + public record GroupDefinition(string Title); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 0658263a8c..e8384a8a2d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -18,7 +18,13 @@ namespace osu.Game.Screens.SelectV2 /// public IDictionary> SetItems => setItems; + /// + /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. + /// + public IDictionary> GroupItems => groupItems; + private readonly Dictionary> setItems = new Dictionary>(); + private readonly Dictionary> groupItems = new Dictionary>(); private readonly Func getCriteria; @@ -31,15 +37,40 @@ namespace osu.Game.Screens.SelectV2 { var criteria = getCriteria(); + int starGroup = int.MinValue; + if (criteria.SplitOutDifficulties) { + var diffItems = new List(items.Count()); + + GroupDefinition? group = null; + foreach (var item in items) { - item.IsVisible = true; + var b = (BeatmapInfo)item.Model; + + if (b.StarRating > starGroup) + { + starGroup = (int)Math.Floor(b.StarRating); + group = new GroupDefinition($"{starGroup} - {++starGroup} *"); + diffItems.Add(new CarouselItem(group) + { + DrawHeight = GroupPanel.HEIGHT, + IsGroupSelectionTarget = true + }); + } + + if (!groupItems.TryGetValue(group!, out var related)) + groupItems[group!] = related = new HashSet(); + related.Add(item); + + diffItems.Add(item); + + item.IsVisible = false; item.IsGroupSelectionTarget = true; } - return items; + return diffItems; } CarouselItem? lastItem = null; @@ -64,7 +95,6 @@ namespace osu.Game.Screens.SelectV2 if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) setItems[b.BeatmapSet!] = related = new HashSet(); - related.Add(item); } diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs new file mode 100644 index 0000000000..e837d8a32f --- /dev/null +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -0,0 +1,113 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class GroupPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + private Box activationFlash = null!; + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(500, HEIGHT); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue.Darken(5), + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + Debug.Assert(Item.IsGroupSelectionTarget); + + GroupDefinition group = (GroupDefinition)Item.Model; + + text.Text = group.Title; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From 764f799dcb3aeb33cb905888d811a91e5a37640f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 22:53:17 +0900 Subject: [PATCH 440/620] Improve selection flow using early exit and invalidation --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 31 +++++++++++--- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 3 ++ osu.Game/Screens/SelectV2/Carousel.cs | 41 ++++++++++--------- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9a87fba140..0a7ca5a6bb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition? lastSelectedGroup; private BeatmapInfo? lastSelectedBeatmap; - protected override void HandleItemSelected(object? model) + protected override bool HandleItemSelected(object? model) { base.HandleItemSelected(model); @@ -104,6 +104,14 @@ namespace osu.Game.Screens.SelectV2 case GroupDefinition group: if (lastSelectedGroup != null) setVisibilityOfGroupItems(lastSelectedGroup, false); + + // Collapsing an open group. + if (lastSelectedGroup == group) + { + lastSelectedGroup = null; + return false; + } + lastSelectedGroup = group; setVisibilityOfGroupItems(group, true); @@ -111,21 +119,34 @@ namespace osu.Game.Screens.SelectV2 // In stable, you can kinda select a group (expand without changing selection) // For simplicity, let's not do that for now and handle similar to a beatmap set header. CurrentSelection = grouping.GroupItems[group].First().Model; - return; + return false; case BeatmapSetInfo setInfo: // Selecting a set isn't valid – let's re-select the first difficulty. CurrentSelection = setInfo.Beatmaps.First(); - return; + return false; case BeatmapInfo beatmapInfo: if (lastSelectedBeatmap != null) setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); lastSelectedBeatmap = beatmapInfo; - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); - break; + // If we have groups, we need to account for them. + if (grouping.GroupItems.Count > 0) + { + // Find the containing group. There should never be too many groups so iterating is efficient enough. + var group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + setVisibilityOfGroupItems(group, true); + } + else + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + + // Ensure the group containing this beatmap is also visible. + // TODO: need to update visibility of correct group? + return true; } + + return true; } private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e8384a8a2d..9ecf735980 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -35,6 +35,9 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + setItems.Clear(); + groupItems.Clear(); + var criteria = getCriteria(); int starGroup = int.MinValue; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 811bb120e1..7184aaa866 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -170,9 +171,8 @@ namespace osu.Game.Screens.SelectV2 /// /// Called when an item is "selected". /// - protected virtual void HandleItemSelected(object? model) - { - } + /// Whether the item should be selected. + protected virtual bool HandleItemSelected(object? model) => true; /// /// Called when an item is "deselected". @@ -410,6 +410,8 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling + private readonly Cached selectionValid = new Cached(); + private Selection currentKeyboardSelection = new Selection(); private Selection currentSelection = new Selection(); @@ -418,29 +420,21 @@ namespace osu.Game.Screens.SelectV2 if (currentSelection.Model == model) return; - var previousSelection = currentSelection; + if (HandleItemSelected(model)) + { + if (currentSelection.Model != null) + HandleItemDeselected(currentSelection.Model); - if (previousSelection.Model != null) - HandleItemDeselected(previousSelection.Model); - - currentSelection = currentKeyboardSelection = new Selection(model); - HandleItemSelected(currentSelection.Model); - - // `HandleItemSelected` can alter `CurrentSelection`, which will recursively call `setSelection()` again. - // if that happens, the rest of this method should be a no-op. - if (currentSelection.Model != model) - return; - - refreshAfterSelection(); - scrollToSelection(); + currentKeyboardSelection = new Selection(model); + currentSelection = currentKeyboardSelection; + selectionValid.Invalidate(); + } } private void setKeyboardSelection(object? model) { currentKeyboardSelection = new Selection(model); - - refreshAfterSelection(); - scrollToSelection(); + selectionValid.Invalidate(); } /// @@ -525,6 +519,13 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems == null) return; + if (!selectionValid.IsValid) + { + refreshAfterSelection(); + scrollToSelection(); + selectionValid.Validate(); + } + var range = getDisplayRange(); if (range != displayedRange) From d74939e6e983267a5bc8be37d94108d46581b02f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jan 2025 20:58:32 +0900 Subject: [PATCH 441/620] Fix backwards traversal of groupings and allow toggling groups without updating selection --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 64 +++++++++++++------ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 9 +-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 1 - osu.Game/Screens/SelectV2/Carousel.cs | 18 +++++- osu.Game/Screens/SelectV2/CarouselItem.cs | 5 -- osu.Game/Screens/SelectV2/GroupPanel.cs | 1 - 6 files changed, 60 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 0a7ca5a6bb..10bc069cfc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -102,23 +102,15 @@ namespace osu.Game.Screens.SelectV2 switch (model) { case GroupDefinition group: - if (lastSelectedGroup != null) - setVisibilityOfGroupItems(lastSelectedGroup, false); - - // Collapsing an open group. + // Special case – collapsing an open group. if (lastSelectedGroup == group) { + setVisibilityOfGroupItems(lastSelectedGroup, false); lastSelectedGroup = null; return false; } - lastSelectedGroup = group; - - setVisibilityOfGroupItems(group, true); - - // In stable, you can kinda select a group (expand without changing selection) - // For simplicity, let's not do that for now and handle similar to a beatmap set header. - CurrentSelection = grouping.GroupItems[group].First().Model; + setVisibleGroup(group); return false; case BeatmapSetInfo setInfo: @@ -127,28 +119,52 @@ namespace osu.Game.Screens.SelectV2 return false; case BeatmapInfo beatmapInfo: - if (lastSelectedBeatmap != null) - setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); - lastSelectedBeatmap = beatmapInfo; // If we have groups, we need to account for them. - if (grouping.GroupItems.Count > 0) + if (Criteria.SplitOutDifficulties) { // Find the containing group. There should never be too many groups so iterating is efficient enough. - var group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - setVisibilityOfGroupItems(group, true); + GroupDefinition group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + + setVisibleGroup(group); } else - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + { + setVisibleSet(beatmapInfo); + } - // Ensure the group containing this beatmap is also visible. - // TODO: need to update visibility of correct group? return true; } return true; } + protected override bool CheckValidForGroupSelection(CarouselItem item) + { + switch (item.Model) + { + case BeatmapSetInfo: + return true; + + case BeatmapInfo: + return Criteria.SplitOutDifficulties; + + case GroupDefinition: + return false; + + default: + throw new ArgumentException($"Unsupported model type {item.Model}"); + } + } + + private void setVisibleGroup(GroupDefinition group) + { + if (lastSelectedGroup != null) + setVisibilityOfGroupItems(lastSelectedGroup, false); + lastSelectedGroup = group; + setVisibilityOfGroupItems(group, true); + } + private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) { if (grouping.GroupItems.TryGetValue(group, out var items)) @@ -158,6 +174,14 @@ namespace osu.Game.Screens.SelectV2 } } + private void setVisibleSet(BeatmapInfo beatmapInfo) + { + if (lastSelectedBeatmap != null) + setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + lastSelectedBeatmap = beatmapInfo; + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + } + private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) { if (grouping.SetItems.TryGetValue(set, out var items)) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 9ecf735980..951b010564 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -56,11 +56,7 @@ namespace osu.Game.Screens.SelectV2 { starGroup = (int)Math.Floor(b.StarRating); group = new GroupDefinition($"{starGroup} - {++starGroup} *"); - diffItems.Add(new CarouselItem(group) - { - DrawHeight = GroupPanel.HEIGHT, - IsGroupSelectionTarget = true - }); + diffItems.Add(new CarouselItem(group) { DrawHeight = GroupPanel.HEIGHT }); } if (!groupItems.TryGetValue(group!, out var related)) @@ -70,7 +66,6 @@ namespace osu.Game.Screens.SelectV2 diffItems.Add(item); item.IsVisible = false; - item.IsGroupSelectionTarget = true; } return diffItems; @@ -92,7 +87,6 @@ namespace osu.Game.Screens.SelectV2 newItems.Add(new CarouselItem(b.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT, - IsGroupSelectionTarget = true }); } @@ -104,7 +98,6 @@ namespace osu.Game.Screens.SelectV2 newItems.Add(item); lastItem = item; - item.IsGroupSelectionTarget = false; item.IsVisible = false; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 37e8b88f71..06e3ad3426 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -67,7 +67,6 @@ namespace osu.Game.Screens.SelectV2 base.PrepareForUse(); Debug.Assert(Item != null); - Debug.Assert(Item.IsGroupSelectionTarget); var beatmapSetInfo = (BeatmapSetInfo)Item.Model; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 7184aaa866..a76b6efee9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -168,6 +168,13 @@ namespace osu.Game.Screens.SelectV2 protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + /// + /// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target. + /// + /// The candidate item. + /// Whether the provided item is a valid group target. If false, more panels will be checked in the user's requested direction until a valid target is found. + protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true; + /// /// Called when an item is "selected". /// @@ -373,7 +380,7 @@ namespace osu.Game.Screens.SelectV2 // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (isGroupSelection && direction < 0) { - while (!carouselItems[selectionIndex].IsGroupSelectionTarget) + while (!CheckValidForGroupSelection(carouselItems[selectionIndex])) selectionIndex--; } @@ -394,7 +401,11 @@ namespace osu.Game.Screens.SelectV2 bool attemptSelection(CarouselItem item) { - if (!item.IsVisible || (isGroupSelection && !item.IsGroupSelectionTarget)) + // Keyboard (non-group) selection should only consider visible items. + if (!isGroupSelection && !item.IsVisible) + return false; + + if (isGroupSelection && !CheckValidForGroupSelection(item)) return false; if (isGroupSelection) @@ -427,8 +438,9 @@ namespace osu.Game.Screens.SelectV2 currentKeyboardSelection = new Selection(model); currentSelection = currentKeyboardSelection; - selectionValid.Invalidate(); } + + selectionValid.Invalidate(); } private void setKeyboardSelection(object? model) diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 2cb96a3d7f..13d5c840cf 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -29,11 +29,6 @@ namespace osu.Game.Screens.SelectV2 /// public float DrawHeight { get; set; } = DEFAULT_HEIGHT; - /// - /// Whether this item should be a valid target for user group selection hotkeys. - /// - public bool IsGroupSelectionTarget { get; set; } - /// /// Whether this item is visible or collapsed (hidden). /// diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index e837d8a32f..882d77cb8d 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -79,7 +79,6 @@ namespace osu.Game.Screens.SelectV2 base.PrepareForUse(); Debug.Assert(Item != null); - Debug.Assert(Item.IsGroupSelectionTarget); GroupDefinition group = (GroupDefinition)Item.Model; From 645c26ca19a16e9c5b33fb66125011c806ca2d78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 11:18:45 +0900 Subject: [PATCH 442/620] Simplify keyboard traversal logic --- osu.Game/Screens/SelectV2/Carousel.cs | 149 +++++++++++++------------- 1 file changed, 73 insertions(+), 76 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a76b6efee9..312dbc1bd9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -309,19 +309,19 @@ namespace osu.Game.Screens.SelectV2 return true; case GlobalAction.SelectNext: - selectNext(1, isGroupSelection: false); - return true; - - case GlobalAction.SelectNextGroup: - selectNext(1, isGroupSelection: true); + traverseKeyboardSelection(1); return true; case GlobalAction.SelectPrevious: - selectNext(-1, isGroupSelection: false); + traverseKeyboardSelection(-1); + return true; + + case GlobalAction.SelectNextGroup: + traverseGroupSelection(1); return true; case GlobalAction.SelectPreviousGroup: - selectNext(-1, isGroupSelection: true); + traverseGroupSelection(-1); return true; } @@ -332,89 +332,86 @@ namespace osu.Game.Screens.SelectV2 { } - /// - /// Select the next valid selection relative to a current selection. - /// This is generally for keyboard based traversal. - /// - /// Positive for downwards, negative for upwards. - /// Whether the selection should traverse groups. Group selection updates the actual selection immediately, while non-group selection will only prepare a future keyboard selection. - /// Whether selection was possible. - private bool selectNext(int direction, bool isGroupSelection) + private void traverseKeyboardSelection(int direction) { - // Ensure sanity - Debug.Assert(direction != 0); - direction = direction > 0 ? 1 : -1; + if (carouselItems == null || carouselItems.Count == 0) return; - if (carouselItems == null || carouselItems.Count == 0) - return false; + int originalIndex; - // If the user has a different keyboard selection and requests - // group selection, first transfer the keyboard selection to actual selection. - if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) - { - TryActivateSelection(); - return true; - } + if (currentKeyboardSelection.Index != null) + originalIndex = currentKeyboardSelection.Index.Value; + else if (direction > 0) + originalIndex = carouselItems.Count - 1; + else + originalIndex = 0; - CarouselItem? selectionItem = currentKeyboardSelection.CarouselItem; - int selectionIndex = currentKeyboardSelection.Index ?? -1; - - // To keep things simple, let's first handle the cases where there's no selection yet. - if (selectionItem == null || selectionIndex < 0) - { - // Start by selecting the first item. - selectionItem = carouselItems.First(); - selectionIndex = 0; - - // In the forwards case, immediately attempt selection of this panel. - // If selection fails, continue with standard logic to find the next valid selection. - if (direction > 0 && attemptSelection(selectionItem)) - return true; - - // In the backwards direction we can just allow the selection logic to go ahead and loop around to the last valid. - } - - Debug.Assert(selectionItem != null); - - // As a second special case, if we're group selecting backwards and the current selection isn't a group, - // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. - if (isGroupSelection && direction < 0) - { - while (!CheckValidForGroupSelection(carouselItems[selectionIndex])) - selectionIndex--; - } - - CarouselItem? newItem; + int newIndex = originalIndex; // Iterate over every item back to the current selection, finding the first valid item. // The fail condition is when we reach the selection after a cyclic loop over every item. do { - selectionIndex += direction; - newItem = carouselItems[(selectionIndex + carouselItems.Count) % carouselItems.Count]; + newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count; + var newItem = carouselItems[newIndex]; - if (attemptSelection(newItem)) - return true; - } while (newItem != selectionItem); + if (newItem.IsVisible) + { + setKeyboardSelection(newItem.Model); + return; + } + } while (newIndex != originalIndex); + } - return false; + /// + /// Select the next valid selection relative to a current selection. + /// This is generally for keyboard based traversal. + /// + /// Positive for downwards, negative for upwards. + /// Whether selection was possible. + private void traverseGroupSelection(int direction) + { + if (carouselItems == null || carouselItems.Count == 0) return; - bool attemptSelection(CarouselItem item) + // If the user has a different keyboard selection and requests + // group selection, first transfer the keyboard selection to actual selection. + if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - // Keyboard (non-group) selection should only consider visible items. - if (!isGroupSelection && !item.IsVisible) - return false; - - if (isGroupSelection && !CheckValidForGroupSelection(item)) - return false; - - if (isGroupSelection) - setSelection(item.Model); - else - setKeyboardSelection(item.Model); - - return true; + TryActivateSelection(); + return; } + + int originalIndex; + + if (currentKeyboardSelection.Index != null) + originalIndex = currentKeyboardSelection.Index.Value; + else if (direction > 0) + originalIndex = carouselItems.Count - 1; + else + originalIndex = 0; + + int newIndex = originalIndex; + + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. + if (direction < 0) + { + while (!CheckValidForGroupSelection(carouselItems[newIndex])) + newIndex--; + } + + // Iterate over every item back to the current selection, finding the first valid item. + // The fail condition is when we reach the selection after a cyclic loop over every item. + do + { + newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count; + var newItem = carouselItems[newIndex]; + + if (CheckValidForGroupSelection(newItem)) + { + setSelection(newItem.Model); + return; + } + } while (newIndex != originalIndex); } #endregion From 9c34819ff4a533f8a39879dd8a5053676bff415a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 14:55:48 +0900 Subject: [PATCH 443/620] Add test coverage for grouped selection --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 50 +++++++- ...estSceneBeatmapCarouselV2GroupSelection.cs | 121 ++++++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 112 +++++++--------- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 4 files changed, 217 insertions(+), 68 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 281be924a1..5143d681a6 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -21,6 +21,7 @@ using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK.Graphics; +using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; namespace osu.Game.Tests.Visual.SongSelect @@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [SetUpSteps] - public void SetUpSteps() + public virtual void SetUpSteps() { RemoveAllBeatmaps(); @@ -135,6 +136,53 @@ namespace osu.Game.Tests.Visual.SongSelect protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); + protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); + protected void SelectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); + protected void SelectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); + protected void SelectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); + + protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter)); + + protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); + protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + + protected void WaitForGroupSelection(int group, int panel) + { + AddUntilStep($"selected is group{group} panel{panel}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group); + CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel); + + return ReferenceEquals(Carousel.CurrentSelection, item.Model); + }); + } + + protected void WaitForSelection(int set, int? diff = null) + { + AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => + { + if (diff != null) + return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); + + return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); + }); + } + + protected void ClickVisiblePanel(int index) + where T : Drawable + { + AddStep($"click panel at index {index}", () => + { + Carousel.ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .Reverse() + .ElementAt(index) + .TriggerClick(); + }); + } + /// /// Add requested beatmap sets count to list. /// diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs new file mode 100644 index 0000000000..bcb609500f --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -0,0 +1,121 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene + { + public override void SetUpSteps() + { + RemoveAllBeatmaps(); + + CreateCarousel(); + + SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + } + + [Test] + public void TestOpenCloseGroupWithNoSelection() + { + AddBeatmaps(10, 5); + WaitForDrawablePanels(); + + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + CheckNoSelection(); + + ClickVisiblePanel(0); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + } + + [Test] + public void TestCarouselRemembersSelection() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + + SelectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + CheckHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + CheckHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + ClickVisiblePanel(0); + AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + + ClickVisiblePanel(0); + AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestKeyboardSelection() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); + + // open first group + Select(); + CheckNoSelection(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 0); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextGroup(); + WaitForGroupSelection(0, 2); + + SelectPrevGroup(); + WaitForGroupSelection(0, 1); + + SelectPrevGroup(); + WaitForGroupSelection(0, 0); + + SelectPrevGroup(); + WaitForGroupSelection(2, 9); + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 3c42969d8c..50395cf1ff 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -22,10 +22,10 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); WaitForDrawablePanels(); - checkNoSelection(); + CheckNoSelection(); - select(); - checkNoSelection(); + Select(); + CheckNoSelection(); AddStep("press down arrow", () => InputManager.PressKey(Key.Down)); checkSelectionIterating(false); @@ -39,8 +39,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up)); checkSelectionIterating(false); - select(); - checkHasSelection(); + Select(); + CheckHasSelection(); } /// @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); WaitForDrawablePanels(); - checkNoSelection(); + CheckNoSelection(); AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); checkSelectionIterating(true); @@ -73,13 +73,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10); WaitForDrawablePanels(); - selectNextGroup(); + SelectNextGroup(); object? selection = null; AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); - checkHasSelection(); + CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); @@ -89,13 +89,14 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10); WaitForDrawablePanels(); - checkHasSelection(); + CheckHasSelection(); AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } @@ -108,10 +109,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(total_set_count); WaitForDrawablePanels(); - selectNextGroup(); - waitForSelection(0, 0); - selectPrevGroup(); - waitForSelection(total_set_count - 1, 0); + SelectNextGroup(); + WaitForSelection(0, 0); + SelectPrevGroup(); + WaitForSelection(total_set_count - 1, 0); } [Test] @@ -122,10 +123,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(total_set_count); WaitForDrawablePanels(); - selectPrevGroup(); - waitForSelection(total_set_count - 1, 0); - selectNextGroup(); - waitForSelection(0, 0); + SelectPrevGroup(); + WaitForSelection(total_set_count - 1, 0); + SelectNextGroup(); + WaitForSelection(0, 0); } [Test] @@ -134,71 +135,50 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10, 3); WaitForDrawablePanels(); - selectNextPanel(); - selectNextPanel(); - selectNextPanel(); - selectNextPanel(); - checkNoSelection(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); - select(); - waitForSelection(3, 0); + Select(); + WaitForSelection(3, 0); - selectNextPanel(); - waitForSelection(3, 0); + SelectNextPanel(); + WaitForSelection(3, 0); - select(); - waitForSelection(3, 1); + Select(); + WaitForSelection(3, 1); - selectNextPanel(); - waitForSelection(3, 1); + SelectNextPanel(); + WaitForSelection(3, 1); - select(); - waitForSelection(3, 2); + Select(); + WaitForSelection(3, 2); - selectNextPanel(); - waitForSelection(3, 2); + SelectNextPanel(); + WaitForSelection(3, 2); - select(); - waitForSelection(4, 0); + Select(); + WaitForSelection(4, 0); } [Test] public void TestEmptyTraversal() { - selectNextPanel(); - checkNoSelection(); + SelectNextPanel(); + CheckNoSelection(); - selectNextGroup(); - checkNoSelection(); + SelectNextGroup(); + CheckNoSelection(); - selectPrevPanel(); - checkNoSelection(); + SelectPrevPanel(); + CheckNoSelection(); - selectPrevGroup(); - checkNoSelection(); + SelectPrevGroup(); + CheckNoSelection(); } - private void waitForSelection(int set, int? diff = null) - { - AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => - { - if (diff != null) - return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); - - return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); - }); - } - - private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); - private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); - private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); - private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); - - private void select() => AddStep("select", () => InputManager.Key(Key.Enter)); - - private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); - private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); - private void checkSelectionIterating(bool isIterating) { object? selection = null; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 312dbc1bd9..0da9cb5c19 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.SelectV2 /// /// A filter may add, mutate or remove items. /// - protected IEnumerable Filters { get; init; } = Enumerable.Empty(); + public IEnumerable Filters { get; init; } = Enumerable.Empty(); /// /// All items which are to be considered for display in this carousel. From 6a18d18feb0ada227cb85fdb9144439196b3cef7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 2 Feb 2025 13:28:31 +0900 Subject: [PATCH 444/620] Fix null handling when no items are populated but a selection is made --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 10bc069cfc..858888c517 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -124,9 +124,10 @@ namespace osu.Game.Screens.SelectV2 if (Criteria.SplitOutDifficulties) { // Find the containing group. There should never be too many groups so iterating is efficient enough. - GroupDefinition group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - setVisibleGroup(group); + if (group != null) + setVisibleGroup(group); } else { From 48e30f4ee80af5fd9c0e6e39bfd28d48a5df6ccf Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Mon, 3 Feb 2025 09:49:37 +0800 Subject: [PATCH 445/620] remove skinning section swell delay test Replaced by TestHitSwellThenHitHit in TestSceneSwellJudgements. --- .../TestSceneDrawableSwellExpireDelay.cs | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs deleted file mode 100644 index ad78ed3b20..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using NUnit.Framework; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Replays; -using osu.Game.Rulesets.Taiko.Tests.Judgements; - -namespace osu.Game.Rulesets.Taiko.Tests.Skinning -{ - public partial class TestSceneDrawableSwellExpireDelay : JudgementTest - { - [Test] - public void TestExpireDelay() - { - const double swell_start = 1000; - const double swell_duration = 1000; - - Swell swell = new Swell - { - StartTime = swell_start, - Duration = swell_duration, - }; - - Hit hit = new Hit { StartTime = swell_start + swell_duration + 50 }; - - List frames = new List - { - new TaikoReplayFrame(0), - new TaikoReplayFrame(2100, TaikoAction.LeftCentre), - }; - - PerformTest(frames, CreateBeatmap(swell, hit)); - - AssertResult(0, HitResult.Ok); - } - } -} From 210fa14759313b8b8f0b1aadc7c5e0c84394a4ee Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Feb 2025 14:15:43 +0900 Subject: [PATCH 446/620] Play sound via results screen instead --- .../Expanded/Accuracy/AccuracyCircle.cs | 49 +----- osu.Game/Screens/Ranking/ResultsScreen.cs | 166 ++++++++++++------ 2 files changed, 116 insertions(+), 99 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 319a87fdfc..4b960b05fb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -91,6 +91,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly ScoreInfo score; + [Resolved] + private ResultsScreen? resultsScreen { get; set; } + private CircularProgress accuracyCircle = null!; private GradedCircles gradedCircles = null!; private Container badges = null!; @@ -101,7 +104,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private PoolableSkinnableSample? badgeMaxSound; private PoolableSkinnableSample? swooshUpSound; private PoolableSkinnableSample? rankImpactSound; - private PoolableSkinnableSample? rankApplauseSound; private readonly Bindable tickPlaybackRate = new Bindable(); @@ -197,15 +199,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (withFlair) { - var applauseSamples = new List { applauseSampleName }; - if (score.Rank >= ScoreRank.B) - // when rank is B or higher, play legacy applause sample on legacy skins. - applauseSamples.Insert(0, @"applause"); - AddRangeInternal(new Drawable[] { rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)), - rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())), scoreTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/score-tick")), badgeTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink")), badgeMaxSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink-max")), @@ -333,16 +329,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }); const double applause_pre_delay = 545f; - const double applause_volume = 0.8f; using (BeginDelayedSequence(applause_pre_delay)) - { - Schedule(() => - { - rankApplauseSound!.VolumeTo(applause_volume); - rankApplauseSound!.Play(); - }); - } + Schedule(() => resultsScreen?.PlayApplause(score.Rank)); } } @@ -384,34 +373,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy } } - private string applauseSampleName - { - get - { - switch (score.Rank) - { - default: - case ScoreRank.D: - return @"Results/applause-d"; - - case ScoreRank.C: - return @"Results/applause-c"; - - case ScoreRank.B: - return @"Results/applause-b"; - - case ScoreRank.A: - return @"Results/applause-a"; - - case ScoreRank.S: - case ScoreRank.SH: - case ScoreRank.X: - case ScoreRank.XH: - return @"Results/applause-s"; - } - } - } - private string impactSampleName { get diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 95dbfb2712..b10684b22e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -29,10 +30,12 @@ using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Expanded.Accuracy; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Ranking { + [Cached] public abstract partial class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler { protected const float BACKGROUND_BLUR = 20; @@ -64,7 +67,6 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; - private AudioContainer audioContainer = null!; private bool lastFetchCompleted; @@ -101,80 +103,76 @@ namespace osu.Game.Screens.Ranking popInSample = audio.Samples.Get(@"UI/overlay-pop-in"); - InternalChild = audioContainer = new AudioContainer + InternalChild = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new PopoverContainer + Child = new GridContainer { RelativeSizeAxes = Axes.Both, - Child = new GridContainer + Content = new[] { - RelativeSizeAxes = Axes.Both, - Content = new[] + new Drawable[] { - new Drawable[] + VerticalScrollContent = new VerticalScrollContainer { - VerticalScrollContent = new VerticalScrollContainer + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new Container { RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - StatisticsPanel = createStatisticsPanel().With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), - ScorePanelList = new ScorePanelList - { - RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => StatisticsPanel.ToggleVisibility() - }, - detachedPanelContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - } - } - }, - }, - new[] - { - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, Children = new Drawable[] { - new Box + StatisticsPanel = createStatisticsPanel().With(panel => + { + panel.RelativeSizeAxes = Axes.Both; + panel.Score.BindTarget = SelectedScore; + }), + ScorePanelList = new ScorePanelList { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = () => StatisticsPanel.ToggleVisibility() }, - buttons = new FillFlowContainer + detachedPanelContainer = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal + RelativeSizeAxes = Axes.Both }, } } - } + }, }, - RowDimensions = new[] + new[] { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + buttons = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal + }, + } + } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } } }; @@ -268,6 +266,64 @@ namespace osu.Game.Screens.Ranking } } + #region Applause + + private PoolableSkinnableSample? rankApplauseSound; + + public void PlayApplause(ScoreRank rank) + { + const double applause_volume = 0.8f; + + if (!this.IsCurrentScreen()) + return; + + rankApplauseSound?.Dispose(); + + var applauseSamples = new List(); + + if (rank >= ScoreRank.B) + // when rank is B or higher, play legacy applause sample on legacy skins. + applauseSamples.Insert(0, @"applause"); + + switch (rank) + { + default: + case ScoreRank.D: + applauseSamples.Add(@"Results/applause-d"); + break; + + case ScoreRank.C: + applauseSamples.Add(@"Results/applause-c"); + break; + + case ScoreRank.B: + applauseSamples.Add(@"Results/applause-b"); + break; + + case ScoreRank.A: + applauseSamples.Add(@"Results/applause-a"); + break; + + case ScoreRank.S: + case ScoreRank.SH: + case ScoreRank.X: + case ScoreRank.XH: + applauseSamples.Add(@"Results/applause-s"); + break; + } + + LoadComponentAsync(rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())), s => + { + if (!this.IsCurrentScreen() || s != rankApplauseSound) + return; + + rankApplauseSound.VolumeTo(applause_volume); + rankApplauseSound.Play(); + }); + } + + #endregion + /// /// Performs a fetch/refresh of scores to be displayed. /// @@ -336,7 +392,7 @@ namespace osu.Game.Screens.Ranking if (!skipExitTransition) this.FadeOut(100); - audioContainer.Volume.Value = 0; + rankApplauseSound?.Stop(); return false; } From 9033a4d480ed78a69c5c57c10c31789b15b688fd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Feb 2025 14:20:56 +0900 Subject: [PATCH 447/620] Remove unused using --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 4b960b05fb..f6cf71d8a6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; From a23de0b1885a3c5f62e4b9971b094167d8c5b1a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 16:29:39 +0900 Subject: [PATCH 448/620] Avoid accessing `WorkingBeatmap.Beatmap` every update call Notice in passing. Comes with overheads that can be easily avoided. Left a note for a future (slightly more involved) optimisation. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 ++ .../Play/MasterGameplayClockContainer.cs | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 890a969415..fd40097c4e 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -203,6 +203,8 @@ namespace osu.Game.Beatmaps { try { + // TODO: This is a touch expensive and can become an issue if being accessed every Update call. + // Optimally we would not involve the async flow if things are already loaded. return loadBeatmapAsync().GetResultSafely(); } catch (AggregateException ae) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index c20d461526..747ea3090c 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play private readonly Bindable playbackRateValid = new Bindable(true); - private readonly WorkingBeatmap beatmap; + private readonly IBeatmap beatmap; private Track track; @@ -63,20 +63,19 @@ namespace osu.Game.Screens.Play /// /// Create a new master gameplay clock container. /// - /// The beatmap to be used for time and metadata references. + /// The beatmap to be used for time and metadata references. /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. - public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) - : base(beatmap.Track, applyOffsets: true, requireDecoupling: true) + public MasterGameplayClockContainer(WorkingBeatmap working, double gameplayStartTime) + : base(working.Track, applyOffsets: true, requireDecoupling: true) { - this.beatmap = beatmap; - - track = beatmap.Track; + beatmap = working.Beatmap; + track = working.Track; GameplayStartTime = gameplayStartTime; - StartTime = findEarliestStartTime(gameplayStartTime, beatmap); + StartTime = findEarliestStartTime(gameplayStartTime, working); } - private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap beatmap) + private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap working) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. @@ -86,15 +85,15 @@ namespace osu.Game.Screens.Play // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. - double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; + double? firstStoryboardEvent = working.Storyboard.EarliestEventTime; if (firstStoryboardEvent != null) time = Math.Min(time, firstStoryboardEvent.Value); // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. - double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - if (beatmap.Beatmap.AudioLeadIn > 0) - time = Math.Min(time, firstHitObjectTime - beatmap.Beatmap.AudioLeadIn); + double firstHitObjectTime = working.Beatmap.HitObjects.First().StartTime; + if (working.Beatmap.AudioLeadIn > 0) + time = Math.Min(time, firstHitObjectTime - working.Beatmap.AudioLeadIn); return time; } @@ -136,7 +135,7 @@ namespace osu.Game.Screens.Play { removeAdjustmentsFromTrack(); - track = new TrackVirtual(beatmap.Track.Length); + track = new TrackVirtual(track.Length); track.Seek(CurrentTime); if (IsRunning) track.Start(); @@ -228,9 +227,8 @@ namespace osu.Game.Screens.Play removeAdjustmentsFromTrack(); } - ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; + ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.ControlPointInfo; + ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => track.CurrentAmplitudes; IClock IBeatSyncProvider.Clock => this; - - ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; } } From c587958f387db1287218801292a1ed9480d8edef Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 1 Feb 2025 03:01:47 -0500 Subject: [PATCH 449/620] Apply depth ordering relative to selected item --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 648c2d090a..f41154b878 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -544,8 +544,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - if (panel.Depth != c.DrawYPosition) - scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition); + double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; + scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos)); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); From 26a8fb6984e66ef3d992db23beec6f86ca0b682d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 17:34:55 +0900 Subject: [PATCH 450/620] Make distance snap settings mutually exclusive --- osu.Game/Screens/Edit/Editor.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d5ed54db81..6b18b05174 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -330,6 +330,18 @@ namespace osu.Game.Screens.Edit editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); editorContractSidebars = config.GetBindable(OsuSetting.EditorContractSidebars); + // These two settings don't work together. Make them mutually exclusive to let the user know. + editorAutoSeekOnPlacement.BindValueChanged(enabled => + { + if (enabled.NewValue) + editorLimitedDistanceSnap.Value = false; + }); + editorLimitedDistanceSnap.BindValueChanged(enabled => + { + if (enabled.NewValue) + editorAutoSeekOnPlacement.Value = false; + }); + AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, From df51d345c5e1e49b98c43b898f38ccd0403b5abf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 17:36:47 +0900 Subject: [PATCH 451/620] Change menus to fade out with a slight delay so settings changes are visible Useful for cases like https://github.com/ppy/osu/pull/31778, where a change to one setting can affect another. --- osu.Game/Graphics/UserInterface/OsuMenu.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 7cc1bab25f..9b099c0884 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -68,7 +68,9 @@ namespace osu.Game.Graphics.UserInterface if (!TopLevelMenu && wasOpened) menuSamples?.PlayCloseSample(); - this.FadeOut(300, Easing.OutQuint); + this.Delay(50) + .FadeOut(300, Easing.OutQuint); + wasOpened = false; } @@ -77,12 +79,21 @@ namespace osu.Game.Graphics.UserInterface if (Direction == Direction.Vertical) { Width = newSize.X; - this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); + + if (newSize.Y > 0) + this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); + else + // Delay until the fade out finishes from AnimateClose. + this.Delay(350).ResizeHeightTo(0); } else { Height = newSize.Y; - this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); + if (newSize.X > 0) + this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); + else + // Delay until the fade out finishes from AnimateClose. + this.Delay(350).ResizeWidthTo(0); } } From 55f46e3b668fbc16856f872044cc011f739e05b8 Mon Sep 17 00:00:00 2001 From: NecoDev <120387312+necocat0918@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:06:27 +0800 Subject: [PATCH 452/620] Added warning --- osu.Game/Screens/Edit/BookmarkResetDialog.cs | 26 ++++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/BookmarkResetDialog.cs diff --git a/osu.Game/Screens/Edit/BookmarkResetDialog.cs b/osu.Game/Screens/Edit/BookmarkResetDialog.cs new file mode 100644 index 0000000000..48a0202c86 --- /dev/null +++ b/osu.Game/Screens/Edit/BookmarkResetDialog.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 osu.Framework.Allocation; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public partial class BookmarkResetDialog : DeletionDialog + { + private readonly EditorBeatmap editor; + + public BookmarkResetDialog(EditorBeatmap editorBeatmap) + { + editor = editorBeatmap; + BodyText = "All Bookmarks"; + } + + [BackgroundDependencyLoader] + private void load() + { + DangerousAction = () => editor.Bookmarks.Clear(); + } + } +} + diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d5ed54db81..8cffab87ea 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using DiffPlex.Model; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; @@ -450,7 +451,7 @@ namespace osu.Game.Screens.Edit { Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) }, - new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => editorBeatmap.Bookmarks.Clear()) + new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) } } } From 444e0970d600d90e087e47af1816b63d7487a796 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 18:54:18 +0900 Subject: [PATCH 453/620] Standardise naming to use "Freestyle" not "FreeStyle" --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 2 +- osu.Game/Online/Rooms/PlaylistItem.cs | 8 ++++---- ...ButtonFreeStyle.cs => FooterButtonFreestyle.cs} | 4 ++-- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- ...eeStyleStatusPill.cs => FreestyleStatusPill.cs} | 12 ++++++------ osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 ++++---- .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 14 +++++++------- .../OnlinePlay/Playlists/PlaylistsSongSelect.cs | 2 +- 9 files changed, 27 insertions(+), 27 deletions(-) rename osu.Game/Screens/OnlinePlay/{FooterButtonFreeStyle.cs => FooterButtonFreestyle.cs} (96%) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{FreeStyleStatusPill.cs => FreestyleStatusPill.cs} (84%) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4dfb3b389d..b737cda4ba 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -60,7 +60,7 @@ namespace osu.Game.Online.Rooms /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [Key(11)] - public bool FreeStyle { get; set; } + public bool Freestyle { get; set; } [SerializationConstructor] public MultiplayerPlaylistItem() diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index e8725b6792..817b42f503 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.Rooms /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [JsonProperty("freestyle")] - public bool FreeStyle { get; set; } + public bool Freestyle { get; set; } /// /// A beatmap representing this playlist item. @@ -107,7 +107,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); - FreeStyle = item.FreeStyle; + Freestyle = item.Freestyle; } public void MarkInvalid() => valid.Value = false; @@ -139,7 +139,7 @@ namespace osu.Game.Online.Rooms PlayedAt = PlayedAt, AllowedMods = AllowedMods, RequiredMods = RequiredMods, - FreeStyle = FreeStyle, + Freestyle = Freestyle, valid = { Value = Valid.Value }, }; } @@ -152,6 +152,6 @@ namespace osu.Game.Online.Rooms && PlaylistOrder == other.PlaylistOrder && AllowedMods.SequenceEqual(other.AllowedMods) && RequiredMods.SequenceEqual(other.RequiredMods) - && FreeStyle == other.FreeStyle; + && Freestyle == other.Freestyle; } } diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs similarity index 96% rename from osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs rename to osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index 0e22b3d3fb..157f90d078 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -16,7 +16,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreeStyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private OsuColour colours { get; set; } = null!; - public FooterButtonFreeStyle() + public FooterButtonFreestyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. base.Action = () => current.Value = !current.Value; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7bc0b612f1..a16267aa10 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, - new FreeStyleStatusPill(Room) + new FreestyleStatusPill(Room) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs similarity index 84% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs index 1c0135fb89..b306e27f84 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public partial class FreeStyleStatusPill : OnlinePlayPill + public partial class FreestyleStatusPill : OnlinePlayPill { private readonly Room room; @@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); - public FreeStyleStatusPill(Room room) + public FreestyleStatusPill(Room room) { this.room = room; } @@ -35,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components TextFlow.Colour = Color4.Black; room.PropertyChanged += onRoomPropertyChanged; - updateFreeStyleStatus(); + updateFreestyleStatus(); } private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -44,15 +44,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { case nameof(Room.CurrentPlaylistItem): case nameof(Room.Playlist): - updateFreeStyleStatus(); + updateFreestyleStatus(); break; } } - private void updateFreeStyleStatus() + private void updateFreestyleStatus() { PlaylistItem? currentItem = room.Playlist.GetCurrentItem() ?? room.CurrentPlaylistItem; - Alpha = currentItem?.FreeStyle == true ? 1 : 0; + Alpha = currentItem?.Freestyle == true ? 1 : 0; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c9c9c3eca7..9f7e193131 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -450,11 +450,11 @@ namespace osu.Game.Screens.OnlinePlay.Match Ruleset.Value = GetGameplayRuleset(); bool freeMod = item.AllowedMods.Any(); - bool freeStyle = item.FreeStyle; + bool freestyle = item.Freestyle; // For now, the game can never be in a state where freemod and freestyle are on at the same time. // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. - Debug.Assert(!freeMod || !freeStyle); + Debug.Assert(!freeMod || !freestyle); if (freeMod) { @@ -468,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = _ => false; } - if (freeStyle) + if (freestyle) { UserStyleSection.Show(); @@ -481,7 +481,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, - AllowEditing = freeStyle, + AllowEditing = freestyle, RequestEdit = _ => OpenStyleSelection() }; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 5754bcb963..b42a58787d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), AllowedMods = item.AllowedMods.ToArray(), - FreeStyle = item.FreeStyle + Freestyle = item.Freestyle }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f6403c010e..8d1e3c3cb1 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable FreeStyle = new Bindable(); + protected readonly Bindable Freestyle = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; @@ -112,17 +112,17 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } - FreeStyle.Value = initialItem.FreeStyle; + Freestyle.Value = initialItem.Freestyle; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); - FreeStyle.BindValueChanged(onFreeStyleChanged, true); + Freestyle.BindValueChanged(onFreestyleChanged, true); freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); } - private void onFreeStyleChanged(ValueChangedEvent enabled) + private void onFreestyleChanged(ValueChangedEvent enabled) { if (enabled.NewValue) { @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - FreeStyle = FreeStyle.Value + Freestyle = Freestyle.Value }; return SelectItem(item); @@ -204,12 +204,12 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; + var freestyleButton = new FooterButtonFreestyle { Current = Freestyle }; baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, null), - (freeStyleButton, null) + (freestyleButton, null) }); return baseButtons; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index abf80c0d44..84446ed0cf 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - FreeStyle = FreeStyle.Value + Freestyle = Freestyle.Value }; } } From 37abb1a21bc24185b8d554fc38f2f0cef09284e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:09:58 +0900 Subject: [PATCH 454/620] Tidy up button construction code --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 8d1e3c3cb1..4ca6abbf7d 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -203,13 +203,10 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; - freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freestyleButton = new FooterButtonFreestyle { Current = Freestyle }; - baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton, null), - (freestyleButton, null) + (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null), + (new FooterButtonFreestyle { Current = Freestyle }, null) }); return baseButtons; From 8bb7bea04e56fab9247baa59ae879e16c8b4bd9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:21:21 +0900 Subject: [PATCH 455/620] Rename freestyle select screen classes for better discoverability --- ...MatchStyleSelect.cs => MultiplayerMatchFreestyleSelect.cs} | 4 ++-- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- ...{OnlinePlayStyleSelect.cs => OnlinePlayFreestyleSelect.cs} | 4 ++-- ...istsRoomStyleSelect.cs => PlaylistsRoomFreestyleSelect.cs} | 4 ++-- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/{MultiplayerMatchStyleSelect.cs => MultiplayerMatchFreestyleSelect.cs} (94%) rename osu.Game/Screens/OnlinePlay/{OnlinePlayStyleSelect.cs => OnlinePlayFreestyleSelect.cs} (94%) rename osu.Game/Screens/OnlinePlay/Playlists/{PlaylistsRoomStyleSelect.cs => PlaylistsRoomFreestyleSelect.cs} (87%) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs index 3fe4926052..0c04c2712c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs @@ -12,7 +12,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerMatchStyleSelect : OnlinePlayStyleSelect + public partial class MultiplayerMatchFreestyleSelect : OnlinePlayFreestyleSelect { [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private LoadingLayer loadingLayer = null!; private IDisposable? selectionOperation; - public MultiplayerMatchStyleSelect(Room room, PlaylistItem item) + public MultiplayerMatchFreestyleSelect(Room room, PlaylistItem item) : base(room, item) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f882fb7f89..b803c5f28b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -258,7 +258,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - this.Push(new MultiplayerMatchStyleSelect(Room, item)); + this.Push(new MultiplayerMatchFreestyleSelect(Room, item)); } protected override Drawable CreateFooter() => new MultiplayerMatchFooter diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs index 4d34000d3c..4844d096ce 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs @@ -16,7 +16,7 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay { - public abstract partial class OnlinePlayStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap + public abstract partial class OnlinePlayFreestyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap { public string ShortTitle => "style selection"; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly Room room; private readonly PlaylistItem item; - protected OnlinePlayStyleSelect(Room room, PlaylistItem item) + protected OnlinePlayFreestyleSelect(Room room, PlaylistItem item) { this.room = room; this.item = item; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs similarity index 87% rename from osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs index 912496ba34..9c85088cc9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs @@ -9,12 +9,12 @@ using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class PlaylistsRoomStyleSelect : OnlinePlayStyleSelect + public partial class PlaylistsRoomFreestyleSelect : OnlinePlayFreestyleSelect { public new readonly Bindable Beatmap = new Bindable(); public new readonly Bindable Ruleset = new Bindable(); - public PlaylistsRoomStyleSelect(Room room, PlaylistItem item) + public PlaylistsRoomFreestyleSelect(Room room, PlaylistItem item) : base(room, item) { } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 2c74767f42..2195ed4722 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -319,7 +319,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - this.Push(new PlaylistsRoomStyleSelect(Room, item) + this.Push(new PlaylistsRoomFreestyleSelect(Room, item) { Beatmap = { BindTarget = userBeatmap }, Ruleset = { BindTarget = userRuleset } From 99192404f125b3f5f380b4a167f7a6be1d6646ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:26:14 +0900 Subject: [PATCH 456/620] Tidy up `WorkingBeatmap` passing in `ctor` --- .../Screens/Play/MasterGameplayClockContainer.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 747ea3090c..07ecb5a5fb 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Overlays; +using osu.Game.Storyboards; namespace osu.Game.Screens.Play { @@ -72,10 +73,10 @@ namespace osu.Game.Screens.Play track = working.Track; GameplayStartTime = gameplayStartTime; - StartTime = findEarliestStartTime(gameplayStartTime, working); + StartTime = findEarliestStartTime(gameplayStartTime, beatmap, working.Storyboard); } - private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap working) + private static double findEarliestStartTime(double gameplayStartTime, IBeatmap beatmap, Storyboard storyboard) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. @@ -85,15 +86,15 @@ namespace osu.Game.Screens.Play // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. - double? firstStoryboardEvent = working.Storyboard.EarliestEventTime; + double? firstStoryboardEvent = storyboard.EarliestEventTime; if (firstStoryboardEvent != null) time = Math.Min(time, firstStoryboardEvent.Value); // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. - double firstHitObjectTime = working.Beatmap.HitObjects.First().StartTime; - if (working.Beatmap.AudioLeadIn > 0) - time = Math.Min(time, firstHitObjectTime - working.Beatmap.AudioLeadIn); + double firstHitObjectTime = beatmap.HitObjects.First().StartTime; + if (beatmap.AudioLeadIn > 0) + time = Math.Min(time, firstHitObjectTime - beatmap.AudioLeadIn); return time; } From c7780c9fdca97525d2f20920bc44951b652e4854 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:53:46 +0900 Subject: [PATCH 457/620] Refactor how grouping is performed --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 2 +- ...estSceneBeatmapCarouselV2GroupSelection.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 133 +++++++++++------- 4 files changed, 85 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 5143d681a6..0a9719423c 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); + protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 0e72ee4f8c..8ffb51b995 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestSorting() { AddBeatmaps(10); - SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); SortBy(new FilterCriteria { Sort = SortMode.Artist }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index bcb609500f..5728583507 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.SongSelect CreateCarousel(); - SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 951b010564..34fbfdbaa6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.SelectV2 { @@ -35,70 +36,100 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + bool groupSetsTogether; + setItems.Clear(); groupItems.Clear(); var criteria = getCriteria(); - - int starGroup = int.MinValue; - - if (criteria.SplitOutDifficulties) - { - var diffItems = new List(items.Count()); - - GroupDefinition? group = null; - - foreach (var item in items) - { - var b = (BeatmapInfo)item.Model; - - if (b.StarRating > starGroup) - { - starGroup = (int)Math.Floor(b.StarRating); - group = new GroupDefinition($"{starGroup} - {++starGroup} *"); - diffItems.Add(new CarouselItem(group) { DrawHeight = GroupPanel.HEIGHT }); - } - - if (!groupItems.TryGetValue(group!, out var related)) - groupItems[group!] = related = new HashSet(); - related.Add(item); - - diffItems.Add(item); - - item.IsVisible = false; - } - - return diffItems; - } - - CarouselItem? lastItem = null; - var newItems = new List(items.Count()); - foreach (var item in items) + // Add criteria groups. + switch (criteria.Group) + { + default: + groupSetsTogether = true; + newItems.AddRange(items); + break; + + case GroupMode.Difficulty: + groupSetsTogether = false; + int starGroup = int.MinValue; + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var b = (BeatmapInfo)item.Model; + + if (b.StarRating > starGroup) + { + starGroup = (int)Math.Floor(b.StarRating); + newItems.Add(new CarouselItem(new GroupDefinition($"{starGroup} - {++starGroup} *")) { DrawHeight = GroupPanel.HEIGHT }); + } + + newItems.Add(item); + } + + break; + } + + // Add set headers wherever required. + CarouselItem? lastItem = null; + + if (groupSetsTogether) + { + for (int i = 0; i < newItems.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var item = newItems[i]; + + if (item.Model is BeatmapInfo beatmap) + { + if (groupSetsTogether) + { + bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + + if (newBeatmapSet) + { + newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + i++; + } + + if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) + setItems[beatmap.BeatmapSet!] = related = new HashSet(); + + related.Add(item); + item.IsVisible = false; + } + } + + lastItem = item; + } + } + + // Link group items to their headers. + GroupDefinition? lastGroup = null; + + foreach (var item in newItems) { cancellationToken.ThrowIfCancellationRequested(); - if (item.Model is BeatmapInfo b) + if (item.Model is GroupDefinition group) { - // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) - { - newItems.Add(new CarouselItem(b.BeatmapSet!) - { - DrawHeight = BeatmapSetPanel.HEIGHT, - }); - } - - if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) - setItems[b.BeatmapSet!] = related = new HashSet(); - related.Add(item); + lastGroup = group; + continue; } - newItems.Add(item); - lastItem = item; + if (lastGroup != null) + { + if (!groupItems.TryGetValue(lastGroup, out var groupRelated)) + groupItems[lastGroup] = groupRelated = new HashSet(); + groupRelated.Add(item); - item.IsVisible = false; + item.IsVisible = false; + } } return newItems; From a1185df2ebb833c0cfb9a4a93987a2a97e547453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 Jan 2025 14:33:14 +0100 Subject: [PATCH 458/620] Refactor `IDistanceSnapProvider` to accept slider velocity objects as a reference Method signatures are also changed to be a lot more explicit as to what inputs they expect. --- .../Edit/CatchDistanceSnapProvider.cs | 2 +- .../Components/PathControlPointVisualiser.cs | 2 +- .../Sliders/SliderPlacementBlueprint.cs | 2 +- .../Sliders/SliderSelectionBlueprint.cs | 4 +- .../Edit/OsuDistanceSnapGrid.cs | 5 +- .../Edit/OsuDistanceSnapProvider.cs | 2 +- .../Edit/OsuHitObjectComposer.cs | 17 ++-- ...tSceneHitObjectComposerDistanceSnapping.cs | 31 +++---- .../Editing/TestSceneDistanceSnapGrid.cs | 14 +-- .../Edit/ComposerDistanceSnapProvider.cs | 46 +++------ .../Rulesets/Edit/IDistanceSnapProvider.cs | 93 ++++++++++++------- .../Rulesets/Objects/SliderPathExtensions.cs | 4 +- .../Components/CircularDistanceSnapGrid.cs | 30 +++--- .../Compose/Components/DistanceSnapGrid.cs | 20 ++-- 14 files changed, 138 insertions(+), 134 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs index ae4025aa2f..420a0eb34f 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit // // The implementation below is probably correct but should be checked if/when exposed via controls. - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX; float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index b9938209ae..bc3d27fd68 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -34,7 +34,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public partial class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu - where T : OsuHitObject, IHasPath + where T : OsuHitObject, IHasPath, IHasSliderVelocity { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield. diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 21817045c4..a747d4fce8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (state == SliderPlacementState.Drawing) HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance; else - HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance((float)HitObject.Path.CalculatedDistance, HitObject.StartTime, HitObject) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 740862c9fd..f7c25b43dd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } else { - double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1; + double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1; // Add a small amount to the proposed distance to make it easier to snap to the full length of the slider. - proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance; + proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance; proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index 848c994974..3323acce15 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -12,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid { - public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) - : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1) + public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null) + : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1, sliderVelocitySource) { Masking = true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs index 4042cfa0e2..3c0889d027 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit { public override double ReadCurrentDistanceSnap(HitObject before, HitObject after) { - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); return actualDistance / expectedDistance; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 563d0b1e3e..60c37cd4a4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; @@ -406,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset); - int sourceIndex = -1; + int positionSourceObjectIndex = -1; + IHasSliderVelocity? sliderVelocitySource = null; for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++) { if (!sourceSelector(EditorBeatmap.HitObjects[i])) break; - sourceIndex = i; + positionSourceObjectIndex = i; + + if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity) + sliderVelocitySource = hasSliderVelocity; } - if (sourceIndex == -1) + if (positionSourceObjectIndex == -1) return null; - HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex]; + HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex]; - int targetIndex = sourceIndex + targetOffset; + int targetIndex = positionSourceObjectIndex + targetOffset; HitObject targetObject = null; // Keep advancing the target object while its start time falls before the end time of the source object @@ -442,7 +447,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (sourceObject is Spinner) return null; - return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject); + return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject, sliderVelocitySource); } } } diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 0f8583253b..af116ad334 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -12,6 +12,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; @@ -67,17 +68,7 @@ namespace osu.Game.Tests.Editing { AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier); - assertSnapDistance(100 * multiplier, null, true); - } - - [TestCase(1)] - [TestCase(2)] - public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier) - { - assertSnapDistance(100, new Slider - { - SliderVelocityMultiplier = multiplier - }, false); + assertSnapDistance(100 * multiplier); } [TestCase(1)] @@ -87,7 +78,7 @@ namespace osu.Game.Tests.Editing assertSnapDistance(100 * multiplier, new Slider { SliderVelocityMultiplier = multiplier - }, true); + }); } [TestCase(1)] @@ -96,7 +87,7 @@ namespace osu.Game.Tests.Editing { AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor); - assertSnapDistance(100f / divisor, null, true); + assertSnapDistance(100f / divisor); } /// @@ -114,7 +105,7 @@ namespace osu.Game.Tests.Editing }; AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject)); - assertSnapDistance(base_distance * slider_velocity, referenceObject, true); + assertSnapDistance(base_distance * slider_velocity, referenceObject); assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject); assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject); @@ -289,20 +280,20 @@ namespace osu.Game.Tests.Editing AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False); } - private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity) - => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null) + => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private partial class TestHitObjectComposer : OsuHitObjectComposer { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 818862d958..51e4f526a1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Editing public new int MaxIntervals => base.MaxIntervals; public TestDistanceSnapGrid(double? endTime = null) - : base(new HitObject(), grid_position, 0, endTime) + : base(grid_position, 0, endTime) { } @@ -191,15 +191,15 @@ namespace osu.Game.Tests.Visual.Editing Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; - public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance; + public float GetBeatSnapDistance(IHasSliderVelocity withVelocity = null) => beat_snap_distance; - public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; + public float DurationToDistance(double duration, double timingReference, IHasSliderVelocity withVelocity = null) => (float)duration; - public double DistanceToDuration(HitObject referenceObject, float distance) => distance; + public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public double FindSnappedDuration(HitObject referenceObject, float distance) => 0; + public double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; - public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0; + public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; } } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 0ca01ccee6..997d1f927b 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -265,57 +265,41 @@ namespace osu.Game.Rulesets.Edit #region IDistanceSnapProvider - public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) + public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null) { - return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 + return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 / beatSnapProvider.BeatDivisor); } - public virtual float DurationToDistance(HitObject referenceObject, double duration) + public virtual float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null) { - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference); + return (float)(duration / beatLength * GetBeatSnapDistance(withVelocity)); } - public virtual double DistanceToDuration(HitObject referenceObject, float distance) + public virtual double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null) { - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; + double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference); + return distance / GetBeatSnapDistance(withVelocity) * beatLength; } - public virtual double FindSnappedDuration(HitObject referenceObject, float distance) - => beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; + public virtual double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) + => beatSnapProvider.SnapTime(snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity), snapReferenceTime) - snapReferenceTime; - public virtual float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) + public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) { - double referenceTime; + double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity); - switch (target) - { - case DistanceSnapTarget.Start: - referenceTime = referenceObject.StartTime; - break; + double snappedTime = beatSnapProvider.SnapTime(actualDuration, snapReferenceTime); - case DistanceSnapTarget.End: - referenceTime = referenceObject.GetEndTime(); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(target), target, $"Unknown {nameof(DistanceSnapTarget)} value"); - } - - double actualDuration = referenceTime + DistanceToDuration(referenceObject, distance); - - double snappedTime = beatSnapProvider.SnapTime(actualDuration, referenceTime); - - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(snapReferenceTime); // we don't want to exceed the actual duration and snap to a point in the future. // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. if (snappedTime > actualDuration + 1) snappedTime -= beatLength; - return DurationToDistance(referenceObject, snappedTime - referenceTime); + return DurationToDistance(snappedTime - snapReferenceTime, snapReferenceTime, withVelocity); } #endregion diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 612e09d3ea..99a9083273 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -4,7 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Edit { @@ -22,53 +22,74 @@ namespace osu.Game.Rulesets.Edit Bindable DistanceSpacingMultiplier { get; } /// - /// Retrieves the distance between two points within a timing point that are one beat length apart. + /// Returns the spatial distance between objects which are temporally one beat apart. + /// Depends on: + /// + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// Whether the 's slider velocity should be factored into the returned distance. - /// The distance between two points residing in the timing point that are one beat length apart. - float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true); + float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null); /// - /// Converts a duration to a distance without applying any snapping. + /// Converts a temporal duration into a spatial distance. + /// Does not perform any snapping. + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// The duration to convert. - /// A value that represents as a distance in the timing point. - float DurationToDistance(HitObject referenceObject, double duration); + float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Converts a distance to a duration without applying any snapping. + /// Converts a spatial distance into a temporal duration. + /// Does not perform any snapping. + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents as a duration in the timing point. - double DistanceToDuration(HitObject referenceObject, float distance); + double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Given a distance from the provided hit object, find the valid snapped duration. + /// Converts a spatial distance into a temporal duration and then snaps said duration to the beat, relative to . + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents as a duration snapped to the closest beat of the timing point. - double FindSnappedDuration(HitObject referenceObject, float distance); + double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); /// - /// Given a distance from the provided hit object, find the valid snapped distance. + /// Snaps a spatial distance to the beat, relative to . + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// Whether the distance measured should be from the start or the end of . - /// - /// A value that represents snapped to the closest beat of the timing point. - /// The distance will always be less than or equal to the provided . - /// - float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target); - } - - public enum DistanceSnapTarget - { - Start, - End, + float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); } } diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index a631274f74..4ce8166421 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Objects /// Snaps the provided 's duration using the . /// public static void SnapTo(this THitObject hitObject, IDistanceSnapProvider? snapProvider) - where THitObject : HitObject, IHasPath + where THitObject : HitObject, IHasPath, IHasSliderVelocity { - hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance; + hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance((float)hitObject.Path.CalculatedDistance, hitObject.StartTime, hitObject) ?? hitObject.Path.CalculatedDistance; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index e84c2ebc35..9ddf54b779 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; @@ -16,8 +16,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { - protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) - : base(referenceObject, startPosition, startTime, endTime) + protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, IHasSliderVelocity? sliderVelocitySource = null) + : base(startPosition, startTime, endTime, sliderVelocitySource) { } @@ -56,14 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components // Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to // 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the // fact that the 1/2 snap reference object is not valid for 1/3 snapping. - float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0, DistanceSnapTarget.End); + float offset = SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource); for (int i = 0; i < requiredCircles; i++) { const float thickness = 4; float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2; - AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i)) + AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i)) { Position = StartPosition, Origin = Anchor.Centre, @@ -98,19 +98,19 @@ namespace osu.Game.Screens.Edit.Compose.Components travelLength = DistanceBetweenTicks; float snappedDistance = fixedTime != null - ? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime()) + ? SnapProvider.DurationToDistance(fixedTime.Value - StartTime, StartTime, SliderVelocitySource) // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // to allow for snapping at a non-multiplied ratio. - : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End); + : SnapProvider.FindSnappedDistance(travelLength / distanceSpacingMultiplier, StartTime, SliderVelocitySource); - double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); + double snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource); if (snappedTime > LatestEndTime) { double tickLength = Beatmap.GetBeatLengthAtTime(StartTime); - snappedDistance = SnapProvider.DurationToDistance(ReferenceObject, MaxIntervals * tickLength); - snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); + snappedDistance = SnapProvider.DurationToDistance(MaxIntervals * tickLength, StartTime, SliderVelocitySource); + snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource); } // The multiplier can then be reapplied to the final position. @@ -127,13 +127,13 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private EditorClock? editorClock { get; set; } - private readonly HitObject referenceObject; + private readonly double startTime; private readonly Color4 baseColour; - public Ring(HitObject referenceObject, Color4 baseColour) + public Ring(double startTime, Color4 baseColour) { - this.referenceObject = referenceObject; + this.startTime = startTime; Colour = this.baseColour = baseColour; @@ -148,9 +148,9 @@ namespace osu.Game.Screens.Edit.Compose.Components return; float distanceSpacingMultiplier = (float)snapProvider.DistanceSpacingMultiplier.Value; - double timeFromReferencePoint = editorClock.CurrentTime - referenceObject.GetEndTime(); + double timeFromReferencePoint = editorClock.CurrentTime - startTime; - float distanceForCurrentTime = snapProvider.DurationToDistance(referenceObject, timeFromReferencePoint) + float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime) * distanceSpacingMultiplier; float timeBasedAlpha = 1 - Math.Clamp(Math.Abs(distanceForCurrentTime - Size.X / 2) / 30, 0, 1); diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index aaf58e0f7a..dd1671cfdd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -12,7 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; @@ -48,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly double? LatestEndTime; + [CanBeNull] + protected readonly IHasSliderVelocity SliderVelocitySource; + [Resolved] protected OsuColour Colours { get; private set; } @@ -62,19 +66,17 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); - protected readonly HitObject ReferenceObject; - /// /// Creates a new . /// - /// A reference object to gather relevant difficulty values from. /// The position at which the grid should start. The first tick is located one distance spacing length away from this point. /// The snapping time at . /// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded. - protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) + /// The reference object with slider velocity to include in the calculations for distance snapping. + protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null) { - ReferenceObject = referenceObject; LatestEndTime = endTime; + SliderVelocitySource = sliderVelocitySource; StartPosition = startPosition; StartTime = startTime; @@ -97,14 +99,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updateSpacing() { float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value; - float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false); + float beatSnapDistance = SnapProvider.GetBeatSnapDistance(SliderVelocitySource); DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier; if (LatestEndTime == null) MaxIntervals = int.MaxValue; else - MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(ReferenceObject, beatSnapDistance)); + MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(beatSnapDistance, StartTime, SliderVelocitySource)); gridCache.Invalidate(); } @@ -132,7 +134,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The original position in coordinate space local to this . /// /// Whether the snap operation should be temporally constrained to a particular time instant, - /// thus fixing the possible positions to a set distance from the . + /// thus fixing the possible positions to a set distance relative from the . /// /// A tuple containing the snapped position in coordinate space local to this and the respective time value. public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null); From df37768ff4075ca4de2c4afee377967d745d5e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Feb 2025 13:55:04 +0100 Subject: [PATCH 459/620] Remove unused method Only used in test code. --- ...tSceneHitObjectComposerDistanceSnapping.cs | 37 ------------------- .../Editing/TestSceneDistanceSnapGrid.cs | 2 - .../Edit/ComposerDistanceSnapProvider.cs | 3 -- .../Rulesets/Edit/IDistanceSnapProvider.cs | 13 ------- 4 files changed, 55 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index af116ad334..408db39d54 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -107,7 +107,6 @@ namespace osu.Game.Tests.Editing assertSnapDistance(base_distance * slider_velocity, referenceObject); assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject); - assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject); assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject); assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject); @@ -155,39 +154,6 @@ namespace osu.Game.Tests.Editing assertDistanceToDuration(400, 1000); } - [Test] - public void TestGetSnappedDurationFromDistance() - { - assertSnappedDuration(0, 0); - assertSnappedDuration(50, 1000); - assertSnappedDuration(100, 1000); - assertSnappedDuration(150, 2000); - assertSnappedDuration(200, 2000); - assertSnappedDuration(250, 3000); - - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); - - assertSnappedDuration(0, 0); - assertSnappedDuration(50, 0); - assertSnappedDuration(100, 1000); - assertSnappedDuration(150, 1000); - assertSnappedDuration(200, 1000); - assertSnappedDuration(250, 1000); - - AddStep("set beat length = 500", () => - { - composer.EditorBeatmap.ControlPointInfo.Clear(); - composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); - }); - - assertSnappedDuration(50, 0); - assertSnappedDuration(100, 500); - assertSnappedDuration(150, 500); - assertSnappedDuration(200, 500); - assertSnappedDuration(250, 500); - assertSnappedDuration(400, 1000); - } - [Test] public void GetSnappedDistanceFromDistance() { @@ -289,9 +255,6 @@ namespace osu.Game.Tests.Editing private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); - private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); - private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 51e4f526a1..af02333468 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -197,8 +197,6 @@ namespace osu.Game.Tests.Visual.Editing public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; - public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 997d1f927b..d0b279f201 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -283,9 +283,6 @@ namespace osu.Game.Rulesets.Edit return distance / GetBeatSnapDistance(withVelocity) * beatLength; } - public virtual double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) - => beatSnapProvider.SnapTime(snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity), snapReferenceTime) - snapReferenceTime; - public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) { double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity); diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 99a9083273..8006db14a3 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -66,19 +66,6 @@ namespace osu.Game.Rulesets.Edit /// double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); - /// - /// Converts a spatial distance into a temporal duration and then snaps said duration to the beat, relative to . - /// Depends on: - /// - /// the provided, - /// a used to retrieve the beat length of the beatmap at that time, - /// the slider velocity taken from , - /// the beatmap's ,, - /// the current beat divisor. - /// - /// - double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); - /// /// Snaps a spatial distance to the beat, relative to . /// Depends on: From 2d6f64e89185e71d755201c52c951236116a0fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Feb 2025 15:17:32 +0100 Subject: [PATCH 460/620] Fix code quality --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs | 2 +- osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 60c37cd4a4..b3e23daa99 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -408,7 +408,7 @@ namespace osu.Game.Rulesets.Osu.Edit ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset); int positionSourceObjectIndex = -1; - IHasSliderVelocity? sliderVelocitySource = null; + IHasSliderVelocity sliderVelocitySource = null; for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++) { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index af02333468..fb57422e66 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.Editing public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; + public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; } } } diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 8006db14a3..195dbf0d46 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Edit double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Snaps a spatial distance to the beat, relative to . + /// Snaps a spatial distance to the beat, relative to . /// Depends on: /// /// the provided, From b433eef1389ae8a07627ee6a9597bebe336d61c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 01:51:43 +0900 Subject: [PATCH 461/620] Remove redundant conditional check --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 34fbfdbaa6..ea737d8b7f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -87,22 +87,19 @@ namespace osu.Game.Screens.SelectV2 if (item.Model is BeatmapInfo beatmap) { - if (groupSetsTogether) + bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + + if (newBeatmapSet) { - bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); - - if (newBeatmapSet) - { - newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); - i++; - } - - if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) - setItems[beatmap.BeatmapSet!] = related = new HashSet(); - - related.Add(item); - item.IsVisible = false; + newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + i++; } + + if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) + setItems[beatmap.BeatmapSet!] = related = new HashSet(); + + related.Add(item); + item.IsVisible = false; } lastItem = item; From b5c4e3bc147e0c4f085de754ed8019dc18ead270 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 02:41:56 +0900 Subject: [PATCH 462/620] Add failing tests for traversal on group headers --- .../TestSceneBeatmapCarouselV2GroupSelection.cs | 14 ++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index 5728583507..04ca0a9085 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -81,6 +81,20 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } + [Test] + public void TestGroupSelectionOnHeader() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + WaitForGroupSelection(0, 0); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForGroupSelection(2, 9); + } + [Test] public void TestKeyboardSelection() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 50395cf1ff..b087c252e4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -129,6 +129,21 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForSelection(0, 0); } + [Test] + public void TestGroupSelectionOnHeader() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + SelectNextGroup(); + WaitForSelection(1, 0); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForSelection(0, 0); + } + [Test] public void TestKeyboardSelection() { From e454fa558cb5891ac6614dd9c626fa21834c168f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 02:55:57 +0900 Subject: [PATCH 463/620] Adjust group traversal logic to handle cases where keyboard selection redirects --- osu.Game/Screens/SelectV2/Carousel.cs | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 0da9cb5c19..a13de0e26d 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -377,26 +377,31 @@ namespace osu.Game.Screens.SelectV2 if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { TryActivateSelection(); - return; + + // There's a chance this couldn't resolve, at which point continue with standard traversal. + if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) + return; } int originalIndex; + int newIndex; - if (currentKeyboardSelection.Index != null) - originalIndex = currentKeyboardSelection.Index.Value; - else if (direction > 0) - originalIndex = carouselItems.Count - 1; - else - originalIndex = 0; - - int newIndex = originalIndex; - - // As a second special case, if we're group selecting backwards and the current selection isn't a group, - // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. - if (direction < 0) + if (currentSelection.Index == null) { - while (!CheckValidForGroupSelection(carouselItems[newIndex])) - newIndex--; + // If there's no current selection, start from either end of the full list. + newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0; + } + else + { + newIndex = originalIndex = currentSelection.Index.Value; + + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. + if (direction < 0) + { + while (!CheckValidForGroupSelection(carouselItems[newIndex])) + newIndex--; + } } // Iterate over every item back to the current selection, finding the first valid item. From 38933039880b3b50eaef5557290a9c806dd79f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Oct 2024 11:59:27 +0200 Subject: [PATCH 464/620] Implement "form button" control --- .../UserInterface/TestSceneFormControls.cs | 166 ++++++++------- .../Graphics/UserInterfaceV2/FormButton.cs | 189 ++++++++++++++++++ 2 files changed, 280 insertions(+), 75 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 118fbca97b..2003f5de83 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; @@ -27,87 +28,102 @@ namespace osu.Game.Tests.Visual.UserInterface Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer + Child = new OsuScrollContainer { - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 400, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Padding = new MarginPadding(10), - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - new FormTextBox + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] { - Caption = "Artist", - HintText = "Poot artist here!", - PlaceholderText = "Here is an artist", - TabbableContentContainer = this, - }, - new FormTextBox - { - Caption = "Artist", - HintText = "Poot artist here!", - PlaceholderText = "Here is an artist", - Current = { Disabled = true }, - TabbableContentContainer = this, - }, - new FormNumberBox(allowDecimals: true) - { - Caption = "Number", - HintText = "Insert your favourite number", - PlaceholderText = "Mine is 42!", - TabbableContentContainer = this, - }, - new FormCheckBox - { - Caption = EditorSetupStrings.LetterboxDuringBreaks, - HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, - }, - new FormCheckBox - { - Caption = EditorSetupStrings.LetterboxDuringBreaks, - HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, - Current = { Disabled = true }, - }, - new FormSliderBar - { - Caption = "Slider", - Current = new BindableFloat + new FormTextBox { - MinValue = 0, - MaxValue = 10, - Value = 5, - Precision = 0.1f, + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + TabbableContentContainer = this, }, - TabbableContentContainer = this, - }, - new FormEnumDropdown - { - Caption = EditorSetupStrings.EnableCountdown, - HintText = EditorSetupStrings.CountdownDescription, - }, - new FormFileSelector - { - Caption = "File selector", - PlaceholderText = "Select a file", - }, - new FormBeatmapFileSelector(true) - { - Caption = "File selector with intermediate choice dialog", - PlaceholderText = "Select a file", - }, - new FormColourPalette - { - Caption = "Combo colours", - Colours = + new FormTextBox { - Colour4.Red, - Colour4.Green, - Colour4.Blue, - Colour4.Yellow, - } + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + Current = { Disabled = true }, + TabbableContentContainer = this, + }, + new FormNumberBox(allowDecimals: true) + { + Caption = "Number", + HintText = "Insert your favourite number", + PlaceholderText = "Mine is 42!", + TabbableContentContainer = this, + }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + Current = { Disabled = true }, + }, + new FormSliderBar + { + Caption = "Slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + }, + TabbableContentContainer = this, + }, + new FormEnumDropdown + { + Caption = EditorSetupStrings.EnableCountdown, + HintText = EditorSetupStrings.CountdownDescription, + }, + new FormFileSelector + { + Caption = "File selector", + PlaceholderText = "Select a file", + }, + new FormBeatmapFileSelector(true) + { + Caption = "File selector with intermediate choice dialog", + PlaceholderText = "Select a file", + }, + new FormColourPalette + { + Caption = "Combo colours", + Colours = + { + Colour4.Red, + Colour4.Green, + Colour4.Blue, + Colour4.Yellow, + } + }, + new FormButton + { + Caption = "No text in button", + Action = () => { }, + }, + new FormButton + { + Caption = "Text in button which is pretty long and is very likely to wrap", + ButtonText = "Foo the bar", + Action = () => { }, + }, }, }, }, diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs new file mode 100644 index 0000000000..fec855153b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -0,0 +1,189 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormButton : CompositeDrawable + { + /// + /// Caption describing this button, displayed on the left of it. + /// + public LocalisableString Caption { get; init; } + + public LocalisableString ButtonText { get; init; } + + public Action? Action { get; init; } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + CornerExponent = 2.5f; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Left = 9, + Right = 5, + Vertical = 5, + }, + Children = new Drawable[] + { + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.45f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = Caption, + }, + new Button + { + Action = Action, + Text = ButtonText, + RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X, + Width = ButtonText == default ? 90 : 0.45f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + }, + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + private void updateState() + { + BorderThickness = IsHovered ? 2 : 0; + + if (IsHovered) + BorderColour = colourProvider.Light4; + } + + public partial class Button : OsuButton + { + private TrianglesV2? triangles { get; set; } + + protected override float HoverLayerFinalAlpha => 0; + + private Color4? triangleGradientSecondColour; + + public override Color4 BackgroundColour + { + get => base.BackgroundColour; + set + { + base.BackgroundColour = value; + triangleGradientSecondColour = BackgroundColour.Lighten(0.2f); + updateColours(); + } + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColourProvider) + { + DefaultBackgroundColour = overlayColourProvider.Colour3; + triangleGradientSecondColour ??= overlayColourProvider.Colour1; + + if (Text == default) + { + Add(new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(16), + Shadow = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Content.CornerRadius = 2; + + Add(triangles = new TrianglesV2 + { + Thickness = 0.02f, + SpawnRatio = 0.6f, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }); + + updateColours(); + } + + private void updateColours() + { + if (triangles == null) + return; + + Debug.Assert(triangleGradientSecondColour != null); + + triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour); + } + + protected override bool OnHover(HoverEvent e) + { + Debug.Assert(triangleGradientSecondColour != null); + + Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + Background.FadeColour(BackgroundColour, 300, Easing.OutQuint); + base.OnHoverLost(e); + } + } + } +} From 4dd4e52e6dc43c1a4f4fe55c4262650b3e4e6919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 08:58:37 +0100 Subject: [PATCH 465/620] Implement visual appearance of beatmap submission wizard --- .../TestSceneBeatmapSubmissionOverlay.cs | 42 ++++++ osu.Game/Configuration/OsuConfigManager.cs | 5 + .../Localisation/BeatmapSubmissionStrings.cs | 124 ++++++++++++++++++ .../Overlays/FirstRunSetup/ScreenWelcome.cs | 2 + osu.Game/Overlays/WizardOverlay.cs | 13 +- osu.Game/Overlays/WizardScreen.cs | 3 + .../Submission/BeatmapSubmissionOverlay.cs | 28 ++++ .../Submission/ScreenContentPermissions.cs | 44 +++++++ .../ScreenFrequentlyAskedQuestions.cs | 62 +++++++++ .../Submission/ScreenSubmissionSettings.cs | 73 +++++++++++ 10 files changed, 389 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs create mode 100644 osu.Game/Localisation/BeatmapSubmissionStrings.cs create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs new file mode 100644 index 0000000000..07a794b7eb --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Screens.Edit.Submission; +using osu.Game.Screens.Footer; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneBeatmapSubmissionOverlay : OsuTestScene + { + private ScreenFooter footer = null!; + private BeatmapSubmissionOverlay overlay = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("add overlay", () => + { + var receptor = new ScreenFooter.BackReceptor(); + footer = new ScreenFooter(receptor); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + Children = new Drawable[] + { + receptor, + overlay = new BeatmapSubmissionOverlay() + { + State = { Value = Visibility.Visible, }, + }, + footer, + } + }; + }); + } + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 908f434655..1244dd8cfc 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -220,6 +220,9 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false); SetDefault(OsuSetting.EditorShowStoryboard, true); + + SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true); + SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -461,5 +464,7 @@ namespace osu.Game.Configuration BeatmapListingFeaturedArtistFilter, ShowMobileDisclaimer, EditorShowStoryboard, + EditorSubmissionNotifyOnDiscussionReplies, + EditorSubmissionLoadInBrowserAfterSubmission, } } diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs new file mode 100644 index 0000000000..85fe922703 --- /dev/null +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class BeatmapSubmissionStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.BeatmapSubmission"; + + /// + /// "Beatmap submission" + /// + public static LocalisableString BeatmapSubmissionTitle => new TranslatableString(getKey(@"beatmap_submission_title"), @"Beatmap submission"); + + /// + /// "Share your beatmap with the world!" + /// + public static LocalisableString BeatmapSubmissionDescription => new TranslatableString(getKey(@"beatmap_submission_description"), @"Share your beatmap with the world!"); + + /// + /// "Content permissions" + /// + public static LocalisableString ContentPermissions => new TranslatableString(getKey(@"content_permissions"), @"Content permissions"); + + /// + /// "I understand" + /// + public static LocalisableString ContentPermissionsAcknowledgement => new TranslatableString(getKey(@"content_permissions_acknowledgement"), @"I understand"); + + /// + /// "Frequently asked questions" + /// + public static LocalisableString FrequentlyAskedQuestions => new TranslatableString(getKey(@"frequently_asked_questions"), @"Frequently asked questions"); + + /// + /// "Submission settings" + /// + public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings"); + + /// + /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" + /// + public static LocalisableString ContentPermissionsDisclaimer => new TranslatableString(getKey(@"content_permissions_disclaimer"), @"Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!"); + + /// + /// "Check the content usage guidelines for more information" + /// + public static LocalisableString CheckContentUsageGuidelines => new TranslatableString(getKey(@"check_content_usage_guidelines"), @"Check the content usage guidelines for more information"); + + /// + /// "Beatmap ranking criteria" + /// + public static LocalisableString BeatmapRankingCriteria => new TranslatableString(getKey(@"beatmap_ranking_criteria"), @"Beatmap ranking criteria"); + + /// + /// "Not sure you meet the guidelines? Check the list and speed up the ranking process!" + /// + public static LocalisableString BeatmapRankingCriteriaDescription => new TranslatableString(getKey(@"beatmap_ranking_criteria_description"), @"Not sure you meet the guidelines? Check the list and speed up the ranking process!"); + + /// + /// "Submission process" + /// + public static LocalisableString SubmissionProcess => new TranslatableString(getKey(@"submission_process"), @"Submission process"); + + /// + /// "Unsure about the submission process? Check out the wiki entry!" + /// + public static LocalisableString SubmissionProcessDescription => new TranslatableString(getKey(@"submission_process_description"), @"Unsure about the submission process? Check out the wiki entry!"); + + /// + /// "Mapping help forum" + /// + public static LocalisableString MappingHelpForum => new TranslatableString(getKey(@"mapping_help_forum"), @"Mapping help forum"); + + /// + /// "Got some questions about mapping and submission? Ask them in the forums!" + /// + public static LocalisableString MappingHelpForumDescription => new TranslatableString(getKey(@"mapping_help_forum_description"), @"Got some questions about mapping and submission? Ask them in the forums!"); + + /// + /// "Modding queues forum" + /// + public static LocalisableString ModdingQueuesForum => new TranslatableString(getKey(@"modding_queues_forum"), @"Modding queues forum"); + + /// + /// "Having trouble getting feedback? Why not ask in a mod queue!" + /// + public static LocalisableString ModdingQueuesForumDescription => new TranslatableString(getKey(@"modding_queues_forum_description"), @"Having trouble getting feedback? Why not ask in a mod queue!"); + + /// + /// "Where would you like to post your map?" + /// + public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your map?"); + + /// + /// "Works in Progress / Help (incomplete, not ready for ranking)" + /// + public static LocalisableString BeatmapSubmissionTargetWIP => new TranslatableString(getKey(@"beatmap_submission_target_wip"), @"Works in Progress / Help (incomplete, not ready for ranking)"); + + /// + /// "Pending (complete, ready for ranking)" + /// + public static LocalisableString BeatmapSubmissionTargetPending => new TranslatableString(getKey(@"beatmap_submission_target_pending"), @"Pending (complete, ready for ranking)"); + + /// + /// "Receive notifications for discussion replies" + /// + public static LocalisableString NotifyOnDiscussionReplies => new TranslatableString(getKey(@"notify_for_discussion_replies"), @"Receive notifications for discussion replies"); + + /// + /// "Load in browser after submission" + /// + public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); + + /// + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs index 93cf555bc9..e03a08dd46 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs @@ -65,6 +65,8 @@ namespace osu.Game.Overlays.FirstRunSetup }; } + public override LocalisableString? NextStepText => FirstRunSetupOverlayStrings.GetStarted; + private partial class LanguageSelectionFlow : FillFlowContainer { private Bindable language = null!; diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 38701efc96..34ffa7bd77 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -227,7 +227,7 @@ namespace osu.Game.Overlays updateButtons(); } - private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps); + private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, CurrentScreen, steps); public partial class WizardFooterContent : VisibilityContainer { @@ -248,24 +248,23 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Width = 1, Text = FirstRunSetupOverlayStrings.GetStarted, - DarkerColour = colourProvider.Colour2, - LighterColour = colourProvider.Colour1, + DarkerColour = colourProvider.Colour3, + LighterColour = colourProvider.Colour2, Action = () => ShowNextStep?.Invoke(), }; } - public void UpdateButtons(int? currentStep, IReadOnlyList steps) + public void UpdateButtons(int? currentStep, WizardScreen? currentScreen, IReadOnlyList steps) { NextButton.Enabled.Value = currentStep != null; if (currentStep == null) return; - bool isFirstStep = currentStep == 0; bool isLastStep = currentStep == steps.Count - 1; - if (isFirstStep) - NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + if (currentScreen?.NextStepText != null) + NextButton.Text = currentScreen.NextStepText.Value; else { NextButton.Text = isLastStep diff --git a/osu.Game/Overlays/WizardScreen.cs b/osu.Game/Overlays/WizardScreen.cs index 7f3b1fe7f4..5112efaa61 100644 --- a/osu.Game/Overlays/WizardScreen.cs +++ b/osu.Game/Overlays/WizardScreen.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -102,5 +103,7 @@ namespace osu.Game.Overlays base.OnSuspending(e); } + + public virtual LocalisableString? NextStepText => null; } } diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs new file mode 100644 index 0000000000..da2abd8c23 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Overlays; +using osu.Game.Localisation; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class BeatmapSubmissionOverlay : WizardOverlay + { + public BeatmapSubmissionOverlay() + : base(OverlayColourScheme.Aquamarine) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AddStep(); + AddStep(); + AddStep(); + + Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle; + Header.Description = BeatmapSubmissionStrings.BeatmapSubmissionDescription; + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs b/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs new file mode 100644 index 0000000000..92a4ac4e4e --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.ContentPermissions))] + public partial class ScreenContentPermissions : WizardScreen + { + [BackgroundDependencyLoader] + private void load(OsuGame? game) + { + Content.AddRange(new Drawable[] + { + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = BeatmapSubmissionStrings.ContentPermissionsDisclaimer, + }, + new RoundedButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 450, + Text = BeatmapSubmissionStrings.CheckContentUsageGuidelines, + Action = () => game?.ShowWiki(@"Rules/Content_usage_permissions"), + }, + }); + } + + public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ContentPermissionsAcknowledgement; + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs new file mode 100644 index 0000000000..c8d226bbcb --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.FrequentlyAskedQuestions))] + public partial class ScreenFrequentlyAskedQuestions : WizardScreen + { + [BackgroundDependencyLoader] + private void load(OsuGame? game, IAPIProvider api) + { + Content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.BeatmapRankingCriteriaDescription, + ButtonText = BeatmapSubmissionStrings.BeatmapRankingCriteria, + Action = () => game?.ShowWiki(@"Ranking_Criteria"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.SubmissionProcessDescription, + ButtonText = BeatmapSubmissionStrings.SubmissionProcess, + Action = () => game?.ShowWiki(@"Beatmap_ranking_procedure"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, + ButtonText = BeatmapSubmissionStrings.MappingHelpForum, + Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/56"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, + ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, + Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/60"), + }, + }, + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs new file mode 100644 index 0000000000..72da94afa1 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -0,0 +1,73 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.SubmissionSettings))] + public partial class ScreenSubmissionSettings : WizardScreen + { + private readonly BindableBool notifyOnDiscussionReplies = new BindableBool(); + private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager configManager, OsuColour colours) + { + configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); + configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); + + Content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FormEnumDropdown + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption, + }, + new FormCheckBox + { + Caption = BeatmapSubmissionStrings.NotifyOnDiscussionReplies, + Current = notifyOnDiscussionReplies, + }, + new FormCheckBox + { + Caption = BeatmapSubmissionStrings.LoadInBrowserAfterSubmission, + Current = loadInBrowserAfterSubmission, + }, + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE, weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + Colour = colours.Orange1, + Text = BeatmapSubmissionStrings.LegacyExportDisclaimer, + Padding = new MarginPadding { Top = 20 } + }, + } + }); + } + + private enum BeatmapSubmissionTarget + { + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] + WIP, + + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] + Pending, + } + } +} From 2f2dc158e0353aa5ba27108980a1bed1466a2f36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:44:59 +0900 Subject: [PATCH 466/620] Ensure test step doesn't consider pooled instances of drawables --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 0a9719423c..2e67e625f9 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -175,7 +175,8 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep($"click panel at index {index}", () => { - Carousel.ChildrenOfType() + Carousel.ChildrenOfType().Single() + .ChildrenOfType() .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .Reverse() .ElementAt(index) From ccdb6e4c4870ef64b3a2e549716c4bf7b412b646 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:50:14 +0900 Subject: [PATCH 467/620] Fix carousel tests failing due to dependency on depth ordering --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 2e67e625f9..f7be5f12e8 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.SongSelect Carousel.ChildrenOfType().Single() .ChildrenOfType() .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) - .Reverse() + .OrderBy(p => p.Y) .ElementAt(index) .TriggerClick(); }); From 58560f8acfe0259795358e969ddee6ca0600d2ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:11:09 +0900 Subject: [PATCH 468/620] Add tracking of expansion states for groups and sets --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 3 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 38 ++++++++++++------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 20 +++++----- osu.Game/Screens/SelectV2/CarouselItem.cs | 7 +++- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f7be5f12e8..72c9611fdb 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -153,7 +153,8 @@ namespace osu.Game.Tests.Visual.SongSelect var groupingFilter = Carousel.Filters.OfType().Single(); GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group); - CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel); + // offset by one because the group itself is included in the items list. + CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel + 1); return ReferenceEquals(Carousel.CurrentSelection, item.Model); }); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 858888c517..9f62780dda 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -105,12 +105,12 @@ namespace osu.Game.Screens.SelectV2 // Special case – collapsing an open group. if (lastSelectedGroup == group) { - setVisibilityOfGroupItems(lastSelectedGroup, false); + setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = null; return false; } - setVisibleGroup(group); + setExpandedGroup(group); return false; case BeatmapSetInfo setInfo: @@ -127,11 +127,11 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; if (group != null) - setVisibleGroup(group); + setExpandedGroup(group); } else { - setVisibleSet(beatmapInfo); + setExpandedSet(beatmapInfo); } return true; @@ -158,37 +158,47 @@ namespace osu.Game.Screens.SelectV2 } } - private void setVisibleGroup(GroupDefinition group) + private void setExpandedGroup(GroupDefinition group) { if (lastSelectedGroup != null) - setVisibilityOfGroupItems(lastSelectedGroup, false); + setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = group; - setVisibilityOfGroupItems(group, true); + setExpansionStateOfGroup(group, true); } - private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) + private void setExpansionStateOfGroup(GroupDefinition group, bool expanded) { if (grouping.GroupItems.TryGetValue(group, out var items)) { foreach (var i in items) - i.IsVisible = visible; + { + if (i.Model is GroupDefinition) + i.IsExpanded = expanded; + else + i.IsVisible = expanded; + } } } - private void setVisibleSet(BeatmapInfo beatmapInfo) + private void setExpandedSet(BeatmapInfo beatmapInfo) { if (lastSelectedBeatmap != null) - setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + setExpansionStateOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); lastSelectedBeatmap = beatmapInfo; - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + setExpansionStateOfSetItems(beatmapInfo.BeatmapSet!, true); } - private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) + private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded) { if (grouping.SetItems.TryGetValue(set, out var items)) { foreach (var i in items) - i.IsVisible = visible; + { + if (i.Model is BeatmapSetInfo) + i.IsExpanded = expanded; + else + i.IsVisible = expanded; + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index ea737d8b7f..e4160cc0fa 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -65,7 +65,11 @@ namespace osu.Game.Screens.SelectV2 if (b.StarRating > starGroup) { starGroup = (int)Math.Floor(b.StarRating); - newItems.Add(new CarouselItem(new GroupDefinition($"{starGroup} - {++starGroup} *")) { DrawHeight = GroupPanel.HEIGHT }); + var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); + var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + newItems.Add(groupItem); + groupItems[groupDefinition] = new HashSet { groupItem }; } newItems.Add(item); @@ -91,14 +95,13 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }; + setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; + newItems.Insert(i, setItem); i++; } - if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) - setItems[beatmap.BeatmapSet!] = related = new HashSet(); - - related.Add(item); + setItems[beatmap.BeatmapSet!].Add(item); item.IsVisible = false; } @@ -121,10 +124,7 @@ namespace osu.Game.Screens.SelectV2 if (lastGroup != null) { - if (!groupItems.TryGetValue(lastGroup, out var groupRelated)) - groupItems[lastGroup] = groupRelated = new HashSet(); - groupRelated.Add(item); - + groupItems[lastGroup].Add(item); item.IsVisible = false; } } diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 13d5c840cf..32be33e99a 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -30,10 +30,15 @@ namespace osu.Game.Screens.SelectV2 public float DrawHeight { get; set; } = DEFAULT_HEIGHT; /// - /// Whether this item is visible or collapsed (hidden). + /// Whether this item is visible or hidden. /// public bool IsVisible { get; set; } = true; + /// + /// Whether this item is expanded or not. Should only be used for headers of groups. + /// + public bool IsExpanded { get; set; } + public CarouselItem(object model) { Model = model; From 61419ec9c840fe55886c338f0eb53a8dd919be89 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 17:54:03 +0900 Subject: [PATCH 469/620] Refactor user presence watching to be tokenised --- .../Online/TestSceneMetadataClient.cs | 52 +++++++++++++++ .../Online/TestSceneCurrentlyOnlineDisplay.cs | 12 ++-- osu.Game/Online/Metadata/MetadataClient.cs | 59 ++++++++++++++--- .../Online/Metadata/OnlineMetadataClient.cs | 63 +++++++++---------- osu.Game/Overlays/DashboardOverlay.cs | 9 ++- .../Visual/Metadata/TestMetadataClient.cs | 20 +++--- 6 files changed, 156 insertions(+), 59 deletions(-) create mode 100644 osu.Game.Tests/Online/TestSceneMetadataClient.cs diff --git a/osu.Game.Tests/Online/TestSceneMetadataClient.cs b/osu.Game.Tests/Online/TestSceneMetadataClient.cs new file mode 100644 index 0000000000..8c738eeca6 --- /dev/null +++ b/osu.Game.Tests/Online/TestSceneMetadataClient.cs @@ -0,0 +1,52 @@ +// 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.Testing; +using osu.Game.Tests.Visual; +using osu.Game.Tests.Visual.Metadata; + +namespace osu.Game.Tests.Online +{ + [TestFixture] + [HeadlessTest] + public class TestSceneMetadataClient : OsuTestScene + { + private TestMetadataClient client = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = client = new TestMetadataClient(); + }); + + [Test] + public void TestWatchingMultipleTimesInvokesServerMethodsOnce() + { + int countBegin = 0; + int countEnd = 0; + + IDisposable token1 = null!; + IDisposable token2 = null!; + + AddStep("setup", () => + { + client.OnBeginWatchingUserPresence += () => countBegin++; + client.OnEndWatchingUserPresence += () => countEnd++; + }); + + AddStep("begin watching presence (1)", () => token1 = client.BeginWatchingUserPresence()); + AddAssert("server method invoked once", () => countBegin, () => Is.EqualTo(1)); + + AddStep("begin watching presence (2)", () => token2 = client.BeginWatchingUserPresence()); + AddAssert("server method not invoked a second time", () => countBegin, () => Is.EqualTo(1)); + + AddStep("end watching presence (1)", () => token1.Dispose()); + AddAssert("server method not invoked", () => countEnd, () => Is.EqualTo(0)); + + AddStep("end watching presence (2)", () => token2.Dispose()); + AddAssert("server method invoked once", () => countEnd, () => Is.EqualTo(1)); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs index b696c5d8ca..2e53ec2ba4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs @@ -65,7 +65,9 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestBasicDisplay() { - AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + IDisposable token = null!; + + AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); @@ -78,14 +80,16 @@ namespace osu.Game.Tests.Visual.Online AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); AddUntilStep("Panel no longer present", () => !currentlyOnline.ChildrenOfType().Any()); - AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + AddStep("End watching user presence", () => token.Dispose()); } [Test] public void TestUserWasPlayingBeforeWatchingUserPresence() { + IDisposable token = null!; + AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); - AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.True); @@ -93,7 +97,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); - AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + AddStep("End watching user presence", () => token.Dispose()); } internal partial class TestUserLookupCache : UserLookupCache diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 356c50bcc0..1da245e80d 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -3,11 +3,13 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Users; namespace osu.Game.Online.Metadata @@ -37,11 +39,6 @@ namespace osu.Game.Online.Metadata #region User presence updates - /// - /// Whether the client is currently receiving user presence updates from the server. - /// - public abstract IBindable IsWatchingUserPresence { get; } - /// /// The information about the current user. /// @@ -82,11 +79,36 @@ namespace osu.Game.Online.Metadata /// public abstract Task UpdateStatus(UserStatus? status); - /// - public abstract Task BeginWatchingUserPresence(); + private int userPresenceWatchCount; + + protected bool IsWatchingUserPresence + => Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0; + + /// + public IDisposable BeginWatchingUserPresence() + => new UserPresenceWatchToken(this); /// - public abstract Task EndWatchingUserPresence(); + Task IMetadataServer.BeginWatchingUserPresence() + { + if (Interlocked.Increment(ref userPresenceWatchCount) == 1) + return BeginWatchingUserPresenceInternal(); + + return Task.CompletedTask; + } + + /// + Task IMetadataServer.EndWatchingUserPresence() + { + if (Interlocked.Decrement(ref userPresenceWatchCount) == 0) + return EndWatchingUserPresenceInternal(); + + return Task.CompletedTask; + } + + protected abstract Task BeginWatchingUserPresenceInternal(); + + protected abstract Task EndWatchingUserPresenceInternal(); /// public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); @@ -94,6 +116,27 @@ namespace osu.Game.Online.Metadata /// public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); + private class UserPresenceWatchToken : IDisposable + { + private readonly IMetadataServer server; + private bool isDisposed; + + public UserPresenceWatchToken(IMetadataServer server) + { + this.server = server; + server.BeginWatchingUserPresence().FireAndForget(); + } + + public void Dispose() + { + if (isDisposed) + return; + + server.EndWatchingUserPresence().FireAndForget(); + isDisposed = true; + } + } + #endregion #region Daily Challenge diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 5aeeb04d11..c7c7dfc58b 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -20,9 +20,6 @@ namespace osu.Game.Online.Metadata { public override IBindable IsConnected { get; } = new Bindable(); - public override IBindable IsWatchingUserPresence => isWatchingUserPresence; - private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserPresence => localUserPresence; private UserPresence localUserPresence; @@ -109,15 +106,18 @@ namespace osu.Game.Online.Metadata { Schedule(() => { - isWatchingUserPresence.Value = false; userPresences.Clear(); friendPresences.Clear(); dailyChallengeInfo.Value = null; localUserPresence = default; }); + return; } + if (IsWatchingUserPresence) + BeginWatchingUserPresenceInternal(); + if (localUser.Value is not GuestUser) { UpdateActivity(userActivity.Value); @@ -201,6 +201,31 @@ namespace osu.Game.Online.Metadata return connection.InvokeAsync(nameof(IMetadataServer.UpdateStatus), status); } + protected override Task BeginWatchingUserPresenceInternal() + { + if (connector?.IsConnected.Value != true) + return Task.CompletedTask; + + Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)); + } + + protected override Task EndWatchingUserPresenceInternal() + { + if (connector?.IsConnected.Value != true) + return Task.CompletedTask; + + Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); + + // must be scheduled before any remote calls to avoid mis-ordering. + Schedule(() => userPresences.Clear()); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)); + } + public override Task UserPresenceUpdated(int userId, UserPresence? presence) { Schedule(() => @@ -237,36 +262,6 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } - public override async Task BeginWatchingUserPresence() - { - if (connector?.IsConnected.Value != true) - throw new OperationCanceledException(); - - Debug.Assert(connection != null); - await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false); - Schedule(() => isWatchingUserPresence.Value = true); - Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network); - } - - public override async Task EndWatchingUserPresence() - { - try - { - if (connector?.IsConnected.Value != true) - throw new OperationCanceledException(); - - // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userPresences.Clear()); - Debug.Assert(connection != null); - await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); - Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); - } - finally - { - Schedule(() => isWatchingUserPresence.Value = false); - } - } - public override Task DailyChallengeUpdated(DailyChallengeInfo? info) { Schedule(() => dailyChallengeInfo.Value = info); diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 1861f892bd..1912736135 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Online.Metadata; -using osu.Game.Online.Multiplayer; using osu.Game.Overlays.Dashboard; using osu.Game.Overlays.Dashboard.Friends; @@ -18,6 +17,7 @@ namespace osu.Game.Overlays private MetadataClient metadataClient { get; set; } = null!; private IBindable metadataConnected = null!; + private IDisposable? userPresenceWatchToken; public DashboardOverlay() : base(OverlayColourScheme.Purple) @@ -61,9 +61,12 @@ namespace osu.Game.Overlays return; if (State.Value == Visibility.Visible) - metadataClient.BeginWatchingUserPresence().FireAndForget(); + userPresenceWatchToken ??= metadataClient.BeginWatchingUserPresence(); else - metadataClient.EndWatchingUserPresence().FireAndForget(); + { + userPresenceWatchToken?.Dispose(); + userPresenceWatchToken = null; + } } } } diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index d14cbd7743..dca1b0e468 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -16,9 +16,6 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindable IsConnected => isConnected; private readonly BindableBool isConnected = new BindableBool(true); - public override IBindable IsWatchingUserPresence => isWatchingUserPresence; - private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserPresence => localUserPresence; private UserPresence localUserPresence; @@ -34,15 +31,18 @@ namespace osu.Game.Tests.Visual.Metadata [Resolved] private IAPIProvider api { get; set; } = null!; - public override Task BeginWatchingUserPresence() + public event Action? OnBeginWatchingUserPresence; + public event Action? OnEndWatchingUserPresence; + + protected override Task BeginWatchingUserPresenceInternal() { - isWatchingUserPresence.Value = true; + OnBeginWatchingUserPresence?.Invoke(); return Task.CompletedTask; } - public override Task EndWatchingUserPresence() + protected override Task EndWatchingUserPresenceInternal() { - isWatchingUserPresence.Value = false; + OnEndWatchingUserPresence?.Invoke(); return Task.CompletedTask; } @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Metadata { localUserPresence = localUserPresence with { Activity = activity }; - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (userPresences.ContainsKey(api.LocalUser.Value.Id)) userPresences[api.LocalUser.Value.Id] = localUserPresence; @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Metadata { localUserPresence = localUserPresence with { Status = status }; - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (userPresences.ContainsKey(api.LocalUser.Value.Id)) userPresences[api.LocalUser.Value.Id] = localUserPresence; @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UserPresenceUpdated(int userId, UserPresence? presence) { - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (presence?.Status != null) { From 2f90bb4d6793475835d1d51bef92b2c40f69112c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 17:55:50 +0900 Subject: [PATCH 470/620] Watch global user presence while in spectator screen --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index ddc638b7c5..84b5889751 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets; @@ -38,6 +39,9 @@ namespace osu.Game.Screens.Spectate [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -50,6 +54,7 @@ namespace osu.Game.Screens.Spectate private readonly Dictionary gameplayStates = new Dictionary(); private IDisposable? realmSubscription; + private IDisposable? userWatchToken; /// /// Creates a new . @@ -64,6 +69,8 @@ namespace osu.Game.Screens.Spectate { base.LoadComplete(); + userWatchToken = metadataClient.BeginWatchingUserPresence(); + userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(task => Schedule(() => { var foundUsers = task.GetResultSafely(); @@ -282,6 +289,7 @@ namespace osu.Game.Screens.Spectate } realmSubscription?.Dispose(); + userWatchToken?.Dispose(); } } } From 599b59cb1447467048bda41105956bd0c532863e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:16:36 +0900 Subject: [PATCH 471/620] Add expanded state to sample drawable representations --- ...estSceneBeatmapCarouselV2GroupSelection.cs | 25 ++++++++++++++++++- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 1 + osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 9 ++++++- osu.Game/Screens/SelectV2/Carousel.cs | 2 ++ osu.Game/Screens/SelectV2/GroupPanel.cs | 10 +++++++- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 7 +++++- 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index 04ca0a9085..f4d97be5a5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestOpenCloseGroupWithNoSelection() + public void TestOpenCloseGroupWithNoSelectionMouse() { AddBeatmaps(10, 5); WaitForDrawablePanels(); @@ -41,6 +41,29 @@ namespace osu.Game.Tests.Visual.SongSelect CheckNoSelection(); } + [Test] + public void TestOpenCloseGroupWithNoSelectionKeyboard() + { + AddBeatmaps(10, 5); + WaitForDrawablePanels(); + + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + SelectNextPanel(); + Select(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + CheckNoSelection(); + + Select(); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + CheckNoSelection(); + + GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + } + [Test] public void TestCarouselRemembersSelection() { diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 4a9e406def..3edfd4203b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -100,6 +100,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 06e3ad3426..79ffe0f68a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.SelectV2 private BeatmapCarousel carousel { get; set; } = null!; private OsuSpriteText text = null!; + private Box box = null!; [BackgroundDependencyLoader] private void load() @@ -34,7 +35,7 @@ namespace osu.Game.Screens.SelectV2 InternalChildren = new Drawable[] { - new Box + box = new Box { Colour = Color4.Yellow.Darken(5), Alpha = 0.8f, @@ -48,6 +49,11 @@ namespace osu.Game.Screens.SelectV2 } }; + Expanded.BindValueChanged(value => + { + box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint); + }); + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) @@ -85,6 +91,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a1bafac620..608ef207d9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -571,6 +571,7 @@ namespace osu.Game.Screens.SelectV2 c.Selected.Value = c.Item == currentSelection?.CarouselItem; c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; + c.Expanded.Value = c.Item.IsExpanded; } } @@ -674,6 +675,7 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.Item = null; carouselPanel.Selected.Value = false; carouselPanel.KeyboardSelected.Value = false; + carouselPanel.Expanded.Value = false; } #endregion diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 882d77cb8d..7ed256ca6a 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 private Box activationFlash = null!; private OsuSpriteText text = null!; + private Box box = null!; + [BackgroundDependencyLoader] private void load() { @@ -34,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 InternalChildren = new Drawable[] { - new Box + box = new Box { Colour = Color4.DarkBlue.Darken(5), Alpha = 0.8f, @@ -60,6 +62,11 @@ namespace osu.Game.Screens.SelectV2 activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); }); + Expanded.BindValueChanged(value => + { + box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint); + }); + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) @@ -97,6 +104,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index a956bb22a3..4fba0d2827 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -14,10 +14,15 @@ namespace osu.Game.Screens.SelectV2 public interface ICarouselPanel { /// - /// Whether this item has selection. Should be read from to update the visual state. + /// Whether this item has selection (see ). Should be read from to update the visual state. /// BindableBool Selected { get; } + /// + /// Whether this item is expanded (see ). Should be read from to update the visual state. + /// + BindableBool Expanded { get; } + /// /// Whether this item has keyboard selection. Should be read from to update the visual state. /// From 0ad97c1fad6c85b2e95864ba007ba670de00b3ba Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 18:24:57 +0900 Subject: [PATCH 472/620] Fix inspection --- osu.Game.Tests/Online/TestSceneMetadataClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Online/TestSceneMetadataClient.cs b/osu.Game.Tests/Online/TestSceneMetadataClient.cs index 8c738eeca6..04e1d91edf 100644 --- a/osu.Game.Tests/Online/TestSceneMetadataClient.cs +++ b/osu.Game.Tests/Online/TestSceneMetadataClient.cs @@ -11,7 +11,7 @@ namespace osu.Game.Tests.Online { [TestFixture] [HeadlessTest] - public class TestSceneMetadataClient : OsuTestScene + public partial class TestSceneMetadataClient : OsuTestScene { private TestMetadataClient client = null!; From 6c6063464aed10ca52237ac764386fd1877a64a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 18:41:26 +0900 Subject: [PATCH 473/620] Remove `Scheduler.AddOnce` from `updateSpecifics` To keep things simple, let's not bother debouncing this. The debouncing was causing spectating handling to fail because of two interdependent components binding to `BeatmapAvailability`: Binding to update the screen's `Beatmap` after a download completes: https://github.com/ppy/osu/blob/58747061171c4ebe70201dfe4d3329ed7f4343f5/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs#L266-L267 Binding to attempt a load request: https://github.com/ppy/osu/blob/8bb7bea04e56fab9247baa59ae879e16c8b4bd9b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs#L67 The first must update the beatmap before the second runs, else gameplay will not load due to `Beatmap.IsDefault`. --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 9f7e193131..f4d50b5170 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -427,7 +427,7 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - private void updateSpecifics() => Scheduler.AddOnce(() => + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; @@ -487,7 +487,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } else UserStyleSection.Hide(); - }); + } protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); From ccc446a8ca8d004ff74cba2b11bb0d438861f3ed Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Tue, 4 Feb 2025 17:48:44 +0800 Subject: [PATCH 474/620] code cleanup --- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Skinning/Legacy/LegacySwellCirclePiece.cs | 23 ------------------- .../Legacy/TaikoLegacySkinTransformer.cs | 2 +- .../TaikoSkinComponents.cs | 2 +- 5 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index d75fdbc40a..1dde4b6f9c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index b588a22d12..26bb1900b9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.SwellBody: + case TaikoSkinComponents.Swell: return new ArgonSwell(); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs deleted file mode 100644 index 40501d1d40..0000000000 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Rulesets.Taiko.Skinning.Legacy -{ - internal partial class LegacySwellCirclePiece : Sprite - { - [BackgroundDependencyLoader] - private void load(ISkinSource skin, SkinManager skinManager) - { - Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 8fa4551fd4..c6221e0589 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); - case TaikoSkinComponents.SwellBody: + case TaikoSkinComponents.Swell: if (GetTexture("spinner-circle") != null) return new LegacySwell(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 05c6316a05..28133ffcb2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - SwellBody, + Swell, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From 731f100aaf656ae8273412dc8bdb3134415d4889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 11:45:15 +0100 Subject: [PATCH 475/620] Fix incorrect snapping behaviour when previous object is not snapped to beat --- osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs | 2 ++ .../Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 195dbf0d46..bb0a0dbd7f 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -76,6 +76,8 @@ namespace osu.Game.Rulesets.Edit /// the beatmap's ,, /// the current beat divisor. /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 9ddf54b779..164a209958 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to // 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the // fact that the 1/2 snap reference object is not valid for 1/3 snapping. - float offset = SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource); + float offset = (float)(SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource) * DistanceSpacingMultiplier.Value); for (int i = 0; i < requiredCircles; i++) { From d28ea7bfbf5a81e2d4a97966c3b04fc1e37729bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 12:30:36 +0100 Subject: [PATCH 476/620] Fix code quality --- .../Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs index 07a794b7eb..e3e8c0de39 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -12,7 +12,6 @@ namespace osu.Game.Tests.Visual.Editing public partial class TestSceneBeatmapSubmissionOverlay : OsuTestScene { private ScreenFooter footer = null!; - private BeatmapSubmissionOverlay overlay = null!; [SetUpSteps] public void SetUpSteps() @@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.Editing Children = new Drawable[] { receptor, - overlay = new BeatmapSubmissionOverlay() + new BeatmapSubmissionOverlay { State = { Value = Visibility.Visible, }, }, From a0b6610054d3385cf39ea43e6e4051e64b52eb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 15:05:22 +0100 Subject: [PATCH 477/620] Always select the closest control point group regardless of whether it has a timing point --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 15 +++------------ osu.Game/Screens/Edit/Timing/TimingScreen.cs | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 12c6390812..86d8ac681f 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit.Timing { public partial class ControlPointList : CompositeDrawable { + public Action? SelectClosestTimingPoint { get; init; } + private ControlPointTable table = null!; private Container controls = null!; private OsuButton deleteButton = null!; @@ -75,7 +77,7 @@ namespace osu.Game.Screens.Edit.Timing new RoundedButton { Text = "Select closest to current time", - Action = goToCurrentGroup, + Action = SelectClosestTimingPoint, Size = new Vector2(220, 30), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -146,17 +148,6 @@ namespace osu.Game.Screens.Edit.Timing table.Padding = new MarginPadding { Bottom = controls.DrawHeight }; } - private void goToCurrentGroup() - { - double accurateTime = clock.CurrentTimeAccurate; - - var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime); - var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime); - - double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); - selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(latestActiveTime); - } - private void delete() { if (selectedGroup.Value == null) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index cddde34aca..e2ef356808 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.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.Graphics; @@ -37,7 +38,10 @@ namespace osu.Game.Screens.Edit.Timing { new Drawable[] { - new ControlPointList(), + new ControlPointList + { + SelectClosestTimingPoint = selectClosestTimingPoint, + }, new ControlPointSettings(), }, } @@ -70,8 +74,13 @@ namespace osu.Game.Screens.Edit.Timing if (editorClock == null) return; - var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime); - SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); + double accurateTime = editorClock.CurrentTimeAccurate; + + var activeTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(accurateTime); + var activeEffectPoint = EditorBeatmap.ControlPointInfo.EffectPointAt(accurateTime); + + double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); } protected override void ConfigureTimeline(TimelineArea timelineArea) From 2dbf30a0965767f0c8be93d918abe59322910a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 12:44:05 +0100 Subject: [PATCH 478/620] Select timing point on enter if no effect point is active at the time Noticed during testing. --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index e2ef356808..e7bf798298 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -79,8 +79,13 @@ namespace osu.Game.Screens.Edit.Timing var activeTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(accurateTime); var activeEffectPoint = EditorBeatmap.ControlPointInfo.EffectPointAt(accurateTime); - double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); - SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); + if (activeEffectPoint.Equals(EffectControlPoint.DEFAULT)) + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(activeTimingPoint.Time); + else + { + double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); + } } protected override void ConfigureTimeline(TimelineArea timelineArea) From 386fb553923f37976bd2e8f53ce169cabfa0e170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 21:48:45 +0900 Subject: [PATCH 479/620] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d2682fc024..6bbd432ee7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 309a9dcc87..ca2604858c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 099ce3953127e075f73ee5b11d0f1307e012fe07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 23:21:41 +0900 Subject: [PATCH 480/620] Use same delay in context menus --- osu.Game/Graphics/UserInterface/OsuContextMenu.cs | 7 +++---- osu.Game/Graphics/UserInterface/OsuMenu.cs | 7 +++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 433d37834f..e81d77ce43 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -12,8 +12,6 @@ namespace osu.Game.Graphics.UserInterface { public partial class OsuContextMenu : OsuMenu { - private const int fade_duration = 250; - [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; @@ -48,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { wasOpened = true; - this.FadeIn(fade_duration, Easing.OutQuint); + this.FadeIn(FADE_DURATION, Easing.OutQuint); if (!playClickSample) return; @@ -59,7 +57,8 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateClose() { - this.FadeOut(fade_duration, Easing.OutQuint); + this.Delay(DELAY_BEFORE_FADE_OUT) + .FadeOut(FADE_DURATION, Easing.OutQuint); if (wasOpened) menuSamples.PlayCloseSample(); diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 9b099c0884..a75769b16b 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -18,6 +18,9 @@ namespace osu.Game.Graphics.UserInterface { public partial class OsuMenu : Menu { + protected const double DELAY_BEFORE_FADE_OUT = 50; + protected const double FADE_DURATION = 280; + // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. private bool wasOpened; @@ -68,8 +71,8 @@ namespace osu.Game.Graphics.UserInterface if (!TopLevelMenu && wasOpened) menuSamples?.PlayCloseSample(); - this.Delay(50) - .FadeOut(300, Easing.OutQuint); + this.Delay(DELAY_BEFORE_FADE_OUT) + .FadeOut(FADE_DURATION, Easing.OutQuint); wasOpened = false; } From db7b665f4dc73d6250183285078734007f728e49 Mon Sep 17 00:00:00 2001 From: NecoDev <120387312+necocat0918@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:31:57 +0800 Subject: [PATCH 481/620] Removed unused using For https://github.com/ppy/osu/pull/31780 --- osu.Game/Screens/Edit/Editor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8cffab87ea..1914aae13c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using DiffPlex.Model; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; From 09d26fbf5ed006339da279ca449d9f87dd5ba961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:58:34 +0900 Subject: [PATCH 482/620] Minor adjustments --- osu.Game/Graphics/UserInterfaceV2/FormButton.cs | 2 +- osu.Game/Localisation/BeatmapSubmissionStrings.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs index fec855153b..1c5d4b5d80 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -148,7 +148,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { base.LoadComplete(); - Content.CornerRadius = 2; + Content.CornerRadius = 4; Add(triangles = new TrianglesV2 { diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 85fe922703..a4c2b36894 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -90,9 +90,9 @@ namespace osu.Game.Localisation public static LocalisableString ModdingQueuesForumDescription => new TranslatableString(getKey(@"modding_queues_forum_description"), @"Having trouble getting feedback? Why not ask in a mod queue!"); /// - /// "Where would you like to post your map?" + /// "Where would you like to post your beatmap?" /// - public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your map?"); + public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your beatmap?"); /// /// "Works in Progress / Help (incomplete, not ready for ranking)" From 2356d3e2d0c02c8e12fb27b8ef1b3b5766d9a5e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 16:24:13 +0900 Subject: [PATCH 483/620] Refactor `OsuContextMenu` to avoid code duplication --- .../Graphics/UserInterface/OsuContextMenu.cs | 33 ++++--------------- osu.Game/Graphics/UserInterface/OsuMenu.cs | 26 ++++++++++----- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index e81d77ce43..72ffde3574 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -15,12 +15,8 @@ namespace osu.Game.Graphics.UserInterface [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; - // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. - private bool wasOpened; - private readonly bool playClickSample; - - public OsuContextMenu(bool playClickSample = false) - : base(Direction.Vertical) + public OsuContextMenu(bool playSamples) + : base(Direction.Vertical, topLevelMenu: false, playSamples) { MaskingContainer.CornerRadius = 5; MaskingContainer.EdgeEffect = new EdgeEffectParameters @@ -33,8 +29,6 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL }; MaxHeight = 250; - - this.playClickSample = playClickSample; } [BackgroundDependencyLoader] @@ -45,27 +39,12 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { - wasOpened = true; - this.FadeIn(FADE_DURATION, Easing.OutQuint); + if (PlaySamples && !WasOpened) + menuSamples.PlayClickSample(); - if (!playClickSample) - return; - - menuSamples.PlayClickSample(); - menuSamples.PlayOpenSample(); + base.AnimateOpen(); } - protected override void AnimateClose() - { - this.Delay(DELAY_BEFORE_FADE_OUT) - .FadeOut(FADE_DURATION, Easing.OutQuint); - - if (wasOpened) - menuSamples.PlayCloseSample(); - - wasOpened = false; - } - - protected override Menu CreateSubMenu() => new OsuContextMenu(); + protected override Menu CreateSubMenu() => new OsuContextMenu(false); // sub menu samples are handled by OsuMenu.OnSubmenuOpen. } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index a75769b16b..11d9000dfa 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -22,20 +22,28 @@ namespace osu.Game.Graphics.UserInterface protected const double FADE_DURATION = 280; // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. - private bool wasOpened; + protected bool WasOpened { get; private set; } + + public bool PlaySamples { get; } [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; public OsuMenu(Direction direction, bool topLevelMenu = false) + : this(direction, topLevelMenu, playSamples: !topLevelMenu) + { + } + + protected OsuMenu(Direction direction, bool topLevelMenu, bool playSamples) : base(direction, topLevelMenu) { + PlaySamples = playSamples; BackgroundColour = Color4.Black.Opacity(0.5f); MaskingContainer.CornerRadius = 4; ItemsContainer.Padding = new MarginPadding(5); - OnSubmenuOpen += _ => { menuSamples?.PlaySubOpenSample(); }; + OnSubmenuOpen += _ => menuSamples?.PlaySubOpenSample(); } protected override void Update() @@ -59,22 +67,22 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { - if (!TopLevelMenu && !wasOpened) + if (PlaySamples && !WasOpened) menuSamples?.PlayOpenSample(); - this.FadeIn(300, Easing.OutQuint); - wasOpened = true; + WasOpened = true; + this.FadeIn(FADE_DURATION, Easing.OutQuint); } protected override void AnimateClose() { - if (!TopLevelMenu && wasOpened) + if (PlaySamples && WasOpened) menuSamples?.PlayCloseSample(); this.Delay(DELAY_BEFORE_FADE_OUT) .FadeOut(FADE_DURATION, Easing.OutQuint); - wasOpened = false; + WasOpened = false; } protected override void UpdateSize(Vector2 newSize) @@ -87,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); else // Delay until the fade out finishes from AnimateClose. - this.Delay(350).ResizeHeightTo(0); + this.Delay(DELAY_BEFORE_FADE_OUT + FADE_DURATION).ResizeHeightTo(0); } else { @@ -96,7 +104,7 @@ namespace osu.Game.Graphics.UserInterface this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); else // Delay until the fade out finishes from AnimateClose. - this.Delay(350).ResizeWidthTo(0); + this.Delay(DELAY_BEFORE_FADE_OUT + FADE_DURATION).ResizeWidthTo(0); } } From 14273824dcce96bf5c0e59a344a10305fc2bf253 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 19:37:00 +0900 Subject: [PATCH 484/620] Fix `Carousel.FilterAsync` not working when called from a non-update thread I was trying to be smart about things and make use of our `SynchronisationContext` setup, but it turns out to not work in all cases due to the context being missing depending on where you are calling the method from. For now let's prefer the "works everywhere" method of scheduling the final work back to update. --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..b29394c55d 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria)); + protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..78c2c99d99 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -228,8 +228,6 @@ namespace osu.Game.Screens.SelectV2 private async Task performFilter() { - Debug.Assert(SynchronizationContext.Current != null); - Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); @@ -266,19 +264,22 @@ namespace osu.Game.Screens.SelectV2 { log("Cancelled due to newer request arriving"); } - }, cts.Token).ConfigureAwait(true); + }, cts.Token).ConfigureAwait(false); if (cts.Token.IsCancellationRequested) return; - log("Items ready for display"); - carouselItems = items.ToList(); - displayedRange = null; + Schedule(() => + { + log("Items ready for display"); + carouselItems = items.ToList(); + displayedRange = null; - // Need to call this to ensure correct post-selection logic is handled on the new items list. - HandleItemSelected(currentSelection.Model); + // Need to call this to ensure correct post-selection logic is handled on the new items list. + HandleItemSelected(currentSelection.Model); - refreshAfterSelection(); + refreshAfterSelection(); + }); void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } From 7f8f528ae20da7ac8e0a0cb9a91e64e633b80c87 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Feb 2025 16:26:21 +0900 Subject: [PATCH 485/620] Add helper for testing mod/freemod validity --- osu.Game.Tests/Mods/ModUtilsTest.cs | 35 ++++++++++++++++ .../Multiplayer/MultiplayerMatchSongSelect.cs | 5 --- .../OnlinePlay/OnlinePlaySongSelect.cs | 20 ++++----- osu.Game/Utils/ModUtils.cs | 41 +++++++++++++++++++ 4 files changed, 86 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index decb0a31ac..2964ca9396 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -6,6 +6,7 @@ using System.Linq; using Moq; using NUnit.Framework; using osu.Framework.Localisation; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -342,6 +343,40 @@ namespace osu.Game.Tests.Mods Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x"); } + [Test] + public void TestRoomModValidity() + { + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.Playlists)); + + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.HeadToHead)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead)); + // For now, adaptive speed isn't allowed in multiplayer because it's a per-user rate adjustment. + Assert.IsFalse(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead)); + } + + [Test] + public void TestRoomFreeModValidity() + { + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.Playlists)); + + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.HeadToHead)); + // For now, all rate adjustment mods aren't allowed as free mods in multiplayer. + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead)); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index b42a58787d..7328e01026 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -11,7 +11,6 @@ using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -122,9 +121,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer; - - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 4ca6abbf7d..1164c4c0fc 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay freeModSelect = new FreeModSelectOverlay { SelectedMods = { BindTarget = FreeMods }, - IsValidMod = IsValidFreeMod, + IsValidMod = isValidFreeMod, }; } @@ -144,10 +144,10 @@ namespace osu.Game.Screens.OnlinePlay private void onModsChanged(ValueChangedEvent> mods) { - FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); + FreeMods.Value = FreeMods.Value.Where(isValidFreeMod).ToList(); // Reset the validity delegate to update the overlay's display. - freeModSelect.IsValidMod = IsValidFreeMod; + freeModSelect.IsValidMod = isValidFreeMod; } private void onRulesetChanged(ValueChangedEvent ruleset) @@ -194,7 +194,7 @@ namespace osu.Game.Screens.OnlinePlay protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum) { - IsValidMod = IsValidMod + IsValidMod = isValidMod }; protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() @@ -217,18 +217,18 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable); + private bool isValidMod(Mod mod) => ModUtils.IsValidModForMatchType(mod, room.Type); /// /// Checks whether a given is valid for per-player free-mod selection. /// /// The to check. /// Whether is a selectable free-mod. - protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod); - - private bool checkCompatibleFreeMod(Mod mod) - => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. - && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. + private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type) + // Mod must not be contained in the required mods. + && Mods.Value.All(m => m.Acronym != mod.Acronym) + // Mod must be compatible with all the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 15fc34b468..ac24bf2130 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -292,5 +293,45 @@ namespace osu.Game.Utils return rate; } + + /// + /// Determines whether a mod can be applied to playlist items in the given match type. + /// + /// The mod to test. + /// The match type. + public static bool IsValidModForMatchType(Mod mod, MatchType type) + { + if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation) + return false; + + switch (type) + { + case MatchType.Playlists: + return true; + + default: + return mod.ValidForMultiplayer; + } + } + + /// + /// Determines whether a mod can be applied as a free mod to playlist items in the given match type. + /// + /// The mod to test. + /// The match type. + public static bool IsValidFreeModForMatchType(Mod mod, MatchType type) + { + if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation) + return false; + + switch (type) + { + case MatchType.Playlists: + return true; + + default: + return mod.ValidForMultiplayerAsFreeMod; + } + } } } From 5c9e84caf0350760c1f7d78cbe80024aed7661de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 17:31:54 +0900 Subject: [PATCH 486/620] Add lock object --- osu.Game/Screens/SelectV2/Carousel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 78c2c99d99..681da84390 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -226,12 +226,14 @@ namespace osu.Game.Screens.SelectV2 private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly object cancellationLock = new object(); + private async Task performFilter() { Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); - lock (this) + lock (cancellationLock) { cancellationSource.Cancel(); cancellationSource = cts; From b7aa71c9759dc7d69249948591ebb60de34e2750 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:24:07 +0900 Subject: [PATCH 487/620] Adjust xmldoc slightly to convey the disposal pattern --- osu.Game/Online/Metadata/MetadataClient.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 1da245e80d..9885419b65 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -73,10 +73,8 @@ namespace osu.Game.Online.Metadata return null; } - /// public abstract Task UpdateActivity(UserActivity? activity); - /// public abstract Task UpdateStatus(UserStatus? status); private int userPresenceWatchCount; @@ -84,11 +82,12 @@ namespace osu.Game.Online.Metadata protected bool IsWatchingUserPresence => Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0; - /// - public IDisposable BeginWatchingUserPresence() - => new UserPresenceWatchToken(this); + /// + /// Signals to the server that we want to begin receiving status updates for all users. + /// + /// An which will end the session when disposed. + public IDisposable BeginWatchingUserPresence() => new UserPresenceWatchToken(this); - /// Task IMetadataServer.BeginWatchingUserPresence() { if (Interlocked.Increment(ref userPresenceWatchCount) == 1) @@ -97,7 +96,6 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } - /// Task IMetadataServer.EndWatchingUserPresence() { if (Interlocked.Decrement(ref userPresenceWatchCount) == 0) @@ -110,10 +108,8 @@ namespace osu.Game.Online.Metadata protected abstract Task EndWatchingUserPresenceInternal(); - /// public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); - /// public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); private class UserPresenceWatchToken : IDisposable @@ -143,7 +139,6 @@ namespace osu.Game.Online.Metadata public abstract IBindable DailyChallengeInfo { get; } - /// public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info); #endregion From c5deb9f36b067f03bd9d597967ac17f8502ade27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 10:28:09 +0100 Subject: [PATCH 488/620] Use alternative lockless solution for atomic cancellation token recreation --- osu.Game/Screens/SelectV2/Carousel.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 681da84390..0b706b4bb8 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -226,18 +226,13 @@ namespace osu.Game.Screens.SelectV2 private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); - private readonly object cancellationLock = new object(); - private async Task performFilter() { Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); - lock (cancellationLock) - { - cancellationSource.Cancel(); - cancellationSource = cts; - } + var previousCancellationSource = Interlocked.Exchange(ref cancellationSource, cts); + await previousCancellationSource.CancelAsync().ConfigureAwait(false); if (DebounceDelay > 0) { From e5943e460d657cb12545078d10b89ca58f6456f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 10:28:23 +0100 Subject: [PATCH 489/620] Unify `ConfigureAwait()` calls across method --- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 0b706b4bb8..3371e45453 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -237,7 +237,7 @@ namespace osu.Game.Screens.SelectV2 if (DebounceDelay > 0) { log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); - await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true); + await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false); } // Copy must be performed on update thread for now (see ConfigureAwait above). From fc5832ce67d7af1b32f88109b705788c4bf07e07 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:44:06 -0500 Subject: [PATCH 490/620] Support variable spacing between carousel items --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 11 +++++++ osu.Game/Screens/SelectV2/Carousel.cs | 33 +++++++++++++------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9f62780dda..12660d8642 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -20,12 +20,23 @@ namespace osu.Game.Screens.SelectV2 [Cached] public partial class BeatmapCarousel : Carousel { + public const float SPACING = 5f; + private IBindableList detachedBeatmaps = null!; private readonly LoadingLayer loading; private readonly BeatmapCarouselFilterGrouping grouping; + protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) + { + if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo) + // Beatmap difficulty panels do not overlap with themselves or any other panel. + return SPACING; + + return -SPACING; + } + public BeatmapCarousel() { DebounceDelay = 100; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..d7b6f251c3 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -50,11 +50,6 @@ namespace osu.Game.Screens.SelectV2 /// public float DistanceOffscreenToPreload { get; set; } - /// - /// Vertical space between panel layout. Negative value can be used to create an overlapping effect. - /// - protected float SpacingBetweenPanels { get; set; } = -5; - /// /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. @@ -116,6 +111,11 @@ namespace osu.Game.Screens.SelectV2 } } + /// + /// Returns the vertical spacing between two given carousel items. Negative value can be used to create an overlapping effect. + /// + protected virtual float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) => 0f; + #endregion #region Properties and methods concerning implementations @@ -260,7 +260,7 @@ namespace osu.Game.Screens.SelectV2 } log("Updating Y positions"); - updateYPositions(items, visibleHalfHeight, SpacingBetweenPanels); + updateYPositions(items, visibleHalfHeight); } catch (OperationCanceledException) { @@ -283,17 +283,26 @@ namespace osu.Game.Screens.SelectV2 void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } - private static void updateYPositions(IEnumerable carouselItems, float offset, float spacing) + private void updateYPositions(IEnumerable carouselItems, float offset) { + CarouselItem? previousVisible = null; + foreach (var item in carouselItems) - updateItemYPosition(item, ref offset, spacing); + updateItemYPosition(item, ref previousVisible, ref offset); } - private static void updateItemYPosition(CarouselItem item, ref float offset, float spacing) + private void updateItemYPosition(CarouselItem item, ref CarouselItem? previousVisible, ref float offset) { + float spacing = previousVisible == null || !item.IsVisible ? 0 : GetSpacingBetweenPanels(previousVisible, item); + + offset += spacing; item.CarouselYPosition = offset; + if (item.IsVisible) - offset += item.DrawHeight + spacing; + { + offset += item.DrawHeight; + previousVisible = item; + } } #endregion @@ -470,7 +479,7 @@ namespace osu.Game.Screens.SelectV2 return; } - float spacing = SpacingBetweenPanels; + CarouselItem? lastVisible = null; int count = carouselItems.Count; Selection prevKeyboard = currentKeyboardSelection; @@ -482,7 +491,7 @@ namespace osu.Game.Screens.SelectV2 { var item = carouselItems[i]; - updateItemYPosition(item, ref yPos, spacing); + updateItemYPosition(item, ref lastVisible, ref yPos); if (ReferenceEquals(item.Model, currentKeyboardSelection.Model)) currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i); From c389dbc711cc90aa2bd7c942d479f9c5336b377f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:45:32 -0500 Subject: [PATCH 491/620] Extend panel input area to cover gaps --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 10 ++++++++++ osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 10 ++++++++++ osu.Game/Screens/SelectV2/GroupPanel.cs | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 3edfd4203b..2fe509402b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -24,6 +24,16 @@ namespace osu.Game.Screens.SelectV2 private Box activationFlash = null!; private OsuSpriteText text = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover the gaps introduced by the spacing between BeatmapPanels. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 79ffe0f68a..85d5cc097d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -27,6 +27,16 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText text = null!; private Box box = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 7ed256ca6a..df930a3111 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -28,6 +28,16 @@ namespace osu.Game.Screens.SelectV2 private Box box = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { From 6037d5d8ce256fe70d6a7b22a723d1e26fb6ea42 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:46:05 -0500 Subject: [PATCH 492/620] Add test coverage --- ...estSceneBeatmapCarouselV2GroupSelection.cs | 62 ++++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 70 ++++++++++++++----- 2 files changed, 115 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index f4d97be5a5..ebdc54864e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -8,6 +9,8 @@ using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; +using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { @@ -154,5 +157,64 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevGroup(); WaitForGroupSelection(2, 9); } + + [Test] + public void TestInputHandlingWithinGaps() + { + AddBeatmaps(5, 2); + WaitForDrawablePanels(); + SelectNextGroup(); + + clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForGroupSelection(0, 1); + + clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForGroupSelection(0, 0); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 1); + + clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + AddAssert("group 0 collapsed", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False); + clickOnGroup(0, p => p.LayoutRectangle.Centre); + AddAssert("group 0 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True); + + AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); + clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForGroupSelection(0, 4); + + clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + AddAssert("group 1 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True); + } + + private void clickOnGroup(int group, Func pos) + { + AddStep($"click on group{group}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + var model = groupingFilter.GroupItems.Keys.ElementAt(group); + + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } + + private void clickOnPanel(int group, int panel, Func pos) + { + AddStep($"click on group{group} panel{panel}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + var g = groupingFilter.GroupItems.Keys.ElementAt(group); + // offset by one because the group itself is included in the items list. + object model = groupingFilter.GroupItems[g].ElementAt(panel + 1).Model; + + var p = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(p.ToScreenSpace(pos(p))); + InputManager.Click(MouseButton.Left); + }); + } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index b087c252e4..5541e217cf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -1,11 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.SelectV2; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -94,9 +96,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); - AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } @@ -129,21 +130,6 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForSelection(0, 0); } - [Test] - public void TestGroupSelectionOnHeader() - { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - - SelectNextGroup(); - SelectNextGroup(); - WaitForSelection(1, 0); - - SelectPrevPanel(); - SelectPrevGroup(); - WaitForSelection(0, 0); - } - [Test] public void TestKeyboardSelection() { @@ -194,6 +180,34 @@ namespace osu.Game.Tests.Visual.SongSelect CheckNoSelection(); } + [Test] + public void TestInputHandlingWithinGaps() + { + AddBeatmaps(2, 5); + WaitForDrawablePanels(); + SelectNextGroup(); + + clickOnDifficulty(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForSelection(0, 1); + + clickOnDifficulty(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 0); + + SelectNextPanel(); + Select(); + WaitForSelection(0, 1); + + clickOnSet(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 0); + + AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); + clickOnDifficulty(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 4); + + clickOnSet(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForSelection(1, 0); + } + private void checkSelectionIterating(bool isIterating) { object? selection = null; @@ -207,5 +221,27 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); } } + + private void clickOnSet(int set, Func pos) + { + AddStep($"click on set{set}", () => + { + var model = BeatmapSets[set]; + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } + + private void clickOnDifficulty(int set, int diff, Func pos) + { + AddStep($"click on set{set} diff{diff}", () => + { + var model = BeatmapSets[set].Beatmaps[diff]; + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } } } From c370c75fe2793dd379b4f4b8983fd3a35da17511 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 05:47:34 -0500 Subject: [PATCH 493/620] Allow ordering certain carousel panels behind others --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 14 ++++++++++++-- osu.Game/Screens/SelectV2/Carousel.cs | 7 +++++-- osu.Game/Screens/SelectV2/CarouselItem.cs | 6 ++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e4160cc0fa..55cb5fa5f9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -66,7 +66,12 @@ namespace osu.Game.Screens.SelectV2 { starGroup = (int)Math.Floor(b.StarRating); var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); - var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + var groupItem = new CarouselItem(groupDefinition) + { + DrawHeight = GroupPanel.HEIGHT, + DepthLayer = -2 + }; newItems.Add(groupItem); groupItems[groupDefinition] = new HashSet { groupItem }; @@ -95,7 +100,12 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }; + var setItem = new CarouselItem(beatmap.BeatmapSet!) + { + DrawHeight = BeatmapSetPanel.HEIGHT, + DepthLayer = -1 + }; + setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; newItems.Insert(i, setItem); i++; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..5dc8d80476 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -550,6 +550,9 @@ namespace osu.Game.Screens.SelectV2 updateDisplayedRange(range); } + double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; + double maximumDistanceFromSelection = scroll.Panels.Select(p => Math.Abs(((ICarouselPanel)p).DrawYPosition - selectedYPos)).DefaultIfEmpty().Max(); + foreach (var panel in scroll.Panels) { var c = (ICarouselPanel)panel; @@ -558,8 +561,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; - scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos)); + float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / maximumDistanceFromSelection); + scroll.Panels.ChangeChildDepth(panel, normalisedDepth + c.Item.DepthLayer); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 32be33e99a..e497c3890c 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -29,6 +29,12 @@ namespace osu.Game.Screens.SelectV2 /// public float DrawHeight { get; set; } = DEFAULT_HEIGHT; + /// + /// A number that defines the layer which this should be placed on depth-wise. + /// The higher the number, the farther the panel associated with this item is taken to the background. + /// + public int DepthLayer { get; set; } = 0; + /// /// Whether this item is visible or hidden. /// From 11de4296210a9727b8f8dbad928409611b669e93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:59:29 +0900 Subject: [PATCH 494/620] Add support for grouping by artist --- osu.Game.Tests/Resources/TestResources.cs | 29 ++++++++++++++----- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 1 + osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 29 ++++++++++--------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 29 ++++++++++++++++++- 5 files changed, 68 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index e0572e604c..bf08097ffd 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -85,7 +85,8 @@ namespace osu.Game.Tests.Resources /// /// Number of difficulties. If null, a random number between 1 and 20 will be used. /// Rulesets to cycle through when creating difficulties. If null, osu! ruleset will be used. - public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) + /// Whether to randomise metadata to create a better distribution. + public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null, bool randomiseMetadata = false) { int j = 0; @@ -95,13 +96,27 @@ namespace osu.Game.Tests.Resources int setId = GetNextTestID(); - var metadata = new BeatmapMetadata + char getRandomCharacter() { - // Create random metadata, then we can check if sorting works based on these - Artist = "Some Artist " + RNG.Next(0, 9), - Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = "Some Guy " + RNG.Next(0, 9) }, - }; + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; + return chars[RNG.Next(chars.Length)]; + } + + var metadata = randomiseMetadata + ? new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), + Title = $"{getRandomCharacter()}ome Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + } + : new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = "Some Artist " + RNG.Next(0, 9), + Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = "Some Guy " + RNG.Next(0, 9) }, + }; Logger.Log($"🛠️ Generating beatmap set \"{metadata}\" for test consumption."); diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..f5ea959c51 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => { for (int i = 0; i < count; i++) - BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); + BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4), randomiseMetadata: true)); }); protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 8ffb51b995..a173920dc6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); SortBy(new FilterCriteria { Sort = SortMode.Artist }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9f62780dda..e7311fbfbc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -119,20 +119,12 @@ namespace osu.Game.Screens.SelectV2 return false; case BeatmapInfo beatmapInfo: + // Find any containing group. There should never be too many groups so iterating is efficient enough. + GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - // If we have groups, we need to account for them. - if (Criteria.SplitOutDifficulties) - { - // Find the containing group. There should never be too many groups so iterating is efficient enough. - GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - - if (group != null) - setExpandedGroup(group); - } - else - { - setExpandedSet(beatmapInfo); - } + if (containingGroup != null) + setExpandedGroup(containingGroup); + setExpandedSet(beatmapInfo); return true; } @@ -170,6 +162,7 @@ namespace osu.Game.Screens.SelectV2 { if (grouping.GroupItems.TryGetValue(group, out var items)) { + // First pass ignoring set groupings. foreach (var i in items) { if (i.Model is GroupDefinition) @@ -177,6 +170,16 @@ namespace osu.Game.Screens.SelectV2 else i.IsVisible = expanded; } + + // Second pass to hide set children when not meant to be displayed. + if (expanded) + { + foreach (var i in items) + { + if (i.Model is BeatmapSetInfo set) + setExpansionStateOfSetItems(set, i.IsExpanded); + } + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e4160cc0fa..d4e0a166ab 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -52,6 +52,33 @@ namespace osu.Game.Screens.SelectV2 newItems.AddRange(items); break; + case GroupMode.Artist: + groupSetsTogether = true; + char groupChar = (char)0; + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var b = (BeatmapInfo)item.Model; + + char beatmapFirstChar = char.ToUpperInvariant(b.Metadata.Artist[0]); + + if (beatmapFirstChar > groupChar) + { + groupChar = beatmapFirstChar; + var groupDefinition = new GroupDefinition($"{groupChar}"); + var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + newItems.Add(groupItem); + groupItems[groupDefinition] = new HashSet { groupItem }; + } + + newItems.Add(item); + } + + break; + case GroupMode.Difficulty: groupSetsTogether = false; int starGroup = int.MinValue; @@ -91,7 +118,7 @@ namespace osu.Game.Screens.SelectV2 if (item.Model is BeatmapInfo beatmap) { - bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + bool newBeatmapSet = lastItem?.Model is not BeatmapInfo lastBeatmap || lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; if (newBeatmapSet) { From 092b953dca56991df3e3c69cafd6a430aac5a115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:31:18 +0100 Subject: [PATCH 495/620] Implement visual component for displaying submission progress --- .../TestSceneSubmissionStageProgress.cs | 47 ++++ .../Submission/SubmissionStageProgress.cs | 212 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs create mode 100644 osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs new file mode 100644 index 0000000000..47414bb24e --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs @@ -0,0 +1,47 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Screens.Edit.Submission; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneSubmissionStageProgress : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Test] + public void TestAppearance() + { + SubmissionStageProgress progress = null!; + + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = progress = new SubmissionStageProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + StageDescription = "Frobnicating the foobarator...", + } + }); + AddStep("not started", () => progress.SetNotStarted()); + AddStep("indeterminate progress", () => progress.SetInProgress()); + AddStep("30% progress", () => progress.SetInProgress(0.3f)); + AddStep("70% progress", () => progress.SetInProgress(0.7f)); + AddStep("completed", () => progress.SetCompleted()); + AddStep("failed", () => progress.SetFailed("the foobarator has defrobnicated")); + AddStep("failed with long message", () => progress.SetFailed("this is a very very very very VERY VEEEEEEEEEEEEEEEEEEEEEEEEERY long error message like you would never believe")); + AddStep("canceled", () => progress.SetCanceled()); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs new file mode 100644 index 0000000000..101313c627 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs @@ -0,0 +1,212 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +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.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class SubmissionStageProgress : CompositeDrawable + { + public LocalisableString StageDescription { get; init; } + + private Bindable status { get; } = new Bindable(); + + private Bindable progress { get; } = new Bindable(); + + private Container progressBarContainer = null!; + private Box progressBar = null!; + private Container iconContainer = null!; + private OsuTextFlowContainer errorMessage = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = StageDescription, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + iconContainer = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Children = + [ + progressBarContainer = new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 150, + Height = 10, + CornerRadius = 5, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + progressBar = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 0, + Colour = colourProvider.Highlight1, + } + } + }, + errorMessage = new OsuTextFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // should really be `CentreRight` too, but that's broken due to a framework bug + // (https://github.com/ppy/osu-framework/issues/5084) + TextAnchor = Anchor.BottomRight, + Width = 450, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Colour = colours.Red1, + } + ] + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + status.BindValueChanged(_ => Scheduler.AddOnce(updateStatus), true); + progress.BindValueChanged(_ => Scheduler.AddOnce(updateProgress), true); + } + + public void SetNotStarted() => status.Value = StageStatusType.NotStarted; + + public void SetInProgress(float? progress = null) + { + this.progress.Value = progress; + status.Value = StageStatusType.InProgress; + } + + public void SetCompleted() => status.Value = StageStatusType.Completed; + + public void SetFailed(string reason) + { + status.Value = StageStatusType.Failed; + errorMessage.Text = reason; + } + + public void SetCanceled() => status.Value = StageStatusType.Canceled; + + private const float transition_duration = 200; + + private void updateProgress() + { + if (progress.Value != null) + progressBar.ResizeWidthTo(progress.Value.Value, transition_duration, Easing.OutQuint); + + progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint); + } + + private void updateStatus() + { + progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint); + errorMessage.FadeTo(status.Value == StageStatusType.Failed ? 1 : 0, transition_duration, Easing.OutQuint); + + iconContainer.Clear(); + iconContainer.ClearTransforms(); + + switch (status.Value) + { + case StageStatusType.InProgress: + iconContainer.Child = new LoadingSpinner + { + Size = new Vector2(16), + State = { Value = Visibility.Visible, }, + }; + iconContainer.Colour = colours.Orange1; + break; + + case StageStatusType.Completed: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.CheckCircle, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Green1; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + + case StageStatusType.Failed: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.ExclamationCircle, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Red1; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + + case StageStatusType.Canceled: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.Ban, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Gray8; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + } + } + + public enum StageStatusType + { + NotStarted, + InProgress, + Completed, + Failed, + Canceled, + } + } +} From 7d299bb2ad5221df7a81f8aa80c644b70af447b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:46:33 +0100 Subject: [PATCH 496/620] Expose `EndpointConfiguration` directly in `IAPIAccess` --- osu.Desktop/DiscordRichPresence.cs | 2 +- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 2 +- .../Visual/Online/TestSceneWikiMarkdownContainer.cs | 10 +++++----- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs | 4 ++-- osu.Game/Online/API/APIAccess.cs | 13 +++++-------- osu.Game/Online/API/APIRequest.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 8 +++++--- osu.Game/Online/API/IAPIProvider.cs | 9 ++------- osu.Game/Online/Chat/ExternalLinkOpener.cs | 4 ++-- osu.Game/Online/Chat/NowPlayingCommand.cs | 2 +- osu.Game/Online/EndpointConfiguration.cs | 4 ++-- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 +- .../Profile/Header/BottomHeaderContainer.cs | 4 ++-- .../Header/Components/DrawableTournamentBanner.cs | 2 +- .../Overlays/Profile/Header/TopHeaderContainer.cs | 2 +- .../Sections/Recent/DrawableRecentActivity.cs | 2 +- osu.Game/Overlays/Wiki/WikiPanelContainer.cs | 2 +- osu.Game/Overlays/WikiOverlay.cs | 4 ++-- .../Submission/ScreenFrequentlyAskedQuestions.cs | 4 ++-- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- .../SelectV2/Leaderboards/LeaderboardScoreV2.cs | 2 +- 25 files changed, 44 insertions(+), 50 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6afb3e319d..cf56fe6115 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -173,7 +173,7 @@ namespace osu.Desktop new Button { Label = "View beatmap", - Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" + Url = $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" } }; } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index f3ea20c1aa..e2d5bc2917 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.EndpointConfiguration.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index 8909305602..cee3f37aea 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLink() { - AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/"); + AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/"); AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Main_page"); AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/FAQ"); AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); } [Test] diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index a82a288239..d0625c64e3 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null) return null; - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs index 8a107ed486..ac191d36a9 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs @@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps return null; if (ruleset != null) - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index f7fbacf76c..ef7b49868c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -40,9 +40,7 @@ namespace osu.Game.Online.API private readonly Queue queue = new Queue(); - public string APIEndpointUrl { get; } - - public string WebsiteRootUrl { get; } + public EndpointConfiguration EndpointConfiguration { get; } /// /// The API response version. @@ -89,14 +87,13 @@ namespace osu.Game.Online.API APIVersion = now.Year * 10000 + now.Month * 100 + now.Day; } - APIEndpointUrl = endpointConfiguration.APIEndpointUrl; - WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; + EndpointConfiguration = endpointConfiguration; NotificationsClient = setUpNotificationsClient(); - authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); + authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, EndpointConfiguration.APIEndpointUrl); log = Logger.GetLogger(LoggingTarget.Network); - log.Add($@"API endpoint root: {APIEndpointUrl}"); + log.Add($@"API endpoint root: {EndpointConfiguration.APIEndpointUrl}"); log.Add($@"API request version: {APIVersion}"); ProvidedUsername = config.Get(OsuSetting.Username); @@ -408,7 +405,7 @@ namespace osu.Game.Online.API var req = new RegistrationRequest { - Url = $@"{APIEndpointUrl}/users", + Url = $@"{EndpointConfiguration.APIEndpointUrl}/users", Method = HttpMethod.Post, Username = username, Email = email, diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 5cbe9040ba..575e6f8a10 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API!.APIEndpointUrl}/api/v2/{Target}"; + protected virtual string Uri => $@"{API!.EndpointConfiguration.APIEndpointUrl}/api/v2/{Target}"; protected IAPIProvider? API; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 48c08afb8c..7b3a8f357b 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -41,9 +41,11 @@ namespace osu.Game.Online.API public string ProvidedUsername => LocalUser.Value.Username; - public string APIEndpointUrl => "http://localhost"; - - public string WebsiteRootUrl => "http://localhost"; + public EndpointConfiguration EndpointConfiguration { get; } = new EndpointConfiguration + { + APIEndpointUrl = "http://localhost", + WebsiteRootUrl = "http://localhost", + }; public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 3b6763d736..048193def7 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -51,14 +51,9 @@ namespace osu.Game.Online.API string ProvidedUsername { get; } /// - /// The URL endpoint for this API. Does not include a trailing slash. + /// Holds configuration for online endpoints. /// - string APIEndpointUrl { get; } - - /// - /// The root URL of the website, excluding the trailing slash. - /// - string WebsiteRootUrl { get; } + EndpointConfiguration EndpointConfiguration { get; } /// /// The version of the API. diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index f76d42c96d..1615b72033 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat if (url.StartsWith('/')) { - url = $"{api.WebsiteRootUrl}{url}"; + url = $"{api.EndpointConfiguration.WebsiteRootUrl}{url}"; isTrustedDomain = true; } else { - isTrustedDomain = url.StartsWith(api.WebsiteRootUrl, StringComparison.Ordinal); + isTrustedDomain = url.StartsWith(api.EndpointConfiguration.WebsiteRootUrl, StringComparison.Ordinal); } if (!url.CheckIsValidUrl()) diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index db44017a1b..5e71980a55 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat string getBeatmapPart() { - return beatmapOnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; + return beatmapOnlineID > 0 ? $"[{api.EndpointConfiguration.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; } string getRulesetPart() diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index bd3c945124..8f76da41fd 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -9,12 +9,12 @@ namespace osu.Game.Online public class EndpointConfiguration { /// - /// The base URL for the website. + /// The base URL for the website. Does not include a trailing slash. /// public string WebsiteRootUrl { get; set; } = string.Empty; /// - /// The endpoint for the main (osu-web) API. + /// The endpoint for the main (osu-web) API. Does not include a trailing slash. /// public string APIEndpointUrl { get; set; } = string.Empty; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 6acf236bf3..f7efa08969 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); if (Score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{Score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{Score.OnlineID}"))); if (Score.Files.Count > 0) { diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index d664a44be9..b06be3e74a 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.EndpointConfiguration.APIEndpointUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 0ff30da2a1..2b6d523b95 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login } }; - forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset"); + forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); password.OnCommit += (_, _) => performLogin(); diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 506cb70d09..e36d62f827 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam); // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something). explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the "); - explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.WebsiteRootUrl}/home/password-reset"); + explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); explainText.AddText(". You can also "); explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () => { diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index d5b4d844b2..d9d23f16fd 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); addSpacer(topLinkContainer); topLinkContainer.AddText("Posted "); - topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); + topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); string websiteWithoutProtocol = user.Website; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index c099009ca4..a66a5c8fe9 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Texture = textures.Get(banner.Image), }; - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 165a576c03..fb1bdca57c 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header cover.User = user; avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; - openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; + openUserExternally.Link = $@"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 8a0003b4ea..a0bcf2dc47 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont()); - private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.AsNonNull(); + private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.EndpointConfiguration.WebsiteRootUrl}{url}").Argument.AsNonNull(); private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index 555dab852e..773dde6436 100644 --- a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs +++ b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki Padding = new MarginPadding(padding), Child = new WikiPanelMarkdownContainer(isFullWidth) { - CurrentPath = $@"{api.WebsiteRootUrl}/wiki/", + CurrentPath = $@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", Text = text, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index ef258da82b..c360d1eb9e 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -167,7 +167,7 @@ namespace osu.Game.Overlays } else { - LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); + LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); } } @@ -176,7 +176,7 @@ namespace osu.Game.Overlays wikiData.Value = null; path.Value = "error"; - LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/", + LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH}).")); } diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs index c8d226bbcb..ff9cb07e2d 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -46,14 +46,14 @@ namespace osu.Game.Screens.Edit.Submission RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, ButtonText = BeatmapSubmissionStrings.MappingHelpForum, - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/56"), + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/56"), }, new FormButton { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/60"), + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/60"), }, }, }); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 377c840d25..7b2e2c02f7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return items.ToArray(); - string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + string formatRoomUrl(long id) => $@"{api.EndpointConfiguration.WebsiteRootUrl}/multiplayer/rooms/{id}"; } } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 732fb2cd8c..2460fbe6f8 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -778,7 +778,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{score.OnlineID}"))); if (score.Files.Count <= 0) return items.ToArray(); From aaffd72032042834bb5b982fd9524a7427aa0f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:48:07 +0100 Subject: [PATCH 497/620] Add beatmap submission service URL to endpoint configuration --- osu.Game/Online/EndpointConfiguration.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index 8f76da41fd..39dd72d41a 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -42,5 +42,10 @@ namespace osu.Game.Online /// The endpoint for the SignalR metadata server. /// public string MetadataEndpointUrl { get; set; } = string.Empty; + + /// + /// The root URL for the service handling beatmap submission. Does not include a trailing slash. + /// + public string? BeatmapSubmissionServiceUrl { get; set; } } } From 8940ee5d9cb3a41a974d30ba3d3efa0dea74c751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:52:43 +0100 Subject: [PATCH 498/620] Add API request & response structures for beatmap submission --- .../Online/API/Requests/APIUploadRequest.cs | 26 ++++++ .../Requests/PatchBeatmapPackageRequest.cs | 51 ++++++++++++ .../API/Requests/PutBeatmapSetRequest.cs | 82 +++++++++++++++++++ .../Requests/ReplaceBeatmapPackageRequest.cs | 45 ++++++++++ .../Responses/PutBeatmapSetResponse.cs | 30 +++++++ 5 files changed, 234 insertions(+) create mode 100644 osu.Game/Online/API/Requests/APIUploadRequest.cs create mode 100644 osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs create mode 100644 osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs create mode 100644 osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs diff --git a/osu.Game/Online/API/Requests/APIUploadRequest.cs b/osu.Game/Online/API/Requests/APIUploadRequest.cs new file mode 100644 index 0000000000..3503b4cebb --- /dev/null +++ b/osu.Game/Online/API/Requests/APIUploadRequest.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.Diagnostics; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public abstract class APIUploadRequest : APIRequest + { + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.UploadProgress += onUploadProgress; + return request; + } + + private void onUploadProgress(long current, long total) + { + Debug.Assert(API != null); + API.Schedule(() => Progressed?.Invoke(current, total)); + } + + public event APIProgressHandler? Progressed; + } +} diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs new file mode 100644 index 0000000000..85981448da --- /dev/null +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -0,0 +1,51 @@ +// 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.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class PatchBeatmapPackageRequest : APIUploadRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; + } + } + + protected override string Target => throw new NotSupportedException(); + + public uint BeatmapSetID { get; } + public Dictionary FilesChanged { get; } = new Dictionary(); + public HashSet FilesDeleted { get; } = new HashSet(); + + public PatchBeatmapPackageRequest(uint beatmapSetId) + { + BeatmapSetID = beatmapSetId; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.Method = HttpMethod.Patch; + + foreach ((string filename, byte[] content) in FilesChanged) + request.AddFile(@"filesChanged", content, filename); + + foreach (string filename in FilesDeleted) + request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form); + + request.Timeout = 60_000; + return request; + } + } +} diff --git a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs new file mode 100644 index 0000000000..03b8397681 --- /dev/null +++ b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using osu.Framework.IO.Network; +using osu.Framework.Localisation; +using osu.Game.Localisation; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class PutBeatmapSetRequest : APIRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets"; + } + } + + protected override string Target => throw new NotSupportedException(); + + [JsonProperty("beatmapset_id")] + public uint? BeatmapSetID { get; init; } + + [JsonProperty("beatmaps_to_create")] + public uint BeatmapsToCreate { get; init; } + + [JsonProperty("beatmaps_to_keep")] + public uint[] BeatmapsToKeep { get; init; } = []; + + [JsonProperty("target")] + public BeatmapSubmissionTarget SubmissionTarget { get; init; } + + private PutBeatmapSetRequest() + { + } + + public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + { + BeatmapsToCreate = beatmapCount, + SubmissionTarget = target, + }; + + public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + { + BeatmapSetID = beatmapSetId, + BeatmapsToKeep = beatmapsToKeep.ToArray(), + BeatmapsToCreate = beatmapsToCreate, + SubmissionTarget = target, + }; + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Put; + req.ContentType = @"application/json"; + req.AddRaw(JsonConvert.SerializeObject(this)); + return req; + } + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum BeatmapSubmissionTarget + { + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] + WIP, + + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] + Pending, + } +} diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs new file mode 100644 index 0000000000..c9dd12d61e --- /dev/null +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -0,0 +1,45 @@ +// 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.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class ReplaceBeatmapPackageRequest : APIUploadRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; + } + } + + protected override string Target => throw new NotSupportedException(); + + public uint BeatmapSetID { get; } + + private readonly byte[] oszPackage; + + public ReplaceBeatmapPackageRequest(uint beatmapSetID, byte[] oszPackage) + { + this.oszPackage = oszPackage; + BeatmapSetID = beatmapSetID; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.AddFile(@"beatmapArchive", oszPackage); + request.Method = HttpMethod.Put; + request.Timeout = 60_000; + return request; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs b/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs new file mode 100644 index 0000000000..e3ec617039 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs @@ -0,0 +1,30 @@ +// 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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class PutBeatmapSetResponse + { + [JsonProperty("beatmapset_id")] + public uint BeatmapSetId { get; set; } + + [JsonProperty("beatmap_ids")] + public ICollection BeatmapIds { get; set; } = Array.Empty(); + + [JsonProperty("files")] + public ICollection Files { get; set; } = Array.Empty(); + } + + public struct BeatmapSetFile + { + [JsonProperty("filename")] + public string Filename { get; set; } + + [JsonProperty("sha2_hash")] + public string SHA2Hash { get; set; } + } +} From b6731ff7738ede0985297fd69d5b32a82c66bdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:34:13 +0100 Subject: [PATCH 499/620] Add completion flag to `WizardOverlay` --- osu.Game/Overlays/WizardOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 34ffa7bd77..2a881045fd 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -45,6 +45,8 @@ namespace osu.Game.Overlays private LoadingSpinner loading = null!; private ScheduledDelegate? loadingShowDelegate; + public bool Completed { get; private set; } + protected WizardOverlay(OverlayColourScheme scheme) : base(scheme) { @@ -221,6 +223,7 @@ namespace osu.Game.Overlays else { CurrentStepIndex = null; + Completed = true; Hide(); } From fff99a8b4008800ce5a870ac600618e84d8ffdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:54:26 +0100 Subject: [PATCH 500/620] Implement special exporter intended specifically for submission flows --- osu.Game/Database/LegacyBeatmapExporter.cs | 23 +++++--- .../Submission/SubmissionBeatmapExporter.cs | 58 +++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 8f94fc9e63..e7e5ddb4d2 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -61,6 +61,20 @@ namespace osu.Game.Database Configuration = new LegacySkinDecoder().Decode(skinStreamReader) }; + MutateBeatmap(model, playableBeatmap); + + // Encode to legacy format + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + + protected virtual void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { // Convert beatmap elements to be compatible with legacy format // So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves @@ -145,15 +159,6 @@ namespace osu.Game.Database hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); } } - - // Encode to legacy format - var stream = new MemoryStream(); - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - return stream; } protected override string FileExtension => @".osz"; diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs new file mode 100644 index 0000000000..3c50a1bf80 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Edit.Submission +{ + public class SubmissionBeatmapExporter : LegacyBeatmapExporter + { + private readonly uint? beatmapSetId; + private readonly HashSet? beatmapIds; + + public SubmissionBeatmapExporter(Storage storage) + : base(storage) + { + } + + public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) + : base(storage) + { + beatmapSetId = putBeatmapSetResponse.BeatmapSetId; + beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + } + + protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { + base.MutateBeatmap(beatmapSet, playableBeatmap); + + if (beatmapSetId != null && beatmapIds != null) + { + playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; + playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; + + if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + { + beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + return; + } + + if (playableBeatmap.BeatmapInfo.OnlineID > 0) + throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); + + if (beatmapIds.Count == 0) + throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); + + int newId = beatmapIds.First(); + beatmapIds.Remove(newId); + playableBeatmap.BeatmapInfo.OnlineID = newId; + } + } + } +} From 78e85dc2c7f773ac8cbde2b226ec6ba9b8791672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 12:22:33 +0100 Subject: [PATCH 501/620] Add beatmap submission support --- .../Localisation/BeatmapSubmissionStrings.cs | 40 ++ osu.Game/Localisation/EditorStrings.cs | 10 + osu.Game/Screens/Edit/Editor.cs | 55 ++- .../Submission/BeatmapSubmissionScreen.cs | 422 ++++++++++++++++++ .../Submission/BeatmapSubmissionSettings.cs | 13 + .../Submission/ScreenSubmissionSettings.cs | 15 +- 6 files changed, 544 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index a4c2b36894..50b65ab572 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -39,6 +39,31 @@ namespace osu.Game.Localisation /// public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings"); + /// + /// "Submit beatmap!" + /// + public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); + + /// + /// "Exporting beatmap set in compatibility mode..." + /// + public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + + /// + /// "Preparing beatmap set online..." + /// + public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + + /// + /// "Uploading beatmap set contents..." + /// + public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + + /// + /// "Updating local beatmap with relevant changes..." + /// + public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" /// @@ -119,6 +144,21 @@ namespace osu.Game.Localisation /// public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + /// + /// "Empty beatmaps cannot be submitted." + /// + public static LocalisableString EmptyBeatmapsCannotBeSubmitted => new TranslatableString(getKey(@"empty_beatmaps_cannot_be_submitted"), @"Empty beatmaps cannot be submitted."); + + /// + /// "Update beatmap!" + /// + public static LocalisableString UpdateBeatmap => new TranslatableString(getKey(@"update_beatmap"), @"Update beatmap!"); + + /// + /// "Upload NEW beatmap!" + /// + public static LocalisableString UploadNewBeatmap => new TranslatableString(getKey(@"upload_new_beatmap"), @"Upload NEW beatmap!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11..2c834c38bb 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -69,6 +69,16 @@ namespace osu.Game.Localisation /// public static LocalisableString DeleteDifficulty => new TranslatableString(getKey(@"delete_difficulty"), @"Delete difficulty"); + /// + /// "Edit externally" + /// + public static LocalisableString EditExternally => new TranslatableString(getKey(@"edit_externally"), @"Edit externally"); + + /// + /// "Submit beatmap" + /// + public static LocalisableString SubmitBeatmap => new TranslatableString(getKey(@"submit_beatmap"), @"Submit beatmap"); + /// /// "setup" /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3302fafbb8..c2a7264243 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -32,6 +32,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -52,6 +53,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Edit.Submission; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.OnlinePlay; @@ -111,6 +113,10 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private INotificationOverlay notifications { get; set; } + [Resolved(canBeNull: true)] + [CanBeNull] + private LoginOverlay loginOverlay { get; set; } + [Resolved] private RealmAccess realm { get; set; } @@ -1309,11 +1315,22 @@ namespace osu.Game.Screens.Edit if (RuntimeInfo.IsDesktop) { - var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); + var externalEdit = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, editExternally); saveRelatedMenuItems.Add(externalEdit); yield return externalEdit; } + bool isSetMadeOfLegacyRulesetBeatmaps = (isNewBeatmap && Ruleset.Value.IsLegacyRuleset()) + || (!isNewBeatmap && Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Ruleset.IsLegacyRuleset())); + bool submissionAvailable = api.EndpointConfiguration.BeatmapSubmissionServiceUrl != null; + + if (isSetMadeOfLegacyRulesetBeatmaps && submissionAvailable) + { + var upload = new EditorMenuItem(EditorStrings.SubmitBeatmap, MenuItemType.Standard, submitBeatmap); + saveRelatedMenuItems.Add(upload); + yield return upload; + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } @@ -1353,6 +1370,42 @@ namespace osu.Game.Screens.Edit } } + private void submitBeatmap() + { + if (api.State.Value != APIState.Online) + { + loginOverlay?.Show(); + return; + } + + if (!editorBeatmap.HitObjects.Any()) + { + notifications?.Post(new SimpleNotification + { + Text = BeatmapSubmissionStrings.EmptyBeatmapsCannotBeSubmitted, + }); + return; + } + + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => + { + if (!Save()) + return false; + + startSubmission(); + return true; + }))); + } + else + { + startSubmission(); + } + + void startSubmission() => this.Push(new BeatmapSubmissionScreen()); + } + private void exportBeatmap(bool legacy) { if (HasUnsavedChanges) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs new file mode 100644 index 0000000000..796d975e4f --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -0,0 +1,422 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Development; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.IO.Archives; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class BeatmapSubmissionScreen : OsuScreen + { + private BeatmapSubmissionOverlay overlay = null!; + + public override bool AllowUserExit => false; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private Storage storage { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Cached] + private BeatmapSubmissionSettings settings { get; } = new BeatmapSubmissionSettings(); + + private Container submissionProgress = null!; + private SubmissionStageProgress exportStep = null!; + private SubmissionStageProgress createSetStep = null!; + private SubmissionStageProgress uploadStep = null!; + private SubmissionStageProgress updateStep = null!; + private Container successContainer = null!; + private Container flashLayer = null!; + private RoundedButton backButton = null!; + + private uint? beatmapSetId; + + private SubmissionBeatmapExporter legacyBeatmapExporter = null!; + private ProgressNotification? exportProgressNotification; + private MemoryStream beatmapPackageStream = null!; + private ProgressNotification? updateProgressNotification; + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + overlay = new BeatmapSubmissionOverlay(), + submissionProgress = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.6f, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), + Spacing = new Vector2(5), + Children = new Drawable[] + { + createSetStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + exportStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + uploadStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + updateStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + successContainer = new Container + { + Padding = new MarginPadding(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Child = flashLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Depth = float.MinValue, + Alpha = 0, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }, + backButton = new RoundedButton + { + Text = CommonStrings.Back, + Width = 150, + Action = this.Exit, + Enabled = { Value = false }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } + } + } + } + }); + + overlay.State.BindValueChanged(_ => + { + if (overlay.State.Value == Visibility.Hidden) + { + if (!overlay.Completed) + this.Exit(); + else + { + submissionProgress.FadeIn(200, Easing.OutQuint); + createBeatmapSet(); + } + } + }); + beatmapPackageStream = new MemoryStream(); + } + + private void createBeatmapSet() + { + bool beatmapHasOnlineId = Beatmap.Value.BeatmapSetInfo.OnlineID > 0; + + var createRequest = beatmapHasOnlineId + ? PutBeatmapSetRequest.UpdateExisting( + (uint)Beatmap.Value.BeatmapSetInfo.OnlineID, + Beatmap.Value.BeatmapSetInfo.Beatmaps.Where(b => b.OnlineID > 0).Select(b => (uint)b.OnlineID).ToArray(), + (uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count(b => b.OnlineID <= 0), + settings.Target.Value) + : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings.Target.Value); + + createRequest.Success += async response => + { + createSetStep.SetCompleted(); + beatmapSetId = response.BeatmapSetId; + + // at this point the set has an assigned online ID. + // it's important to proactively store it to the realm database, + // so that in the event in further failures in the process, the online ID is not lost. + // losing it can incur creation of redundant new sets server-side, or even cause online ID confusion. + if (!beatmapHasOnlineId) + { + await realmAccess.WriteAsync(r => + { + var refetchedSet = r.Find(Beatmap.Value.BeatmapSetInfo.ID); + refetchedSet!.OnlineID = (int)beatmapSetId.Value; + }).ConfigureAwait(true); + } + + legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + await createBeatmapPackage(response.Files).ConfigureAwait(true); + }; + createRequest.Failure += ex => + { + createSetStep.SetFailed(ex.Message); + backButton.Enabled.Value = true; + Logger.Log($"Beatmap set submission failed on creation: {ex}"); + }; + + createSetStep.SetInProgress(); + api.Queue(createRequest); + } + + private async Task createBeatmapPackage(ICollection onlineFiles) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); + + try + { + await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) + .ConfigureAwait(true); + } + catch (Exception ex) + { + exportStep.SetFailed(ex.Message); + Logger.Log($"Beatmap set submission failed on export: {ex}"); + backButton.Enabled.Value = true; + exportProgressNotification = null; + } + + exportStep.SetCompleted(); + exportProgressNotification = null; + + if (onlineFiles.Count > 0) + await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + else + replaceBeatmapSet(); + } + + private async Task patchBeatmapSet(ICollection onlineFiles) + { + Debug.Assert(beatmapSetId != null); + + var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); + + // disposing the `ArchiveReader` makes the underlying stream no longer readable which we don't want. + // make a local copy to defend against it. + using var archiveReader = new ZipArchiveReader(new MemoryStream(beatmapPackageStream.ToArray())); + var filesToUpdate = new HashSet(); + + foreach (string filename in archiveReader.Filenames) + { + string localHash = archiveReader.GetStream(filename).ComputeSHA2Hash(); + + if (!onlineFilesByFilename.Remove(filename, out string? onlineHash)) + { + filesToUpdate.Add(filename); + continue; + } + + if (localHash != onlineHash) + filesToUpdate.Add(filename); + } + + var changedFiles = new Dictionary(); + + foreach (string file in filesToUpdate) + changedFiles.Add(file, await archiveReader.GetStream(file).ReadAllBytesToArrayAsync().ConfigureAwait(true)); + + var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); + patchRequest.FilesChanged.AddRange(changedFiles); + patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); + patchRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + patchRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); + + api.Queue(patchRequest); + uploadStep.SetInProgress(); + } + + private void replaceBeatmapSet() + { + Debug.Assert(beatmapSetId != null); + + var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); + + uploadRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + uploadRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); + + api.Queue(uploadRequest); + uploadStep.SetInProgress(); + } + + private async Task updateLocalBeatmap() + { + Debug.Assert(beatmapSetId != null); + updateStep.SetInProgress(); + + Live? importedSet; + + try + { + importedSet = await beatmaps.ImportAsUpdate( + updateProgressNotification = new ProgressNotification(), + new ImportTask(beatmapPackageStream, $"{beatmapSetId}.osz"), + Beatmap.Value.BeatmapSetInfo).ConfigureAwait(true); + } + catch (Exception ex) + { + updateStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on local update: {ex}"); + Schedule(() => backButton.Enabled.Value = true); + return; + } + + updateStep.SetCompleted(); + backButton.Enabled.Value = true; + backButton.Action = () => + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + }; + showBeatmapCard(); + } + + private void showBeatmapCard() + { + Debug.Assert(beatmapSetId != null); + + var getBeatmapSetRequest = new GetBeatmapSetRequest((int)beatmapSetId.Value); + getBeatmapSetRequest.Success += beatmapSet => + { + LoadComponentAsync(new BeatmapCardExtra(beatmapSet, false), loaded => + { + successContainer.Add(loaded); + flashLayer.FadeOutFromOne(2000, Easing.OutQuint); + }); + }; + + api.Queue(getBeatmapSetRequest); + } + + protected override void Update() + { + base.Update(); + + if (exportProgressNotification != null && exportProgressNotification.Ongoing) + exportStep.SetInProgress(exportProgressNotification.Progress); + + if (updateProgressNotification != null && updateProgressNotification.Ongoing) + updateStep.SetInProgress(updateProgressNotification.Progress); + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + overlay.Show(); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs new file mode 100644 index 0000000000..359dc11f39 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Screens.Edit.Submission +{ + public class BeatmapSubmissionSettings + { + public Bindable Target { get; } = new Bindable(); + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs index 72da94afa1..08b4d9f712 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osuTK; @@ -22,8 +23,10 @@ namespace osu.Game.Screens.Edit.Submission private readonly BindableBool notifyOnDiscussionReplies = new BindableBool(); private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool(); + public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ConfirmSubmission; + [BackgroundDependencyLoader] - private void load(OsuConfigManager configManager, OsuColour colours) + private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings) { configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); @@ -39,6 +42,7 @@ namespace osu.Game.Screens.Edit.Submission { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption, + Current = settings.Target, }, new FormCheckBox { @@ -60,14 +64,5 @@ namespace osu.Game.Screens.Edit.Submission } }); } - - private enum BeatmapSubmissionTarget - { - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] - WIP, - - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] - Pending, - } } } From abce42b1c8fa48dc9fc64ff5e756b90f66d947a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:28:27 +0100 Subject: [PATCH 502/620] Improve bookmark controls - Bookmark menu items get disabled when they would do nothing. - Bookmark deletion only deletes the closest bookmark instead of all of them within the proximity of 2 seconds to current clock time. Action is only however *enabled* within 2 seconds of a bookmark. Additionally, logic was moved out of `Editor` because it's a huge class and I dislike huge classes if they can be at all avoided. --- osu.Game/Screens/Edit/BookmarkController.cs | 148 ++++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 66 +-------- 2 files changed, 152 insertions(+), 62 deletions(-) create mode 100644 osu.Game/Screens/Edit/BookmarkController.cs diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs new file mode 100644 index 0000000000..8c048ba871 --- /dev/null +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -0,0 +1,148 @@ +// 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.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Screens.Edit.Components.Menus; + +namespace osu.Game.Screens.Edit +{ + public class BookmarkController : Component, IKeyBindingHandler + { + public EditorMenuItem Menu { get; private set; } + + [Resolved] + private EditorClock clock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + private readonly BindableList bookmarks = new BindableList(); + + private readonly EditorMenuItem removeBookmarkMenuItem; + private readonly EditorMenuItem seekToPreviousBookmarkMenuItem; + private readonly EditorMenuItem seekToNextBookmarkMenuItem; + private readonly EditorMenuItem resetBookmarkMenuItem; + + public BookmarkController() + { + Menu = new EditorMenuItem(EditorStrings.Bookmarks) + { + Items = new MenuItem[] + { + new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime) + { + Hotkey = new Hotkey(GlobalAction.EditorAddBookmark), + }, + removeBookmarkMenuItem = new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeClosestBookmark) + { + Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark) + }, + seekToPreviousBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1)) + { + Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark) + }, + seekToNextBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1)) + { + Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) + }, + resetBookmarkMenuItem = new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + bookmarks.BindTo(editorBeatmap.Bookmarks); + } + + protected override void Update() + { + base.Update(); + + bool hasAnyBookmark = bookmarks.Count > 0; + bool hasBookmarkCloseEnoughForDeletion = bookmarks.Any(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000); + + removeBookmarkMenuItem.Action.Disabled = !hasBookmarkCloseEnoughForDeletion; + seekToPreviousBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + seekToNextBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + resetBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + } + + private void addBookmarkAtCurrentTime() + { + int bookmark = (int)clock.CurrentTimeAccurate; + int idx = bookmarks.BinarySearch(bookmark); + if (idx < 0) + bookmarks.Insert(~idx, bookmark); + } + + private void removeClosestBookmark() + { + if (removeBookmarkMenuItem.Action.Disabled) + return; + + int closestBookmark = bookmarks.MinBy(b => Math.Abs(b - clock.CurrentTimeAccurate)); + bookmarks.Remove(closestBookmark); + } + + private void seekBookmark(int direction) + { + int? targetBookmark = direction < 1 + ? bookmarks.Cast().LastOrDefault(b => b < clock.CurrentTimeAccurate) + : bookmarks.Cast().FirstOrDefault(b => b > clock.CurrentTimeAccurate); + + if (targetBookmark != null) + clock.SeekSmoothlyTo(targetBookmark.Value); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.EditorSeekToPreviousBookmark: + seekBookmark(-1); + return true; + + case GlobalAction.EditorSeekToNextBookmark: + seekBookmark(1); + return true; + } + + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.EditorAddBookmark: + addBookmarkAtCurrentTime(); + return true; + + case GlobalAction.EditorRemoveClosestBookmark: + removeClosestBookmark(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3302fafbb8..a5dfda9c95 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -317,6 +317,9 @@ namespace osu.Game.Screens.Edit workingBeatmapUpdated = true; }); + var bookmarkController = new BookmarkController(); + AddInternal(bookmarkController); + OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; @@ -442,29 +445,7 @@ namespace osu.Game.Screens.Edit Items = new MenuItem[] { new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime), - new EditorMenuItem(EditorStrings.Bookmarks) - { - Items = new MenuItem[] - { - new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime) - { - Hotkey = new Hotkey(GlobalAction.EditorAddBookmark), - }, - new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeBookmarksInProximityToCurrentTime) - { - Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark) - }, - new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1)) - { - Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark) - }, - new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1)) - { - Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) - }, - new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) - } - } + bookmarkController.Menu, } } } @@ -800,14 +781,6 @@ namespace osu.Game.Screens.Edit case GlobalAction.EditorSeekToNextSamplePoint: seekSamplePoint(1); return true; - - case GlobalAction.EditorSeekToPreviousBookmark: - seekBookmark(-1); - return true; - - case GlobalAction.EditorSeekToNextBookmark: - seekBookmark(1); - return true; } if (e.Repeat) @@ -815,14 +788,6 @@ namespace osu.Game.Screens.Edit switch (e.Action) { - case GlobalAction.EditorAddBookmark: - addBookmarkAtCurrentTime(); - return true; - - case GlobalAction.EditorRemoveClosestBookmark: - removeBookmarksInProximityToCurrentTime(); - return true; - case GlobalAction.EditorCloneSelection: Clone(); return true; @@ -855,19 +820,6 @@ namespace osu.Game.Screens.Edit return false; } - private void addBookmarkAtCurrentTime() - { - int bookmark = (int)clock.CurrentTimeAccurate; - int idx = editorBeatmap.Bookmarks.BinarySearch(bookmark); - if (idx < 0) - editorBeatmap.Bookmarks.Insert(~idx, bookmark); - } - - private void removeBookmarksInProximityToCurrentTime() - { - editorBeatmap.Bookmarks.RemoveAll(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000); - } - public void OnReleased(KeyBindingReleaseEvent e) { } @@ -1202,16 +1154,6 @@ namespace osu.Game.Screens.Edit clock.SeekSmoothlyTo(found.StartTime); } - private void seekBookmark(int direction) - { - int? targetBookmark = direction < 1 - ? editorBeatmap.Bookmarks.Cast().LastOrDefault(b => b < clock.CurrentTimeAccurate) - : editorBeatmap.Bookmarks.Cast().FirstOrDefault(b => b > clock.CurrentTimeAccurate); - - if (targetBookmark != null) - clock.SeekSmoothlyTo(targetBookmark.Value); - } - private void seekSamplePoint(int direction) { double currentTime = clock.CurrentTimeAccurate; From 4cbfb5170790c301ecfa08214df9795e82b754e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:30:11 +0100 Subject: [PATCH 503/620] Fix undoing bookmark operations potentially making them unsorted Found in testing of previous commit. This would break seeking between bookmarks. Reproduction steps on `master`: - open map with bookmark - delete the first bookmark - undo the deletion of the first bookmark - seek to previous bookmark will now always seek to the first bookmark rather than closest preceding regardless of current clock time --- osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index f3d58a3c3c..e84b6bfc72 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -115,7 +115,9 @@ namespace osu.Game.Screens.Edit if (editorBeatmap.Bookmarks.Contains(newBookmark)) continue; - editorBeatmap.Bookmarks.Add(newBookmark); + int idx = editorBeatmap.Bookmarks.BinarySearch(newBookmark); + if (idx < 0) + editorBeatmap.Bookmarks.Insert(~idx, newBookmark); } } From 10711e5e2721ab11b28c4fc5f00e6769d9aad3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:39:36 +0100 Subject: [PATCH 504/620] Add missing `partial` --- osu.Game/Screens/Edit/BookmarkController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs index 8c048ba871..3d2cb4663f 100644 --- a/osu.Game/Screens/Edit/BookmarkController.cs +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -17,7 +17,7 @@ using osu.Game.Screens.Edit.Components.Menus; namespace osu.Game.Screens.Edit { - public class BookmarkController : Component, IKeyBindingHandler + public partial class BookmarkController : Component, IKeyBindingHandler { public EditorMenuItem Menu { get; private set; } From 84206e9ad8253ae0acc5169787fb6d6b516e16ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 13:29:16 +0900 Subject: [PATCH 505/620] Initial support for freemod+freestyle --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 11 +-- .../Multiplayer/MultiplayerMatchSubScreen.cs | 89 ++++++++---------- .../Playlists/PlaylistsRoomSubScreen.cs | 93 +++++++++---------- 3 files changed, 86 insertions(+), 107 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ce51bb3c21..312253774f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -441,7 +440,9 @@ namespace osu.Game.Screens.OnlinePlay.Match var rulesetInstance = GetGameplayRuleset().CreateInstance(); // Remove any user mods that are no longer allowed. - Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + Mod[] allowedMods = item.Freestyle + ? rulesetInstance.CreateAllMods().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() + : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(UserMods.Value)) UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); @@ -455,12 +456,8 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - bool freeMod = item.AllowedMods.Any(); bool freestyle = item.Freestyle; - - // For now, the game can never be in a state where freemod and freestyle are on at the same time. - // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. - Debug.Assert(!freeMod || !freestyle); + bool freeMod = freestyle || item.AllowedMods.Any(); if (freeMod) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b803c5f28b..a16c5c9442 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -98,7 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { new Drawable?[] { - // Participants column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -118,9 +117,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } }, - // Spacer null, - // Beatmap column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -147,67 +144,63 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem } }, - new Drawable[] + new[] { - new Container + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 10 }, - Children = new[] + Alpha = 0, + Children = new Drawable[] { - UserModsSection = new FillFlowContainer + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + new UserModSelectButton { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, }, - } - }, - UserStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container + new ModDisplay { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, } }, } } }, + new[] + { + UserStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, + }, }, RowDimensions = new[] { @@ -218,9 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - // Spacer null, - // Main right column new GridContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 2195ed4722..957a51c467 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -146,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable?[] { - // Playlist items column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -176,73 +175,66 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Dimension(), } }, - // Spacer null, - // Middle column (mods and leaderboard) new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] + new[] { - new Container + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Bottom = 10 }, - Children = new[] + Alpha = 0, + Children = new Drawable[] { - UserModsSection = new FillFlowContainer + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + new UserModSelectButton { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, } - }, - UserStyleSection = new FillFlowContainer + } + } + }, + }, + new[] + { + UserStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, + AutoSizeAxes = Axes.Y + } } }, }, @@ -273,12 +265,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), new Dimension(), } }, - // Spacer null, - // Main right column new GridContainer { RelativeSizeAxes = Axes.Both, From 9cc90a51df7aa2a0043690ff873ed836741993b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:32:11 +0900 Subject: [PATCH 506/620] Adjust xmldoc and avoid LINQ overheads --- osu.Game/Screens/SelectV2/Carousel.cs | 7 +++---- osu.Game/Screens/SelectV2/CarouselItem.cs | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 5dc8d80476..07d9c988f5 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -550,8 +550,7 @@ namespace osu.Game.Screens.SelectV2 updateDisplayedRange(range); } - double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; - double maximumDistanceFromSelection = scroll.Panels.Select(p => Math.Abs(((ICarouselPanel)p).DrawYPosition - selectedYPos)).DefaultIfEmpty().Max(); + double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0; foreach (var panel in scroll.Panels) { @@ -561,8 +560,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / maximumDistanceFromSelection); - scroll.Panels.ChangeChildDepth(panel, normalisedDepth + c.Item.DepthLayer); + float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight); + scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index e497c3890c..0ac8180028 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -30,10 +30,9 @@ namespace osu.Game.Screens.SelectV2 public float DrawHeight { get; set; } = DEFAULT_HEIGHT; /// - /// A number that defines the layer which this should be placed on depth-wise. - /// The higher the number, the farther the panel associated with this item is taken to the background. + /// Defines the display depth relative to other s. /// - public int DepthLayer { get; set; } = 0; + public int DepthLayer { get; set; } /// /// Whether this item is visible or hidden. From d9b370e3a1d384758dee89ef704dd0c38a694ec8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:41:16 +0900 Subject: [PATCH 507/620] Add xmldoc for menu implying external consumption --- osu.Game/Screens/Edit/BookmarkController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs index 3d2cb4663f..80e77364e5 100644 --- a/osu.Game/Screens/Edit/BookmarkController.cs +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -19,6 +19,9 @@ namespace osu.Game.Screens.Edit { public partial class BookmarkController : Component, IKeyBindingHandler { + /// + /// Bookmarks menu item (with submenu containing options). Should be added to the 's global menu. + /// public EditorMenuItem Menu { get; private set; } [Resolved] From 0257b8c2ffd2dffa2b81fbf41ad88889db0ff14a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:57:38 +0900 Subject: [PATCH 508/620] Move metadata randomisation local to usage --- osu.Game.Tests/Resources/TestResources.cs | 29 +++++------------- .../SongSelect/BeatmapCarouselV2TestScene.cs | 30 +++++++++++++++++-- .../TestSceneBeatmapCarouselV2Basics.cs | 3 +- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index bf08097ffd..e0572e604c 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -85,8 +85,7 @@ namespace osu.Game.Tests.Resources /// /// Number of difficulties. If null, a random number between 1 and 20 will be used. /// Rulesets to cycle through when creating difficulties. If null, osu! ruleset will be used. - /// Whether to randomise metadata to create a better distribution. - public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null, bool randomiseMetadata = false) + public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) { int j = 0; @@ -96,27 +95,13 @@ namespace osu.Game.Tests.Resources int setId = GetNextTestID(); - char getRandomCharacter() + var metadata = new BeatmapMetadata { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; - return chars[RNG.Next(chars.Length)]; - } - - var metadata = randomiseMetadata - ? new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), - Title = $"{getRandomCharacter()}ome Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, - } - : new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = "Some Artist " + RNG.Next(0, 9), - Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = "Some Guy " + RNG.Next(0, 9) }, - }; + // Create random metadata, then we can check if sorting works based on these + Artist = "Some Artist " + RNG.Next(0, 9), + Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = "Some Guy " + RNG.Next(0, 9) }, + }; Logger.Log($"🛠️ Generating beatmap set \"{metadata}\" for test consumption."); diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f5ea959c51..a55f79c42e 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -190,12 +191,37 @@ namespace osu.Game.Tests.Visual.SongSelect /// /// The count of beatmap sets to add. /// If not null, the number of difficulties per set. If null, randomised difficulty count will be used. - protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => + /// Whether to randomise the metadata to make groupings more uniform. + protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () => { for (int i = 0; i < count; i++) - BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4), randomiseMetadata: true)); + { + var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); + + if (randomMetadata) + { + var metadata = new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), + Title = $"{getRandomCharacter()}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + }; + + foreach (var beatmap in beatmapSetInfo.Beatmaps) + beatmap.Metadata = metadata.DeepClone(); + } + + BeatmapSets.Add(beatmapSetInfo); + } }); + private static char getRandomCharacter() + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; + return chars[RNG.Next(chars.Length)]; + } + protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); protected void RemoveFirstBeatmap() => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index a173920dc6..41ceff3183 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -26,8 +26,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestBasics() { - AddBeatmaps(1); AddBeatmaps(10); + AddBeatmaps(10, randomMetadata: true); + AddBeatmaps(1); RemoveFirstBeatmap(); RemoveAllBeatmaps(); } From d93f7509b6545489f405faf8e9a60f4800b7e040 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 14:12:15 +0900 Subject: [PATCH 509/620] Fix participant panels not displaying mods from other rulesets correctly --- .../TestSceneMultiplayerParticipantsList.cs | 37 +++++++++++++++++++ .../Participants/ParticipantPanel.cs | 22 ++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 238a716f91..d3c967a8d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -12,11 +12,14 @@ using osu.Framework.Utils; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Users; using osuTK; @@ -393,6 +396,40 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestModsAndRuleset() + { + AddStep("add another user", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = 0, + Username = "User 0", + RulesetsStatistics = new Dictionary + { + { + Ruleset.Value.ShortName, + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } + } + }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()); + }); + + AddStep("set user styles", () => + { + MultiplayerClient.ChangeUserStyle(API.LocalUser.Value.OnlineID, 259, 1); + MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, + [new APIMod(new TaikoModConstantSpeed()), new APIMod(new TaikoModHidden()), new APIMod(new TaikoModFlashlight()), new APIMod(new TaikoModHardRock())]); + + MultiplayerClient.ChangeUserStyle(0, 259, 2); + MultiplayerClient.ChangeUserMods(0, + [new APIMod(new CatchModFloatingFruits()), new APIMod(new CatchModHidden()), new APIMod(new CatchModMirror())]); + }); + } + private void createNewParticipantsList() { ParticipantsList? participantsList = null; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index a2657019a3..d6666de2b6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -27,7 +28,6 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -210,13 +210,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); - Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; + Debug.Assert(currentItem != null); - int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); + Debug.Assert(userRuleset != null); userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); + int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) { userModsDisplay.FadeIn(fade_time); @@ -228,20 +233,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID)) + if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) userStyleDisplay.Style = null; else - userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); + userStyleDisplay.Style = (userBeatmapId, userRulesetId); kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => - { - userModsDisplay.Current.Value = ruleset != null ? User.Mods.Select(m => m.ToMod(ruleset)).ToList() : Array.Empty(); - }); + Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } public MenuItem[]? ContextMenuItems From 885ae7c735a82740710fce395a456d8e1280abf9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 14:25:08 +0900 Subject: [PATCH 510/620] Adjust styling --- .../Multiplayer/Participants/ParticipantPanel.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index d6666de2b6..51ff52c63e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -161,11 +161,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Right = 70 }, + Spacing = new Vector2(2), Children = new Drawable[] { - userStyleDisplay = new StyleDisplayIcon(), + userStyleDisplay = new StyleDisplayIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, userModsDisplay = new ModDisplay { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Scale = new Vector2(0.5f), ExpansionMode = ExpansionMode.AlwaysContracted, } From 88ad87a78e36a7170d0ce05dd0a0a29433977f88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:30:15 +0900 Subject: [PATCH 511/620] Expose set grouping state --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index e7311fbfbc..36e57c9067 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.SelectV2 return true; case BeatmapInfo: - return Criteria.SplitOutDifficulties; + return !grouping.BeatmapSetsGroupedTogether; case GroupDefinition: return false; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index d4e0a166ab..29c534cbe2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -14,6 +14,8 @@ namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + public bool BeatmapSetsGroupedTogether { get; private set; } + /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// @@ -36,8 +38,6 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - bool groupSetsTogether; - setItems.Clear(); groupItems.Clear(); @@ -48,12 +48,12 @@ namespace osu.Game.Screens.SelectV2 switch (criteria.Group) { default: - groupSetsTogether = true; + BeatmapSetsGroupedTogether = true; newItems.AddRange(items); break; case GroupMode.Artist: - groupSetsTogether = true; + BeatmapSetsGroupedTogether = true; char groupChar = (char)0; foreach (var item in items) @@ -80,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 break; case GroupMode.Difficulty: - groupSetsTogether = false; + BeatmapSetsGroupedTogether = false; int starGroup = int.MinValue; foreach (var item in items) @@ -108,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 // Add set headers wherever required. CarouselItem? lastItem = null; - if (groupSetsTogether) + if (BeatmapSetsGroupedTogether) { for (int i = 0; i < newItems.Count; i++) { From 342a66b9e21e619c9192a4bd63bb2f32563c2e20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:39:11 +0900 Subject: [PATCH 512/620] Fix keyboard traversal on a collapsed group not working as intended --- osu.Game/Screens/SelectV2/Carousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..6b7b1f3a9b 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -378,7 +378,7 @@ namespace osu.Game.Screens.SelectV2 { TryActivateSelection(); - // There's a chance this couldn't resolve, at which point continue with standard traversal. + // Is the selection actually changed, then we should not perform any further traversal. if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) return; } @@ -386,20 +386,20 @@ namespace osu.Game.Screens.SelectV2 int originalIndex; int newIndex; - if (currentSelection.Index == null) + if (currentKeyboardSelection.Index == null) { // If there's no current selection, start from either end of the full list. newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0; } else { - newIndex = originalIndex = currentSelection.Index.Value; + newIndex = originalIndex = currentKeyboardSelection.Index.Value; // As a second special case, if we're group selecting backwards and the current selection isn't a group, // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (direction < 0) { - while (!CheckValidForGroupSelection(carouselItems[newIndex])) + while (newIndex > 0 && !CheckValidForGroupSelection(carouselItems[newIndex])) newIndex--; } } From bf377e081ad36ab88b1c7b6ef415bcb2db888bdd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:38:51 +0900 Subject: [PATCH 513/620] Reorganise tests to make more logical when manually testing --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 23 ++--- ...asics.cs => TestSceneBeatmapCarouselV2.cs} | 93 ++++++------------- ...eneBeatmapCarouselV2DifficultyGrouping.cs} | 25 ++--- ...> TestSceneBeatmapCarouselV2NoGrouping.cs} | 12 ++- .../TestSceneBeatmapCarouselV2Scrolling.cs | 65 +++++++++++++ 5 files changed, 118 insertions(+), 100 deletions(-) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2Basics.cs => TestSceneBeatmapCarouselV2.cs} (52%) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2GroupSelection.cs => TestSceneBeatmapCarouselV2DifficultyGrouping.cs} (92%) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2Selection.cs => TestSceneBeatmapCarouselV2NoGrouping.cs} (94%) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index a55f79c42e..36226a13cc 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -17,7 +17,6 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -54,16 +53,6 @@ namespace osu.Game.Tests.Visual.SongSelect Scheduler.AddDelayed(updateStats, 100, true); } - [SetUpSteps] - public virtual void SetUpSteps() - { - RemoveAllBeatmaps(); - - CreateCarousel(); - - SortBy(new FilterCriteria { Sort = SortMode.Title }); - } - protected void CreateCarousel() { AddStep("create components", () => @@ -200,12 +189,14 @@ namespace osu.Game.Tests.Visual.SongSelect if (randomMetadata) { + char randomCharacter = getRandomCharacter(); + var metadata = new BeatmapMetadata { // Create random metadata, then we can check if sorting works based on these - Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), - Title = $"{getRandomCharacter()}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", - Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), + Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, }; foreach (var beatmap in beatmapSetInfo.Beatmaps) @@ -216,10 +207,12 @@ namespace osu.Game.Tests.Visual.SongSelect } }); + private static long randomCharPointer; + private static char getRandomCharacter() { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; - return chars[RNG.Next(chars.Length)]; + return chars[(int)((randomCharPointer++ / 2) % chars.Length)]; } protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs similarity index 52% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 41ceff3183..3c5cf16e92 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -2,102 +2,65 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.SongSelect { /// - /// Currently covers adding and removing of items and scrolling. - /// If we add more tests here, these two categories can likely be split out into separate scenes. + /// Covers common steps which can be used for manual testing. /// [TestFixture] - public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene { [Test] + [Explicit] public void TestBasics() { - AddBeatmaps(10); + CreateCarousel(); + RemoveAllBeatmaps(); + AddBeatmaps(10, randomMetadata: true); + AddBeatmaps(10); AddBeatmaps(1); + } + + [Test] + [Explicit] + public void TestSorting() + { + SortBy(new FilterCriteria { Sort = SortMode.Artist }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + } + + [Test] + [Explicit] + public void TestRemovals() + { RemoveFirstBeatmap(); RemoveAllBeatmaps(); } [Test] - public void TestOffScreenLoading() - { - AddStep("disable masking", () => Scroll.Masking = false); - AddStep("enable masking", () => Scroll.Masking = true); - } - - [Test] - public void TestAddRemoveOneByOne() + [Explicit] + public void TestAddRemoveRepeatedOps() { AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20); } [Test] - public void TestSorting() + [Explicit] + public void TestMasking() { - AddBeatmaps(10); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); - SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); - SortBy(new FilterCriteria { Sort = SortMode.Artist }); - } - - [Test] - public void TestScrollPositionMaintainedOnAddSecondSelected() - { - Quad positionBefore = default; - - AddBeatmaps(10); - WaitForDrawablePanels(); - - AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); - - WaitForScrolling(); - - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - RemoveFirstBeatmap(); - WaitForSorting(); - - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestScrollPositionMaintainedOnAddLastSelected() - { - Quad positionBefore = default; - - AddBeatmaps(10); - WaitForDrawablePanels(); - - AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); - - WaitForScrolling(); - - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - RemoveFirstBeatmap(); - WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); + AddStep("disable masking", () => Scroll.Masking = false); + AddStep("enable masking", () => Scroll.Masking = true); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs similarity index 92% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index f4d97be5a5..e861d8bf30 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -12,23 +12,22 @@ using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelect { [TestFixture] - public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene { - public override void SetUpSteps() + [SetUpSteps] + public void SetUpSteps() { RemoveAllBeatmaps(); - CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + + AddBeatmaps(10, 3); + WaitForDrawablePanels(); } [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddBeatmaps(10, 5); - WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); @@ -44,9 +43,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddBeatmaps(10, 5); - WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); @@ -67,9 +63,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestCarouselRemembersSelection() { - AddBeatmaps(10); - WaitForDrawablePanels(); - SelectNextGroup(); object? selection = null; @@ -107,9 +100,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestGroupSelectionOnHeader() { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - SelectNextGroup(); WaitForGroupSelection(0, 0); @@ -121,9 +111,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestKeyboardSelection() { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - SelectNextPanel(); SelectNextPanel(); SelectNextPanel(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs similarity index 94% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index b087c252e4..82f35af0ec 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -5,14 +5,24 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { [TestFixture] - public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria { Sort = SortMode.Title }); + } + /// /// Keyboard selection via up and down arrows doesn't actually change the selection until /// the select key is pressed. diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs new file mode 100644 index 0000000000..1d5d8e2a2d --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -0,0 +1,65 @@ +// 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.Graphics.Primitives; +using osu.Framework.Testing; +using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria()); + + AddBeatmaps(10); + WaitForDrawablePanels(); + } + + [Test] + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveFirstBeatmap(); + WaitForSorting(); + + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() + { + Quad positionBefore = default; + + AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); + + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveFirstBeatmap(); + WaitForSorting(); + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + } +} From 5b8b9589d8347fbf4a6d4d0ff9f89f24ed3274a5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 15:25:14 +0900 Subject: [PATCH 514/620] Add ruleset icon to expanded score panel --- .../Expanded/ExpandedPanelMiddleContent.cs | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index d1dc1a81db..4bc559694a 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly List statisticDisplays = new List(); - private FillFlowContainer starAndModDisplay; private RollingCounter scoreCounter; [Resolved] @@ -139,12 +138,35 @@ namespace osu.Game.Screens.Ranking.Expanded Alpha = 0, AlwaysPresent = true }, - starAndModDisplay = new FillFlowContainer + new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new DifficultyIcon(beatmap, score.Ruleset) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + ExpansionMode = ExpansionMode.AlwaysExpanded, + Scale = new Vector2(0.5f), + Current = { Value = score.Mods } + } + } }, new FillFlowContainer { @@ -225,29 +247,6 @@ namespace osu.Game.Screens.Ranking.Expanded if (score.Date != default) AddInternal(new PlayedOnText(score.Date)); - - var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely(); - - if (starDifficulty != null) - { - starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }); - } - - if (score.Mods.Any()) - { - starAndModDisplay.Add(new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - ExpansionMode = ExpansionMode.AlwaysExpanded, - Scale = new Vector2(0.5f), - Current = { Value = score.Mods } - }); - } } protected override void LoadComplete() From 5e74d82fc101f03d945033be96e184b0199016a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Feb 2025 08:32:08 +0100 Subject: [PATCH 515/620] Suppress inspections for now --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index 85981448da..bb9d32f77b 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -25,7 +25,11 @@ namespace osu.Game.Online.API.Requests protected override string Target => throw new NotSupportedException(); public uint BeatmapSetID { get; } + + // ReSharper disable once CollectionNeverUpdated.Global public Dictionary FilesChanged { get; } = new Dictionary(); + + // ReSharper disable once CollectionNeverUpdated.Global public HashSet FilesDeleted { get; } = new HashSet(); public PatchBeatmapPackageRequest(uint beatmapSetId) From e1a146d487300feb616adcf100563945aa3d17e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Feb 2025 08:38:28 +0100 Subject: [PATCH 516/620] Remove unnecessary suppressions --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index bb9d32f77b..a59a708079 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -26,10 +26,8 @@ namespace osu.Game.Online.API.Requests public uint BeatmapSetID { get; } - // ReSharper disable once CollectionNeverUpdated.Global public Dictionary FilesChanged { get; } = new Dictionary(); - // ReSharper disable once CollectionNeverUpdated.Global public HashSet FilesDeleted { get; } = new HashSet(); public PatchBeatmapPackageRequest(uint beatmapSetId) From a25e1f4f9b3e9796905419b8aa310a356a3276e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 15:13:17 +0900 Subject: [PATCH 517/620] Add test coverage of artist grouping --- ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs new file mode 100644 index 0000000000..c7ab9de5e5 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -0,0 +1,170 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + + AddBeatmaps(10, 3, true); + WaitForDrawablePanels(); + } + + [Test] + public void TestOpenCloseGroupWithNoSelectionMouse() + { + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + } + + [Test] + public void TestOpenCloseGroupWithNoSelectionKeyboard() + { + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + SelectNextPanel(); + Select(); + + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + CheckNoSelection(); + + Select(); + + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + CheckNoSelection(); + + GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + } + + [Test] + public void TestCarouselRemembersSelection() + { + SelectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + CheckHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + CheckHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + ClickVisiblePanel(0); + AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + + ClickVisiblePanel(0); + AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestGroupSelectionOnHeader() + { + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForGroupSelection(4, 5); + } + + [Test] + public void TestKeyboardSelection() + { + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); + + // open first group + Select(); + CheckNoSelection(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(3, 1); + + SelectNextGroup(); + WaitForGroupSelection(3, 5); + + SelectNextGroup(); + WaitForGroupSelection(4, 1); + + SelectPrevGroup(); + WaitForGroupSelection(3, 5); + + SelectNextGroup(); + WaitForGroupSelection(4, 1); + + SelectNextGroup(); + WaitForGroupSelection(4, 5); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextPanel(); + SelectNextGroup(); + WaitForGroupSelection(1, 1); + } + } +} From 4026ca84f887979555d32484f32ec8f20f178c7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 15:41:57 +0900 Subject: [PATCH 518/620] Move selected retrieval functions to base class --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 3 +++ ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 22 +++++++---------- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 24 ++++++++----------- .../TestSceneBeatmapCarouselV2NoGrouping.cs | 14 ++++------- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 36226a13cc..5ace306c7d 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -136,6 +136,9 @@ namespace osu.Game.Tests.Visual.SongSelect protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + protected BeatmapPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + protected GroupPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + protected void WaitForGroupSelection(int group, int panel) { AddUntilStep($"selected is group{group} panel{panel}", () => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index c7ab9de5e5..3c518fc7a6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -57,17 +57,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); - - GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); } [Test] @@ -77,34 +75,32 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); ClickVisiblePanel(0); - AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); ClickVisiblePanel(0); - AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index e861d8bf30..da3ef75487 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -49,15 +49,13 @@ namespace osu.Game.Tests.Visual.SongSelect SelectNextPanel(); Select(); AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); - - GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); } [Test] @@ -67,34 +65,32 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); ClickVisiblePanel(0); - AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); ClickVisiblePanel(0); - AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] @@ -105,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevPanel(); SelectPrevGroup(); - WaitForGroupSelection(2, 9); + WaitForGroupSelection(0, 0); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 82f35af0ec..56bc7790bf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -87,28 +85,26 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] From 024fbde0fd723a721eba48279085e0539bec0dde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 16:21:18 +0900 Subject: [PATCH 519/620] Refactor selection and activation handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I had a bit of a struggle getting header traversal logic to work well. The constraints I had in place were a bit weird: - Group panels should toggle or potentially fall into the prev/next group - Set panels should just traverse around them The current method of using `CheckValidForGroupSelection` return type for traversal did not mesh with the above two cases. Just trust me on this one since it's quite hard to explain in words. After some re-thinking, I've gone with a simpler approach with one important change to UX: Now when group traversing with a beatmap set header currently keyboard focused, the first operation will be to reset keyboard selection to the selected beatmap, rather than traverse. I find this non-offensive – at most it means a user will need to press their group traversal key one extra time. I've also changed group headers to always toggle expansion when doing group traversal with them selected. To make all this work, the meaning of `Activation` has changed somewhat. It is now the primary path for carousel implementations to change selection of an item. It is what the `Drawable` panels call when they are clicked. Selection changes are not performed implicitly by `Carousel` – an implementation should decide when it actually wants to change the selection, usually in `HandleItemActivated`. Having less things mutating `CurrentSelection` is better in my eyes, as we see this variable as only being mutated internally when utmost required (ie the user has requested the change). With this change, `CurrentSelection` can no longer become of a non-`T` type (in the beatmap carousel implementation at least). This might pave a path forward for making `CurrentSelection` typed, but that comes with a few other concerns so I'll look at that as a follow-up. --- ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 13 ++- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 9 ++ .../TestSceneBeatmapCarouselV2NoGrouping.cs | 6 +- .../TestSceneBeatmapCarouselV2Scrolling.cs | 4 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 33 ++++--- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 8 +- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 5 +- osu.Game/Screens/SelectV2/Carousel.cs | 98 +++++++++---------- osu.Game/Screens/SelectV2/GroupPanel.cs | 5 +- 9 files changed, 98 insertions(+), 83 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index 3c518fc7a6..d3eeee151a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -110,8 +110,19 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForGroupSelection(0, 1); SelectPrevPanel(); + SelectPrevPanel(); + + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + SelectPrevGroup(); - WaitForGroupSelection(4, 5); + + WaitForGroupSelection(0, 1); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + SelectPrevGroup(); + + WaitForGroupSelection(0, 1); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index da3ef75487..151f1f5fec 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -100,8 +100,17 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForGroupSelection(0, 0); SelectPrevPanel(); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + SelectPrevGroup(); + WaitForGroupSelection(0, 0); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + SelectPrevGroup(); + + WaitForGroupSelection(0, 0); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 56bc7790bf..34bdd1265d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -147,7 +147,11 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevPanel(); SelectPrevGroup(); - WaitForSelection(0, 0); + WaitForSelection(1, 0); + + SelectPrevPanel(); + SelectNextGroup(); + WaitForSelection(1, 0); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs index 1d5d8e2a2d..ee6c11595a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.SongSelect { Quad positionBefore = default; - AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last()); WaitForScrolling(); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 36e57c9067..6032989ad0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -95,11 +95,9 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition? lastSelectedGroup; private BeatmapInfo? lastSelectedBeatmap; - protected override bool HandleItemSelected(object? model) + protected override void HandleItemActivated(CarouselItem item) { - base.HandleItemSelected(model); - - switch (model) + switch (item.Model) { case GroupDefinition group: // Special case – collapsing an open group. @@ -107,16 +105,32 @@ namespace osu.Game.Screens.SelectV2 { setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = null; - return false; + return; } setExpandedGroup(group); - return false; + return; case BeatmapSetInfo setInfo: // Selecting a set isn't valid – let's re-select the first difficulty. CurrentSelection = setInfo.Beatmaps.First(); - return false; + return; + + case BeatmapInfo beatmapInfo: + CurrentSelection = beatmapInfo; + return; + } + } + + protected override void HandleItemSelected(object? model) + { + base.HandleItemSelected(model); + + switch (model) + { + case BeatmapSetInfo: + case GroupDefinition: + throw new InvalidOperationException("Groups should never become selected"); case BeatmapInfo beatmapInfo: // Find any containing group. There should never be too many groups so iterating is efficient enough. @@ -125,11 +139,8 @@ namespace osu.Game.Screens.SelectV2 if (containingGroup != null) setExpandedGroup(containingGroup); setExpandedSet(beatmapInfo); - - return true; + break; } - - return true; } protected override bool CheckValidForGroupSelection(CarouselItem item) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 3edfd4203b..9280e1c2c1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -86,13 +86,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - if (carousel.CurrentSelection != Item!.Model) - { - carousel.CurrentSelection = Item!.Model; - return true; - } - - carousel.TryActivateSelection(); + carousel.Activate(Item!); return true; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 79ffe0f68a..f6c9324077 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.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.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -83,7 +82,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + carousel.Activate(Item!); return true; } @@ -98,8 +97,6 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - // sets should never be activated. - throw new InvalidOperationException(); } #endregion diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6b7b1f3a9b..603a792847 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -94,26 +94,39 @@ namespace osu.Game.Screens.SelectV2 public object? CurrentSelection { get => currentSelection.Model; - set => setSelection(value); + set + { + if (currentSelection.Model != value) + { + HandleItemSelected(value); + + if (currentSelection.Model != null) + HandleItemDeselected(currentSelection.Model); + + currentKeyboardSelection = new Selection(value); + currentSelection = currentKeyboardSelection; + selectionValid.Invalidate(); + } + else if (currentKeyboardSelection.Model != value) + { + // Even if the current selection matches, let's ensure the keyboard selection is reset + // to the newly selected object. This matches user expectations (for now). + currentKeyboardSelection = currentSelection; + selectionValid.Invalidate(); + } + } } /// - /// Activate the current selection, if a selection exists and matches keyboard selection. - /// If keyboard selection does not match selection, this will transfer the selection on first invocation. + /// Activate the specified item. /// - public void TryActivateSelection() + /// + public void Activate(CarouselItem item) { - if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) - { - CurrentSelection = currentKeyboardSelection.Model; - return; - } + (GetMaterialisedDrawableForItem(item) as ICarouselPanel)?.Activated(); + HandleItemActivated(item); - if (currentSelection.CarouselItem != null) - { - (GetMaterialisedDrawableForItem(currentSelection.CarouselItem) as ICarouselPanel)?.Activated(); - HandleItemActivated(currentSelection.CarouselItem); - } + selectionValid.Invalidate(); } #endregion @@ -176,30 +189,28 @@ namespace osu.Game.Screens.SelectV2 protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true; /// - /// Called when an item is "selected". + /// Called after an item becomes the . + /// Should be used to handle any group expansion, item visibility changes, etc. /// - /// Whether the item should be selected. - protected virtual bool HandleItemSelected(object? model) => true; + protected virtual void HandleItemSelected(object? model) { } /// - /// Called when an item is "deselected". + /// Called when the changes to a new selection. + /// Should be used to handle any group expansion, item visibility changes, etc. /// - protected virtual void HandleItemDeselected(object? model) - { - } + protected virtual void HandleItemDeselected(object? model) { } /// - /// Called when an item is "activated". + /// Called when an item is activated via user input (keyboard traversal or a mouse click). /// /// - /// An activated item should for instance: - /// - Open or close a folder - /// - Start gameplay on a beatmap difficulty. + /// An activated item should decide to perform an action, such as: + /// - Change its expanded state (and show / hide children items). + /// - Set the item to the . + /// - Start gameplay on a beatmap difficulty if already selected. /// /// The carousel item which was activated. - protected virtual void HandleItemActivated(CarouselItem item) - { - } + protected virtual void HandleItemActivated(CarouselItem item) { } #endregion @@ -305,7 +316,8 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.Select: - TryActivateSelection(); + if (currentKeyboardSelection.CarouselItem != null) + Activate(currentKeyboardSelection.CarouselItem); return true; case GlobalAction.SelectNext: @@ -374,13 +386,10 @@ namespace osu.Game.Screens.SelectV2 // If the user has a different keyboard selection and requests // group selection, first transfer the keyboard selection to actual selection. - if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) + if (currentKeyboardSelection.CarouselItem != null && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - TryActivateSelection(); - - // Is the selection actually changed, then we should not perform any further traversal. - if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) - return; + Activate(currentKeyboardSelection.CarouselItem); + return; } int originalIndex; @@ -413,7 +422,7 @@ namespace osu.Game.Screens.SelectV2 if (CheckValidForGroupSelection(newItem)) { - setSelection(newItem.Model); + HandleItemActivated(newItem); return; } } while (newIndex != originalIndex); @@ -428,23 +437,6 @@ namespace osu.Game.Screens.SelectV2 private Selection currentKeyboardSelection = new Selection(); private Selection currentSelection = new Selection(); - private void setSelection(object? model) - { - if (currentSelection.Model == model) - return; - - if (HandleItemSelected(model)) - { - if (currentSelection.Model != null) - HandleItemDeselected(currentSelection.Model); - - currentKeyboardSelection = new Selection(model); - currentSelection = currentKeyboardSelection; - } - - selectionValid.Invalidate(); - } - private void setKeyboardSelection(object? model) { currentKeyboardSelection = new Selection(model); diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 7ed256ca6a..e10521f63e 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.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.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -96,7 +95,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + carousel.Activate(Item!); return true; } @@ -111,8 +110,6 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - // sets should never be activated. - throw new InvalidOperationException(); } #endregion From bff686f01289f68ec8b12de2bb62107ddb49d76a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 17:09:58 +0900 Subject: [PATCH 520/620] Avoid double iteration when updating group states --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 47 +++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 6032989ad0..4126889892 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -173,22 +173,45 @@ namespace osu.Game.Screens.SelectV2 { if (grouping.GroupItems.TryGetValue(group, out var items)) { - // First pass ignoring set groupings. - foreach (var i in items) - { - if (i.Model is GroupDefinition) - i.IsExpanded = expanded; - else - i.IsVisible = expanded; - } - - // Second pass to hide set children when not meant to be displayed. if (expanded) { foreach (var i in items) { - if (i.Model is BeatmapSetInfo set) - setExpansionStateOfSetItems(set, i.IsExpanded); + switch (i.Model) + { + case GroupDefinition: + i.IsExpanded = true; + break; + + case BeatmapSetInfo set: + // Case where there are set headers, header should be visible + // and items should use the set's expanded state. + i.IsVisible = true; + setExpansionStateOfSetItems(set, i.IsExpanded); + break; + + default: + // Case where there are no set headers, all items should be visible. + if (!grouping.BeatmapSetsGroupedTogether) + i.IsVisible = true; + break; + } + } + } + else + { + foreach (var i in items) + { + switch (i.Model) + { + case GroupDefinition: + i.IsExpanded = false; + break; + + default: + i.IsVisible = false; + break; + } } } } From cb42ef95c57cf6f86c66dd882962b1532401d823 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 17:48:42 +0900 Subject: [PATCH 521/620] Add invalidation on draw size change in beatmap carousel v2 Matching old implementation. --- osu.Game/Screens/SelectV2/Carousel.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 1fd2f0a9b0..4248641a43 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Graphics.Containers; @@ -678,6 +679,15 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.Expanded.Value = false; } + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + // handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed). + if (invalidation.HasFlag(Invalidation.DrawSize)) + selectionValid.Invalidate(); + + return base.OnInvalidate(invalidation, source); + } + #endregion #region Internal helper classes From b7483b9442596fa367105f62effe81addb8bd8ec Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 07:25:45 -0700 Subject: [PATCH 522/620] Add playlist collection button w/ tests --- .../TestSceneAddPlaylistToCollectionButton.cs | 94 +++++++++++++++++++ .../AddPlaylistToCollectionButton.cs | 78 +++++++++++++++ .../Playlists/PlaylistsRoomSubScreen.cs | 10 ++ 3 files changed, 182 insertions(+) create mode 100644 osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs new file mode 100644 index 0000000000..acf2c4b3f9 --- /dev/null +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Playlists; +using osuTK; + +namespace osu.Game.Tests.Visual.Playlists +{ + public partial class TestSceneAddPlaylistToCollectionButton : OsuTestScene + { + private BeatmapManager manager = null!; + private BeatmapSetInfo importedBeatmap = null!; + private Room room = null!; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + } + + [Cached(typeof(INotificationOverlay))] + private NotificationOverlay notificationOverlay = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; + + [SetUpSteps] + public void SetUpSteps() + { + importBeatmap(); + + setupRoom(); + + AddStep("create button", () => + { + AddRange(new Drawable[] + { + notificationOverlay, + new AddPlaylistToCollectionButton(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 40), + } + }); + }); + } + + private void importBeatmap() => AddStep("import beatmap", () => + { + var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); + + Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); + + importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach(); + }); + + private void setupRoom() => AddStep("setup room", () => + { + room = new Room + { + Name = "my awesome room", + MaxAttempts = 5, + Host = API.LocalUser.Value + }; + room.RecentParticipants = [room.Host]; + room.EndDate = DateTimeOffset.Now.AddMinutes(5); + room.Playlist = + [ + new PlaylistItem(importedBeatmap.Beatmaps.First()) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs new file mode 100644 index 0000000000..643e274335 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.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 System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Game.Collections; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class AddPlaylistToCollectionButton : RoundedButton + { + private readonly Room room; + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved(canBeNull: true)] + private INotificationOverlay? notifications { get; set; } + + public AddPlaylistToCollectionButton(Room room) + { + this.room = room; + Text = "Add Maps to Collection"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray5; + + Action = () => + { + int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); + + if (ids.Length == 0) + { + notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); + return; + } + + beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => + { + var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); + + var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + + if (collection == null) + { + collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); + realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); + notifications?.Post(new SimpleNotification { Text = $"Created new playlist: {room.Name}" }); + } + else + { + collection.ToLive(realmAccess).PerformWrite(c => + { + beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); + foreach (var item in beatmaps) + c.BeatmapMD5Hashes.Add(item!.MD5Hash); + notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); + }); + } + }), TaskContinuationOptions.OnlyOnRanToCompletion); + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9b4630ac0b..afab8a9721 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -153,11 +153,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, + new Drawable[] + { + new AddPlaylistToCollectionButton(Room) + { + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 40) + } + } }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(), + new Dimension(GridSizeMode.AutoSize), } }, // Spacer From 6769a74c92937eead5628a4a3b0080059c2d2e85 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 17:23:06 -0700 Subject: [PATCH 523/620] Add loading in case cache lookup takes longer than expected --- .../Playlists/AddPlaylistToCollectionButton.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 643e274335..d28776cac2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -19,6 +20,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { private readonly Room room; + private LoadingLayer loading = null!; + [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -39,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { BackgroundColour = colours.Gray5; + Add(loading = new LoadingLayer(true, false)); + Action = () => { int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); @@ -49,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } + Enabled.Value = false; + loading.Show(); beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => { var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); @@ -71,6 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); }); } + + loading.Hide(); + Enabled.Value = true; }), TaskContinuationOptions.OnlyOnRanToCompletion); }; } From 2aa930a36c87d579c1cde09a11a56342f8ca960f Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 17:46:49 -0700 Subject: [PATCH 524/620] Corrected notification strings --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index d28776cac2..ab3e481f9f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new playlist: {room.Name}" }); + notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); } else { @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); foreach (var item in beatmaps) c.BeatmapMD5Hashes.Add(item!.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); }); } From 25846b232748ae71b288fec35a43534512bdf5ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 14:21:43 +0900 Subject: [PATCH 525/620] Adjust results screen designs and tests slightly --- .../Visual/Ranking/TestSceneResultsScreen.cs | 16 ++++++++++------ .../Contracted/ContractedPanelMiddleContent.cs | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index fca1d0f82a..3a08756090 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -62,12 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking if (beatmapInfo != null) Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); }); - - AddToggleStep("toggle legacy classic skin", v => - { - if (skins != null) - skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default; - }); } [SetUp] @@ -84,6 +78,16 @@ namespace osu.Game.Tests.Visual.Ranking })); } + [Test] + public void TestLegacySkin() + { + AddToggleStep("toggle legacy classic skin", v => + { + if (skins != null) + skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default; + }); + } + private int onlineScoreID = 1; [TestCase(1, ScoreRank.X, 0)] diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index cfb6465e62..2f863a95ec 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Ranking.Contracted Colour = Color4.Black.Opacity(0.25f), Type = EdgeEffectType.Shadow, Radius = 1, - Offset = new Vector2(0, 4) + Offset = new Vector2(0, 2) }, Children = new Drawable[] { @@ -100,10 +100,10 @@ namespace osu.Game.Screens.Ranking.Contracted CornerRadius = 20, EdgeEffect = new EdgeEffectParameters { - Colour = Color4.Black.Opacity(0.25f), + Colour = Color4.Black.Opacity(0.15f), Type = EdgeEffectType.Shadow, Radius = 8, - Offset = new Vector2(0, 4), + Offset = new Vector2(0, 1), } }, new OsuSpriteText From 975c35f5ac48735367ba792784d5572b69fe9a4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 14:27:37 +0900 Subject: [PATCH 526/620] Also add difficulty icon to contracted panel --- .../ContractedPanelMiddleContent.cs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 2f863a95ec..e9d0bf3403 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -11,13 +11,15 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; using osu.Game.Utils; @@ -134,14 +136,33 @@ namespace osu.Game.Screens.Ranking.Contracted createStatistic(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, $"{score.Accuracy.FormatAccuracy()}"), } }, - new ModFlowDisplay + new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Current = { Value = score.Mods }, - IconScale = 0.5f, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(3), + ChildrenEnumerable = + [ + new DifficultyIcon(score.BeatmapInfo!, score.Ruleset) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + Margin = new MarginPadding { Right = 2 } + }, + .. + score.Mods.AsOrdered().Select(m => new ModIcon(m) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Scale = new Vector2(0.3f), + Margin = new MarginPadding { Top = -6 } + }) + ] } } } From d73f275143a7c36a8b629f15ea061678cba5ba1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:15:58 +0900 Subject: [PATCH 527/620] Don't inflate set / group panels for simplicity --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 7 +++++-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 10 ---------- osu.Game/Screens/SelectV2/GroupPanel.cs | 10 ---------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index b690e35a48..ddf2fdcb57 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -28,8 +28,11 @@ namespace osu.Game.Screens.SelectV2 { var inputRectangle = DrawRectangle; - // Cover the gaps introduced by the spacing between BeatmapPanels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. + // + // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly + // larger hit target. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index d869e0af75..f6c9324077 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -26,16 +26,6 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText text = null!; private Box box = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index c5e5c7745f..e10521f63e 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -27,16 +27,6 @@ namespace osu.Game.Screens.SelectV2 private Box box = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; - - // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { From d505c529cd217abfbf697a5e9f9f8c1ebb2da14c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:06:21 +0900 Subject: [PATCH 528/620] Adjust tests in line with new expectations --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 28 ++++++++ ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 64 ++++--------------- .../TestSceneBeatmapCarouselV2NoGrouping.cs | 54 +++++----------- 3 files changed, 58 insertions(+), 88 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..be0d0bf79a 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -20,6 +21,7 @@ using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; +using osuTK; using osuTK.Graphics; using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; @@ -164,6 +166,15 @@ namespace osu.Game.Tests.Visual.SongSelect }); } + protected IEnumerable GetVisiblePanels() + where T : Drawable + { + return Carousel.ChildrenOfType().Single() + .ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .OrderBy(p => p.Y); + } + protected void ClickVisiblePanel(int index) where T : Drawable { @@ -178,6 +189,23 @@ namespace osu.Game.Tests.Visual.SongSelect }); } + protected void ClickVisiblePanelWithOffset(int index, Vector2 positionOffsetFromCentre) + where T : Drawable + { + AddStep($"move mouse to panel {index} with offset {positionOffsetFromCentre}", () => + { + var panel = Carousel.ChildrenOfType().Single() + .ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .OrderBy(p => p.Y) + .ElementAt(index); + + InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre + panel.ToScreenSpace(positionOffsetFromCentre) - panel.ToScreenSpace(Vector2.Zero)); + }); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + } + /// /// Add requested beatmap sets count to list. /// diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index 83e0e77fa6..f631dfc562 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.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.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -10,7 +9,6 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { @@ -153,60 +151,24 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestInputHandlingWithinGaps() { - AddBeatmaps(5, 2); - WaitForDrawablePanels(); - SelectNextGroup(); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForGroupSelection(0, 1); + // Clicks just above the first group panel should not actuate any action. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1))); - clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + + ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2))); + + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + CheckNoSelection(); + + // Beatmap panels expand their selection area to cover holes from spacing. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 0); - SelectNextPanel(); - Select(); + ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); - - clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - AddAssert("group 0 collapsed", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False); - clickOnGroup(0, p => p.LayoutRectangle.Centre); - AddAssert("group 0 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True); - - AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - WaitForGroupSelection(0, 4); - - clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - AddAssert("group 1 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True); - } - - private void clickOnGroup(int group, Func pos) - { - AddStep($"click on group{group}", () => - { - var groupingFilter = Carousel.Filters.OfType().Single(); - var model = groupingFilter.GroupItems.Keys.ElementAt(group); - - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } - - private void clickOnPanel(int group, int panel, Func pos) - { - AddStep($"click on group{group} panel{panel}", () => - { - var groupingFilter = Carousel.Filters.OfType().Single(); - - var g = groupingFilter.GroupItems.Keys.ElementAt(group); - // offset by one because the group itself is included in the items list. - object model = groupingFilter.GroupItems[g].ElementAt(panel + 1).Model; - - var p = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(p.ToScreenSpace(pos(p))); - InputManager.Click(MouseButton.Left); - }); } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 566c2f1798..1359b5c58e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.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.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -209,31 +208,34 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [Solo] public void TestInputHandlingWithinGaps() { AddBeatmaps(2, 5); WaitForDrawablePanels(); - SelectNextGroup(); - clickOnDifficulty(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForSelection(0, 1); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - clickOnDifficulty(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + // Clicks just above the first group panel should not actuate any action. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1))); + + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + + ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2))); + + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); WaitForSelection(0, 0); - SelectNextPanel(); - Select(); - WaitForSelection(0, 1); - - clickOnSet(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + // Beatmap panels expand their selection area to cover holes from spacing. + ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 0); - AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnDifficulty(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - WaitForSelection(0, 4); + // Panels with higher depth will handle clicks in the gutters for simplicity. + ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForSelection(0, 2); - clickOnSet(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForSelection(1, 0); + ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForSelection(0, 3); } private void checkSelectionIterating(bool isIterating) @@ -249,27 +251,5 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); } } - - private void clickOnSet(int set, Func pos) - { - AddStep($"click on set{set}", () => - { - var model = BeatmapSets[set]; - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } - - private void clickOnDifficulty(int set, int diff, Func pos) - { - AddStep($"click on set{set} diff{diff}", () => - { - var model = BeatmapSets[set].Beatmaps[diff]; - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } } } From aa329f397e684f41e5ca040d4d33a13026d464a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:30:31 +0900 Subject: [PATCH 529/620] Remove stray `[Solo]`s --- osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs | 1 - .../Visual/Navigation/TestSceneBeatmapEditorNavigation.cs | 1 - .../Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs | 1 - osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 1 - 4 files changed, 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 966e6513bb..4953cf83c9 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - [Solo] public void TestCommitPlacementViaRightClick() { Playfield playfield = null!; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index d76e0290ef..ee5b1797ed 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -165,7 +165,6 @@ namespace osu.Game.Tests.Visual.Navigation } [Test] - [Solo] public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() { prepareBeatmap(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 1359b5c58e..09ded342c3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -208,7 +208,6 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - [Solo] public void TestInputHandlingWithinGaps() { AddBeatmaps(2, 5); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index c415fc876f..d8ab367ebd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1239,7 +1239,6 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - [Solo] public void TestHardDeleteHandledCorrectly() { createSongSelect(); From 4d1167fdccbfee3d0ecf425a969925c9baf5b222 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:36:59 +0900 Subject: [PATCH 530/620] Don't attempt to submit zero scores --- osu.Game/Screens/Play/SubmittingPlayer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 0a230ea00b..b667963a70 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -284,6 +284,13 @@ namespace osu.Game.Screens.Play return Task.CompletedTask; } + // zero scores should also never be submitted. + if (score.ScoreInfo.TotalScore == 0) + { + Logger.Log("Zero score, skipping score submission"); + return Task.CompletedTask; + } + // mind the timing of this. // once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background, // so all exceptional circumstances that would disallow submission must be handled above. From 75ef6f6a0e02e1bf4b898186141376d2ccf7b80a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:10:08 +0900 Subject: [PATCH 531/620] Use random generation in carousel stress test --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 45 ++++++++++--------- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 2 +- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..db433b93d2 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -187,29 +187,32 @@ namespace osu.Game.Tests.Visual.SongSelect protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () => { for (int i = 0; i < count; i++) - { - var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); - - if (randomMetadata) - { - char randomCharacter = getRandomCharacter(); - - var metadata = new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), - Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", - Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, - }; - - foreach (var beatmap in beatmapSetInfo.Beatmaps) - beatmap.Metadata = metadata.DeepClone(); - } - - BeatmapSets.Add(beatmapSetInfo); - } + BeatmapSets.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata)); }); + protected static BeatmapSetInfo CreateTestBeatmapSetInfo(int? fixedDifficultiesPerSet, bool randomMetadata) + { + var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); + + if (randomMetadata) + { + char randomCharacter = getRandomCharacter(); + + var metadata = new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), + Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, + }; + + foreach (var beatmap in beatmapSetInfo.Beatmaps) + beatmap.Metadata = metadata.DeepClone(); + } + + return beatmapSetInfo; + } + private static long randomCharPointer; private static char getRandomCharacter() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 3c5cf16e92..30ca26ce68 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.SongSelect Task.Run(() => { for (int j = 0; j < count; j++) - generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + generated.Add(CreateTestBeatmapSetInfo(3, true)); }).ConfigureAwait(true); }); From 50d880e2ae3e3abfd58a795d26461ff39aa82070 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:10:38 +0900 Subject: [PATCH 532/620] Fix unnecessary `BeatmapSet.Metadata` lookups --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index 0298616aa8..3cdbbb4fed 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 switch (criteria.Sort) { case SortMode.Artist: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.Metadata.Artist, bb.Metadata.Artist); if (comparison == 0) goto case SortMode.Title; break; @@ -46,7 +46,7 @@ namespace osu.Game.Screens.SelectV2 break; case SortMode.Title: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.Metadata.Title, bb.Metadata.Title); break; default: From a49b1b61b4dfe7ff394f8f68c70d0e61bbc657d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 08:21:34 +0100 Subject: [PATCH 533/620] Add test coverage for scores with zero total not submitting --- .../TestScenePlayerScoreSubmission.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index c382f0828b..381f49d9eb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -16,6 +16,7 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -234,6 +235,31 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } + [Test] + public void TestNoSubmissionWhenScoreZero() + { + prepareTestAPI(true); + + createPlayerTest(); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + AddUntilStep("wait for first result", () => Player.Results.Count > 0); + + AddStep("add fake non-scoring hit", () => + { + Player.ScoreProcessor.RevertResult(Player.Results.First()); + Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new IgnoreJudgement()) + { + Type = HitResult.IgnoreHit, + }); + }); + + AddStep("exit", () => Player.Exit()); + AddAssert("ensure no submission", () => Player.SubmittedScore == null); + } + [Test] public void TestSubmissionOnExit() { From 9d979dc3f4adb523269fb14f6d049986dab9d61b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:37:16 +0900 Subject: [PATCH 534/620] Refactor grouping to be much more efficient --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 207 ++++++++---------- 2 files changed, 93 insertions(+), 116 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4126889892..137a8e8eab 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -289,5 +289,5 @@ namespace osu.Game.Screens.SelectV2 #endregion } - public record GroupDefinition(string Title); + public record GroupDefinition(object Data, string Title); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index a8caebad7a..8838ce67ad 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; @@ -36,137 +35,115 @@ namespace osu.Game.Screens.SelectV2 this.getCriteria = getCriteria; } - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) { - setItems.Clear(); - groupItems.Clear(); + return await Task.Run(() => + { + setItems.Clear(); + groupItems.Clear(); - var criteria = getCriteria(); - var newItems = new List(items.Count()); + var criteria = getCriteria(); + var newItems = new List(); - // Add criteria groups. + BeatmapInfo? lastBeatmap = null; + GroupDefinition? lastGroup = null; + + HashSet? groupRefItems = null; + HashSet? setRefItems = null; + + switch (criteria.Group) + { + default: + BeatmapSetsGroupedTogether = true; + break; + + case GroupMode.Difficulty: + BeatmapSetsGroupedTogether = false; + break; + } + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var beatmap = (BeatmapInfo)item.Model; + + if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup) + { + // When reaching a new group, ensure we reset any beatmap set tracking. + setRefItems = null; + lastBeatmap = null; + + groupItems[newGroup] = groupRefItems = new HashSet(); + lastGroup = newGroup; + + addItem(new CarouselItem(newGroup) + { + DrawHeight = GroupPanel.HEIGHT, + DepthLayer = -2, + }); + } + + if (BeatmapSetsGroupedTogether) + { + bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; + + if (newBeatmapSet) + { + setItems[beatmap.BeatmapSet!] = setRefItems = new HashSet(); + + addItem(new CarouselItem(beatmap.BeatmapSet!) + { + DrawHeight = BeatmapSetPanel.HEIGHT, + DepthLayer = -1 + }); + } + } + + addItem(item); + lastBeatmap = beatmap; + + void addItem(CarouselItem i) + { + newItems.Add(i); + + groupRefItems?.Add(i); + setRefItems?.Add(i); + + i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || setRefItems == null)); + } + } + + return newItems; + }, cancellationToken).ConfigureAwait(false); + } + + private GroupDefinition? createGroupIfRequired(FilterCriteria criteria, BeatmapInfo beatmap, GroupDefinition? lastGroup) + { switch (criteria.Group) { - default: - BeatmapSetsGroupedTogether = true; - newItems.AddRange(items); - break; - case GroupMode.Artist: - BeatmapSetsGroupedTogether = true; - char groupChar = (char)0; + char groupChar = lastGroup?.Data as char? ?? (char)0; + char beatmapFirstChar = char.ToUpperInvariant(beatmap.Metadata.Artist[0]); - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - var b = (BeatmapInfo)item.Model; - - char beatmapFirstChar = char.ToUpperInvariant(b.Metadata.Artist[0]); - - if (beatmapFirstChar > groupChar) - { - groupChar = beatmapFirstChar; - var groupDefinition = new GroupDefinition($"{groupChar}"); - var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; - - newItems.Add(groupItem); - groupItems[groupDefinition] = new HashSet { groupItem }; - } - - newItems.Add(item); - } + if (beatmapFirstChar > groupChar) + return new GroupDefinition(beatmapFirstChar, $"{beatmapFirstChar}"); break; case GroupMode.Difficulty: - BeatmapSetsGroupedTogether = false; - int starGroup = int.MinValue; + int starGroup = lastGroup?.Data as int? ?? -1; - foreach (var item in items) + if (beatmap.StarRating > starGroup) { - cancellationToken.ThrowIfCancellationRequested(); - - var b = (BeatmapInfo)item.Model; - - if (b.StarRating > starGroup) - { - starGroup = (int)Math.Floor(b.StarRating); - var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); - - var groupItem = new CarouselItem(groupDefinition) - { - DrawHeight = GroupPanel.HEIGHT, - DepthLayer = -2 - }; - - newItems.Add(groupItem); - groupItems[groupDefinition] = new HashSet { groupItem }; - } - - newItems.Add(item); + starGroup = (int)Math.Floor(beatmap.StarRating); + return new GroupDefinition(starGroup + 1, $"{starGroup} - {starGroup + 1} *"); } break; } - // Add set headers wherever required. - CarouselItem? lastItem = null; - - if (BeatmapSetsGroupedTogether) - { - for (int i = 0; i < newItems.Count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var item = newItems[i]; - - if (item.Model is BeatmapInfo beatmap) - { - bool newBeatmapSet = lastItem?.Model is not BeatmapInfo lastBeatmap || lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; - - if (newBeatmapSet) - { - var setItem = new CarouselItem(beatmap.BeatmapSet!) - { - DrawHeight = BeatmapSetPanel.HEIGHT, - DepthLayer = -1 - }; - - setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; - newItems.Insert(i, setItem); - i++; - } - - setItems[beatmap.BeatmapSet!].Add(item); - item.IsVisible = false; - } - - lastItem = item; - } - } - - // Link group items to their headers. - GroupDefinition? lastGroup = null; - - foreach (var item in newItems) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.Model is GroupDefinition group) - { - lastGroup = group; - continue; - } - - if (lastGroup != null) - { - groupItems[lastGroup].Add(item); - item.IsVisible = false; - } - } - - return newItems; - }, cancellationToken).ConfigureAwait(false); + return null; + } } } From c935c3154b33739020c42b597fbd83480e6cd0e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 16:54:30 +0900 Subject: [PATCH 535/620] Always transfer keyboard selection on activation --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 4 ++-- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 22 ++++++++++++++++++- osu.Game/Screens/SelectV2/Carousel.cs | 5 +++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..cfef2882be 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -136,8 +136,8 @@ namespace osu.Game.Tests.Visual.SongSelect protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); - protected BeatmapPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); - protected GroupPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); protected void WaitForGroupSelection(int group, int panel) { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index 151f1f5fec..f46e79caf7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestGroupSelectionOnHeader() + public void TestGroupSelectionOnHeaderKeyboard() { SelectNextGroup(); WaitForGroupSelection(0, 0); @@ -113,6 +113,26 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } + [Test] + public void TestGroupSelectionOnHeaderMouse() + { + SelectNextGroup(); + WaitForGroupSelection(0, 0); + + AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); + + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + + AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); + } + [Test] public void TestKeyboardSelection() { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 5220781ce8..89dec6a7ae 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -124,6 +124,11 @@ namespace osu.Game.Screens.SelectV2 /// public void Activate(CarouselItem item) { + // Regardless of how the item handles activation, update keyboard selection to the activated panel. + // In other words, when a panel is clicked, keyboard selection should default to matching the clicked + // item. + setKeyboardSelection(item.Model); + (GetMaterialisedDrawableForItem(item) as ICarouselPanel)?.Activated(); HandleItemActivated(item); From cef9d2eac50ac11bdbc964fcb243d1225c06dae3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 16:55:02 +0900 Subject: [PATCH 536/620] Reduce number of beatmaps added in selection test This is because with the new keyboard selection logic, adding too many can cause the re-added selection to be off-screen in the headless test setup. --- .../SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index f46e79caf7..33d9d3a363 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); - AddBeatmaps(10); + AddBeatmaps(5); WaitForDrawablePanels(); CheckHasSelection(); From 41c8f648063d2cef2ba36021095cb7ca4e5fd0c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:33:32 +0900 Subject: [PATCH 537/620] Simplify naming of endpoints --- osu.Desktop/DiscordRichPresence.cs | 2 +- .../Visual/Menus/TestSceneMainMenu.cs | 2 +- .../Online/TestSceneWikiMarkdownContainer.cs | 10 ++--- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs | 4 +- osu.Game/Online/API/APIAccess.cs | 12 +++--- osu.Game/Online/API/APIRequest.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 6 +-- osu.Game/Online/API/IAPIProvider.cs | 2 +- .../Requests/PatchBeatmapPackageRequest.cs | 4 +- .../API/Requests/PutBeatmapSetRequest.cs | 4 +- .../Requests/ReplaceBeatmapPackageRequest.cs | 4 +- osu.Game/Online/Chat/ExternalLinkOpener.cs | 4 +- osu.Game/Online/Chat/NowPlayingCommand.cs | 2 +- .../DevelopmentEndpointConfiguration.cs | 8 ++-- osu.Game/Online/EndpointConfiguration.cs | 38 +++++++++---------- .../Online/Leaderboards/LeaderboardScore.cs | 2 +- .../Online/Metadata/OnlineMetadataClient.cs | 2 +- .../Multiplayer/OnlineMultiplayerClient.cs | 2 +- .../Online/ProductionEndpointConfiguration.cs | 8 ++-- .../Online/Spectator/OnlineSpectatorClient.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- .../Overlays/Login/SecondFactorAuthForm.cs | 2 +- .../Profile/Header/BottomHeaderContainer.cs | 4 +- .../Components/DrawableTournamentBanner.cs | 2 +- .../Profile/Header/TopHeaderContainer.cs | 2 +- .../Sections/Recent/DrawableRecentActivity.cs | 2 +- osu.Game/Overlays/Wiki/WikiPanelContainer.cs | 2 +- osu.Game/Overlays/WikiOverlay.cs | 4 +- .../ScreenFrequentlyAskedQuestions.cs | 4 +- .../Lounge/Components/DrawableRoom.cs | 2 +- .../Leaderboards/LeaderboardScoreV2.cs | 2 +- osu.Game/Utils/SentryLogger.cs | 2 +- 35 files changed, 78 insertions(+), 78 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index cf56fe6115..668f63b910 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -173,7 +173,7 @@ namespace osu.Desktop new Button { Label = "View beatmap", - Url = $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" + Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" } }; } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index e2d5bc2917..cd391519f4 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = $@"{API.EndpointConfiguration.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.Endpoints.WebsiteUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index cee3f37aea..e453a32652 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLink() { - AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/"); + AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/"); AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Main_page"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Main_page"); AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/FAQ"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/FAQ"); AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Writing"); AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Formatting"); } [Test] diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index d0625c64e3..16b4b04ce4 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null) return null; - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs index ac191d36a9..1af0e7a9ee 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs @@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps return null; if (ruleset != null) - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ef7b49868c..88f9b3f242 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online.API private readonly Queue queue = new Queue(); - public EndpointConfiguration EndpointConfiguration { get; } + public EndpointConfiguration Endpoints { get; } /// /// The API response version. @@ -73,7 +73,7 @@ namespace osu.Game.Online.API private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; - public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash) + public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpoints, string versionHash) { this.game = game; this.config = config; @@ -87,13 +87,13 @@ namespace osu.Game.Online.API APIVersion = now.Year * 10000 + now.Month * 100 + now.Day; } - EndpointConfiguration = endpointConfiguration; + Endpoints = endpoints; NotificationsClient = setUpNotificationsClient(); - authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, EndpointConfiguration.APIEndpointUrl); + authentication = new OAuth(endpoints.APIClientID, endpoints.APIClientSecret, Endpoints.APIUrl); log = Logger.GetLogger(LoggingTarget.Network); - log.Add($@"API endpoint root: {EndpointConfiguration.APIEndpointUrl}"); + log.Add($@"API endpoint root: {Endpoints.APIUrl}"); log.Add($@"API request version: {APIVersion}"); ProvidedUsername = config.Get(OsuSetting.Username); @@ -405,7 +405,7 @@ namespace osu.Game.Online.API var req = new RegistrationRequest { - Url = $@"{EndpointConfiguration.APIEndpointUrl}/users", + Url = $@"{Endpoints.APIUrl}/users", Method = HttpMethod.Post, Username = username, Email = email, diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 575e6f8a10..9d9873cc6f 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API!.EndpointConfiguration.APIEndpointUrl}/api/v2/{Target}"; + protected virtual string Uri => $@"{API!.Endpoints.APIUrl}/api/v2/{Target}"; protected IAPIProvider? API; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 7b3a8f357b..f9649cdd88 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -41,10 +41,10 @@ namespace osu.Game.Online.API public string ProvidedUsername => LocalUser.Value.Username; - public EndpointConfiguration EndpointConfiguration { get; } = new EndpointConfiguration + public EndpointConfiguration Endpoints { get; } = new EndpointConfiguration { - APIEndpointUrl = "http://localhost", - WebsiteRootUrl = "http://localhost", + APIUrl = "http://localhost", + WebsiteUrl = "http://localhost", }; public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 048193def7..54eaaaafc2 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online.API /// /// Holds configuration for online endpoints. /// - EndpointConfiguration EndpointConfiguration { get; } + EndpointConfiguration Endpoints { get; } /// /// The version of the API. diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index bb9d32f77b..ffe7b5d1ec 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -15,10 +15,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; } } diff --git a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs index 03b8397681..fb25749786 100644 --- a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs @@ -21,10 +21,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets"; } } diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs index c9dd12d61e..2e224ce602 100644 --- a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -14,10 +14,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; } } diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 1615b72033..258cca2ad5 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat if (url.StartsWith('/')) { - url = $"{api.EndpointConfiguration.WebsiteRootUrl}{url}"; + url = $"{api.Endpoints.WebsiteUrl}{url}"; isTrustedDomain = true; } else { - isTrustedDomain = url.StartsWith(api.EndpointConfiguration.WebsiteRootUrl, StringComparison.Ordinal); + isTrustedDomain = url.StartsWith(api.Endpoints.WebsiteUrl, StringComparison.Ordinal); } if (!url.CheckIsValidUrl()) diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 5e71980a55..43452a768c 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat string getBeatmapPart() { - return beatmapOnlineID > 0 ? $"[{api.EndpointConfiguration.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; + return beatmapOnlineID > 0 ? $"[{api.Endpoints.WebsiteUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; } string getRulesetPart() diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 5f3c353f4d..f4e1b257ee 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -7,12 +7,12 @@ namespace osu.Game.Online { public DevelopmentEndpointConfiguration() { - WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh"; + WebsiteUrl = APIUrl = @"https://dev.ppy.sh"; APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; APIClientID = "5"; - SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator"; - MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer"; - MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata"; + SpectatorUrl = $@"{APIUrl}/signalr/spectator"; + MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; + MetadataUrl = $@"{APIUrl}/signalr/metadata"; } } } diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index 39dd72d41a..2d5ea32345 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -8,16 +8,6 @@ namespace osu.Game.Online /// public class EndpointConfiguration { - /// - /// The base URL for the website. Does not include a trailing slash. - /// - public string WebsiteRootUrl { get; set; } = string.Empty; - - /// - /// The endpoint for the main (osu-web) API. Does not include a trailing slash. - /// - public string APIEndpointUrl { get; set; } = string.Empty; - /// /// The OAuth client secret. /// @@ -29,23 +19,33 @@ namespace osu.Game.Online public string APIClientID { get; set; } = string.Empty; /// - /// The endpoint for the SignalR spectator server. + /// The base URL for the website. Does not include a trailing slash. /// - public string SpectatorEndpointUrl { get; set; } = string.Empty; + public string WebsiteUrl { get; set; } = string.Empty; /// - /// The endpoint for the SignalR multiplayer server. + /// The endpoint for the main (osu-web) API. Does not include a trailing slash. /// - public string MultiplayerEndpointUrl { get; set; } = string.Empty; - - /// - /// The endpoint for the SignalR metadata server. - /// - public string MetadataEndpointUrl { get; set; } = string.Empty; + public string APIUrl { get; set; } = string.Empty; /// /// The root URL for the service handling beatmap submission. Does not include a trailing slash. /// public string? BeatmapSubmissionServiceUrl { get; set; } + + /// + /// The endpoint for the SignalR spectator server. + /// + public string SpectatorUrl { get; set; } = string.Empty; + + /// + /// The endpoint for the SignalR multiplayer server. + /// + public string MultiplayerUrl { get; set; } = string.Empty; + + /// + /// The endpoint for the SignalR metadata server. + /// + public string MetadataUrl { get; set; } = string.Empty; } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index f7efa08969..52074119b8 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); if (Score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{Score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); if (Score.Files.Count > 0) { diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index c7c7dfc58b..6637fc8dba 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online.Metadata public OnlineMetadataClient(EndpointConfiguration endpoints) { - endpoint = endpoints.MetadataEndpointUrl; + endpoint = endpoints.MetadataUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 2660cd94e4..a485a6b262 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -32,7 +32,7 @@ namespace osu.Game.Online.Multiplayer public OnlineMultiplayerClient(EndpointConfiguration endpoints) { - endpoint = endpoints.MultiplayerEndpointUrl; + endpoint = endpoints.MultiplayerUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index 0244761b65..6e06abbeed 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -7,12 +7,12 @@ namespace osu.Game.Online { public ProductionEndpointConfiguration() { - WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh"; + WebsiteUrl = APIUrl = @"https://osu.ppy.sh"; APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; APIClientID = "5"; - SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; - MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; - MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; + SpectatorUrl = "https://spectator.ppy.sh/spectator"; + MultiplayerUrl = "https://spectator.ppy.sh/multiplayer"; + MetadataUrl = "https://spectator.ppy.sh/metadata"; } } } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 645d7054dc..29d174f8e3 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -24,7 +24,7 @@ namespace osu.Game.Online.Spectator public OnlineSpectatorClient(EndpointConfiguration endpoints) { - endpoint = endpoints.SpectatorEndpointUrl; + endpoint = endpoints.SpectatorUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5e247ca877..7d35207bbe 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -295,7 +295,7 @@ namespace osu.Game EndpointConfiguration endpoints = CreateEndpoints(); - MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl; + MessageFormatter.WebsiteRootUrl = endpoints.WebsiteUrl; frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale); frameworkLocale.BindValueChanged(_ => updateLanguage()); diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index b06be3e74a..0d566174bb 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - clipboard.SetText($@"{api.EndpointConfiguration.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 2b6d523b95..215a946b42 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login } }; - forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); + forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.Endpoints.WebsiteUrl}/home/password-reset"); password.OnCommit += (_, _) => performLogin(); diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index e36d62f827..74db58e225 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam); // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something). explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the "); - explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); + explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.Endpoints.WebsiteUrl}/home/password-reset"); explainText.AddText(". You can also "); explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () => { diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index d9d23f16fd..03c849052b 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/users/{user.Id}/posts", creationParameters: embolden); addSpacer(topLinkContainer); topLinkContainer.AddText("Posted "); - topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); + topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/comments?user_id={user.Id}", creationParameters: embolden); string websiteWithoutProtocol = user.Website; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index a66a5c8fe9..b036b0a305 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Texture = textures.Get(banner.Image), }; - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/tournaments/{banner.TournamentId}"); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index fb1bdca57c..ba2cd5b705 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header cover.User = user; avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; - openUserExternally.Link = $@"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user?.Id ?? 0}"; + openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index a0bcf2dc47..05762f29f9 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont()); - private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.EndpointConfiguration.WebsiteRootUrl}{url}").Argument.AsNonNull(); + private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.Endpoints.WebsiteUrl}{url}").Argument.AsNonNull(); private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index 773dde6436..81bdae5525 100644 --- a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs +++ b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki Padding = new MarginPadding(padding), Child = new WikiPanelMarkdownContainer(isFullWidth) { - CurrentPath = $@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", + CurrentPath = $@"{api.Endpoints.WebsiteUrl}/wiki/", Text = text, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index c360d1eb9e..e9099f1deb 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -167,7 +167,7 @@ namespace osu.Game.Overlays } else { - LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); + LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/{path.Value}/", response.Markdown)); } } @@ -176,7 +176,7 @@ namespace osu.Game.Overlays wikiData.Value = null; path.Value = "error"; - LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", + LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/", $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH}).")); } diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs index ff9cb07e2d..861c5051f4 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -46,14 +46,14 @@ namespace osu.Game.Screens.Edit.Submission RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, ButtonText = BeatmapSubmissionStrings.MappingHelpForum, - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/56"), + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/56"), }, new FormButton { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/60"), + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/60"), }, }, }); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7b2e2c02f7..de5813ce0d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return items.ToArray(); - string formatRoomUrl(long id) => $@"{api.EndpointConfiguration.WebsiteRootUrl}/multiplayer/rooms/{id}"; + string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}"; } } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 2460fbe6f8..a2253b413c 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -778,7 +778,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); if (score.Files.Count <= 0) return items.ToArray(); diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index ed644bf5cb..2172ea895e 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -41,7 +41,7 @@ namespace osu.Game.Utils { this.game = game; - if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) + if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) return; sentrySession = SentrySdk.Init(options => From 3da615481eb59a2aad22501e74121f2b0e06323e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:38:24 +0900 Subject: [PATCH 538/620] Change `switch` to simple conditional for now --- .../Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 8838ce67ad..db407fd647 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -51,16 +51,7 @@ namespace osu.Game.Screens.SelectV2 HashSet? groupRefItems = null; HashSet? setRefItems = null; - switch (criteria.Group) - { - default: - BeatmapSetsGroupedTogether = true; - break; - - case GroupMode.Difficulty: - BeatmapSetsGroupedTogether = false; - break; - } + BeatmapSetsGroupedTogether = criteria.Group != GroupMode.Difficulty; foreach (var item in items) { From 29b0b62ffa55ebb4ac4b107a691c95a3f72f516d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:39:38 +0900 Subject: [PATCH 539/620] Rename variables to something more sane --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index db407fd647..cb5a40918c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -48,8 +48,8 @@ namespace osu.Game.Screens.SelectV2 BeatmapInfo? lastBeatmap = null; GroupDefinition? lastGroup = null; - HashSet? groupRefItems = null; - HashSet? setRefItems = null; + HashSet? currentGroupItems = null; + HashSet? currentSetItems = null; BeatmapSetsGroupedTogether = criteria.Group != GroupMode.Difficulty; @@ -62,10 +62,10 @@ namespace osu.Game.Screens.SelectV2 if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup) { // When reaching a new group, ensure we reset any beatmap set tracking. - setRefItems = null; + currentSetItems = null; lastBeatmap = null; - groupItems[newGroup] = groupRefItems = new HashSet(); + groupItems[newGroup] = currentGroupItems = new HashSet(); lastGroup = newGroup; addItem(new CarouselItem(newGroup) @@ -81,7 +81,7 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - setItems[beatmap.BeatmapSet!] = setRefItems = new HashSet(); + setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); addItem(new CarouselItem(beatmap.BeatmapSet!) { @@ -98,10 +98,10 @@ namespace osu.Game.Screens.SelectV2 { newItems.Add(i); - groupRefItems?.Add(i); - setRefItems?.Add(i); + currentGroupItems?.Add(i); + currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || setRefItems == null)); + i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || currentSetItems == null)); } } From bf57fef4125bba86595850a6ec13f5f1fcb3f980 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:50:32 +0900 Subject: [PATCH 540/620] Fix missing cached settings in `BetamapSubmissionOverlay` test --- .../Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs index e3e8c0de39..f83d424d56 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -24,7 +24,11 @@ namespace osu.Game.Tests.Visual.Editing Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + CachedDependencies = new[] + { + (typeof(ScreenFooter), (object)footer), + (typeof(BeatmapSubmissionSettings), new BeatmapSubmissionSettings()), + }, Children = new Drawable[] { receptor, From 46290ae76b81d953253b670c752968906ced6e5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:05:47 +0900 Subject: [PATCH 541/620] Disallow changing beatmap / ruleset while submitting beatmap --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9794402061..4c7ea39c35 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -40,6 +40,8 @@ namespace osu.Game.Screens.Edit.Submission public override bool AllowUserExit => false; + public override bool DisallowExternalBeatmapRulesetChanges => true; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From 12881f3f366625ecdd861c66e24120541c428995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:06:31 +0900 Subject: [PATCH 542/620] Don't show informational screens for subsequent submissions These are historically only presented to the user when uploading a new beatmap for the first time. --- .../Edit/Submission/BeatmapSubmissionOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs index da2abd8c23..cf2fef25d5 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; @@ -15,10 +17,14 @@ namespace osu.Game.Screens.Edit.Submission } [BackgroundDependencyLoader] - private void load() + private void load(IBindable beatmap) { - AddStep(); - AddStep(); + if (beatmap.Value.BeatmapSetInfo.OnlineID <= 0) + { + AddStep(); + AddStep(); + } + AddStep(); Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle; From 95967a2fde5ae2015c206d35f3edc86eff318388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:17:49 +0900 Subject: [PATCH 543/620] Adjust beatmap stream creation to make a bit more sense --- .../Edit/Submission/BeatmapSubmissionScreen.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 4c7ea39c35..44b2778869 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,10 +76,10 @@ namespace osu.Game.Screens.Edit.Submission private RoundedButton backButton = null!; private uint? beatmapSetId; + private MemoryStream? beatmapPackageStream; private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; - private MemoryStream beatmapPackageStream = null!; private ProgressNotification? updateProgressNotification; [BackgroundDependencyLoader] @@ -189,7 +189,6 @@ namespace osu.Game.Screens.Edit.Submission } } }); - beatmapPackageStream = new MemoryStream(); } private void createBeatmapSet() @@ -239,10 +238,12 @@ namespace osu.Game.Screens.Edit.Submission private async Task createBeatmapPackage(ICollection onlineFiles) { Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); try { + beatmapPackageStream = new MemoryStream(); await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) .ConfigureAwait(true); } @@ -266,6 +267,7 @@ namespace osu.Game.Screens.Edit.Submission private async Task patchBeatmapSet(ICollection onlineFiles) { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); @@ -320,6 +322,7 @@ namespace osu.Game.Screens.Edit.Submission private void replaceBeatmapSet() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); @@ -347,6 +350,8 @@ namespace osu.Game.Screens.Edit.Submission private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); + updateStep.SetInProgress(); Live? importedSet; @@ -420,5 +425,12 @@ namespace osu.Game.Screens.Edit.Submission overlay.Show(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapPackageStream?.Dispose(); + } } } From 783ef0078533c7bf90f13675861a88c03c4242e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:34:48 +0900 Subject: [PATCH 544/620] Change `BeatmapSubmissionScreen` to use global back button instead of custom implementation --- .../Submission/BeatmapSubmissionScreen.cs | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 44b2778869..8536ba5f02 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.IO.Archives; using osu.Game.Localisation; using osu.Game.Online.API; @@ -38,8 +37,6 @@ namespace osu.Game.Screens.Edit.Submission { private BeatmapSubmissionOverlay overlay = null!; - public override bool AllowUserExit => false; - public override bool DisallowExternalBeatmapRulesetChanges => true; [Cached] @@ -73,7 +70,6 @@ namespace osu.Game.Screens.Edit.Submission private SubmissionStageProgress updateStep = null!; private Container successContainer = null!; private Container flashLayer = null!; - private RoundedButton backButton = null!; private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; @@ -82,6 +78,8 @@ namespace osu.Game.Screens.Edit.Submission private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; + private Live? importedSet; + [BackgroundDependencyLoader] private void load() { @@ -161,15 +159,6 @@ namespace osu.Game.Screens.Edit.Submission } } }, - backButton = new RoundedButton - { - Text = CommonStrings.Back, - Width = 150, - Action = this.Exit, - Enabled = { Value = false }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - } } } } @@ -181,7 +170,10 @@ namespace osu.Game.Screens.Edit.Submission if (overlay.State.Value == Visibility.Hidden) { if (!overlay.Completed) + { + allowExit(); this.Exit(); + } else { submissionProgress.FadeIn(200, Easing.OutQuint); @@ -227,8 +219,8 @@ namespace osu.Game.Screens.Edit.Submission createRequest.Failure += ex => { createSetStep.SetFailed(ex.Message); - backButton.Enabled.Value = true; Logger.Log($"Beatmap set submission failed on creation: {ex}"); + allowExit(); }; createSetStep.SetInProgress(); @@ -250,9 +242,9 @@ namespace osu.Game.Screens.Edit.Submission catch (Exception ex) { exportStep.SetFailed(ex.Message); - Logger.Log($"Beatmap set submission failed on export: {ex}"); - backButton.Enabled.Value = true; exportProgressNotification = null; + Logger.Log($"Beatmap set submission failed on export: {ex}"); + allowExit(); } exportStep.SetCompleted(); @@ -311,7 +303,7 @@ namespace osu.Game.Screens.Edit.Submission { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); @@ -339,7 +331,7 @@ namespace osu.Game.Screens.Edit.Submission { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); @@ -354,8 +346,6 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetInProgress(); - Live? importedSet; - try { importedSet = await beatmaps.ImportAsUpdate( @@ -367,28 +357,13 @@ namespace osu.Game.Screens.Edit.Submission { updateStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on local update: {ex}"); - Schedule(() => backButton.Enabled.Value = true); + allowExit(); return; } updateStep.SetCompleted(); - backButton.Enabled.Value = true; - backButton.Action = () => - { - game?.PerformFromScreen(s => - { - if (s is OsuScreen osuScreen) - { - Debug.Assert(importedSet != null); - var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) - ?? importedSet.Value.Beatmaps.First(); - osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); - } - - s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); - }; showBeatmapCard(); + allowExit(); } private void showBeatmapCard() @@ -408,6 +383,11 @@ namespace osu.Game.Screens.Edit.Submission api.Queue(getBeatmapSetRequest); } + private void allowExit() + { + BackButtonVisibility.Value = true; + } + protected override void Update() { base.Update(); @@ -419,6 +399,33 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetInProgress(updateProgressNotification.Progress); } + public override bool OnExiting(ScreenExitEvent e) + { + // We probably want a method of cancelling in the future… + if (!BackButtonVisibility.Value) + return true; + + if (importedSet != null) + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + + return true; + } + + return base.OnExiting(e); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From ce88ecfb3cbfb2df90663b6f7ac1d3b8021da22e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:39:01 +0900 Subject: [PATCH 545/620] Adjust timeouts to be much higher for upload requests It seems that right now these timeouts do not check for actual data movement, which is to say if a user with a very slow connection is uploading and it takes more than `Timeout`, their upload will fail. For now let's set these values high enough that most users will not be affected. --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 +- osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index 5728dbe3fa..df3c9d071c 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.API.Requests foreach (string filename in FilesDeleted) request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form); - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs index 2e224ce602..de8af6a623 100644 --- a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -38,7 +38,7 @@ namespace osu.Game.Online.API.Requests var request = base.CreateWebRequest(); request.AddFile(@"beatmapArchive", oszPackage); request.Method = HttpMethod.Put; - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } From 753eae426d7c33978621025424b8dd43081a31fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:42:36 +0900 Subject: [PATCH 546/620] Update strings --- .../Localisation/BeatmapSubmissionStrings.cs | 20 +++++++++---------- .../Submission/BeatmapSubmissionScreen.cs | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 50b65ab572..3abe8cc515 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -45,24 +45,24 @@ namespace osu.Game.Localisation public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); /// - /// "Exporting beatmap set in compatibility mode..." + /// "Exporting beatmap for compatibility..." /// - public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + public static LocalisableString Exporting => new TranslatableString(getKey(@"exporting"), @"Exporting beatmap for compatibility..."); /// - /// "Preparing beatmap set online..." + /// "Preparing for upload..." /// - public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + public static LocalisableString Preparing => new TranslatableString(getKey(@"preparing"), @"Preparing for upload..."); /// - /// "Uploading beatmap set contents..." + /// "Uploading beatmap contents..." /// - public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + public static LocalisableString Uploading => new TranslatableString(getKey(@"uploading"), @"Uploading beatmap contents..."); /// - /// "Updating local beatmap with relevant changes..." + /// "Finishing up..." /// - public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + public static LocalisableString Finishing => new TranslatableString(getKey(@"finishing"), @"Finishing up..."); /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); /// - /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." /// - public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); /// /// "Empty beatmaps cannot be submitted." diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 8536ba5f02..41c875ac1f 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -114,25 +114,25 @@ namespace osu.Game.Screens.Edit.Submission { createSetStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Preparing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, exportStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Exporting, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, uploadStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + StageDescription = BeatmapSubmissionStrings.Uploading, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, updateStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + StageDescription = BeatmapSubmissionStrings.Finishing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, From fab5cfd275bb827ab9c81c7d1e1be2a298a403d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:57:26 +0900 Subject: [PATCH 547/620] Fix slider ball rotation not being updated when rewinding to a slider --- .../Objects/Drawables/DrawableSliderBall.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 24c0d0fcf0..9b8b197804 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -66,8 +66,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Slider slider = drawableSlider.HitObject; Position = slider.CurvePositionAt(completionProgress); - //0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1 - var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance)); + // 0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1 + double checkDistance = 0.1 / slider.Path.Distance; + var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance)); // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. // Needed for when near completion, or in case of a very short slider. From aad12024b0db512c32b77cd2b48dd50a64cb7d05 Mon Sep 17 00:00:00 2001 From: Layendan Date: Fri, 7 Feb 2025 03:13:51 -0700 Subject: [PATCH 548/620] remove using cache, improve tests, and revert loading --- .../TestSceneAddPlaylistToCollectionButton.cs | 37 ++++++++--- .../AddPlaylistToCollectionButton.cs | 62 +++++++------------ 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index acf2c4b3f9..f18488170d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -4,12 +4,14 @@ using System; using System.Diagnostics; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -17,14 +19,17 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; +using osuTK.Input; +using SharpCompress; namespace osu.Game.Tests.Visual.Playlists { - public partial class TestSceneAddPlaylistToCollectionButton : OsuTestScene + public partial class TestSceneAddPlaylistToCollectionButton : OsuManualInputManagerTestScene { private BeatmapManager manager = null!; private BeatmapSetInfo importedBeatmap = null!; private Room room = null!; + private AddPlaylistToCollectionButton button = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -32,6 +37,8 @@ namespace osu.Game.Tests.Visual.Playlists Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); + + Add(notificationOverlay); } [Cached(typeof(INotificationOverlay))] @@ -44,25 +51,37 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetUpSteps() { + AddStep("clear realm", () => Realm.Realm.Write(() => Realm.Realm.RemoveAll())); + + AddStep("clear notifications", () => notificationOverlay.AllNotifications.Empty()); + importBeatmap(); setupRoom(); AddStep("create button", () => { - AddRange(new Drawable[] + Add(button = new AddPlaylistToCollectionButton(room) { - notificationOverlay, - new AddPlaylistToCollectionButton(room) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(300, 40), - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 40), }); }); } + [Test] + public void TestButtonFlow() + { + AddStep("move mouse to button", () => InputManager.MoveMouseTo(button)); + + AddStep("click button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("notification shown", () => notificationOverlay.AllNotifications.FirstOrDefault(n => n.Text.ToString().StartsWith("Created", StringComparison.Ordinal)) != null); + + AddAssert("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); + } + private void importBeatmap() => AddStep("import beatmap", () => { var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index ab3e481f9f..8801d73e9e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,17 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -20,14 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { private readonly Room room; - private LoadingLayer loading = null!; - [Resolved] private RealmAccess realmAccess { get; set; } = null!; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - [Resolved(canBeNull: true)] private INotificationOverlay? notifications { get; set; } @@ -38,12 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - BackgroundColour = colours.Gray5; - - Add(loading = new LoadingLayer(true, false)); - Action = () => { int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); @@ -54,34 +43,27 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } - Enabled.Value = false; - loading.Show(); - beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => + string filter = string.Join(" OR ", ids.Select(id => $"(OnlineID == {id})")); + var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); + + var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + + if (collection == null) { - var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); - - var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); - - if (collection == null) + collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i.MD5Hash).Distinct().ToList()); + realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); + notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); + } + else + { + collection.ToLive(realmAccess).PerformWrite(c => { - collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); - realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); - } - else - { - collection.ToLive(realmAccess).PerformWrite(c => - { - beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); - foreach (var item in beatmaps) - c.BeatmapMD5Hashes.Add(item!.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - }); - } - - loading.Hide(); - Enabled.Value = true; - }), TaskContinuationOptions.OnlyOnRanToCompletion); + beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i.MD5Hash)).ToList(); + foreach (var item in beatmaps) + c.BeatmapMD5Hashes.Add(item.MD5Hash); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); + }); + } }; } } From d4c69f0c9063c7c4d56f75ecc37a1819b616e4dc Mon Sep 17 00:00:00 2001 From: Layendan Date: Fri, 7 Feb 2025 04:04:29 -0700 Subject: [PATCH 549/620] Assume room is setup correctly and remove duplicate maps before querying realm --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 8801d73e9e..c24c7d834d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -35,15 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { Action = () => { - int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); - - if (ids.Length == 0) + if (room.Playlist.Count == 0) { notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); return; } - string filter = string.Join(" OR ", ids.Select(id => $"(OnlineID == {id})")); + string filter = string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); From 5ace8e911bde4a5f9c9318f57a98519995b5b55f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 21:45:31 +0900 Subject: [PATCH 550/620] Fix failing test --- .../SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index c043fd87a9..a0c56020ab 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); - AddBeatmaps(5); + AddBeatmaps(3); WaitForDrawablePanels(); CheckHasSelection(); From de0aabbfc59963923637bc08edcc3c205a3e1f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:34:52 +0100 Subject: [PATCH 551/620] Add staging submission service URL to development endpoint config --- osu.Game/Online/DevelopmentEndpointConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index f4e1b257ee..e36e36ee9f 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Online SpectatorUrl = $@"{APIUrl}/signalr/spectator"; MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; MetadataUrl = $@"{APIUrl}/signalr/metadata"; + BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission"; } } } From 64f0d234d84222b00363397b43c9cda55c772a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:37:27 +0100 Subject: [PATCH 552/620] Fix exiting being eternally blocked after successful beatmap submission --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 41c875ac1f..9dfe998138 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -420,7 +420,7 @@ namespace osu.Game.Screens.Edit.Submission s.Push(new EditorLoader()); }, [typeof(MainMenu)]); - return true; + return false; } return base.OnExiting(e); From bcd4fcbeed3a2a4057849f3e01defdcea17b849e Mon Sep 17 00:00:00 2001 From: SebastianPeP Date: Sun, 9 Feb 2025 01:29:22 -0300 Subject: [PATCH 553/620] Changed the Currently Playing Text when no track is selected Changed the currently playing text for when the track isnt selected/loaded --- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 35067f4055..5dc73d8679 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -30,8 +30,8 @@ namespace osu.Game.Beatmaps { Metadata = new BeatmapMetadata { - Artist = "please load a beatmap!", - Title = "no beatmaps available!" + Artist = "please select or load a beatmap!", + Title = "no beatmap selected!" }, BeatmapSet = new BeatmapSetInfo(), Difficulty = new BeatmapDifficulty From f9bda0524ada81a9bbc440b88195af3d8ec9786e Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 9 Feb 2025 18:45:13 -0700 Subject: [PATCH 554/620] Update button text to include downloaded beatmaps and collection status --- .../AddPlaylistToCollectionButton.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index c24c7d834d..cc875b707d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; @@ -17,6 +20,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class AddPlaylistToCollectionButton : RoundedButton { private readonly Room room; + private readonly Bindable downloadedBeatmapsCount = new Bindable(0); + private readonly Bindable collectionExists = new Bindable(false); + private IDisposable? beatmapSubscription; + private IDisposable? collectionSubscription; [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -27,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public AddPlaylistToCollectionButton(Room room) { this.room = room; - Text = "Add Maps to Collection"; + Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value); } [BackgroundDependencyLoader] @@ -41,8 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } - string filter = string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); - var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); + var beatmaps = realmAccess.Realm.All().Filter(formatFilterQuery(room.Playlist)).ToList(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); @@ -64,5 +70,30 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); + + downloadedBeatmapsCount.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value)); + + collectionExists.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value), true); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapSubscription?.Dispose(); + collectionSubscription?.Dispose(); + } + + private string formatFilterQuery(IReadOnlyList playlistItems) => string.Join(" OR ", playlistItems.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); + + private string formatButtonText(int count, bool collectionExists) => $"Add {count} {(count == 1 ? "beatmap" : "beatmaps")} to {(collectionExists ? "collection" : "new collection")}"; } } From 274b4221398ba232edfd101ebaab862cbd12c6c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 14:51:48 +0900 Subject: [PATCH 555/620] Add percent progress display to editor footer --- .../Edit/Components/TimeInfoContainer.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 8f2a3d49ca..d17f9011f4 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -18,6 +18,7 @@ namespace osu.Game.Screens.Edit.Components public partial class TimeInfoContainer : BottomBarContainer { private OsuSpriteText bpm = null!; + private OsuSpriteText progress = null!; [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; @@ -36,26 +37,44 @@ namespace osu.Game.Screens.Edit.Components bpm = new OsuSpriteText { Colour = colours.Orange1, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1, 0), + Position = new Vector2(0, 4), + Anchor = Anchor.CentreRight, + Origin = Anchor.TopRight, + }, + progress = new OsuSpriteText + { + Colour = colours.Purple1, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1, 0), Anchor = Anchor.CentreLeft, - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), Position = new Vector2(2, 4), } }; } private double? lastBPM; + private double? lastProgress; protected override void Update() { base.Update(); double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM; + double newProgress = (int)(editorClock.CurrentTime / editorClock.TrackLength * 100); if (lastBPM != newBPM) { lastBPM = newBPM; bpm.Text = @$"{newBPM:0} BPM"; } + + if (lastProgress != newProgress) + { + lastProgress = newProgress; + progress.Text = @$"{newProgress:0}%"; + } } private partial class TimestampControl : OsuClickableContainer From 7853456c06abf8c7e46d233580b50cdf070f2efe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:12:59 +0900 Subject: [PATCH 556/620] Add delay before browser displays beatmap --- .../Submission/BeatmapSubmissionScreen.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9dfe998138..039c919ed6 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -290,15 +290,7 @@ namespace osu.Game.Screens.Edit.Submission var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); patchRequest.FilesChanged.AddRange(changedFiles); patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); - patchRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + patchRequest.Success += uploadCompleted; patchRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -318,15 +310,7 @@ namespace osu.Game.Screens.Edit.Submission var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); - uploadRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + uploadRequest.Success += uploadCompleted; uploadRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -339,6 +323,12 @@ namespace osu.Game.Screens.Edit.Submission uploadStep.SetInProgress(); } + private void uploadCompleted() + { + uploadStep.SetCompleted(); + updateLocalBeatmap().ConfigureAwait(true); + } + private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); @@ -364,6 +354,12 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetCompleted(); showBeatmapCard(); allowExit(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + { + await Task.Delay(1000).ConfigureAwait(true); + game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); + } } private void showBeatmapCard() From 930aaecd7fc39a9455f3e56fe7baffe97b9dc360 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:22:31 +0900 Subject: [PATCH 557/620] Fix back button displaying before it should --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 039c919ed6..0967bcfc65 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Edit.Submission public override bool DisallowExternalBeatmapRulesetChanges => true; + protected override bool InitialBackButtonVisibility => false; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From eae1ea7e32484c03cd24b656c68c3138f4197b82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:23:25 +0900 Subject: [PATCH 558/620] Adjust animations and induce some short delays to make things more graceful --- .../Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 0967bcfc65..121e25d8b7 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -92,6 +92,8 @@ namespace osu.Game.Screens.Edit.Submission { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + AutoSizeDuration = 400, + AutoSizeEasing = Easing.OutQuint, Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -144,9 +146,6 @@ namespace osu.Game.Screens.Edit.Submission Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - AutoSizeDuration = 500, - AutoSizeEasing = Easing.OutQuint, - Masking = true, CornerRadius = BeatmapCard.CORNER_RADIUS, Child = flashLayer = new Container { @@ -252,6 +251,8 @@ namespace osu.Game.Screens.Edit.Submission exportStep.SetCompleted(); exportProgressNotification = null; + await Task.Delay(200).ConfigureAwait(true); + if (onlineFiles.Count > 0) await patchBeatmapSet(onlineFiles).ConfigureAwait(true); else @@ -337,6 +338,7 @@ namespace osu.Game.Screens.Edit.Submission Debug.Assert(beatmapPackageStream != null); updateStep.SetInProgress(); + await Task.Delay(200).ConfigureAwait(true); try { From 5e9f195117307feb555e663fe8544c9a2527bc51 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 9 Feb 2025 23:27:28 -0700 Subject: [PATCH 559/620] Fix tests failing if playlist was empty --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index cc875b707d..8b5d5c752c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -75,7 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); - beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + if (room.Playlist.Count > 0) + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); From a3cd62ec7295b3dd5f16350ae6439a623acab111 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:17:21 -0500 Subject: [PATCH 560/620] Flip mania judgement anchor on flipped scroll direction --- .../UI/DrawableManiaJudgement.cs | 65 +++++-------------- osu.Game.Rulesets.Mania/UI/Stage.cs | 8 +-- 2 files changed, 17 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 75f56bffa4..40fef1a56a 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -3,65 +3,32 @@ #nullable disable +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { public partial class DrawableManiaJudgement : DrawableJudgement { - protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); + private IBindable direction; - private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) { - private const float judgement_y_position = -180f; - - public DefaultManiaJudgementPiece(HitResult result) - : base(result) - { - Y = judgement_y_position; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - JudgementText.Font = JudgementText.Font.With(size: 25); - } - - public override void PlayAnimation() - { - switch (Result) - { - case HitResult.None: - this.FadeOutFromOne(800); - break; - - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveToY(judgement_y_position); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); - break; - - default: - this.ScaleTo(0.8f); - this.ScaleTo(1, 250, Easing.OutElastic); - - this.Delay(50) - .ScaleTo(0.75f, 250) - .FadeOut(200); - break; - } - } + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); } + + private void onDirectionChanged() + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Origin = Anchor.Centre; + } + + protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); } } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index fb9671c14d..faa9fc318c 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -216,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI return; judgements.Clear(false); - judgements.Add(judgementPooler.Get(result.Type, j => - { - j.Apply(result, judgedObject); - - j.Anchor = Anchor.BottomCentre; - j.Origin = Anchor.Centre; - })!); + judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!); } protected override void Update() From 1e06c5cc4ac823f773101899b0ea55e23e82e291 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:17:36 -0500 Subject: [PATCH 561/620] Flip the Y offset of skin judgement pieces on flipped scroll direction --- .../Skinning/Argon/ArgonJudgementPiece.cs | 14 +++- .../Legacy/LegacyManiaJudgementPiece.cs | 29 +++++-- .../UI/DefaultManiaJudgementPiece.cs | 75 +++++++++++++++++++ 3 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index a1c81d3a6a..6098459f6b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -12,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; @@ -26,18 +28,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon [Resolved] private OsuColour colours { get; set; } = null!; + private IBindable direction = null!; + public ArgonJudgementPiece(HitResult result) : base(result) { AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - Y = judgement_y_position; } [BackgroundDependencyLoader] - private void load() + private void load(IScrollingInfo scrollingInfo) { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); + if (Result.IsHit()) { AddInternal(ringExplosion = new RingExplosion(Result) @@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon } } + private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + protected override SpriteText CreateJudgementText() => new OsuSpriteText { @@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - this.MoveToY(judgement_y_position); + this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); this.RotateTo(0); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 4b0cc482d9..3752c5f27a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning.Legacy @@ -28,14 +30,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy AutoSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; - float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; + private IBindable direction = null!; - float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; - Y = scorePosition - absoluteHitPosition; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); InternalChild = animation.With(d => { @@ -44,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }); } + private void onDirectionChanged() + { + float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; + float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; + + float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; + float finalPosition = scorePosition - absoluteHitPosition; + + Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition; + } + public void PlayAnimation() { (animation as IFramedAnimation)?.GotoFrame(0); diff --git a/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs new file mode 100644 index 0000000000..f0af6085d0 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; + +namespace osu.Game.Rulesets.Mania.UI +{ + public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece + { + private const float judgement_y_position = -180f; + + private IBindable direction = null!; + + public DefaultManiaJudgementPiece(HitResult result) + : base(result) + { + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); + } + + private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + + protected override void LoadComplete() + { + base.LoadComplete(); + + JudgementText.Font = JudgementText.Font.With(size: 25); + } + + public override void PlayAnimation() + { + switch (Result) + { + case HitResult.None: + this.FadeOutFromOne(800); + break; + + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); + break; + + default: + this.ScaleTo(0.8f); + this.ScaleTo(1, 250, Easing.OutElastic); + + this.Delay(50) + .ScaleTo(0.75f, 250) + .FadeOut(200); + + // osu!mania uses a custom fade length, so the base call is intentionally omitted. + break; + } + } + } +} From 895493877cd0f04699099a4228657b05365c7b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:02:47 +0100 Subject: [PATCH 562/620] Allow performing beatmap reload after submission from song select --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 121e25d8b7..f53d10d23b 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -29,6 +29,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; using osuTK; namespace osu.Game.Screens.Edit.Submission @@ -418,7 +419,7 @@ namespace osu.Game.Screens.Edit.Submission } s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); + }, [typeof(SongSelect)]); return false; } From 45259b374a2fdd6626e06a7ed9c526cf28cd5fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:09:43 +0100 Subject: [PATCH 563/620] Remove unused using --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index f53d10d23b..9672e4360a 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -28,7 +28,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osuTK; From b8e33a28d25c8590cf4d0b93e59deeaa21daa1d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 17:40:00 +0900 Subject: [PATCH 564/620] Minor code refactors --- .../Submission/BeatmapSubmissionScreen.cs | 19 ++++++++++------- .../Submission/SubmissionBeatmapExporter.cs | 21 +++++++------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9672e4360a..201888e078 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,7 +76,6 @@ namespace osu.Game.Screens.Edit.Submission private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; - private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; @@ -214,8 +213,7 @@ namespace osu.Game.Screens.Edit.Submission }).ConfigureAwait(true); } - legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); - await createBeatmapPackage(response.Files).ConfigureAwait(true); + await createBeatmapPackage(response).ConfigureAwait(true); }; createRequest.Failure += ex => { @@ -228,7 +226,7 @@ namespace osu.Game.Screens.Edit.Submission api.Queue(createRequest); } - private async Task createBeatmapPackage(ICollection onlineFiles) + private async Task createBeatmapPackage(PutBeatmapSetResponse response) { Debug.Assert(ThreadSafety.IsUpdateThread); @@ -237,8 +235,13 @@ namespace osu.Game.Screens.Edit.Submission try { beatmapPackageStream = new MemoryStream(); - await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) - .ConfigureAwait(true); + exportProgressNotification = new ProgressNotification(); + + var legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + + await legacyBeatmapExporter + .ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification) + .ConfigureAwait(true); } catch (Exception ex) { @@ -253,8 +256,8 @@ namespace osu.Game.Screens.Edit.Submission await Task.Delay(200).ConfigureAwait(true); - if (onlineFiles.Count > 0) - await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + if (response.Files.Count > 0) + await patchBeatmapSet(response.Files).ConfigureAwait(true); else replaceBeatmapSet(); } diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs index 3c50a1bf80..fab080cdba 100644 --- a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -14,43 +14,38 @@ namespace osu.Game.Screens.Edit.Submission public class SubmissionBeatmapExporter : LegacyBeatmapExporter { private readonly uint? beatmapSetId; - private readonly HashSet? beatmapIds; - - public SubmissionBeatmapExporter(Storage storage) - : base(storage) - { - } + private readonly HashSet? allocatedBeatmapIds; public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) : base(storage) { beatmapSetId = putBeatmapSetResponse.BeatmapSetId; - beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + allocatedBeatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); } protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) { base.MutateBeatmap(beatmapSet, playableBeatmap); - if (beatmapSetId != null && beatmapIds != null) + if (beatmapSetId != null && allocatedBeatmapIds != null) { playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; - if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + if (allocatedBeatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) { - beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + allocatedBeatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); return; } if (playableBeatmap.BeatmapInfo.OnlineID > 0) throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); - if (beatmapIds.Count == 0) + if (allocatedBeatmapIds.Count == 0) throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); - int newId = beatmapIds.First(); - beatmapIds.Remove(newId); + int newId = allocatedBeatmapIds.First(); + allocatedBeatmapIds.Remove(newId); playableBeatmap.BeatmapInfo.OnlineID = newId; } } From 3ba56e009e347942089dbfe8533020ad7a4b63e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 10:41:10 +0100 Subject: [PATCH 565/620] Privatise a few members --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 35 +++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index c784fc298a..72e866cb24 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -28,17 +28,16 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - public BindableList Spectators { get; } = new BindableList(); - public Bindable UserPlayingState { get; } = new Bindable(); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - protected OsuSpriteText Header { get; private set; } = null!; + private BindableList spectators { get; } = new BindableList(); + private Bindable userPlayingState { get; } = new Bindable(); + private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; private FillFlowContainer spectatorsFlow = null!; private DrawablePool pool = null!; @@ -63,7 +62,7 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = new OsuSpriteText + header = new OsuSpriteText { Colour = colours.Blue0, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), @@ -78,18 +77,18 @@ namespace osu.Game.Screens.Play.HUD pool = new DrawablePool(max_spectators_displayed), }; - HeaderColour.Value = Header.Colour; + HeaderColour.Value = header.Colour; } protected override void LoadComplete() { base.LoadComplete(); - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); + ((IBindableList)spectators).BindTo(client.WatchingUsers); + ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - Spectators.BindCollectionChanged(onSpectatorsChanged, true); - UserPlayingState.BindValueChanged(_ => updateVisibility()); + spectators.BindCollectionChanged(onSpectatorsChanged, true); + userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); @@ -125,10 +124,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, Spectators[i]); + addNewSpectatorToList(i, spectators[i]); } break; @@ -144,7 +143,7 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); + header.Text = SpectatorListStrings.SpectatorCount(spectators.Count).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -160,7 +159,7 @@ namespace osu.Game.Screens.Play.HUD var entry = pool.Get(entry => { entry.Current.Value = spectator; - entry.UserPlayingState = UserPlayingState; + entry.UserPlayingState = userPlayingState; }); spectatorsFlow.Insert(i, entry); @@ -169,15 +168,15 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(spectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() { - Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); - Header.Colour = HeaderColour.Value; + header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + header.Colour = HeaderColour.Value; - Width = Header.DrawWidth; + Width = header.DrawWidth; } private partial class SpectatorListEntry : PoolableDrawable From ad642b84258497b0140ae3d45680f52988a1429f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 11:17:17 +0100 Subject: [PATCH 566/620] Fix spectator list showing other users in multiplayer room even if they're not spectating --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 17 ++++++--- osu.Game/Screens/Play/HUD/SpectatorList.cs | 37 +++++++++++++++---- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 66a87c0715..66c465cbed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -8,11 +8,14 @@ using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Gameplay @@ -28,20 +31,23 @@ namespace osu.Game.Tests.Visual.Gameplay SpectatorList list = null!; Bindable playingState = new Bindable(); GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); - TestSpectatorClient client = new TestSpectatorClient(); + TestSpectatorClient spectatorClient = new TestSpectatorClient(); + TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler())); AddStep("create spectator list", () => { Children = new Drawable[] { - client, + spectatorClient, + multiplayerClient, new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, CachedDependencies = [ (typeof(GameplayState), gameplayState), - (typeof(SpectatorClient), client) + (typeof(SpectatorClient), spectatorClient), + (typeof(MultiplayerClient), multiplayerClient), ], Child = list = new SpectatorList { @@ -57,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); - ((ISpectatorClient)client).UserStartedWatching([ + ((ISpectatorClient)spectatorClient).UserStartedWatching([ new SpectatorUser { OnlineID = id, @@ -66,7 +72,8 @@ namespace osu.Game.Tests.Visual.Gameplay ]); }, 10); - AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5); + AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching( + spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 72e866cb24..9f97121a92 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -17,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Skinning; using osuTK; @@ -34,8 +37,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - private BindableList spectators { get; } = new BindableList(); + private BindableList watchingUsers { get; } = new BindableList(); private Bindable userPlayingState { get; } = new Bindable(); + private int displayedSpectatorCount; private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; @@ -48,6 +52,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private GameplayState gameplayState { get; set; } = null!; + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -84,10 +91,10 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)spectators).BindTo(client.WatchingUsers); + ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - spectators.BindCollectionChanged(onSpectatorsChanged, true); + watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); @@ -99,6 +106,18 @@ namespace osu.Game.Screens.Play.HUD private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) { + // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. + // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. + // we do not generally wish to display other players in the room as spectators due to that implementation detail, + // therefore this code is intended to filter out those players on the client side. + // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions + // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). + // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) + // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component. + var excludedUserIds = new HashSet(); + if (multiplayerClient.Room != null) + excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID)); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -108,6 +127,9 @@ namespace osu.Game.Screens.Play.HUD var spectator = (SpectatorUser)e.NewItems![i]!; int index = Math.Max(e.NewStartingIndex, 0) + i; + if (excludedUserIds.Contains(spectator.OnlineID)) + continue; + if (index >= max_spectators_displayed) break; @@ -124,10 +146,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, spectators[i]); + addNewSpectatorToList(i, watchingUsers[i]); } break; @@ -143,7 +165,8 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - header.Text = SpectatorListStrings.SpectatorCount(spectators.Count).ToUpper(); + displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID)); + header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -168,7 +191,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(spectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() From 4b8890ef0c86e7ccdd415181b89ca132331a7024 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Feb 2025 05:55:21 -0500 Subject: [PATCH 567/620] Fix incorrect thread access in recent iOS orientation changes --- osu.iOS/OsuGameIOS.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a5a42c1e66..a0132de966 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -41,7 +41,7 @@ namespace osu.iOS updateOrientation(); } - private void updateOrientation() + private void updateOrientation() => UIApplication.SharedApplication.InvokeOnMainThread(() => { bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad); @@ -60,7 +60,7 @@ namespace osu.iOS appDelegate.Orientations = null; break; } - } + }); protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 288851c606682165d7bb9f0cc8604eefa2b1604b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 12:13:46 +0100 Subject: [PATCH 568/620] Fix score position not being displayed in solo results screen Closes https://github.com/ppy/osu/issues/31842. To be honest, I recall this working too, but I don't recall when it might have broken, nor do I want to go look for the point of breakage because it might be borderline impossible to find it now. So I'm just fixing as if it was just a straight omission. Opting for a client-side fix because server-side inclusion of the score position for an entire leaderboard has been previously rejected as too expensive: https://github.com/ppy/osu-web/pull/11354#discussion_r1689217450 --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 33b4bf976b..9f7604aa82 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -35,7 +34,32 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => + { + var toDisplay = new List(); + + for (int i = 0; i < r.Scores.Count; ++i) + { + var score = r.Scores[i]; + int position = i + 1; + + if (score.MatchesOnlineID(Score)) + { + // we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect, + // so we have to fish out the actual drawable panel and set the position to it directly. + var panel = ScorePanelList.GetPanelForScore(Score); + Score.Position = panel.ScorePosition.Value = position; + } + else + { + var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo); + converted.Position = position; + toDisplay.Add(converted); + } + } + + scoresCallback.Invoke(toDisplay); + }; return getScoreRequest; } From 38e2f793cae38148dc5c2eae7af7c20dd46c98b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 12:47:38 +0100 Subject: [PATCH 569/620] Add menu items to open beatmap info & discussion pages in browser from editor --- osu.Game/Localisation/EditorStrings.cs | 12 +++++++++++- osu.Game/Screens/Edit/Editor.cs | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11..1681e541fc 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -184,6 +184,16 @@ namespace osu.Game.Localisation /// public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); + /// + /// "Open beatmap info page in browser" + /// + public static LocalisableString OpenInfoPageInBrowser => new TranslatableString(getKey(@"open_info_page_in_browser"), @"Open beatmap info page in browser"); + + /// + /// "Open beatmap discussion page in browser" + /// + public static LocalisableString OpenDiscussionPageInBrowser => new TranslatableString(getKey(@"open_discussion_page_in_browser"), @"Open beatmap discussion page in browser"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a5dfda9c95..ecb0731c16 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1256,6 +1256,15 @@ namespace osu.Game.Screens.Edit yield return externalEdit; } + if (editorBeatmap.BeatmapInfo.OnlineID > 0) + { + yield return new OsuMenuItemSpacer(); + yield return new EditorMenuItem(EditorStrings.OpenInfoPageInBrowser, MenuItemType.Standard, + () => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset))); + yield return new EditorMenuItem(EditorStrings.OpenDiscussionPageInBrowser, MenuItemType.Standard, + () => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}")); + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } From 310700b4e7a4d3570605195babc78826751f0de6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 21:48:27 +0900 Subject: [PATCH 570/620] Space out comment --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 9f97121a92..4297c62712 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -108,8 +108,10 @@ namespace osu.Game.Screens.Play.HUD { // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. + // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. + // // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) From 78e5e0eddd1e20e480b3e49b59c2f1c3f5319e8e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 12:17:00 +0900 Subject: [PATCH 571/620] Refactor with a bit more null safety In particular I don't like the non-null assert around `GetCurrentItem()`, because there's no reason why it _couldn't_ be `null`. Consider, for example, if these panels are used in matchmaking where there are no items initially present in the playlist. The ruleset nullability part is debatable, but I've chosen to restore the original code here. --- .../Participants/ParticipantPanel.cs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 51ff52c63e..230245e926 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,6 +27,7 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -216,20 +216,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; - MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); - Debug.Assert(currentItem != null); + if (client.Room.GetCurrentItem() is MultiplayerPlaylistItem currentItem) + { + int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; - int userRulesetId = User.RulesetId ?? currentItem.RulesetID; - Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - Debug.Assert(userRuleset != null); + int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset?.ShortName)?.GlobalRank; + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + + if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) + userStyleDisplay.Style = null; + else + userStyleDisplay.Style = (userBeatmapId, userRulesetId); + + // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 + // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. + Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty() : User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); + } userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; - - if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) + if (User.BeatmapAvailability.State == DownloadState.LocallyAvailable && User.State != MultiplayerUserState.Spectating) { userModsDisplay.FadeIn(fade_time); userStyleDisplay.FadeIn(fade_time); @@ -240,17 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) - userStyleDisplay.Style = null; - else - userStyleDisplay.Style = (userBeatmapId, userRulesetId); - kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; - - // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 - // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } public MenuItem[]? ContextMenuItems From 748c2eb3904bdd23ab60bd2e1dbb5a2c772aecb8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 12:43:51 +0900 Subject: [PATCH 572/620] Refactor `RoomSubScreen` update --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 312253774f..59acd3c17f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -439,13 +439,14 @@ namespace osu.Game.Screens.OnlinePlay.Match var rulesetInstance = GetGameplayRuleset().CreateInstance(); - // Remove any user mods that are no longer allowed. Mod[] allowedMods = item.Freestyle - ? rulesetInstance.CreateAllMods().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() + ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + // Remove any user mods that are no longer allowed. Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(UserMods.Value)) - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); + UserMods.Value = newUserMods; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; @@ -456,10 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - bool freestyle = item.Freestyle; - bool freeMod = freestyle || item.AllowedMods.Any(); - - if (freeMod) + if (allowedMods.Length > 0) { UserModsSection.Show(); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); @@ -471,7 +469,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = _ => false; } - if (freestyle) + if (item.Freestyle) { UserStyleSection.Show(); @@ -484,7 +482,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, - AllowEditing = freestyle, + AllowEditing = true, RequestEdit = _ => OpenStyleSelection() }; } From e51c09ec3d94823ea6707b3541da6d74a738344a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 14:23:51 +0900 Subject: [PATCH 573/620] Fix inspection Interestingly, this is not a compiler error nor does R# warn about it. No problem, because this is just restoring the original code anyway. --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 230245e926..0fa2be44f3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants int userRulesetId = User.RulesetId ?? currentItem.RulesetID; Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset?.ShortName)?.GlobalRank; + int? currentModeRank = userRuleset == null ? null : User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) From daf0130b2307e435641a9485fb026f1071aaff6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 08:06:12 +0100 Subject: [PATCH 574/620] Reword copy to be less verbose --- osu.Game/Localisation/EditorStrings.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 1681e541fc..0a15752961 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -187,12 +187,12 @@ namespace osu.Game.Localisation /// /// "Open beatmap info page in browser" /// - public static LocalisableString OpenInfoPageInBrowser => new TranslatableString(getKey(@"open_info_page_in_browser"), @"Open beatmap info page in browser"); + public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page"); /// /// "Open beatmap discussion page in browser" /// - public static LocalisableString OpenDiscussionPageInBrowser => new TranslatableString(getKey(@"open_discussion_page_in_browser"), @"Open beatmap discussion page in browser"); + public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ecb0731c16..d73384af7f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1259,9 +1259,9 @@ namespace osu.Game.Screens.Edit if (editorBeatmap.BeatmapInfo.OnlineID > 0) { yield return new OsuMenuItemSpacer(); - yield return new EditorMenuItem(EditorStrings.OpenInfoPageInBrowser, MenuItemType.Standard, + yield return new EditorMenuItem(EditorStrings.OpenInfoPage, MenuItemType.Standard, () => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset))); - yield return new EditorMenuItem(EditorStrings.OpenDiscussionPageInBrowser, MenuItemType.Standard, + yield return new EditorMenuItem(EditorStrings.OpenDiscussionPage, MenuItemType.Standard, () => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}")); } From 7db0a6f81775248bbcb41a57f363b2d6a73b8875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 08:06:12 +0100 Subject: [PATCH 575/620] Update xmldoc --- osu.Game/Localisation/EditorStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 0a15752961..b74a546eca 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -185,12 +185,12 @@ namespace osu.Game.Localisation public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); /// - /// "Open beatmap info page in browser" + /// "Open beatmap info page" /// public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page"); /// - /// "Open beatmap discussion page in browser" + /// "Open beatmap discussion page" /// public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); From 1fa8d53232931e0edc37ddba22cde7aacb48e799 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 17:11:20 +0900 Subject: [PATCH 576/620] Disable scale animation when holding editor "test" button --- .../Timelines/Summary/TestGameplayButton.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 169e72fe3f..065f52b929 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -33,5 +34,16 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Text = EditorStrings.TestBeatmap; } + + protected override bool OnMouseDown(MouseDownEvent e) + { + // block scale animation + return false; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + // block scale animation + } } } From 884fa20b286264482f6e965f946369e42d9fe356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 09:13:08 +0100 Subject: [PATCH 577/620] Remove completely unnecessary subscriptions per collection --- .../Collections/DrawableCollectionList.cs | 1 - .../Collections/DrawableCollectionListItem.cs | 32 +++++++------------ 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 85af1d383d..c494b830d1 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -96,7 +96,6 @@ namespace osu.Game.Collections lastCreated = collections[changes.InsertedIndices[0]].ID; foreach (int i in changes.NewModifiedIndices) - { var updatedItem = collections[i]; diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 0060dacc01..703def9546 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -126,15 +126,10 @@ namespace osu.Game.Collections private const float count_text_size = 12; - [Resolved] - private RealmAccess realm { get; set; } = null!; - private readonly Live collection; private OsuSpriteText countText = null!; - private IDisposable? itemCountSubscription; - public ItemTextBox(Live collection) { this.collection = collection; @@ -163,29 +158,24 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => - Scheduler.AddOnce(() => - { - int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + // interestingly, it is not required to subscribe to change notifications on this collection at all for this to work correctly. + // the reasoning for this is that `DrawableCollectionList` already takes out a subscription on the set of all `BeatmapCollection`s - + // but that subscription does not only cover *changes to the set of collections* (i.e. addition/removal/rearrangement of collections), + // but also covers *changes to the properties of collections*, which `BeatmapMD5Hashes` is one. + // when a collection item changes due to `BeatmapMD5Hashes` changing, the list item is deleted and re-inserted, thus guaranteeing this to work correctly. + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); - countText.Text = count == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{count:#,0} beatmap" - : $"{count:#,0} beatmaps"; - })); + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} beatmap" + : $"{count:#,0} beatmaps"; } else { PlaceholderText = "Create a new collection"; } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - itemCountSubscription?.Dispose(); - } } public partial class DeleteButton : OsuClickableContainer From b9ed217308f3ebe7405274d8fbd257835bc259dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 17:13:34 +0900 Subject: [PATCH 578/620] Add basic brighten animation instead --- .../Timelines/Summary/TestGameplayButton.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 065f52b929..f5c0ed2382 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -15,6 +15,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary { public partial class TestGameplayButton : OsuButton { + [Resolved] + private OsuColour colours { get; set; } = null!; + protected override SpriteText CreateText() => new OsuSpriteText { Depth = -1, @@ -25,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }; [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { BackgroundColour = colours.Orange1; SpriteText.Colour = colourProvider.Background6; @@ -37,13 +40,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary protected override bool OnMouseDown(MouseDownEvent e) { - // block scale animation + Background.FadeColour(colours.Orange0, 500, Easing.OutQuint); + // don't call base in order to block scale animation return false; } protected override void OnMouseUp(MouseUpEvent e) { - // block scale animation + Background.FadeColour(colours.Orange1, 300, Easing.OutQuint); + // don't call base in order to block scale animation } } } From d8b3c28c2e5cb3c666ae937d4cd13feb7d5475d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 09:17:11 +0100 Subject: [PATCH 579/620] Use more neutral terminology to avoid contentious 'beatmap' term --- osu.Game/Collections/DrawableCollectionListItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 703def9546..f2b00004e2 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -168,8 +168,8 @@ namespace osu.Game.Collections countText.Text = count == 1 // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). - ? $"{count:#,0} beatmap" - : $"{count:#,0} beatmaps"; + ? $"{count:#,0} item" + : $"{count:#,0} items"; } else { From b9c4e235958796bb4f85b9734b5f685541ea13d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 20:05:48 +0900 Subject: [PATCH 580/620] Fix potential bad realm access to collection name --- osu.Game/Collections/DrawableCollectionListItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index f2b00004e2..b0dd70227c 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -255,7 +255,7 @@ namespace osu.Game.Collections private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); } - public IEnumerable FilterTerms => [(LocalisableString)Model.Value.Name]; + public IEnumerable FilterTerms => Model.PerformRead(m => m.IsValid ? new[] { (LocalisableString)m.Name } : []); private bool matchingFilter = true; From be035538c241f29ef609c9f73c670b0056278222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 14:01:32 +0100 Subject: [PATCH 581/620] Fix remaining hit counter scaling in the incorrect direction --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 0eb80d333f..c819cb7937 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -108,8 +108,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsText.Text = $"{requiredHits - numHits}"; - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)numHits / requiredHits)), 60, Easing.OutQuad); + int remainingHits = requiredHits - numHits; + remainingHitsText.Text = remainingHits.ToString(); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.OutQuad); spinnerCircle.ClearTransforms(); spinnerCircle From 231988bc9de21a5e7cdc0fbd838e6cb20c75990a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 15:20:36 +0100 Subject: [PATCH 582/620] Adjust things to be closer to stable (but not close enough yet) --- .../Skinning/Legacy/LegacySwell.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index c819cb7937..d3b5d54828 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -14,6 +14,7 @@ using osuTK; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; using System; +using System.Globalization; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) + private void load(DrawableHitObject hitObject, ISkinSource skin) { Children = new Drawable[] { @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), + Position = new Vector2(250f, 100f), // ballparked to be horizontally centred on 4:3 resolution Children = new Drawable[] { @@ -109,14 +110,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { int remainingHits = requiredHits - numHits; - remainingHitsText.Text = remainingHits.ToString(); - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.OutQuad); + remainingHitsText.Text = remainingHits.ToString(CultureInfo.InvariantCulture); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.Out); spinnerCircle.ClearTransforms(); spinnerCircle .RotateTo(180f * numHits, 1000, Easing.OutQuint) .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) - .ScaleTo(0.8f, 400, Easing.OutQuad); + .ScaleTo(0.8f, 400, Easing.Out); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -134,7 +135,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy samplePlayed = false; } - const double body_transition_duration = 100; + const double body_transition_duration = 200; warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); @@ -146,9 +147,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double clear_transition_duration = 300; const double clear_fade_in = 120; - bodyContainer - .FadeOut(clear_transition_duration, Easing.OutQuad) - .ScaleTo(1.05f, clear_transition_duration, Easing.OutQuad); + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + spinnerCircle.ScaleTo(spinnerCircle.Scale.X + 0.05f, clear_transition_duration, Easing.OutQuad); if (state == ArmedState.Hit) { @@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .FadeIn(clear_fade_in) .MoveTo(new Vector2(0, 0)) + .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.Out) .ScaleTo(0.4f) - .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.OutQuad) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .FadeIn(clear_fade_in) .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); } From a8f07ae7b1ebce7579cc97a14264b7132b017f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 11 Feb 2025 18:04:23 +0100 Subject: [PATCH 583/620] Add comment warning about enum entry order in `GlobalAction` --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 599ca6d6c1..e4dc2d503b 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -227,6 +227,10 @@ namespace osu.Game.Input.Bindings }; } + /// + /// IMPORTANT: New entries should always be added at the end of the enum, as key bindings are stored using the enum's numeric value and + /// changes in order would cause key bindings to get associated with the wrong action. + /// public enum GlobalAction { [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChat))] From ffd8bd7bf4dd4d238986c90e598ad11580667d01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:14:12 +0900 Subject: [PATCH 584/620] Rename `ParentObject` to `DrawableObject` It's not a parent. The follow circle is directly part of the slider itself. --- .../Skinning/FollowCircle.cs | 51 ++++++++++--------- .../Skinning/Legacy/LegacyFollowCircle.cs | 4 +- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 4fadb09948..d1836010fb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -13,8 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { public abstract partial class FollowCircle : CompositeDrawable { - [Resolved] - protected DrawableHitObject? ParentObject { get; private set; } + protected DrawableSlider? DrawableObject { get; private set; } protected FollowCircle() { @@ -22,16 +21,18 @@ namespace osu.Game.Rulesets.Osu.Skinning } [BackgroundDependencyLoader] - private void load() + private void load(DrawableHitObject? hitObject) { - ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking => - { - Debug.Assert(ParentObject != null); + DrawableObject = hitObject as DrawableSlider; - if (ParentObject.Judged) + DrawableObject?.Tracking.BindValueChanged(tracking => + { + Debug.Assert(DrawableObject != null); + + if (DrawableObject.Judged) return; - using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0))) + using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) { if (tracking.NewValue) OnSliderPress(); @@ -45,13 +46,13 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - if (ParentObject != null) + if (DrawableObject != null) { - ParentObject.HitObjectApplied += onHitObjectApplied; - onHitObjectApplied(ParentObject); + DrawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(DrawableObject); - ParentObject.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(ParentObject, ParentObject.State.Value); + DrawableObject.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(DrawableObject, DrawableObject.State.Value); } } @@ -61,26 +62,26 @@ namespace osu.Game.Rulesets.Osu.Skinning .FadeOut(); } - private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state) + private void updateStateTransforms(DrawableHitObject d, ArmedState state) { - Debug.Assert(ParentObject != null); + Debug.Assert(DrawableObject != null); switch (state) { case ArmedState.Hit: - switch (drawableObject) + switch (d) { case DrawableSliderTail: - // Use ParentObject instead of drawableObject because slider tail's + // Use DrawableObject instead of local object because slider tail's // HitStateUpdateTime is ~36ms before the actual slider end (aka slider // tail leniency) - using (BeginAbsoluteSequence(ParentObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(DrawableObject.HitStateUpdateTime)) OnSliderEnd(); break; case DrawableSliderTick: case DrawableSliderRepeat: - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) OnSliderTick(); break; } @@ -88,15 +89,15 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case ArmedState.Miss: - switch (drawableObject) + switch (d) { case DrawableSliderTail: case DrawableSliderTick: case DrawableSliderRepeat: - // Despite above comment, ok to use drawableObject.HitStateUpdateTime + // Despite above comment, ok to use d.HitStateUpdateTime // here, since on stable, the break anim plays right when the tail is // missed, not when the slider ends - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) OnSliderBreak(); break; } @@ -109,10 +110,10 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.Dispose(isDisposing); - if (ParentObject != null) + if (DrawableObject != null) { - ParentObject.HitObjectApplied -= onHitObjectApplied; - ParentObject.ApplyCustomUpdateState -= updateStateTransforms; + DrawableObject.HitObjectApplied -= onHitObjectApplied; + DrawableObject.ApplyCustomUpdateState -= updateStateTransforms; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs index 4a8b737206..f60b5cfe12 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs @@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void OnSliderPress() { - Debug.Assert(ParentObject != null); + Debug.Assert(DrawableObject != null); - double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current); + double remainingTime = Math.Max(0, DrawableObject.HitStateUpdateTime - Time.Current); // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour. // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this). From f97708e6b3bd4bc516e7837e43599b5f1c88c6f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:28:14 +0900 Subject: [PATCH 585/620] Avoid binding directly to DHO's bindable --- .../Skinning/FollowCircle.cs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index d1836010fb..903ba08010 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; @@ -15,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { protected DrawableSlider? DrawableObject { get; private set; } + private readonly IBindable tracking = new Bindable(); + protected FollowCircle() { RelativeSizeAxes = Axes.Both; @@ -25,21 +28,23 @@ namespace osu.Game.Rulesets.Osu.Skinning { DrawableObject = hitObject as DrawableSlider; - DrawableObject?.Tracking.BindValueChanged(tracking => + if (DrawableObject != null) { - Debug.Assert(DrawableObject != null); - - if (DrawableObject.Judged) - return; - - using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) + tracking.BindTo(DrawableObject.Tracking); + tracking.BindValueChanged(tracking => { - if (tracking.NewValue) - OnSliderPress(); - else - OnSliderRelease(); - } - }, true); + if (DrawableObject.Judged) + return; + + using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) + { + if (tracking.NewValue) + OnSliderPress(); + else + OnSliderRelease(); + } + }, true); + } } protected override void LoadComplete() From 84b5ea3dbf6ab7b6209820468d3369e477f9d1b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:33:23 +0900 Subject: [PATCH 586/620] Fix weird follow circle display when rewinding through sliders in editor Closes https://github.com/ppy/osu/issues/31812. --- osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 903ba08010..db789166c6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -63,8 +63,12 @@ namespace osu.Game.Rulesets.Osu.Skinning private void onHitObjectApplied(DrawableHitObject drawableObject) { + // Sane defaults when a new hitobject is applied to the drawable slider. this.ScaleTo(1f) .FadeOut(); + + // Immediately play out any pending transforms from press/release + FinishTransforms(true); } private void updateStateTransforms(DrawableHitObject d, ArmedState state) From b92e9f515bd291a19546538355aeb48001933829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:31:55 +0900 Subject: [PATCH 587/620] Fix layout of user setting areas when aspect ratio is vertically tall --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a16c5c9442..ff4c8c2fd9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -121,9 +121,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new GridContainer { RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, Content = new[] { - new Drawable[] { new OverlinedHeader("Beatmap") }, + new Drawable[] { new OverlinedHeader("Beatmap queue") }, new Drawable[] { addItemButton = new AddItemButton @@ -202,14 +211,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, }, }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } }, null, new GridContainer From 9aef95c38127ae72b2538326e561a28db5d3acda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:43:49 +0900 Subject: [PATCH 588/620] Adjust some paddings and text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mostly trying to give more space to the queue as we add more vertical elements to the middle area of multiplayer / playerlists. This whole UI will likely change – this is just a stop-gap fix. --- osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs | 2 -- .../Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index d9cdcac7d7..6dfde183f0 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -53,13 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Components { RelativeSizeAxes = Axes.X, Height = 2, - Margin = new MarginPadding { Bottom = 2 } }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 5 }, Spacing = new Vector2(10, 0), Children = new Drawable[] { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs index e5d94c5358..a7f3e17efa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); - QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true); + QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Up next ({QueueItems.Count})" : "Up next", true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ff4c8c2fd9..083c8e070e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -176,6 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 90, + Height = 30, Text = "Select", Action = ShowUserModSelect, }, From 9c3e9e7c55b8aad452151c2c1b13a00660b3f52d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:56:15 +0900 Subject: [PATCH 589/620] Change free mods button to show "all" when freestyle is enabled --- .../TestSceneFreeModSelectOverlay.cs | 2 +- .../OnlinePlay/FooterButtonFreeMods.cs | 28 ++++++------------- .../OnlinePlay/FooterButtonFreestyle.cs | 15 ++++------ .../OnlinePlay/OnlinePlaySongSelect.cs | 20 +++++++++---- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index fb54b89a4b..fd589e928a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Y = -ScreenFooter.HEIGHT, - Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods }, }, footer = new ScreenFooter(), }, diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 402f538716..695ed74ab9 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -11,31 +11,20 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osuTK; -using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue> + public partial class FooterButtonFreeMods : FooterButton { - private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); - - public Bindable> Current - { - get => current.Current; - set - { - ArgumentNullException.ThrowIfNull(value); - - current.Current = value; - } - } + public readonly Bindable> FreeMods = new Bindable>(); + public readonly IBindable Freestyle = new Bindable(); public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } @@ -104,7 +93,8 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - Current.BindValueChanged(_ => updateModDisplay(), true); + Freestyle.BindValueChanged(_ => updateModDisplay()); + FreeMods.BindValueChanged(_ => updateModDisplay(), true); } /// @@ -114,16 +104,16 @@ namespace osu.Game.Screens.OnlinePlay { var availableMods = allAvailableAndValidMods.ToArray(); - Current.Value = Current.Value.Count == availableMods.Length + FreeMods.Value = FreeMods.Value.Count == availableMods.Length ? Array.Empty() : availableMods; } private void updateModDisplay() { - int currentCount = Current.Value.Count; + int currentCount = FreeMods.Value.Count; - if (currentCount == allAvailableAndValidMods.Count()) + if (currentCount == allAvailableAndValidMods.Count() || Freestyle.Value) { count.Text = "all"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index 157f90d078..d907fec489 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -16,15 +16,10 @@ using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreestyle : FooterButton { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + public readonly Bindable Freestyle = new Bindable(); - public Bindable Current - { - get => current.Current; - set => current.Current = value; - } public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } @@ -37,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay public FooterButtonFreestyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - base.Action = () => current.Value = !current.Value; + base.Action = () => Freestyle.Value = !Freestyle.Value; } [BackgroundDependencyLoader] @@ -81,12 +76,12 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - Current.BindValueChanged(_ => updateDisplay(), true); + Freestyle.BindValueChanged(_ => updateDisplay(), true); } private void updateDisplay() { - if (current.Value) + if (Freestyle.Value) { text.Text = "on"; text.FadeColour(colours.Gray2, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1164c4c0fc..cf351b31bf 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -126,6 +126,7 @@ namespace osu.Game.Screens.OnlinePlay { if (enabled.NewValue) { + freeModsFooterButton.Enabled.Value = false; freeModsFooterButton.Enabled.Value = false; ModsFooterButton.Enabled.Value = false; @@ -205,8 +206,15 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null), - (new FooterButtonFreestyle { Current = Freestyle }, null) + (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) + { + FreeMods = { BindTarget = FreeMods }, + Freestyle = { BindTarget = Freestyle } + }, null), + (new FooterButtonFreestyle + { + Freestyle = { BindTarget = Freestyle } + }, null) }); return baseButtons; @@ -225,10 +233,10 @@ namespace osu.Game.Screens.OnlinePlay /// The to check. /// Whether is a selectable free-mod. private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type) - // Mod must not be contained in the required mods. - && Mods.Value.All(m => m.Acronym != mod.Acronym) - // Mod must be compatible with all the required mods. - && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); + // Mod must not be contained in the required mods. + && Mods.Value.All(m => m.Acronym != mod.Acronym) + // Mod must be compatible with all the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); protected override void Dispose(bool isDisposing) { From 218151bb3c7af0fe77b32e55757cc0079b40cce6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 18:27:53 +0900 Subject: [PATCH 590/620] Flash footer freemod/freestyle buttons when active --- .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 2 ++ .../Screens/OnlinePlay/FooterButtonFreestyle.cs | 4 ++-- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- osu.Game/Screens/Select/FooterButton.cs | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 695ed74ab9..3605412b2b 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.OnlinePlay public readonly Bindable> FreeMods = new Bindable>(); public readonly IBindable Freestyle = new Bindable(); + protected override bool IsActive => FreeMods.Value.Count > 0; + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } private OsuSpriteText count = null!; diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index d907fec489..6ee983af20 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -8,11 +8,10 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Select; using osu.Game.Localisation; +using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { @@ -20,6 +19,7 @@ namespace osu.Game.Screens.OnlinePlay { public readonly Bindable Freestyle = new Bindable(); + protected override bool IsActive => Freestyle.Value; public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index cf351b31bf..9bedecc221 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable Freestyle = new Bindable(); + protected readonly Bindable Freestyle = new Bindable(true); private readonly Room room; private readonly PlaylistItem? initialItem; diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 128e750dca..dafa0b0c1c 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -25,6 +25,11 @@ namespace osu.Game.Screens.Select protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0); + /// + /// Used to show an initial animation hinting at the enabled state. + /// + protected virtual bool IsActive => false; + public LocalisableString Text { get => SpriteText?.Text ?? default; @@ -124,6 +129,18 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); Enabled.BindValueChanged(_ => updateDisplay(), true); + + if (IsActive) + { + box.ClearTransforms(); + + using (box.BeginDelayedSequence(200)) + { + box.FadeIn(200) + .Then() + .FadeOut(1500, Easing.OutQuint); + } + } } public Action Hovered; From c049ae69370629f8c8c888705b6cb6feb7ad2ef4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 18:45:00 +0900 Subject: [PATCH 591/620] Update height specification for playlist screen too --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 957a51c467..7f2255e482 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -204,6 +204,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 90, + Height = 30, Text = "Select", Action = ShowUserModSelect, }, From 96db6964df2e1045eacedebae3bfdf95eb250983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 10:55:57 +0100 Subject: [PATCH 592/620] Adjust things further --- .../Skinning/Legacy/LegacySwell.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index d3b5d54828..5d65ac6058 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -15,11 +15,14 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; using System; using System.Globalization; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class LegacySwell : Container { + private const float scale_adjust = 768f / 480; + private DrawableSwell drawableSwell = null!; private Container bodyContainer = null!; @@ -80,12 +83,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.86f * 0.8f), + Alpha = 0.8f, }, - remainingHitsText = new LegacySpriteText(LegacyFont.Combo) + remainingHitsText = new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(0f, 165f), + Position = new Vector2(0f, 130f), Scale = Vector2.One, }, } @@ -96,6 +100,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, + Y = -40, }, }, }, @@ -159,11 +164,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .MoveTo(new Vector2(0, 0)) - .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.Out) + .MoveToOffset(new Vector2(0, -90 * scale_adjust), clear_fade_in * 2, Easing.Out) .ScaleTo(0.4f) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) - .FadeIn(clear_fade_in) + .FadeIn() .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); } From 0ac08158e33867092f76f94b1534ba3bc1ce962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 11:20:27 +0100 Subject: [PATCH 593/620] Fix transforms from swell progress being cleared on completion by not using transforms --- .../Objects/Drawables/DrawableSwell.cs | 4 +-- .../Skinning/Default/DefaultSwell.cs | 26 +++++++++++------ .../Skinning/Legacy/LegacySwell.cs | 28 +++++++++++++------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 1dde4b6f9c..6ad14c87d1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; - public event Action UpdateHitProgress; + public event Action UpdateHitProgress; public DrawableSwell() : this(null) @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - UpdateHitProgress?.Invoke(numHits, HitObject.RequiredHits); + UpdateHitProgress?.Invoke(numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index a588f866c6..ac72ba73b8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -8,10 +8,12 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -29,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; private readonly Drawable centreCircle; + private int numHits; public DefaultSwell() { @@ -125,18 +128,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }; } - private void animateSwellProgress(int numHits, int requiredHits) + private void animateSwellProgress(int numHits) { - float completion = (float)numHits / requiredHits; + this.numHits = numHits; - centreCircle.RotateTo((float)(completion * drawableSwell.HitObject.Duration / 8), 4000, Easing.OutQuint); + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + expandingRing.Alpha += Math.Clamp(completion / 16, 0.1f, 0.6f); + } - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + protected override void Update() + { + base.Update(); - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + + centreCircle.Rotation = (float)Interpolation.DampContinuously(centreCircle.Rotation, + (float)(completion * drawableSwell.HitObject.Duration / 8), 500, Math.Abs(Time.Elapsed)); + expandingRing.Scale = new Vector2((float)Interpolation.DampContinuously(expandingRing.Scale.X, + 1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 35, Math.Abs(Time.Elapsed))); + expandingRing.Alpha = (float)Interpolation.DampContinuously(expandingRing.Alpha, completion / 16, 250, Math.Abs(Time.Elapsed)); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 5d65ac6058..62ccd05a06 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private bool samplePlayed; + private int numHits; + public LegacySwell() { Anchor = Anchor.Centre; @@ -112,17 +114,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - private void animateSwellProgress(int numHits, int requiredHits) + private void animateSwellProgress(int numHits) { - int remainingHits = requiredHits - numHits; - remainingHitsText.Text = remainingHits.ToString(CultureInfo.InvariantCulture); - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.Out); + this.numHits = numHits; + remainingHitsText.Text = (drawableSwell.HitObject.RequiredHits - numHits).ToString(CultureInfo.InvariantCulture); + spinnerCircle.Scale = new Vector2(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)); + } - spinnerCircle.ClearTransforms(); - spinnerCircle - .RotateTo(180f * numHits, 1000, Easing.OutQuint) - .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) - .ScaleTo(0.8f, 400, Easing.Out); + protected override void Update() + { + base.Update(); + + int requiredHits = drawableSwell.HitObject.RequiredHits; + int remainingHits = requiredHits - numHits; + remainingHitsText.Scale = new Vector2((float)Interpolation.DampContinuously( + remainingHitsText.Scale.X, 1.6f - (0.6f * ((float)remainingHits / requiredHits)), 17.5, Math.Abs(Time.Elapsed))); + + spinnerCircle.Rotation = (float)Interpolation.DampContinuously(spinnerCircle.Rotation, 180f * numHits, 130, Math.Abs(Time.Elapsed)); + spinnerCircle.Scale = new Vector2((float)Interpolation.DampContinuously( + spinnerCircle.Scale.X, 0.8f, 120, Math.Abs(Time.Elapsed))); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) From e385848edcbfbab7eaf0618a01ffb98aeed209d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 11:30:30 +0100 Subject: [PATCH 594/620] Add missing animation of warning sprite --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 62ccd05a06..c9e03d3508 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public partial class LegacySwell : Container { private const float scale_adjust = 768f / 480; + private static readonly Vector2 swell_display_position = new Vector2(250f, 100f); private DrawableSwell drawableSwell = null!; @@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(250f, 100f), // ballparked to be horizontally centred on 4:3 resolution + Position = swell_display_position, // ballparked to be horizontally centred on 4:3 resolution Children = new Drawable[] { @@ -152,7 +153,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double body_transition_duration = 200; - warning.FadeOut(body_transition_duration); + warning.MoveTo(swell_display_position, body_transition_duration) + .ScaleTo(3, body_transition_duration, Easing.Out) + .FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); } From d87a775e716801705b1de47cc4d2776770c348ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 13:19:55 +0100 Subject: [PATCH 595/620] Fix clear sample potentially playing multiple times simultaneously --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index c9e03d3508..9f1b692984 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -8,7 +8,6 @@ using osu.Game.Skinning; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; -using osu.Framework.Audio.Sample; using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; @@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Sprite spinnerCircle = null!; private Sprite approachCircle = null!; private Sprite clearAnimation = null!; - private ISample? clearSample; + private SkinnableSound clearSample = null!; private LegacySpriteText remainingHitsText = null!; private bool samplePlayed; @@ -107,12 +106,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy }, }, }, + clearSample = new SkinnableSound(new SampleInfo("spinner-osu")), }; drawableSwell = (DrawableSwell)hitObject; drawableSwell.UpdateHitProgress += animateSwellProgress; drawableSwell.ApplyCustomUpdateState += updateStateTransforms; - clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } private void animateSwellProgress(int numHits) @@ -173,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { if (!samplePlayed) { - clearSample?.Play(); + clearSample.Play(); samplePlayed = true; } From ee6dcbd80899c3865803311b372c8f8623092ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 14:12:43 +0100 Subject: [PATCH 596/620] Fix android build again Another month, another freak android build failure. From what I can tell, this time the build is broken because the second- -to-last workaround applied to fix it, namely explicitly specifying the version of workloads to install, stopped working, presumably because github pushed a new .NET SDK version to runners, and microsoft didn't actually put up a set of workload packages whose version matches the .NET SDK version 1:1. Thankfully, the fix to the *last* android build breakage (which caused the workload installation to completely and irrecoverably fail), appears to be rolling out this week, and *also* fix that same second-last issue, so both workarounds of specifying the workload version and pinning the image to `windows-2019` appear to no longer be required. Note that the newest image version, 20250209.1.0, is still not fully rolled out yet, thus rather than just fix all builds, this will fix like 20% of builds until the newest image is fully rolled out. But I guess a 20% passing build is better than a 0% passing build, in a sense...? --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a88f1320cd..d75f09f184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: build-only-android: name: Build only (Android) - runs-on: windows-2019 + runs-on: windows-latest timeout-minutes: 60 steps: - name: Checkout @@ -114,10 +114,7 @@ jobs: dotnet-version: "8.0.x" - name: Install .NET workloads - # since windows image 20241113.3.0, not specifying a version here - # installs the .NET 7 version of android workload for very unknown reasons. - # revisit once we upgrade to .NET 9, it's probably fixed there. - run: dotnet workload install android --version (dotnet --version) + run: dotnet workload install android - name: Compile run: dotnet build -c Debug osu.Android.slnf From 550d21df42a11202b932194e6e40bd90e384b2e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Feb 2025 00:21:08 +0900 Subject: [PATCH 597/620] Fix failing tests due to text change --- .../Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 36f5bba384..37a3cc2faf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertQueueTabCount(int count) { - string queueTabText = count > 0 ? $"Queue ({count})" : "Queue"; + string queueTabText = count > 0 ? $"Up next ({count})" : "Up next"; AddUntilStep($"Queue tab shows \"{queueTabText}\"", () => { return this.ChildrenOfType.OsuTabItem>() From 7d6701f8e9383f1a1790103f8b29d598fdc13bb7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 01:20:42 +0900 Subject: [PATCH 598/620] Attempt to fix intermittent collections test --- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 0f2f716a07..60675018e9 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -376,6 +376,6 @@ namespace osu.Game.Tests.Visual.Collections private void assertCollectionName(int index, string name) => AddUntilStep($"item {index + 1} has correct name", - () => dialog.ChildrenOfType().Single().OrderedItems.ElementAt(index).ChildrenOfType().First().Text == name); + () => dialog.ChildrenOfType().Single().OrderedItems.ElementAtOrDefault(index)?.ChildrenOfType().First().Text == name); } } From 315a480931e256c8e79a7193c54dad451e75cd94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 00:03:30 +0900 Subject: [PATCH 599/620] Disallow focus on difficulty range slider Alternative to https://github.com/ppy/osu/pull/31749. Closes https://github.com/ppy/osu/issues/31559. --- osu.Game/Graphics/UserInterface/RangeSlider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/RangeSlider.cs b/osu.Game/Graphics/UserInterface/RangeSlider.cs index 422c2ca4a3..acf10ce827 100644 --- a/osu.Game/Graphics/UserInterface/RangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/RangeSlider.cs @@ -162,6 +162,8 @@ namespace osu.Game.Graphics.UserInterface protected partial class BoundSlider : RoundedSliderBar { + public override bool AcceptsFocus => false; + public new Nub Nub => base.Nub; public string? DefaultString; From 965038598975043dc148bf14b14a3adf6b688eb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 00:06:20 +0900 Subject: [PATCH 600/620] Also disable sliderbar focus when disabled --- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 334fe343ae..4b52ac4a3a 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -18,6 +18,8 @@ namespace osu.Game.Graphics.UserInterface public abstract partial class OsuSliderBar : SliderBar, IHasTooltip where T : struct, INumber, IMinMaxValue { + public override bool AcceptsFocus => !Current.Disabled; + public bool PlaySamplesOnAdjust { get; set; } = true; /// From 601e6d8a70e953b59f0066fbe6de75ed16091c09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 13:53:42 +0900 Subject: [PATCH 601/620] Refactor pass for code quality --- .../AddPlaylistToCollectionButton.cs | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 8b5d5c752c..d4b89a5b28 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; @@ -17,7 +18,7 @@ using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class AddPlaylistToCollectionButton : RoundedButton + public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip { private readonly Room room; private readonly Bindable downloadedBeatmapsCount = new Bindable(0); @@ -34,7 +35,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public AddPlaylistToCollectionButton(Room room) { this.room = room; - Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value); } [BackgroundDependencyLoader] @@ -43,31 +43,31 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Action = () => { if (room.Playlist.Count == 0) - { - notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); return; - } - var beatmaps = realmAccess.Realm.All().Filter(formatFilterQuery(room.Playlist)).ToList(); + var beatmaps = getBeatmapsForPlaylist(realmAccess.Realm).ToArray(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); if (collection == null) { - collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i.MD5Hash).Distinct().ToList()); + collection = new BeatmapCollection(room.Name); realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); } else { - collection.ToLive(realmAccess).PerformWrite(c => - { - beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i.MD5Hash)).ToList(); - foreach (var item in beatmaps) - c.BeatmapMD5Hashes.Add(item.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - }); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); } + + collection.ToLive(realmAccess).PerformWrite(c => + { + foreach (var item in beatmaps) + { + if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash)) + c.BeatmapMD5Hashes.Add(item.MD5Hash); + } + }); }; } @@ -76,13 +76,28 @@ namespace osu.Game.Screens.OnlinePlay.Playlists base.LoadComplete(); if (room.Playlist.Count > 0) - beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + { + beatmapSubscription = + realmAccess.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + } - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Any()); - downloadedBeatmapsCount.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value)); + downloadedBeatmapsCount.BindValueChanged(_ => updateButtonText()); + collectionExists.BindValueChanged(_ => updateButtonText(), true); + } - collectionExists.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value), true); + private IQueryable getBeatmapsForPlaylist(Realm r) + { + return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); + } + + private void updateButtonText() + { + if (!collectionExists.Value) + Text = $"Create new collection with {downloadedBeatmapsCount.Value} beatmaps"; + else + Text = $"Update collection with {downloadedBeatmapsCount.Value} beatmaps"; } protected override void Dispose(bool isDisposing) @@ -93,8 +108,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - private string formatFilterQuery(IReadOnlyList playlistItems) => string.Join(" OR ", playlistItems.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); - - private string formatButtonText(int count, bool collectionExists) => $"Add {count} {(count == 1 ? "beatmap" : "beatmaps")} to {(collectionExists ? "collection" : "new collection")}"; + public LocalisableString TooltipText => "Only downloaded beatmaps will be added to the collection"; } } From 8561df40c52bc60a16335e77b6024ae6d50c6984 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:30:33 +0900 Subject: [PATCH 602/620] Add better messaging and handling of edge cases --- .../AddPlaylistToCollectionButton.cs | 110 ++++++++++++------ 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index d4b89a5b28..595e9ad15c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -21,13 +21,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip { private readonly Room room; - private readonly Bindable downloadedBeatmapsCount = new Bindable(0); - private readonly Bindable collectionExists = new Bindable(false); + private IDisposable? beatmapSubscription; private IDisposable? collectionSubscription; + private Live? collection; + private HashSet localBeatmapHashes = new HashSet(); + [Resolved] - private RealmAccess realmAccess { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; [Resolved(canBeNull: true)] private INotificationOverlay? notifications { get; set; } @@ -45,29 +47,29 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Playlist.Count == 0) return; - var beatmaps = getBeatmapsForPlaylist(realmAccess.Realm).ToArray(); + var beatmaps = getBeatmapsForPlaylist(realm.Realm).ToArray(); - var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + int countBefore = 0; + int countAfter = 0; - if (collection == null) + collection ??= realm.Realm.Write(() => realm.Realm.Add(new BeatmapCollection(room.Name)).ToLive(realm)); + collection.PerformWrite(c => { - collection = new BeatmapCollection(room.Name); - realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); - } - else - { - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - } + countBefore = c.BeatmapMD5Hashes.Count; - collection.ToLive(realmAccess).PerformWrite(c => - { foreach (var item in beatmaps) { if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash)) c.BeatmapMD5Hashes.Add(item.MD5Hash); } + + countAfter = c.BeatmapMD5Hashes.Count; }); + + if (countBefore == 0) + notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + else + notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); }; } @@ -75,16 +77,53 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); - if (room.Playlist.Count > 0) + Enabled.Value = false; + + if (room.Playlist.Count == 0) + return; + + beatmapSubscription = realm.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => { - beatmapSubscription = - realmAccess.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => downloadedBeatmapsCount.Value = sender.Count); - } + localBeatmapHashes = sender.Select(b => b.MD5Hash).ToHashSet(); + Schedule(updateButtonState); + }); - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Any()); + collectionSubscription = realm.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => + { + collection = sender.FirstOrDefault()?.ToLive(realm); + Schedule(updateButtonState); + }); + } - downloadedBeatmapsCount.BindValueChanged(_ => updateButtonText()); - collectionExists.BindValueChanged(_ => updateButtonText(), true); + private void updateButtonState() + { + int countToAdd = getCountToBeAdded(); + + if (collection == null) + Text = $"Create new collection with {countToAdd} beatmaps"; + else + Text = $"Update collection with {countToAdd} beatmaps"; + + Enabled.Value = countToAdd > 0; + } + + private int getCountToBeAdded() + { + if (collection == null) + return localBeatmapHashes.Count; + + return collection.PerformRead(c => + { + int count = localBeatmapHashes.Count; + + foreach (string hash in localBeatmapHashes) + { + if (c.BeatmapMD5Hashes.Contains(hash)) + count--; + } + + return count; + }); } private IQueryable getBeatmapsForPlaylist(Realm r) @@ -92,14 +131,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); } - private void updateButtonText() - { - if (!collectionExists.Value) - Text = $"Create new collection with {downloadedBeatmapsCount.Value} beatmaps"; - else - Text = $"Update collection with {downloadedBeatmapsCount.Value} beatmaps"; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -108,6 +139,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - public LocalisableString TooltipText => "Only downloaded beatmaps will be added to the collection"; + public LocalisableString TooltipText + { + get + { + if (Enabled.Value) + return string.Empty; + + int currentCollectionCount = collection?.PerformRead(c => c.BeatmapMD5Hashes.Count) ?? 0; + if (room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == currentCollectionCount) + return "All beatmaps have been added!"; + + return "Download some beatmaps first."; + } + } } } From f9b7a8ed103e39fbd5a791699e5c99b366736766 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:49:25 +0900 Subject: [PATCH 603/620] Make realm operation asynchronous for good measure --- .../AddPlaylistToCollectionButton.cs | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 595e9ad15c..741173f9a3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -47,14 +47,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Playlist.Count == 0) return; - var beatmaps = getBeatmapsForPlaylist(realm.Realm).ToArray(); - int countBefore = 0; int countAfter = 0; - collection ??= realm.Realm.Write(() => realm.Realm.Add(new BeatmapCollection(room.Name)).ToLive(realm)); - collection.PerformWrite(c => + Text = "Updating collection..."; + Enabled.Value = false; + + realm.WriteAsync(r => { + var beatmaps = getBeatmapsForPlaylist(r).ToArray(); + var c = getCollectionsForPlaylist(r).FirstOrDefault() + ?? r.Add(new BeatmapCollection(room.Name)); + countBefore = c.BeatmapMD5Hashes.Count; foreach (var item in beatmaps) @@ -64,12 +68,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } countAfter = c.BeatmapMD5Hashes.Count; - }); - - if (countBefore == 0) - notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); - else - notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + }).ContinueWith(_ => Schedule(() => + { + if (countBefore == 0) + notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + else + notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + })); }; } @@ -77,6 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); + // will be updated via updateButtonState() when ready. Enabled.Value = false; if (room.Playlist.Count == 0) @@ -88,7 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Schedule(updateButtonState); }); - collectionSubscription = realm.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => + collectionSubscription = realm.RegisterForNotifications(getCollectionsForPlaylist, (sender, _) => { collection = sender.FirstOrDefault()?.ToLive(realm); Schedule(updateButtonState); @@ -101,8 +107,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (collection == null) Text = $"Create new collection with {countToAdd} beatmaps"; + else if (hasAllItemsInCollection) + Text = "Collection complete!"; else - Text = $"Update collection with {countToAdd} beatmaps"; + Text = $"Add {countToAdd} beatmaps to collection"; Enabled.Value = countToAdd > 0; } @@ -126,11 +134,25 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); } + private IQueryable getCollectionsForPlaylist(Realm r) => r.All().Where(c => c.Name == room.Name); + private IQueryable getBeatmapsForPlaylist(Realm r) { return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); } + private bool hasAllItemsInCollection + { + get + { + if (collection == null) + return false; + + return room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == + collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -146,8 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (Enabled.Value) return string.Empty; - int currentCollectionCount = collection?.PerformRead(c => c.BeatmapMD5Hashes.Count) ?? 0; - if (room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == currentCollectionCount) + if (hasAllItemsInCollection) return "All beatmaps have been added!"; return "Download some beatmaps first."; From 8ce28d56bbe245eed781e0055ea0befd72533f8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:58:04 +0900 Subject: [PATCH 604/620] Fix tests not waiting enough --- .../Playlists/TestSceneAddPlaylistToCollectionButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index f18488170d..46c93d9ae2 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -77,9 +77,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("click button", () => InputManager.Click(MouseButton.Left)); - AddAssert("notification shown", () => notificationOverlay.AllNotifications.FirstOrDefault(n => n.Text.ToString().StartsWith("Created", StringComparison.Ordinal)) != null); + AddUntilStep("notification shown", () => notificationOverlay.AllNotifications.Any(n => n.Text.ToString().StartsWith("Created new collection", StringComparison.Ordinal))); - AddAssert("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); + AddUntilStep("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); } private void importBeatmap() => AddStep("import beatmap", () => From 3f3cb3df2a5b12ae2fb9cfa8b3db1daa076f9c44 Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Mon, 20 Jan 2025 16:35:21 +0100 Subject: [PATCH 605/620] Fix toolbox settings hiding when dragging a slider --- osu.Game/Overlays/SettingsToolboxGroup.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index f8cf218564..cf72125007 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Game.Graphics; @@ -54,6 +55,8 @@ namespace osu.Game.Overlays private IconButton expandButton = null!; + private InputManager inputManager = null!; + /// /// Create a new instance. /// @@ -125,6 +128,8 @@ namespace osu.Game.Overlays { base.LoadComplete(); + inputManager = GetContainingInputManager()!; + Expanded.BindValueChanged(_ => updateExpandedState(true)); updateExpandedState(false); @@ -172,7 +177,9 @@ namespace osu.Game.Overlays // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - if (Expanded.Value || IsHovered) + bool sliderDraggedInHimself = inputManager.DraggedDrawable.IsRootedAt(this); + + if (Expanded.Value || IsHovered || sliderDraggedInHimself) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; From 9456e376f370b2ea0260a781fd6f90e1e87ad106 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 15:15:05 +0900 Subject: [PATCH 606/620] Fix expanded state not updating on drag end --- osu.Game/Overlays/SettingsToolboxGroup.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index cf72125007..dd41f156f3 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -57,6 +57,8 @@ namespace osu.Game.Overlays private InputManager inputManager = null!; + private Drawable? draggedChild; + /// /// Create a new instance. /// @@ -161,6 +163,13 @@ namespace osu.Game.Overlays headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); headerTextVisibilityCache.Validate(); } + + // Dragged child finished its drag operation. + if (draggedChild != null && inputManager.DraggedDrawable != draggedChild) + { + draggedChild = null; + updateExpandedState(true); + } } protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) @@ -173,13 +182,17 @@ namespace osu.Game.Overlays private void updateExpandedState(bool animate) { + // before we collapse down, let's double check the user is not dragging a UI control contained within us. + if (inputManager.DraggedDrawable.IsRootedAt(this)) + { + draggedChild = inputManager.DraggedDrawable; + } + // clearing transforms is necessary to avoid a previous height transform // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - bool sliderDraggedInHimself = inputManager.DraggedDrawable.IsRootedAt(this); - - if (Expanded.Value || IsHovered || sliderDraggedInHimself) + if (Expanded.Value || IsHovered || draggedChild != null) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; From 88188e8fcb4b15d0214d7106810f10b1f5c66fbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:00:19 +0900 Subject: [PATCH 607/620] Add API models for teams --- .../Online/API/Requests/Responses/APITeam.cs | 23 +++++++++++++++++++ .../Online/API/Requests/Responses/APIUser.cs | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 osu.Game/Online/API/Requests/Responses/APITeam.cs diff --git a/osu.Game/Online/API/Requests/Responses/APITeam.cs b/osu.Game/Online/API/Requests/Responses/APITeam.cs new file mode 100644 index 0000000000..b4fcc2d26e --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITeam.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class APITeam + { + [JsonProperty(@"id")] + public int Id { get; set; } = 1; + + [JsonProperty(@"name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty(@"short_name")] + public string ShortName { get; set; } = string.Empty; + + [JsonProperty(@"flag_url")] + public string FlagUrl = string.Empty; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 30fceab852..92b7d9d874 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -55,6 +55,10 @@ namespace osu.Game.Online.API.Requests.Responses set => countryCodeString = value.ToString(); } + [JsonProperty(@"team")] + [CanBeNull] + public APITeam Team { get; set; } + [JsonProperty(@"profile_colour")] public string Colour; From 303961d1015f2e32549680a76fa2b68112236166 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:19:55 +0900 Subject: [PATCH 608/620] Add drawable implementations of team logo --- .../Online/Leaderboards/LeaderboardScore.cs | 6 ++ .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 15 +++- .../BeatmapSet/Scores/TopScoreUserSection.cs | 27 +++++- .../Profile/Header/TopHeaderContainer.cs | 6 ++ .../Participants/ParticipantPanel.cs | 6 ++ .../Leaderboards/LeaderboardScoreV2.cs | 6 ++ .../OnlinePlay/TestRoomRequestsHandler.cs | 11 ++- .../Users/Drawables/UpdateableTeamFlag.cs | 86 +++++++++++++++++++ osu.Game/Users/UserGridPanel.cs | 3 +- osu.Game/Users/UserPanel.cs | 5 ++ osu.Game/Users/UserRankPanel.cs | 3 +- 11 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Users/Drawables/UpdateableTeamFlag.cs diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 52074119b8..11e1710e75 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -199,6 +199,12 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreLeft, Size = new Vector2(28, 20), }, + new UpdateableTeamFlag(user.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new DateLabel(Score.Date) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index c70c41feed..be6ad49150 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -160,7 +160,20 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Size = new Vector2(19, 14), }, - username, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + new UpdateableTeamFlag(score.User.Team) + { + Size = new Vector2(28, 14), + }, + username, + } + }, #pragma warning disable 618 new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"), #pragma warning restore 618 diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 13ba9fb74b..14c9bedc67 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -27,7 +27,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly UpdateableAvatar avatar; private readonly LinkFlowContainer usernameText; private readonly DrawableDate achievedOn; + private readonly UpdateableFlag flag; + private readonly UpdateableTeamFlag teamFlag; public TopScoreUserSection() { @@ -112,12 +114,30 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, } }, - flag = new UpdateableFlag + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(19, 14), - Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + flag = new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(19, 14), + Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + }, + teamFlag = new UpdateableTeamFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(28, 14), + Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + }, + } }, } } @@ -139,6 +159,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { avatar.User = value.User; flag.CountryCode = value.User.CountryCode; + teamFlag.Team = value.User.Team; achievedOn.Date = value.Date; usernameText.Clear(); diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index ba2cd5b705..5f404375e6 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -42,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; + private UpdateableTeamFlag teamFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; @@ -166,6 +167,10 @@ namespace osu.Game.Overlays.Profile.Header { Size = new Vector2(28, 20), }, + teamFlag = new UpdateableTeamFlag + { + Size = new Vector2(40, 20), + }, userCountryContainer = new OsuHoverContainer { AutoSizeAxes = Axes.Both, @@ -215,6 +220,7 @@ namespace osu.Game.Overlays.Profile.Header usernameText.Text = user?.Username ?? string.Empty; openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; + teamFlag.Team = user?.Team; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); supporterTag.SupportLevel = user?.SupportLevel ?? 0; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 0fa2be44f3..0cedfb9909 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -140,6 +140,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Size = new Vector2(28, 20), CountryCode = user?.CountryCode ?? default }, + new UpdateableTeamFlag(user?.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new OsuSpriteText { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index a2253b413c..978d6eca32 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -339,6 +339,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Origin = Anchor.CentreLeft, Size = new Vector2(24, 16), }, + new UpdateableTeamFlag(user.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new DateLabel(score.Date) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index c9149bda22..d73fd5ab22 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; @@ -221,7 +222,15 @@ namespace osu.Game.Tests.Visual.OnlinePlay : new APIUser { Id = id, - Username = $"User {id}" + Username = $"User {id}", + Team = RNG.NextBool() + ? new APITeam + { + Name = "Collective Wangs", + ShortName = "WANG", + FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + } + : null, }) .Where(u => u != null).ToList(), }); diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs new file mode 100644 index 0000000000..486cb697a1 --- /dev/null +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -0,0 +1,86 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Users.Drawables +{ + /// + /// A team logo which can update to a new team when needed. + /// + public partial class UpdateableTeamFlag : ModelBackedDrawable + { + public APITeam? Team + { + get => Model; + set => Model = value; + } + + protected override double LoadDelay => 200; + + public UpdateableTeamFlag(APITeam? team = null) + { + Team = team; + + Masking = true; + } + + protected override Drawable? CreateDrawable(APITeam? team) + { + if (team == null) + return Empty(); + + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new TeamFlag(team) + { + RelativeSizeAxes = Axes.Both + }, + new HoverClickSounds() + } + }; + } + + // Generally we just want team flags to disappear if the user doesn't have one. + // This also handles fill flow cases and avoids spacing being added for non-displaying flags. + public override bool IsPresent => base.IsPresent && Team != null; + + protected override void Update() + { + base.Update(); + + CornerRadius = DrawHeight / 8; + } + + public partial class TeamFlag : Sprite, IHasTooltip + { + private readonly APITeam team; + + public LocalisableString TooltipText { get; } + + public TeamFlag(APITeam team) + { + this.team = team; + TooltipText = team.Name; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + if (!string.IsNullOrEmpty(team.Name)) + Texture = textures.Get(team.FlagUrl); + } + } + } +} diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index fce543415d..f62c9ab4e7 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -82,9 +82,10 @@ namespace osu.Game.Users AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(6), - Children = new Drawable[] + Children = new[] { CreateFlag(), + CreateTeamLogo(), // supporter icon is being added later } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 0d3ea52611..09a5cb414f 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -130,6 +130,11 @@ namespace osu.Game.Users Action = Action, }; + protected Drawable CreateTeamLogo() => new UpdateableTeamFlag(User.Team) + { + Size = new Vector2(52, 26), + }; + public MenuItem[] ContextMenuItems { get diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 5e3ae172be..ff8adf055c 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -147,9 +147,10 @@ namespace osu.Game.Users AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(6), - Children = new Drawable[] + Children = new[] { CreateFlag(), + CreateTeamLogo(), // supporter icon is being added later } } From 44faabddcd79b0ada819d03cb10044b377e5fe89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:41:59 +0900 Subject: [PATCH 609/620] Add skinnable team flag --- osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs diff --git a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs new file mode 100644 index 0000000000..f8ef03c58c --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs @@ -0,0 +1,56 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Skinning; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class PlayerTeamFlag : CompositeDrawable, ISerialisableDrawable + { + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => false; + + private readonly UpdateableTeamFlag flag; + + private const float default_size = 40f; + + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + + public PlayerTeamFlag() + { + Size = new Vector2(default_size, default_size / 2f); + + InternalChild = flag = new UpdateableTeamFlag + { + RelativeSizeAxes = Axes.Both, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + if (gameplayState != null) + flag.Team = gameplayState.Score.ScoreInfo.User.Team; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => flag.Team = u.NewValue.Team, true); + } + } + + public bool UsesFixedAnchor { get; set; } + } +} From 4184dd27180b3ae8407c8d06d86894950e8b1b67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 17:18:25 +0900 Subject: [PATCH 610/620] Give more breathing room in leaderboard scores --- .../Online/Leaderboards/LeaderboardScore.cs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 11e1710e75..0181c28218 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -189,7 +189,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Width = 87f, + Width = 114f, Masking = true, Children = new Drawable[] { @@ -212,15 +212,6 @@ namespace osu.Game.Online.Leaderboards }, }, }, - new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = edge_margin }, - Children = statisticsLabels - }, }, }, }, @@ -240,6 +231,7 @@ namespace osu.Game.Online.Leaderboards GlowColour = Color4Extensions.FromHex(@"83ccfa"), Current = scoreManager.GetBindableTotalScoreString(Score), Font = OsuFont.Numeric.With(size: 23), + Margin = new MarginPadding { Top = 1 }, }, RankContainer = new Container { @@ -256,13 +248,32 @@ namespace osu.Game.Online.Leaderboards }, }, }, - modsContainer = new FillFlowContainer + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = edge_margin }, + Children = statisticsLabels + }, + modsContainer = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) }) + }, + } }, }, }, @@ -330,7 +341,7 @@ namespace osu.Game.Online.Leaderboards private partial class ScoreComponentLabel : Container, IHasTooltip { - private const float icon_size = 20; + private const float icon_size = 16; private readonly FillFlowContainer content; public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); @@ -346,7 +357,7 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Right = 10 }, + Padding = new MarginPadding { Right = 5 }, Children = new Drawable[] { new Container @@ -381,7 +392,8 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Text = statistic.Value, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true) + Spacing = new Vector2(-1, 0), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true) }, }, }; @@ -412,7 +424,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true); + Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From 4e043e7cabc242b051275e84b17b88553c28844b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 18:35:27 +0900 Subject: [PATCH 611/620] Change how values are applied to (hopefully) simplify things --- osu.Game/Graphics/Containers/ScalingContainer.cs | 3 ++- osu.Game/OsuGame.cs | 6 ++++-- osu.iOS/OsuGameIOS.cs | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index ac76c0546b..2a5ce23b64 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -116,7 +116,8 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - TargetDrawSize = new Vector2(1024, 1024 / (game?.BaseAspectRatio ?? 1f)); + if (game != null) + TargetDrawSize = game.ScalingContainerTargetDrawSize; Scale = new Vector2(CurrentScale); Size = new Vector2(1 / CurrentScale); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ecc71822af..d379392a7d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -72,6 +72,7 @@ using osu.Game.Skinning; using osu.Game.Updater; using osu.Game.Users; using osu.Game.Utils; +using osuTK; using osuTK.Graphics; using Sentry; @@ -814,9 +815,10 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); /// - /// The base aspect ratio to use in all s. + /// Adjust the globally applied in every . + /// Useful for changing how the game handles different aspect ratios. /// - protected internal virtual float BaseAspectRatio => 4f / 3f; + protected internal virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768); protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 64b2292d62..883e89e38a 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -11,6 +11,7 @@ using osu.Game; using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using osuTK; using UIKit; namespace osu.iOS @@ -22,7 +23,7 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; - protected override float BaseAspectRatio => (float)(UIScreen.MainScreen.Bounds.Width / UIScreen.MainScreen.Bounds.Height); + protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameIOS(AppDelegate appDelegate) { From 248bf43ec9c84d2ea31eb0c51cf814760d79e035 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 18:35:43 +0900 Subject: [PATCH 612/620] Apply nullability to `ScalingContainer` --- .../Graphics/Containers/ScalingContainer.cs | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 2a5ce23b64..9d2a1c16af 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -27,17 +24,17 @@ namespace osu.Game.Graphics.Containers { internal const float TRANSITION_DURATION = 500; - private Bindable sizeX; - private Bindable sizeY; - private Bindable posX; - private Bindable posY; - private Bindable applySafeAreaPadding; + private Bindable sizeX = null!; + private Bindable sizeY = null!; + private Bindable posX = null!; + private Bindable posY = null!; + private Bindable applySafeAreaPadding = null!; - private Bindable safeAreaPadding; + private Bindable safeAreaPadding = null!; private readonly ScalingMode? targetMode; - private Bindable scalingMode; + private Bindable scalingMode = null!; private readonly Container content; protected override Container Content => content; @@ -46,9 +43,9 @@ namespace osu.Game.Graphics.Containers private readonly Container sizableContainer; - private BackgroundScreenStack backgroundStack; + private BackgroundScreenStack? backgroundStack; - private Bindable scalingMenuBackgroundDim; + private Bindable scalingMenuBackgroundDim = null!; private RectangleF? customRect; private bool customRectIsRelativePosition; @@ -89,7 +86,8 @@ namespace osu.Game.Graphics.Containers public partial class ScalingDrawSizePreservingFillContainer : DrawSizePreservingFillContainer { private readonly bool applyUIScale; - private Bindable uiScale; + + private Bindable? uiScale; protected float CurrentScale { get; private set; } = 1; @@ -101,8 +99,7 @@ namespace osu.Game.Graphics.Containers } [Resolved(canBeNull: true)] - [CanBeNull] - private OsuGame game { get; set; } + private OsuGame? game { get; set; } [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) @@ -240,13 +237,13 @@ namespace osu.Game.Graphics.Containers private partial class SizeableAlwaysInputContainer : Container { [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; [Resolved] - private ISafeArea safeArea { get; set; } + private ISafeArea safeArea { get; set; } = null!; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; private readonly bool confineHostCursor; private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); From 26a2d0394e5d39de630524166691d86a929a501f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:04:26 +0900 Subject: [PATCH 613/620] Invalidate drawable on potential presence change --- osu.Game/Users/Drawables/UpdateableTeamFlag.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 486cb697a1..1efde2af68 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -21,7 +21,11 @@ namespace osu.Game.Users.Drawables public APITeam? Team { get => Model; - set => Model = value; + set + { + Model = value; + Invalidate(Invalidation.Presence); + } } protected override double LoadDelay => 200; From 82c16dee60e1e8702d95657d654d36934c083ac2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:05:13 +0900 Subject: [PATCH 614/620] Add missing `LongRunningLoad` attribute --- .../Users/Drawables/UpdateableTeamFlag.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 1efde2af68..9c2bbb7e3e 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -42,18 +42,7 @@ namespace osu.Game.Users.Drawables if (team == null) return Empty(); - return new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new TeamFlag(team) - { - RelativeSizeAxes = Axes.Both - }, - new HoverClickSounds() - } - }; + return new TeamFlag(team) { RelativeSizeAxes = Axes.Both }; } // Generally we just want team flags to disappear if the user doesn't have one. @@ -67,7 +56,8 @@ namespace osu.Game.Users.Drawables CornerRadius = DrawHeight / 8; } - public partial class TeamFlag : Sprite, IHasTooltip + [LongRunningLoad] + public partial class TeamFlag : CompositeDrawable, IHasTooltip { private readonly APITeam team; @@ -82,8 +72,15 @@ namespace osu.Game.Users.Drawables [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (!string.IsNullOrEmpty(team.Name)) - Texture = textures.Get(team.FlagUrl); + InternalChildren = new Drawable[] + { + new HoverClickSounds(), + new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(team.FlagUrl) + } + }; } } } From b86eeabef08d8eb3d45848939f2ea36a44790cc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:07:02 +0900 Subject: [PATCH 615/620] Fix one more misalignment on leaderboard scores --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0181c28218..fc30f158f0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -180,6 +180,7 @@ namespace osu.Game.Online.Leaderboards Height = 28, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Bottom = -2 }, Children = new Drawable[] { flagBadgeAndDateContainer = new FillFlowContainer From 1b5101ed5e155c19c0a37894ed3c5ea374ec55a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:30:23 +0900 Subject: [PATCH 616/620] Add team flag display to rankings overlays --- osu.Game/Overlays/KudosuTable.cs | 4 ++-- .../Overlays/Rankings/Tables/CountriesTable.cs | 2 +- .../Overlays/Rankings/Tables/RankingsTable.cs | 17 +++++++---------- .../Overlays/Rankings/Tables/UserBasedTable.cs | 6 ++++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/KudosuTable.cs b/osu.Game/Overlays/KudosuTable.cs index 93884435a4..d6eaf586b9 100644 --- a/osu.Game/Overlays/KudosuTable.cs +++ b/osu.Game/Overlays/KudosuTable.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays protected override CountryCode GetCountryCode(APIUser item) => item.CountryCode; - protected override Drawable CreateFlagContent(APIUser item) + protected override Drawable[] CreateFlagContent(APIUser item) { var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { @@ -89,7 +89,7 @@ namespace osu.Game.Overlays TextAnchor = Anchor.CentreLeft }; username.AddUserLink(item); - return username; + return [username]; } } } diff --git a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs index fb3e58d2ac..733aa7ca54 100644 --- a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected override CountryCode GetCountryCode(CountryStatistics item) => item.Code; - protected override Drawable CreateFlagContent(CountryStatistics item) => new CountryName(item.Code); + protected override Drawable[] CreateFlagContent(CountryStatistics item) => [new CountryName(item.Code)]; protected override Drawable[] CreateAdditionalContent(CountryStatistics item) => new Drawable[] { diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs index b9f7e443ca..f4ed41800a 100644 --- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected abstract CountryCode GetCountryCode(TModel item); - protected abstract Drawable CreateFlagContent(TModel item); + protected abstract Drawable[] CreateFlagContent(TModel item); private OsuSpriteText createIndexDrawable(int index) => new RowText { @@ -92,16 +92,13 @@ namespace osu.Game.Overlays.Rankings.Tables { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + Spacing = new Vector2(5, 0), Margin = new MarginPadding { Bottom = row_spacing }, - Children = new[] - { - new UpdateableFlag(GetCountryCode(item)) - { - Size = new Vector2(28, 20), - }, - CreateFlagContent(item) - } + Children = + [ + new UpdateableFlag(GetCountryCode(item)) { Size = new Vector2(28, 20) }, + ..CreateFlagContent(item) + ] }; protected class RankingsTableColumn : TableColumn diff --git a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs index 4d25065578..c651108ec3 100644 --- a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs @@ -14,6 +14,8 @@ using osu.Game.Users; using osu.Game.Scoring; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Overlays.Rankings.Tables { @@ -61,7 +63,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected sealed override CountryCode GetCountryCode(UserStatistics item) => item.User.CountryCode; - protected sealed override Drawable CreateFlagContent(UserStatistics item) + protected sealed override Drawable[] CreateFlagContent(UserStatistics item) { var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { @@ -70,7 +72,7 @@ namespace osu.Game.Overlays.Rankings.Tables TextAnchor = Anchor.CentreLeft }; username.AddUserLink(item.User); - return username; + return [new UpdateableTeamFlag(item.User.Team) { Size = new Vector2(40, 20) }, username]; } protected sealed override Drawable[] CreateAdditionalContent(UserStatistics item) => new[] From 55809f5e0d7429dcaf8a59d6c1c82323bc8055de Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 14 Feb 2025 06:15:32 -0500 Subject: [PATCH 617/620] Apply changes to Android --- osu.Android/OsuGameAndroid.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 0f2451f0a0..e725f9245f 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -12,6 +12,7 @@ using osu.Game; using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using osuTK; namespace osu.Android { @@ -20,6 +21,8 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; + protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); + public OsuGameAndroid(OsuGameActivity activity) : base(null) { From 27b9a6b7a386fb975df780dfd78d3ce3bcf114e9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 14 Feb 2025 06:15:56 -0500 Subject: [PATCH 618/620] Reset UI scale for mobile platforms --- .../.idea/deploymentTargetSelector.xml | 10 ++++++++++ osu.Game/Configuration/OsuConfigManager.cs | 13 ++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 .idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml diff --git a/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000000..4432459b86 --- /dev/null +++ b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 1244dd8cfc..76d06f3665 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -238,7 +238,7 @@ namespace osu.Game.Configuration public void Migrate() { - // arrives as 2020.123.0 + // arrives as 2020.123.0-lazer string rawVersion = Get(OsuSetting.Version); if (rawVersion.Length < 6) @@ -251,11 +251,14 @@ namespace osu.Game.Configuration if (!int.TryParse(pieces[0], out int year)) return; if (!int.TryParse(pieces[1], out int monthDay)) return; - // ReSharper disable once UnusedVariable - int combined = (year * 10000) + monthDay; + int combined = year * 10000 + monthDay; - // migrations can be added here using a condition like: - // if (combined < 20220103) { performMigration() } + if (combined < 20250214) + { + // UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before. + if (RuntimeInfo.IsMobile) + GetBindable(OsuSetting.UIScale).SetDefault(); + } } public override TrackedSettings CreateTrackedSettings() From ef2f482d041840bb4875a19b3f9a351b0415a63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Feb 2025 12:40:54 +0100 Subject: [PATCH 619/620] Fix skin deserialisation test --- .../Archives/modified-argon-20250214.osk | Bin 0 -> 1724 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk new file mode 100644 index 0000000000000000000000000000000000000000..74abef25caa81004c11911a9d223909871da3299 GIT binary patch literal 1724 zcmZ{k2T;>l5QqPvBSjL5l|$*B&^+NzKsZ`35Q0HasRALCgd|9F4y8*dCn_Ze7J3UH zEf|nGO6Q0`b1=#Rl_Vz3y6>7BlANrc9&8eklY|tv zVI`T2nXcBXvieq!P2UZFqL3J_Ku%O3Z-w6t$7~^D)+eGen7nB8?2Pwp;etW*BCrxH z;eHc#u^#w7K~UhK(TCgsur31tQk(?g9vG|@>Nhi7U=R-LjSVJ#$#jW4g&RL1)-|9Ejozs>R#YJ&u;ia&3oMki*M;&wH(~k5NDD`iJ)KX=B);?Ela*&jx*7ht5pW>`|Qe8KWThWWUIAzhf zde~z&Rkq)rUlt++ai~{!35_8+=tzcw~yXl5`PiWV`aid|IF$LuJ+n}T+);r%ID zh!xQ!l1!HM3L7EUg^iWCvuxlV>t#W7j&K=Jy{vyY^Pb}fQ#O?-zfu!;dcMgr%Z)8r zKw=+tO_P|dH!K#$>Rm{*)HXeWfCF#2bUQ9(36$L$!qZW=^QcP?MZi86(Df>HOK7D< zGN`d&UL>#0_%wGuBia240Rg|_{Jd1w4W2Y+Omp3kGPiiFoM2U`*YfIEQsiz1PR%2U z6g`&#Gb{{F`i?ge{wUZd{>C`iwzHgQ)rApzF|lyPmk;i!JIrY zSa-N__^cGQ<)t!axQ?RMP7T%l;OZ{2_Ex>*)`55}*t2H36C{2rWpcG3a~gPm*y@Cj zJVt}=>MIhkJW`c8n`+U^nxICo=qpl*J zPySSNNUCcil)@YbCk(e4t_O~brt*<#FUuThMJ7Itlout+d@)abt4N;dSX)wl)hE#R zf4#Ti^)+42A2;BbD8~>19^pP1?*OlFG}>pQq54< From b21dd01de7263ecb6fa2817409b23e9eb16427c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Feb 2025 00:03:41 +0900 Subject: [PATCH 620/620] Use fixed width for digital clock display Supersedes and closes https://github.com/ppy/osu/pull/31093. --- .../Overlays/Toolbar/DigitalClockDisplay.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs index ada2f6ff86..bd1c944847 100644 --- a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs @@ -7,8 +7,10 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK; namespace osu.Game.Overlays.Toolbar { @@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Toolbar private OsuSpriteText realTime; private OsuSpriteText gameTime; + private FillFlowContainer runningText; + private bool showRuntime = true; public bool ShowRuntime @@ -52,17 +56,36 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load(OsuColour colours) { - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; InternalChildren = new Drawable[] { - realTime = new OsuSpriteText(), - gameTime = new OsuSpriteText + realTime = new OsuSpriteText + { + Font = OsuFont.Default.With(fixedWidth: true), + Spacing = new Vector2(-1.5f, 0), + }, + runningText = new FillFlowContainer { Y = 14, Colour = colours.PinkLight, - Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), - } + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "running", + Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), + }, + gameTime = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 10, fixedWidth: true, weight: FontWeight.SemiBold), + Spacing = new Vector2(-0.5f, 0), + } + } + }, }; updateMetrics(); @@ -71,14 +94,12 @@ namespace osu.Game.Overlays.Toolbar protected override void UpdateDisplay(DateTimeOffset now) { realTime.Text = now.ToLocalisableString(use24HourDisplay ? @"HH:mm:ss" : @"h:mm:ss tt"); - gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; + gameTime.Text = $"{new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; } private void updateMetrics() { - Width = showRuntime || !use24HourDisplay ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). - - gameTime.FadeTo(showRuntime ? 1 : 0); + runningText.FadeTo(showRuntime ? 1 : 0); } } }