From 42b76294db48e604b48ade266444190a29bf1424 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Apr 2024 09:48:57 +0800 Subject: [PATCH 001/100] 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 002/100] 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 003/100] 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 004/100] 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 005/100] 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 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 006/100] 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 007/100] 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 008/100] 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 009/100] 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 010/100] 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 011/100] 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 012/100] 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#;9%fjunnIb$p8h{7#SF(7!(-NiV~AcGV}8ib99sQ^NUh4^Abx^i}mu0 zOG86=8Q9gU^3rvy^3tuU^3qEyxEUB(UNAE-fQirv2ZIh72(-Pg?BaXwr@mD8=skgu z8H|ZK#&R-zigzD*%__OHrDOfGgYKV1eYpCPi*CI6cmLV_C%->?o30IC#IQbOSJJW9 zhaS$DFr9tEf|)ladPXnUwY?!lG)1mBGvMk)*9azUF{95KH#1%c_(hk5%sIBW!zWbR zccI_jd7D>>O=v#3$NjmAN1lYeo}-dSUD*jQ(Fc!&dKmYHzvr%8-6$&^UUQ5|Y8_)r z-W0p{qL*EtwU%9xoTmCIY{uulGv>x;1MCcw^Sw@pm~LP8<6`>jbGDv;KCZJ^nY~t` z{%`KdR?|z`+R3^CTfTBEnqAj>Ow)IAphWCK-A|8~p6xBX-e_{RFJS&JX0a=u3^u4| ziT+mC;FoUQE62BTzS@=kzZYI9F4c?5>Py>O&-`wNip;Nz7T?-eE*3tXy5)YjCf6N@ zm8?p$*iP0zVGr Date: Fri, 27 Dec 2024 10:56:52 +0100 Subject: [PATCH 013/100] 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 014/100] 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 015/100] 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 016/100] 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 017/100] 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 018/100] 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 019/100] 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 020/100] 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 021/100] 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 022/100] 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 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 023/100] 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 024/100] 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 025/100] 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 026/100] 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 027/100] 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 028/100] 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 029/100] 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 030/100] 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 031/100] 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 032/100] 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 033/100] 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 034/100] 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 035/100] 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 036/100] 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 037/100] 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 038/100] 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 3ac2d90f19a1da783a45f721fdf4d9046dfe3886 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 20:44:50 -0500 Subject: [PATCH 039/100] 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 1211f6cf4cfc7a214e026e584ba6f704ea3471e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 13:06:34 +0900 Subject: [PATCH 040/100] 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 041/100] 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 042/100] 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 043/100] 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 044/100] 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 045/100] 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 046/100] 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 047/100] 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 048/100] 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 049/100] 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 050/100] 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 051/100] 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 052/100] 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 053/100] 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 054/100] 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 055/100] 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 056/100] 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 057/100] 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 37da72d764896b6678738bf9ea175b8a3ae2bed5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 Jan 2025 00:32:06 +0900 Subject: [PATCH 058/100] 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 059/100] 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 060/100] 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 061/100] 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 062/100] 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 063/100] 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 064/100] 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 065/100] 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 066/100] 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 067/100] 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 068/100] 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 069/100] 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 070/100] 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 071/100] 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 072/100] 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 073/100] 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 074/100] 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 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 075/100] 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 076/100] 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 077/100] 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 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 078/100] 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 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 079/100] 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 080/100] 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 081/100] 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 082/100] 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 083/100] 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 084/100] 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 085/100] 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 086/100] 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 087/100] 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 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 088/100] 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 089/100] 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 090/100] 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 091/100] 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 092/100] 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 2a7a3d932edebd82d2a2fa26f20957a88ea5edc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jan 2025 13:24:12 +0900 Subject: [PATCH 093/100] 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 094/100] 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 095/100] 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 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 096/100] 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 097/100] 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 098/100] 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 099/100] 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 83a2fe09c5cede3991615135c10e1853c8e22164 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jan 2025 13:07:20 +0900 Subject: [PATCH 100/100] 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